relay_ffi/
lib.rs

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