Skip to main content

ffi_bridge/
bridge.rs

1//! # bridge — High-level bridge call helpers
2//!
3//! Provides [`BridgeCall`], a convenience struct that encapsulates an input
4//! buffer, executes a Rust handler, and returns an `FfiResult` — all panic-safe.
5//!
6//! This is the recommended entry point for implementing new `extern "C"`
7//! functions that process an input buffer and return an output buffer.
8
9use crate::errors::{catch_panic, FfiError, FfiResult};
10use crate::memory::FfiBuffer;
11
12// ─── BridgeCall ───────────────────────────────────────────────────────────────
13
14/// Convenience builder for a single FFI call.
15///
16/// # Example
17///
18/// ```rust,ignore
19/// #[no_mangle]
20/// pub extern "C" fn my_fn(input: FfiBuffer) -> FfiResult {
21///     BridgeCall::new(input).run(|buf| {
22///         // buf is the input, return Result<FfiBuffer, FfiError>
23///         let val: u32 = unsafe { buf.to_json()? };
24///         FfiBuffer::from_json(&(val * 2))
25///     })
26/// }
27/// ```
28pub struct BridgeCall {
29    input: FfiBuffer,
30}
31
32impl BridgeCall {
33    /// Create a `BridgeCall` from an input buffer received over FFI.
34    pub fn new(input: FfiBuffer) -> Self {
35        BridgeCall { input }
36    }
37
38    /// Execute `handler` with the input buffer and return an [`FfiResult`].
39    ///
40    /// The closure is run inside [`catch_panic`], so panics are safely
41    /// converted to `FfiErrorCode::Panic` results.
42    pub fn run<F>(self, handler: F) -> FfiResult
43    where
44        F: FnOnce(&FfiBuffer) -> Result<FfiBuffer, FfiError> + std::panic::UnwindSafe,
45    {
46        let input = self.input;
47        catch_panic(move || handler(&input))
48    }
49
50    /// Execute `handler` with a JSON-deserialized input value.
51    ///
52    /// Deserializes `I` from the input buffer's JSON content, then calls
53    /// `handler`. The output is serialized back to JSON and returned.
54    pub fn run_json<I, O, F>(self, handler: F) -> FfiResult
55    where
56        I: serde::de::DeserializeOwned + std::panic::UnwindSafe,
57        O: serde::Serialize,
58        F: FnOnce(I) -> Result<O, FfiError> + std::panic::UnwindSafe,
59    {
60        let input = self.input;
61        catch_panic(move || {
62            let value: I = unsafe { input.to_json() }?;
63            let output = handler(value)?;
64            FfiBuffer::from_json(&output)
65        })
66    }
67
68    /// Return the raw input buffer (consumes self).
69    pub fn into_buffer(self) -> FfiBuffer {
70        self.input
71    }
72}
73
74// ─── FFI-exported health check ────────────────────────────────────────────────
75
76/// Echo the input buffer back as the output — useful for round-trip tests.
77///
78/// **Exported as:** `ffi_echo`
79#[no_mangle]
80pub extern "C" fn ffi_echo(input: FfiBuffer) -> FfiResult {
81    BridgeCall::new(input).run(|buf| {
82        // Copy the slice into a new allocation so the input can be freed independently.
83        let bytes = unsafe { buf.as_slice() }.to_vec();
84        Ok(FfiBuffer::from_vec(bytes))
85    })
86}
87
88/// Return a version string buffer.
89///
90/// **Exported as:** `ffi_version`
91#[no_mangle]
92pub extern "C" fn ffi_version() -> FfiBuffer {
93    FfiBuffer::from_vec(env!("CARGO_PKG_VERSION").as_bytes().to_vec())
94}
95
96// ─── Tests ────────────────────────────────────────────────────────────────────
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::errors::ffi_result_free;
102    use crate::memory::ffi_buffer_free;
103
104    #[test]
105    fn bridge_call_run_ok() {
106        let input = FfiBuffer::from_vec(b"ping".to_vec());
107        let result = BridgeCall::new(input).run(|buf| {
108            let bytes = unsafe { buf.as_slice() }.to_vec();
109            assert_eq!(&bytes, b"ping");
110            Ok(FfiBuffer::from_vec(b"pong".to_vec()))
111        });
112        assert!(result.is_ok());
113        let payload = unsafe { result.payload.as_slice() };
114        assert_eq!(payload, b"pong");
115        ffi_result_free(result);
116    }
117
118    #[test]
119    fn bridge_call_run_json() {
120        #[derive(serde::Serialize, serde::Deserialize)]
121        struct Req {
122            n: u32,
123        }
124        #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
125        struct Res {
126            doubled: u32,
127        }
128
129        let input = FfiBuffer::from_json(&Req { n: 21 }).unwrap();
130        let result = BridgeCall::new(input).run_json(|req: Req| Ok(Res { doubled: req.n * 2 }));
131        assert!(result.is_ok());
132        let decoded: Res = unsafe { result.payload.to_json() }.unwrap();
133        assert_eq!(decoded, Res { doubled: 42 });
134        ffi_result_free(result);
135    }
136
137    #[test]
138    fn ffi_echo_round_trip() {
139        let input = FfiBuffer::from_vec(b"hello echo".to_vec());
140        let result = ffi_echo(input);
141        assert!(result.is_ok());
142        let output = unsafe { result.payload.as_slice() };
143        assert_eq!(output, b"hello echo");
144        ffi_result_free(result);
145    }
146
147    #[test]
148    fn ffi_version_returns_version_string() {
149        let buf = ffi_version();
150        let s = unsafe { buf.as_slice() };
151        assert!(!s.is_empty());
152        ffi_buffer_free(buf);
153    }
154}