nandi/jolt-nativepublic Fork 0
d682bfa94c04bddb877d4b5e21ef938b826cc9be
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.

Lift freeq's AV media plane out of sleek 90f8b89 · on d682bfa94c04bddb877d4b5e21ef938b826cc9be · nandi · 19d ago
lib.rs · 182 lines · 6.6 KBRust Blame HistoryRaw
  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
//! 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<R>(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<String> {
    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<Option<CString>>);

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<Vec<u8>>) -> *const c_char {
        let bytes: Vec<u8> = 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");
    }
}