relay_ffi/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
//! Utilities for error handling in FFI bindings.
//!
//! This crate facilitates an [`errno`]-like error handling pattern: On success, the result of a
//! function call is returned. On error, a thread-local marker is set that allows to retrieve the
//! error, message, and a backtrace if available.
//!
//! # Catch Errors and Panics
//!
//! The [`catch_unwind`] attribute annotates functions that can internally throw errors. It allows
//! the use of the questionmark operator `?` in a function that does not return `Result`. The error
//! is then available using [`with_last_error`]:
//!
//! ```
//! use relay_ffi::catch_unwind;
//!
//! #[catch_unwind]
//! unsafe fn parse_number() -> i32 {
//! // use the questionmark operator for errors:
//! let number: i32 = "42".parse()?;
//!
//! // return the value directly, not `Ok`:
//! number * 2
//! }
//! ```
//!
//! # Safety
//!
//! Since function calls always need to return a value, this crate has to return
//! `std::mem::zeroed()` as a placeholder in case of an error. This is unsafe for reference types
//! and function pointers. Because of this, functions must be marked `unsafe`.
//!
//! In most cases, FFI functions should return either `repr(C)` structs or pointers, in which case
//! this is safe in principle. The author of the API is responsible for defining the contract,
//! however, and document the behavior of custom structures in case of an error.
//!
//! # Examples
//!
//! Annotate FFI functions with [`catch_unwind`] to capture errors. The error can be inspected via
//! [`with_last_error`]:
//!
//! ```
//! use relay_ffi::{catch_unwind, with_last_error};
//!
//! #[catch_unwind]
//! unsafe fn parse_number() -> i32 {
//! "42".parse()?
//! }
//!
//! let parsed = unsafe { parse_number() };
//! match with_last_error(|e| e.to_string()) {
//! Some(error) => println!("errored with: {error}"),
//! None => println!("result: {parsed}"),
//! }
//! ```
//!
//! To capture panics, register the panic hook early during library initialization:
//!
//! ```
//! use relay_ffi::{catch_unwind, with_last_error};
//!
//! relay_ffi::set_panic_hook();
//!
//! #[catch_unwind]
//! unsafe fn fail() {
//! panic!("expected panic");
//! }
//!
//! unsafe { fail() };
//!
//! if let Some(description) = with_last_error(|e| e.to_string()) {
//! println!("{description}");
//! }
//! ```
//!
//! # Creating C-APIs
//!
//! This is an example for exposing an API to C:
//!
//! ```
//! use std::ffi::CString;
//! use std::os::raw::c_char;
//!
//! #[no_mangle]
//! pub unsafe extern "C" fn init_ffi() {
//! relay_ffi::set_panic_hook();
//! }
//!
//! #[no_mangle]
//! pub unsafe extern "C" fn last_strerror() -> *mut c_char {
//! let ptr_opt = relay_ffi::with_last_error(|err| {
//! CString::new(err.to_string())
//! .unwrap_or_default()
//! .into_raw()
//! });
//!
//! ptr_opt.unwrap_or(std::ptr::null_mut())
//! }
//! ```
//!
//! [`errno`]: https://man7.org/linux/man-pages/man3/errno.3.html
#![warn(missing_docs)]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
)]
#![allow(clippy::derive_partial_eq_without_eq)]
use std::cell::RefCell;
use std::error::Error;
use std::{fmt, panic, thread};
pub use relay_ffi_macros::catch_unwind;
thread_local! {
static LAST_ERROR: RefCell<Option<anyhow::Error>> = const { RefCell::new(None) };
}
fn set_last_error(err: anyhow::Error) {
LAST_ERROR.with(|e| {
*e.borrow_mut() = Some(err);
});
}
#[doc(hidden)]
pub mod __internal {
use super::*;
/// Catches down panics and errors from the given closure.
///
/// Returns the result of the passed function on success. On error or panic, returns
/// zero-initialized memory and sets the thread-local error.
///
/// # Safety
///
/// Returns `std::mem::zeroed` on error, which is unsafe for reference types and function
/// pointers.
#[inline]
pub unsafe fn catch_errors<F, T>(f: F) -> T
where
F: FnOnce() -> Result<T, anyhow::Error> + panic::UnwindSafe,
{
match panic::catch_unwind(f) {
Ok(Ok(result)) => result,
Ok(Err(err)) => {
set_last_error(err);
std::mem::zeroed()
}
Err(_) => std::mem::zeroed(),
}
}
}
/// Acquires a reference to the last error and passes it to the callback, if any.
///
/// Returns `Some(R)` if there was an error, otherwise `None`. The error resets when it is taken
/// with [`take_last_error`].
///
/// # Example
///
/// ```
/// use relay_ffi::{catch_unwind, with_last_error};
///
/// #[catch_unwind]
/// unsafe fn run_ffi() -> i32 {
/// "invalid".parse()?
/// }
///
/// let parsed = unsafe { run_ffi() };
/// match with_last_error(|e| e.to_string()) {
/// Some(error) => println!("errored with: {error}"),
/// None => println!("result: {parsed}"),
/// }
/// ```
pub fn with_last_error<R, F>(f: F) -> Option<R>
where
F: FnOnce(&anyhow::Error) -> R,
{
LAST_ERROR.with(|e| e.borrow().as_ref().map(f))
}
/// Takes the last error, leaving `None` in its place.
///
/// To inspect the error without removing it, use [`with_last_error`].
///
/// # Example
///
/// ```
/// use relay_ffi::{catch_unwind, take_last_error};
///
/// #[catch_unwind]
/// unsafe fn run_ffi() -> i32 {
/// "invalid".parse()?
/// }
///
/// let parsed = unsafe { run_ffi() };
/// match take_last_error() {
/// Some(error) => println!("errored with: {error}"),
/// None => println!("result: {parsed}"),
/// }
/// ```
pub fn take_last_error() -> Option<anyhow::Error> {
LAST_ERROR.with(|e| e.borrow_mut().take())
}
/// An error representing a panic carrying the message as payload.
///
/// To capture panics, register the hook using [`set_panic_hook`].
///
/// # Example
///
/// ```
/// use relay_ffi::{catch_unwind, with_last_error, Panic};
///
/// #[catch_unwind]
/// unsafe fn panics() {
/// panic!("this is fine");
/// }
///
/// relay_ffi::set_panic_hook();
///
/// unsafe { panics() };
///
/// with_last_error(|error| {
/// if let Some(panic) = error.downcast_ref::<Panic>() {
/// println!("{}", panic.description());
/// }
/// });
/// ```
#[derive(Debug)]
pub struct Panic(String);
impl Panic {
fn new(info: &panic::PanicHookInfo) -> Self {
let thread = thread::current();
let thread = thread.name().unwrap_or("unnamed");
let message = match info.payload().downcast_ref::<&str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &**s,
None => "Box<Any>",
},
};
let description = match info.location() {
Some(location) => format!(
"thread '{thread}' panicked with '{message}' at {}:{}",
location.file(),
location.line()
),
None => format!("thread '{thread}' panicked with '{message}'"),
};
Self(description)
}
/// Returns a description containing the location and message of the panic.
#[inline]
pub fn description(&self) -> &str {
&self.0
}
}
impl fmt::Display for Panic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "panic: {}", self.description())
}
}
impl Error for Panic {}
/// Registers a hook for capturing panics with backtraces.
///
/// This function must be registered early when the FFI is initialized before any other calls are
/// made. Usually, this would be exported from an initialization function.
///
/// See the [`Panic`] documentation for more information.
///
/// # Example
///
/// ```
/// pub unsafe extern "C" fn init_ffi() {
/// relay_ffi::set_panic_hook();
/// }
/// ```
pub fn set_panic_hook() {
panic::set_hook(Box::new(|info| set_last_error(Panic::new(info).into())));
}