nandi/jolt-nativepublic Fork 0
5ba95e0164dfaf9110357b5e041001f98d4f13a9
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

lib.rs · 184 lines · 6.6 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 20d ago1//! The rules every `.so` in this workspace keeps at its edge.
2//!
3//! A jolt library binds a shared object through `jolt.ffi/defcfn`: it declares
4//! typed foreign functions and gets back integers, doubles, and borrowed UTF-8
5//! strings. That is the whole vocabulary. Everything a native crate here wants
6//! to say has to be said in it, and the same three problems come up every time
7//! — so they are solved once, here, rather than three times slightly
8//! differently.
9//!
10//! # Unwinding
11//!
12//! A panic that crosses into a C caller is undefined behaviour, and the caller
13//! on the other side of this boundary is Chez Scheme, which will not survive
14//! it in any useful way. [`guard`] catches at the edge and answers a fallback,
15//! so a bug in a decoder is a black tile rather than a dead process.
16//!
17//! # Strings out
18//!
19//! Nothing here can hand out memory the caller must free: jolt has no place to
20//! put a free, and a leak per call is not a design. [`Scratch`] lends instead —
21//! a returned string is valid until the next one, which is long enough because
22//! jolt copies a `:string` return into a Scheme string as it crosses.
23//!
24//! # Asking, not telling
25//!
26//! None of these libraries call back. Native work happens on native threads,
27//! and a callback into a foreign runtime from one of them is a rule about
28//! threads that the caller has to keep and that nothing can check. So state
29//! that arrives asynchronously is queued and *polled*, in the shape vidya's
30//! event ABI already uses: a `poll` that answers 1 while there was something,
31//! and accessors that describe whatever it last handed over.
32
33use std::ffi::{c_char, CString};
34use std::panic::AssertUnwindSafe;
35use std::sync::Mutex;
36
37/// Run `f`, answering `fallback` if it panics.
38///
39/// Every `#[no_mangle]` entry point in this workspace starts here. The panic
40/// is logged rather than swallowed silently — a fallback that appears from
41/// nowhere is worse to debug than a crash.
42pub fn guard<R>(fallback: R, f: impl FnOnce() -> R) -> R {
43 match std::panic::catch_unwind(AssertUnwindSafe(f)) {
44 Ok(value) => value,
45 Err(_) => {
46 log::error!("jolt-abi: panic caught at the FFI boundary");
47 fallback
48 }
49 }
50}
51
52/// Read a caller's string.
53///
54/// # Safety
55/// `ptr` is null or a NUL-terminated string valid for the duration of the call.
56pub unsafe fn borrowed(ptr: *const c_char) -> String {
57 if ptr.is_null() {
58 String::new()
59 } else {
60 std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()
61 }
62}
63
64/// An empty string is how a C caller spells "no preference"; `None` is how
65/// Rust spells it. Worth naming, because the two are not the same absence and
66/// treating them as one is how a default device becomes a device called "".
67pub fn preference(s: String) -> Option<String> {
68 if s.is_empty() {
69 None
70 } else {
71 Some(s)
72 }
73}
74
75/// A place to keep the one string a library has most recently lent out.
76///
77/// Hold one per library in a `static`. [`Scratch::lend`] answers a pointer good
78/// until the next `lend` on the same scratch, which is the same contract
79/// vidya's tree ABI states for `vidya_tree_event_text` and friends.
80#[derive(Default)]
81pub struct Scratch(Mutex<Option<CString>>);
82
83impl Scratch {
84 pub const fn new() -> Self {
85 Self(Mutex::new(None))
86 }
87
88 /// Hand `value` to the caller as a borrowed C string.
89 ///
90 /// Valid until the next call to `lend` on this scratch. An interior NUL —
91 /// which no string this workspace produces should have, but a device name
92 /// comes from the operating system — truncates rather than failing the
93 /// call, since a name is being shown to someone, not parsed.
94 pub fn lend(&self, value: impl Into<Vec<u8>>) -> *const c_char {
95 let bytes: Vec<u8> = value.into();
96 let owned = CString::new(bytes.clone()).unwrap_or_else(|_| {
97 let cut = bytes.iter().position(|b| *b == 0).unwrap_or(0);
98 CString::new(&bytes[..cut]).unwrap_or_default()
99 });
100 match self.0.lock() {
101 Ok(mut slot) => {
102 let stored = slot.insert(owned);
103 stored.as_ptr()
104 }
105 Err(_) => {
106 // The lock is poisoned, which means a panic happened while a
107 // string was being lent. Leak this one rather than hand back a
108 // dangling pointer: one leaked string beats one use-after-free.
109 let ptr = owned.as_ptr();
110 std::mem::forget(owned);
111 ptr
112 }
113 }
114 }
115}
116
117// A `Scratch` in a `static` is shared across threads by definition; the Mutex
118// is what makes that sound, and the pointer it lends is only promised until the
119// next lend, which is a contract the caller keeps, not a lifetime Rust checks.
120unsafe impl Sync for Scratch {}
121
122/// The empty C string — what a string-returning entry point answers when there
123/// is nothing to say. Static, so it outlives any scratch.
124pub fn empty_str() -> *const c_char {
125 c"".as_ptr()
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use std::ffi::CStr;
132
133 fn read(ptr: *const c_char) -> String {
Run the formatter over the tree 3e8c6f0 nandi 13d ago134 unsafe { CStr::from_ptr(ptr) }
135 .to_string_lossy()
136 .into_owned()
Lift freeq's AV media plane out of sleek 90f8b89 nandi 20d ago137 }
138
139 #[test]
140 fn a_lent_string_lasts_until_the_next_one() {
141 let scratch = Scratch::new();
142 let first = scratch.lend("one");
143 assert_eq!(read(first), "one");
144 let second = scratch.lend("two");
145 assert_eq!(read(second), "two");
146 }
147
148 #[test]
149 fn two_scratches_do_not_tread_on_each_other() {
150 let a = Scratch::new();
151 let b = Scratch::new();
152 let from_a = a.lend("keys");
153 let _ = b.lend("status");
154 // b's lend must not have replaced a's — a library with two
155 // string-returning families keeps one scratch per family for exactly
156 // this reason.
157 assert_eq!(read(from_a), "keys");
158 }
159
160 #[test]
161 fn an_interior_nul_truncates_rather_than_failing() {
162 let scratch = Scratch::new();
163 assert_eq!(read(scratch.lend(&b"we\0bcam"[..])), "we");
164 }
165
166 #[test]
167 fn guard_answers_the_fallback_instead_of_unwinding() {
168 assert_eq!(guard(7, || panic!("decoder")), 7);
169 assert_eq!(guard(7, || 1), 1);
170 }
171
172 #[test]
173 fn an_empty_preference_is_no_preference() {
174 assert_eq!(preference(String::new()), None);
175 assert_eq!(preference("hw:1".to_owned()), Some("hw:1".to_owned()));
176 }
177
178 #[test]
179 fn a_null_pointer_reads_as_the_empty_string() {
180 assert_eq!(unsafe { borrowed(std::ptr::null()) }, "");
181 let owned = CString::new("nandi").unwrap();
182 assert_eq!(unsafe { borrowed(owned.as_ptr()) }, "nandi");
183 }
184}