//! The rules every `.so` in this workspace keeps at its edge. //! //! A jolt library binds a shared object through `jolt.ffi/defcfn`: it declares //! typed foreign functions and gets back integers, doubles, and borrowed UTF-8 //! strings. That is the whole vocabulary. Everything a native crate here wants //! to say has to be said in it, and the same three problems come up every time //! — so they are solved once, here, rather than three times slightly //! differently. //! //! # Unwinding //! //! A panic that crosses into a C caller is undefined behaviour, and the caller //! on the other side of this boundary is Chez Scheme, which will not survive //! it in any useful way. [`guard`] catches at the edge and answers a fallback, //! so a bug in a decoder is a black tile rather than a dead process. //! //! # Strings out //! //! Nothing here can hand out memory the caller must free: jolt has no place to //! put a free, and a leak per call is not a design. [`Scratch`] lends instead — //! a returned string is valid until the next one, which is long enough because //! jolt copies a `:string` return into a Scheme string as it crosses. //! //! # Asking, not telling //! //! None of these libraries call back. Native work happens on native threads, //! and a callback into a foreign runtime from one of them is a rule about //! threads that the caller has to keep and that nothing can check. So state //! that arrives asynchronously is queued and *polled*, in the shape vidya's //! event ABI already uses: a `poll` that answers 1 while there was something, //! and accessors that describe whatever it last handed over. use std::ffi::{c_char, CString}; use std::panic::AssertUnwindSafe; use std::sync::Mutex; /// Run `f`, answering `fallback` if it panics. /// /// Every `#[no_mangle]` entry point in this workspace starts here. The panic /// is logged rather than swallowed silently — a fallback that appears from /// nowhere is worse to debug than a crash. pub fn guard(fallback: R, f: impl FnOnce() -> R) -> R { match std::panic::catch_unwind(AssertUnwindSafe(f)) { Ok(value) => value, Err(_) => { log::error!("jolt-abi: panic caught at the FFI boundary"); fallback } } } /// Read a caller's string. /// /// # Safety /// `ptr` is null or a NUL-terminated string valid for the duration of the call. pub unsafe fn borrowed(ptr: *const c_char) -> String { if ptr.is_null() { String::new() } else { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() } } /// An empty string is how a C caller spells "no preference"; `None` is how /// Rust spells it. Worth naming, because the two are not the same absence and /// treating them as one is how a default device becomes a device called "". pub fn preference(s: String) -> Option { if s.is_empty() { None } else { Some(s) } } /// A place to keep the one string a library has most recently lent out. /// /// Hold one per library in a `static`. [`Scratch::lend`] answers a pointer good /// until the next `lend` on the same scratch, which is the same contract /// vidya's tree ABI states for `vidya_tree_event_text` and friends. #[derive(Default)] pub struct Scratch(Mutex>); impl Scratch { pub const fn new() -> Self { Self(Mutex::new(None)) } /// Hand `value` to the caller as a borrowed C string. /// /// Valid until the next call to `lend` on this scratch. An interior NUL — /// which no string this workspace produces should have, but a device name /// comes from the operating system — truncates rather than failing the /// call, since a name is being shown to someone, not parsed. pub fn lend(&self, value: impl Into>) -> *const c_char { let bytes: Vec = value.into(); let owned = CString::new(bytes.clone()).unwrap_or_else(|_| { let cut = bytes.iter().position(|b| *b == 0).unwrap_or(0); CString::new(&bytes[..cut]).unwrap_or_default() }); match self.0.lock() { Ok(mut slot) => { let stored = slot.insert(owned); stored.as_ptr() } Err(_) => { // The lock is poisoned, which means a panic happened while a // string was being lent. Leak this one rather than hand back a // dangling pointer: one leaked string beats one use-after-free. let ptr = owned.as_ptr(); std::mem::forget(owned); ptr } } } } // A `Scratch` in a `static` is shared across threads by definition; the Mutex // is what makes that sound, and the pointer it lends is only promised until the // next lend, which is a contract the caller keeps, not a lifetime Rust checks. unsafe impl Sync for Scratch {} /// The empty C string — what a string-returning entry point answers when there /// is nothing to say. Static, so it outlives any scratch. pub fn empty_str() -> *const c_char { c"".as_ptr() } #[cfg(test)] mod tests { use super::*; use std::ffi::CStr; fn read(ptr: *const c_char) -> String { unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned() } #[test] fn a_lent_string_lasts_until_the_next_one() { let scratch = Scratch::new(); let first = scratch.lend("one"); assert_eq!(read(first), "one"); let second = scratch.lend("two"); assert_eq!(read(second), "two"); } #[test] fn two_scratches_do_not_tread_on_each_other() { let a = Scratch::new(); let b = Scratch::new(); let from_a = a.lend("keys"); let _ = b.lend("status"); // b's lend must not have replaced a's — a library with two // string-returning families keeps one scratch per family for exactly // this reason. assert_eq!(read(from_a), "keys"); } #[test] fn an_interior_nul_truncates_rather_than_failing() { let scratch = Scratch::new(); assert_eq!(read(scratch.lend(&b"we\0bcam"[..])), "we"); } #[test] fn guard_answers_the_fallback_instead_of_unwinding() { assert_eq!(guard(7, || panic!("decoder")), 7); assert_eq!(guard(7, || 1), 1); } #[test] fn an_empty_preference_is_no_preference() { assert_eq!(preference(String::new()), None); assert_eq!(preference("hw:1".to_owned()), Some("hw:1".to_owned())); } #[test] fn a_null_pointer_reads_as_the_empty_string() { assert_eq!(unsafe { borrowed(std::ptr::null()) }, ""); let owned = CString::new("nandi").unwrap(); assert_eq!(unsafe { borrowed(owned.as_ptr()) }, "nandi"); } }