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}