Skip to main content

rgpre/ffi/
buffer.rs

1use std::cell::RefCell;
2use std::fmt::Display;
3use std::mem::ManuallyDrop;
4use std::ptr;
5
6#[repr(C)]
7pub struct ByteBuffer {
8    pub ptr: *mut u8,
9    pub length: i32,
10    pub capacity: i32,
11}
12
13impl ByteBuffer {
14    #[must_use]
15    pub fn from_vec(bytes: Vec<u8>) -> Self {
16        let Ok(length) = i32::try_from(bytes.len()) else {
17            set_last_error("buffer length exceeds i32::MAX");
18            return Self::null();
19        };
20        let Ok(capacity) = i32::try_from(bytes.capacity()) else {
21            set_last_error("buffer capacity exceeds i32::MAX");
22            return Self::null();
23        };
24
25        let mut bytes = ManuallyDrop::new(bytes);
26        Self {
27            ptr: bytes.as_mut_ptr(),
28            length,
29            capacity,
30        }
31    }
32
33    pub fn destroy(self) {
34        if self.ptr.is_null() {
35            return;
36        }
37
38        if self.length < 0 || self.capacity < 0 || self.length > self.capacity {
39            return;
40        }
41
42        let Ok(length) = usize::try_from(self.length) else {
43            return;
44        };
45        let Ok(capacity) = usize::try_from(self.capacity) else {
46            return;
47        };
48
49        unsafe {
50            let _ = Vec::from_raw_parts(self.ptr, length, capacity);
51        }
52    }
53
54    #[must_use]
55    pub const fn null() -> Self {
56        Self {
57            ptr: ptr::null_mut(),
58            length: 0,
59            capacity: 0,
60        }
61    }
62}
63
64thread_local! {
65    static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
66}
67
68pub fn set_last_error(err: impl Display) {
69    LAST_ERROR.with(|cell| {
70        *cell.borrow_mut() = Some(err.to_string());
71    });
72}
73
74pub fn clear_last_error() {
75    LAST_ERROR.with(|cell| {
76        *cell.borrow_mut() = None;
77    });
78}
79
80#[must_use]
81pub fn last_error_message() -> Option<String> {
82    LAST_ERROR.with(|cell| cell.borrow().clone())
83}
84
85pub fn into_ffi_result(result: crate::Result<Vec<u8>>) -> *mut ByteBuffer {
86    match result {
87        Ok(bytes) => {
88            clear_last_error();
89            Box::into_raw(Box::new(ByteBuffer::from_vec(bytes)))
90        }
91        Err(err) => {
92            set_last_error(err);
93            ptr::null_mut()
94        }
95    }
96}
97
98pub fn into_ffi_ptr_result<T>(result: crate::Result<T>) -> *mut T {
99    match result {
100        Ok(value) => {
101            clear_last_error();
102            Box::into_raw(Box::new(value))
103        }
104        Err(err) => {
105            set_last_error(err);
106            ptr::null_mut()
107        }
108    }
109}
110
111pub fn into_ffi_i32_result(result: crate::Result<i32>, error_value: i32) -> i32 {
112    match result {
113        Ok(value) => {
114            clear_last_error();
115            value
116        }
117        Err(err) => {
118            set_last_error(err);
119            error_value
120        }
121    }
122}
123
124/// Runs the given closure on a dedicated thread with a large stack.
125///
126/// FFI callers (e.g. Unity/C# via P/Invoke) often run Rust code on threads
127/// whose stacks are much smaller than Rust's default 8 MiB (Unity worker
128/// threads typically have ~1 MiB).  Heavy parsing and GLTF-conversion call
129/// chains can overflow that.  Spawning a scoped thread guarantees enough
130/// stack space while still allowing the closure to borrow caller-owned data.
131///
132/// As a bonus the scoped-thread join catches any Rust panics, converting
133/// them into an [`Error::Conversion`] instead of unwinding across the
134/// `extern "C"` boundary (which is undefined behaviour).
135pub fn run_on_large_stack<F, T>(f: F) -> crate::Result<T>
136where
137    F: FnOnce() -> crate::Result<T> + Send,
138    T: Send,
139{
140    const STACK_SIZE: usize = 64 * 1024 * 1024; // 64 MiB
141
142    std::thread::scope(|scope| {
143        std::thread::Builder::new()
144            .stack_size(STACK_SIZE)
145            .spawn_scoped(scope, f)
146            .map_err(|e| {
147                crate::error::Error::Conversion(format!("failed to spawn worker thread: {e}"))
148            })?
149            .join()
150            .unwrap_or_else(|payload| {
151                let msg = payload
152                    .downcast_ref::<&str>()
153                    .copied()
154                    .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
155                    .unwrap_or("unknown panic");
156                Err(crate::error::Error::Conversion(format!(
157                    "worker thread panicked: {msg}"
158                )))
159            })
160    })
161}