diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3f19e61 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Chromium diff --git a/.gitignore b/.gitignore index 1e7caa9..d5f8235 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.cargo/ Cargo.lock target/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..69243ce --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "third_party/breakpad"] + path = third_party/breakpad + url = https://chromium.googlesource.com/breakpad/breakpad +[submodule "third_party/lss"] + path = third_party/lss + url = https://chromium.googlesource.com/linux-syscall-support diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..697b195 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,5 @@ +edition = "2021" +newline_style = "Unix" +use_small_heuristics = "Max" +version = "Two" +wrap_comments = true diff --git a/Cargo.toml b/Cargo.toml index a0818e5..bdf2af7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,3 +2,14 @@ edition = "2021" name = "breakpad-rs" version = "0.1.0" + +[build-dependencies] +autocxx-build = "0.27.0" +autocxx-engine = "0.27.0" +miette = { version = "7.2.0", features = ["fancy"] } + +[dependencies] +autocxx = "0.27.0" +cxx = "1.0.129" +env_logger = "0.11.5" +log = "0.4.22" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..a7e68b2 --- /dev/null +++ b/build.rs @@ -0,0 +1,164 @@ +use miette::IntoDiagnostic; + +const THIRD_PARTY_BREAKPAD_APPLE: &[&str] = &[ + "third_party/breakpad/src/client/mac/handler/breakpad_nlist_64.cc", + "third_party/breakpad/src/client/mac/handler/dynamic_images.cc", + "third_party/breakpad/src/client/mac/handler/minidump_generator.cc", + "third_party/breakpad/src/client/minidump_file_writer.cc", + "third_party/breakpad/src/common/convert_UTF.cc", + "third_party/breakpad/src/common/mac/arch_utilities.cc", + "third_party/breakpad/src/common/mac/file_id.cc", + "third_party/breakpad/src/common/mac/macho_id.cc", + "third_party/breakpad/src/common/mac/macho_utilities.cc", + "third_party/breakpad/src/common/mac/macho_walker.cc", + "third_party/breakpad/src/common/mac/string_utilities.cc", + "third_party/breakpad/src/common/md5.cc", + "third_party/breakpad/src/common/string_conversion.cc", +]; + +const THIRD_PARTY_BREAKPAD_UNIX: &[&str] = &[ + "third_party/breakpad/src/client/linux/crash_generation/crash_generation_client.cc", + "third_party/breakpad/src/client/linux/dump_writer_common/thread_info.cc", + "third_party/breakpad/src/client/linux/dump_writer_common/ucontext_reader.cc", + "third_party/breakpad/src/client/linux/handler/exception_handler.cc", + "third_party/breakpad/src/client/linux/handler/minidump_descriptor.cc", + "third_party/breakpad/src/client/linux/log/log.cc", + "third_party/breakpad/src/client/linux/microdump_writer/microdump_writer.cc", + "third_party/breakpad/src/client/linux/minidump_writer/linux_dumper.cc", + "third_party/breakpad/src/client/linux/minidump_writer/linux_ptrace_dumper.cc", + "third_party/breakpad/src/client/linux/minidump_writer/minidump_writer.cc", + "third_party/breakpad/src/client/linux/minidump_writer/pe_file.cc", + "third_party/breakpad/src/client/minidump_file_writer.cc", + "third_party/breakpad/src/common/convert_UTF.cc", + "third_party/breakpad/src/common/linux/elfutils.cc", + "third_party/breakpad/src/common/linux/file_id.cc", + "third_party/breakpad/src/common/linux/guid_creator.cc", + "third_party/breakpad/src/common/linux/linux_libc_support.cc", + "third_party/breakpad/src/common/linux/memory_mapped_file.cc", + "third_party/breakpad/src/common/linux/safe_readlink.cc", + "third_party/breakpad/src/common/string_conversion.cc", +]; + +fn autocxx_include_paths() -> Vec { + // The order of these paths is important to prevent the following error: + // + // tried including but didn't find libc++'s header. This + // usually means that your header search paths are not configured properly. The header search + // paths should contain the C++ Standard Library headers before any C Standard Library, and + // you are probably using compiler flags that make that not be the case. + // + let target = std::env::var("TARGET").unwrap_or_default(); + let sysroot = std::env::var(format!("SYSROOT_{target}")).unwrap_or_default(); + let sysroot_target = std::env::var(format!("SYSROOT_TARGET_{target}")).unwrap_or_default(); + vec![ + "src/cplusplus".to_string(), + format!("{sysroot}/usr/include/c++/v1"), + format!("{sysroot}/usr/include"), + format!("{sysroot}/usr/include/{sysroot_target}"), + ] +} + +fn configure_build_android(build: &mut autocxx_engine::BuilderBuild) { + build + .files(THIRD_PARTY_BREAKPAD_UNIX) + .files([ + "src/cplusplus/breakpad_unix.cc", + "src/cplusplus/utility.cc", + "src/cplusplus/utility_unix.cc", + ]) + .flag_if_supported("-Wno-missing-field-initializers") + .flag_if_supported("-Wno-sign-compare") + .flag_if_supported("-Wno-unused-parameter") + .pic(true); +} + +fn configure_build_ios(build: &mut autocxx_engine::BuilderBuild) { + build + .files(THIRD_PARTY_BREAKPAD_APPLE) + .files([ + "src/cplusplus/breakpad_apple.cc", + "src/cplusplus/utility.cc", + "third_party/breakpad/src/client/ios/exception_handler_no_mach.cc", + ]) + .flag_if_supported("-Wno-mismatched-tags") + .flag_if_supported("-Wno-unused-parameter"); + println!("cargo:rustc-link-lib=framework=CoreFoundation"); +} + +fn configure_build_linux(build: &mut autocxx_engine::BuilderBuild) { + build + .files(THIRD_PARTY_BREAKPAD_UNIX) + .files([ + "src/cplusplus/breakpad_unix.cc", + "src/cplusplus/utility.cc", + "src/cplusplus/utility_unix.cc", + ]) + .flag_if_supported("-Wno-implicit-fallthrough") + .flag_if_supported("-Wno-maybe-uninitialized") + .flag_if_supported("-Wno-missing-field-initializers") + .pic(true); +} + +fn configure_build_macos(build: &mut autocxx_engine::BuilderBuild) { + build + .files(THIRD_PARTY_BREAKPAD_APPLE) + .files([ + "src/cplusplus/breakpad_apple.cc", + "src/cplusplus/utility.cc", + "third_party/breakpad/src/client/mac/crash_generation/crash_generation_client.cc", + "third_party/breakpad/src/client/mac/handler/exception_handler.cc", + "third_party/breakpad/src/common/mac/MachIPC.mm", + ]) + .flag_if_supported("-Wno-deprecated-copy-with-user-provided-copy") + .flag_if_supported("-Wno-unused-parameter"); + println!("cargo:rustc-link-lib=framework=CoreFoundation"); +} + +fn configure_build_windows(build: &mut autocxx_engine::BuilderBuild) { + build + .define("NOMINMAX", None) + .define("UNICODE", None) + .define("WIN32_LEAN_AND_MEAN", None) + .define("_UNICODE", None) + .files([ + "src/cplusplus/breakpad_windows.cc", + "src/cplusplus/utility.cc", + "src/cplusplus/utility_windows.cc", + "third_party/breakpad/src/client/windows/crash_generation/crash_generation_client.cc", + "third_party/breakpad/src/client/windows/handler/exception_handler.cc", + "third_party/breakpad/src/common/windows/guid_string.cc", + ]); +} + +fn configure_build(build: &mut autocxx_engine::BuilderBuild) { + match std::env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { + "android" => configure_build_android(build), + "ios" => configure_build_ios(build), + "linux" => configure_build_linux(build), + "macos" => configure_build_macos(build), + "windows" => configure_build_windows(build), + target_os => panic!("Unsupported Platform: {target_os}"), + } +} + +fn main() -> miette::Result<()> { + let mut build = autocxx_build::Builder::new("src/lib.rs", autocxx_include_paths()) + .auto_allowlist(true) + .build() + .into_diagnostic()?; + configure_build(&mut build); + + #[cfg(not(debug_assertions))] + build.define("NDEBUG", None); + + build + .flag_if_supported("-std=c++17") + .flag_if_supported("/std:c++17") + .includes(["./", "third_party/breakpad/src/"]) + .compile("breakpad"); + + println!("cargo:rerun-if-changed=src/"); + println!("cargo:rerun-if-changed=third_party/"); + + Ok(()) +} diff --git a/examples/crash.rs b/examples/crash.rs new file mode 100644 index 0000000..d9a8f7c --- /dev/null +++ b/examples/crash.rs @@ -0,0 +1,26 @@ +struct Delegate; + +impl breakpad_rs::ExceptionHandlerDelegate for Delegate { + fn did_write_minidump(&self, working_path: String, minidump_id: String) { + log::debug!("[did_write_minidump] working_path={working_path} minidump_id={minidump_id}"); + } + + fn get_working_path(&self) -> String { + log::debug!("[get_working_path] return=."); + String::from(".") + } + + fn should_write_minidump(&self) -> bool { + log::debug!("[should_write_minidump] return=true"); + true + } +} + +fn main() { + env_logger::init(); + let _breakpad = breakpad_rs::Breakpad::new(Some(Box::new(Delegate))); + unsafe { + let null: *mut u8 = std::ptr::null_mut(); + *null = 42; + } +} diff --git a/src/cplusplus/breakpad.h b/src/cplusplus/breakpad.h new file mode 100644 index 0000000..e2717bc --- /dev/null +++ b/src/cplusplus/breakpad.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "breakpad_exception_handler_delegate.h" +#include "platform.h" + +#if defined(PLATFORM_ANDROID) +#include "third_party/breakpad/src/client/linux/handler/exception_handler.h" +#elif defined(PLATFORM_IOS) +#include "third_party/breakpad/src/client/ios/exception_handler_no_mach.h" +#elif defined(PLATFORM_LINUX) +#include "third_party/breakpad/src/client/linux/handler/exception_handler.h" +#elif defined(PLATFORM_MACOS) +#include "third_party/breakpad/src/client/mac/handler/exception_handler.h" +#elif defined(PLATFORM_WINDOWS) +#include "third_party/breakpad/src/client/windows/handler/exception_handler.h" +#else +#error "Unsupported Platform!" +#endif + +namespace breakpad { + +using ExceptionHandler = google_breakpad::ExceptionHandler; + +std::unique_ptr CreateExceptionHandler( + const ExceptionHandlerDelegate& delegate); + +} // namespace breakpad diff --git a/src/cplusplus/breakpad_apple.cc b/src/cplusplus/breakpad_apple.cc new file mode 100644 index 0000000..e97074c --- /dev/null +++ b/src/cplusplus/breakpad_apple.cc @@ -0,0 +1,37 @@ +#include "breakpad.h" + +#include "utility.h" + +namespace breakpad { + +namespace { + +bool FilterCallback(void* context) { + if (const auto* delegate = static_cast(context)) { + return delegate->ShouldWriteMinidump(); + } + return true; +} + +bool MinidumpCallback(const char* working_path, + const char* minidump_id, + void* context, + bool succeeded) { + if (const auto* delegate = static_cast(context)) { + delegate->DidWriteMinidump(working_path, minidump_id); + } + return succeeded; +} + +} // namespace + +std::unique_ptr CreateExceptionHandler( + const ExceptionHandlerDelegate& delegate) { + return std::make_unique( + GetValidWorkingPath(delegate.GetWorkingPath()), FilterCallback, + MinidumpCallback, + static_cast(const_cast(&delegate)), + true, nullptr); +} + +} // namespace breakpad diff --git a/src/cplusplus/breakpad_exception_handler_delegate.h b/src/cplusplus/breakpad_exception_handler_delegate.h new file mode 100644 index 0000000..235a18b --- /dev/null +++ b/src/cplusplus/breakpad_exception_handler_delegate.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace breakpad { + +class ExceptionHandlerDelegate { + public: + virtual void DidWriteMinidump(const std::string& working_path, + const std::string& minidump_id) const = 0; + virtual std::string GetWorkingPath() const = 0; + virtual bool ShouldWriteMinidump() const = 0; + + virtual ~ExceptionHandlerDelegate() = default; +}; + +} // namespace breakpad diff --git a/src/cplusplus/breakpad_unix.cc b/src/cplusplus/breakpad_unix.cc new file mode 100644 index 0000000..877030f --- /dev/null +++ b/src/cplusplus/breakpad_unix.cc @@ -0,0 +1,39 @@ +#include "breakpad.h" + +#include "utility.h" + +namespace breakpad { + +namespace { + +bool FilterCallback(void* context) { + if (const auto* delegate = static_cast(context)) { + return delegate->ShouldWriteMinidump(); + } + return true; +} + +bool MinidumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, + void* context, + bool succeeded) { + if (const auto* delegate = static_cast(context)) { + const auto& [working_path, minidump_id] = + GetMinidumpPathComponents(descriptor.path()); + delegate->DidWriteMinidump(working_path, minidump_id); + } + return succeeded; +} + +} // namespace + +std::unique_ptr CreateExceptionHandler( + const ExceptionHandlerDelegate& delegate) { + return std::make_unique( + google_breakpad::MinidumpDescriptor( + GetValidWorkingPath(delegate.GetWorkingPath())), + FilterCallback, MinidumpCallback, + static_cast(const_cast(&delegate)), + true, -1); +} + +} // namespace breakpad diff --git a/src/cplusplus/breakpad_windows.cc b/src/cplusplus/breakpad_windows.cc new file mode 100644 index 0000000..9e5ef3d --- /dev/null +++ b/src/cplusplus/breakpad_windows.cc @@ -0,0 +1,42 @@ +#include "breakpad.h" + +#include "utility.h" + +namespace breakpad { + +namespace { + +bool FilterCallback(void* context, + [[maybe_unused]] EXCEPTION_POINTERS* exinfo, + [[maybe_unused]] MDRawAssertionInfo* assertion) { + if (const auto* delegate = static_cast(context)) { + return delegate->ShouldWriteMinidump(); + } + return true; +} + +bool MinidumpCallback(const wchar_t* working_path, + const wchar_t* minidump_id, + void* context, + [[maybe_unused]] EXCEPTION_POINTERS* exinfo, + [[maybe_unused]] MDRawAssertionInfo* assertion, + bool succeeded) { + if (const auto* delegate = static_cast(context)) { + delegate->DidWriteMinidump(ToStdString(working_path), + ToStdString(minidump_id)); + } + return succeeded; +} + +} // namespace + +std::unique_ptr CreateExceptionHandler( + const ExceptionHandlerDelegate& delegate) { + return std::make_unique( + ToStdWstring(GetValidWorkingPath(delegate.GetWorkingPath())), + FilterCallback, MinidumpCallback, + static_cast(const_cast(&delegate)), + ExceptionHandler::HANDLER_ALL); +} + +} // namespace breakpad diff --git a/src/cplusplus/platform.h b/src/cplusplus/platform.h new file mode 100644 index 0000000..794e5c7 --- /dev/null +++ b/src/cplusplus/platform.h @@ -0,0 +1,31 @@ +#pragma once + +// PLATFORM_ANDROID: Android +// PLATFORM_IOS: iOS +// PLATFORM_LINUX: Linux (non-Android) +// PLATFORM_MACOS: macOS +// PLATFORM_WINDOWS: Windows + +#if defined(_WIN32) +#define PLATFORM_WINDOWS 1 + +#elif defined(__ANDROID__) +#define PLATFORM_ANDROID 1 + +#elif defined(__APPLE__) +#include +#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE +#define PLATFORM_IOS 1 +#elif defined(TARGET_OS_OSX) && TARGET_OS_OSX +#define PLATFORM_MACOS 1 +#else +#error "Unsupported Apple Platform!" +#endif + +#elif defined(__linux__) && !defined(__ANDROID__) +#define PLATFORM_LINUX 1 + +#else +#error "Unsupported Platform!" + +#endif diff --git a/src/cplusplus/utility.cc b/src/cplusplus/utility.cc new file mode 100644 index 0000000..cf6ed29 --- /dev/null +++ b/src/cplusplus/utility.cc @@ -0,0 +1,9 @@ +#include "utility.h" + +namespace { +inline constexpr char kDefaultWorkingPath[] = "."; +} // namespace + +std::string GetValidWorkingPath(const std::string& path) { + return path.empty() ? kDefaultWorkingPath : path; +} diff --git a/src/cplusplus/utility.h b/src/cplusplus/utility.h new file mode 100644 index 0000000..4bb2a3f --- /dev/null +++ b/src/cplusplus/utility.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "platform.h" + +std::string GetValidWorkingPath(const std::string& path); + +#if defined(PLATFORM_ANDROID) || defined(PLATFORM_LINUX) +#include +std::tuple GetMinidumpPathComponents( + const std::string& path); +#endif // defined(PLATFORM_ANDROID) || defined(PLATFORM_LINUX) + +#if defined(PLATFORM_WINDOWS) +std::string ToStdString(const std::wstring& from); +std::wstring ToStdWstring(const std::string& from); +#endif // defined(PLATFORM_WINDOWS) diff --git a/src/cplusplus/utility_unix.cc b/src/cplusplus/utility_unix.cc new file mode 100644 index 0000000..0d38e85 --- /dev/null +++ b/src/cplusplus/utility_unix.cc @@ -0,0 +1,24 @@ +#include "utility.h" + +std::tuple GetMinidumpPathComponents( + const std::string& path) { + // A minidump path consists of two components: a working path and a minidump + // id. A working path is a directory where a minidump is located, and a + // minidump id is a unique identifier of a minidump. + // + // /minidump/path/F79622A0-AD12-4C91-A0DB-904B2B167317.dmp + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // | | + // | minidump id: F79622A0-AD12-4C91-A0DB-904B2B167317 + // | + // working path: /minidump/path/ + // + size_t slash = path.rfind("/"); + size_t dot_dmp = path.rfind(".dmp"); + if (slash == std::string::npos || dot_dmp == std::string::npos) { + return std::make_tuple(std::string(), std::string()); + } + std::string working_path = slash == 0 ? "/" : path.substr(0, slash); + std::string minidump_id = path.substr(slash + 1, dot_dmp - slash - 1); + return std::make_tuple(working_path, minidump_id); +} diff --git a/src/cplusplus/utility_windows.cc b/src/cplusplus/utility_windows.cc new file mode 100644 index 0000000..6946ff5 --- /dev/null +++ b/src/cplusplus/utility_windows.cc @@ -0,0 +1,17 @@ +#include "utility.h" + +#include +#include + +namespace { +using WstringConverter = + std::wstring_convert, wchar_t>; +} // namespace + +std::string ToStdString(const std::wstring& from) { + return WstringConverter().to_bytes(from); +} + +std::wstring ToStdWstring(const std::string& from) { + return WstringConverter().from_bytes(from); +} diff --git a/src/lib.rs b/src/lib.rs index b93cf3f..b3f8d2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,92 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +use autocxx::subclass::prelude::*; +use ffi::ToCppString; + +autocxx::include_cpp! { + #include "breakpad_exception_handler_delegate.h" + safety!(unsafe) + generate!("breakpad::ExceptionHandlerDelegate") +} + +#[cxx::bridge(namespace = "breakpad")] +mod breakpad { + unsafe extern "C++" { + include!("breakpad.h"); + type ExceptionHandler; + type ExceptionHandlerDelegate = crate::ffi::breakpad::ExceptionHandlerDelegate; + fn CreateExceptionHandler( + delegate: &ExceptionHandlerDelegate, + ) -> UniquePtr; + } } -#[cfg(test)] -mod tests { - use super::*; +// TODO: This struct is for internal use only, so its visibility should be private or pub(crate). +// The reason for making this struct public is due to the limitation of autocxx. +#[subclass(superclass("breakpad::ExceptionHandlerDelegate"))] +pub struct ExceptionHandlerDelegateBridge { + delegate: Box, +} + +impl ExceptionHandlerDelegateBridge { + fn new( + delegate: Option>, + ) -> std::rc::Rc> { + Self::new_rust_owned(Self { + cpp_peer: Default::default(), + delegate: match delegate { + Some(delegate) => delegate, + None => Box::new(DefaultExceptionHandlerDelegate), + }, + }) + } +} + +#[allow(non_snake_case)] +impl ffi::breakpad::ExceptionHandlerDelegate_methods for ExceptionHandlerDelegateBridge { + fn DidWriteMinidump(&self, working_path: &cxx::CxxString, minidump_id: &cxx::CxxString) { + self.delegate.did_write_minidump(working_path.to_string(), minidump_id.to_string()); + } + + fn GetWorkingPath(&self) -> cxx::UniquePtr { + self.delegate.get_working_path().into_cpp() + } + + fn ShouldWriteMinidump(&self) -> bool { + self.delegate.should_write_minidump() + } +} + +pub trait ExceptionHandlerDelegate { + fn did_write_minidump(&self, working_path: String, minidump_id: String) { + log::debug!("[did_write_minidump] working_path={working_path} minidump_id={minidump_id}"); + } + + fn get_working_path(&self) -> String { + log::debug!("[get_working_path] return=empty string"); + String::new() + } + + fn should_write_minidump(&self) -> bool { + log::debug!("[should_write_minidump] return=true"); + true + } +} + +struct DefaultExceptionHandlerDelegate; +impl ExceptionHandlerDelegate for DefaultExceptionHandlerDelegate {} + +#[allow(dead_code)] +pub struct Breakpad { + bridge: std::rc::Rc>, + exception_handler: cxx::UniquePtr, +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +impl Breakpad { + pub fn new(delegate: Option>) -> Self { + let bridge = ExceptionHandlerDelegateBridge::new(delegate); + let breakpad = Self { + bridge: bridge.clone(), + exception_handler: breakpad::CreateExceptionHandler(bridge.as_ref().borrow().as_ref()), + }; + breakpad } } diff --git a/third_party/breakpad b/third_party/breakpad new file mode 160000 index 0000000..e92bea3 --- /dev/null +++ b/third_party/breakpad @@ -0,0 +1 @@ +Subproject commit e92bea30759edbae08205bccd14dc25bf1806f93 diff --git a/third_party/lss b/third_party/lss new file mode 160000 index 0000000..ed31caa --- /dev/null +++ b/third_party/lss @@ -0,0 +1 @@ +Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0