pforge_bridge/
lib.rs

1//! Language bridge FFI for pforge
2//!
3//! This crate provides a stable C ABI for calling Rust handlers from other languages.
4//! It enables zero-copy parameter passing and preserves type safety across language boundaries.
5
6use once_cell::sync::OnceCell;
7use pforge_runtime::HandlerRegistry;
8use std::ffi::{CStr, CString};
9use std::os::raw::{c_char, c_int};
10use std::slice;
11use std::sync::Arc;
12use tokio::runtime::Runtime;
13use tokio::sync::RwLock;
14
15/// Global handler registry for FFI access
16static GLOBAL_REGISTRY: OnceCell<Arc<RwLock<HandlerRegistry>>> = OnceCell::new();
17
18/// Global tokio runtime for async operations
19static RUNTIME: OnceCell<Runtime> = OnceCell::new();
20
21/// Initialize the runtime (called once)
22fn get_runtime() -> &'static Runtime {
23    RUNTIME.get_or_init(|| Runtime::new().expect("Failed to create tokio runtime"))
24}
25
26/// Opaque handle to a handler context
27#[repr(C)]
28pub struct HandlerContext {
29    _private: [u8; 0],
30}
31
32/// Result structure for FFI calls
33#[repr(C)]
34pub struct FfiResult {
35    /// 0 = success, non-zero = error code
36    pub code: c_int,
37    /// Pointer to result data (JSON bytes)
38    pub data: *mut u8,
39    /// Length of result data
40    pub data_len: usize,
41    /// Error message (null if success)
42    pub error: *const c_char,
43}
44
45/// Initialize the global handler registry
46///
47/// # Safety
48/// Must be called before any handler dispatch operations.
49/// Can only be called once.
50#[no_mangle]
51pub unsafe extern "C" fn pforge_init() -> c_int {
52    if GLOBAL_REGISTRY.get().is_some() {
53        return -1; // Already initialized
54    }
55
56    let registry = Arc::new(RwLock::new(HandlerRegistry::new()));
57    if GLOBAL_REGISTRY.set(registry).is_err() {
58        return -2; // Race condition during init
59    }
60
61    0 // Success
62}
63
64/// Register a native handler with the global registry
65///
66/// # Safety
67/// - `name` must be a valid null-terminated string
68/// - `pforge_init` must have been called first
69#[no_mangle]
70pub unsafe extern "C" fn pforge_register_handler(
71    name: *const c_char,
72    _handler_ptr: *mut std::ffi::c_void,
73) -> c_int {
74    if name.is_null() {
75        return -1;
76    }
77
78    let registry = match GLOBAL_REGISTRY.get() {
79        Some(r) => r,
80        None => return -2, // Not initialized
81    };
82
83    // SAFETY: Caller guarantees name is a valid null-terminated string
84    let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
85        Ok(s) => s,
86        Err(_) => return -3, // Invalid UTF-8
87    };
88
89    // For now, just verify we can access the registry
90    // Full handler registration requires more complex FFI patterns
91    let rt = get_runtime();
92    let _ = rt.block_on(async { registry.read().await });
93
94    eprintln!("Handler '{}' registered via FFI", name_str);
95    0
96}
97
98/// Execute a handler by name with JSON input
99///
100/// # Safety
101/// - `handler_name` must be a valid null-terminated string
102/// - `input_json` must be a valid pointer to JSON bytes
103/// - `input_len` must be the correct length of input data
104/// - Caller must free result data with `pforge_free_result`
105#[no_mangle]
106pub unsafe extern "C" fn pforge_execute_handler(
107    handler_name: *const c_char,
108    input_json: *const u8,
109    input_len: usize,
110) -> FfiResult {
111    // Validate inputs
112    if handler_name.is_null() || input_json.is_null() {
113        return FfiResult {
114            code: -1,
115            data: std::ptr::null_mut(),
116            data_len: 0,
117            error: create_error_string("Null pointer provided"),
118        };
119    }
120
121    // Convert handler name
122    // SAFETY: Caller guarantees handler_name is a valid null-terminated string
123    let name = match unsafe { CStr::from_ptr(handler_name) }.to_str() {
124        Ok(s) => s,
125        Err(_) => {
126            return FfiResult {
127                code: -2,
128                data: std::ptr::null_mut(),
129                data_len: 0,
130                error: create_error_string("Invalid UTF-8 in handler name"),
131            }
132        }
133    };
134
135    // Get input bytes
136    // SAFETY: Caller guarantees input_json points to input_len valid bytes
137    let input = unsafe { slice::from_raw_parts(input_json, input_len) };
138
139    // Try to dispatch through global registry if available
140    if let Some(registry) = GLOBAL_REGISTRY.get() {
141        let rt = get_runtime();
142        let result = rt.block_on(async {
143            let reg = registry.read().await;
144            reg.dispatch(name, input).await
145        });
146
147        match result {
148            Ok(output) => {
149                let mut boxed = output.into_boxed_slice();
150                let data_ptr = boxed.as_mut_ptr();
151                let data_len = boxed.len();
152                // SAFETY: Transfer ownership to C caller
153                #[allow(clippy::mem_forget)]
154                std::mem::forget(boxed);
155
156                return FfiResult {
157                    code: 0,
158                    data: data_ptr,
159                    data_len,
160                    error: std::ptr::null(),
161                };
162            }
163            Err(e) => {
164                // Check if it's a "not found" error - use fallback in that case
165                let err_str = e.to_string();
166                if err_str.contains("not found") || err_str.contains("ToolNotFound") {
167                    // Fall through to echo fallback
168                } else {
169                    return FfiResult {
170                        code: -4,
171                        data: std::ptr::null_mut(),
172                        data_len: 0,
173                        error: create_error_string(&format!("Handler error: {}", e)),
174                    };
175                }
176            }
177        }
178    }
179
180    // Fallback: Return echo response if no registry available
181    let response = serde_json::json!({
182        "handler": name,
183        "input_size": input_len,
184        "status": "ok",
185        "note": "No global registry - using echo fallback"
186    });
187
188    match serde_json::to_vec(&response) {
189        Ok(data) => {
190            let mut boxed = data.into_boxed_slice();
191            let data_ptr = boxed.as_mut_ptr();
192            let data_len = boxed.len();
193            // SAFETY: Transfer ownership to C caller. Memory will be freed via pforge_free_result.
194            #[allow(clippy::mem_forget)]
195            std::mem::forget(boxed);
196
197            FfiResult {
198                code: 0,
199                data: data_ptr,
200                data_len,
201                error: std::ptr::null(),
202            }
203        }
204        Err(e) => FfiResult {
205            code: -3,
206            data: std::ptr::null_mut(),
207            data_len: 0,
208            error: create_error_string(&format!("Serialization error: {}", e)),
209        },
210    }
211}
212
213/// Free result data allocated by pforge
214///
215/// # Safety
216/// - Must only be called once per FfiResult
217/// - `result` must have been returned from pforge_execute_handler
218#[no_mangle]
219pub unsafe extern "C" fn pforge_free_result(result: FfiResult) {
220    if !result.data.is_null() && result.data_len > 0 {
221        // SAFETY: result.data was allocated via Vec::into_boxed_slice() with capacity = len
222        let _ = unsafe { Vec::from_raw_parts(result.data, result.data_len, result.data_len) };
223    }
224    if !result.error.is_null() {
225        // SAFETY: result.error was allocated via CString::into_raw()
226        let _ = unsafe { CString::from_raw(result.error as *mut c_char) };
227    }
228}
229
230/// Get the pforge version
231///
232/// # Safety
233/// - Returned string is valid for program lifetime
234#[no_mangle]
235pub unsafe extern "C" fn pforge_version() -> *const c_char {
236    static VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
237    VERSION.as_ptr() as *const c_char
238}
239
240/// Check if the global registry is initialized
241///
242/// # Safety
243/// - Thread-safe, can be called at any time
244#[no_mangle]
245pub extern "C" fn pforge_is_initialized() -> c_int {
246    if GLOBAL_REGISTRY.get().is_some() {
247        1
248    } else {
249        0
250    }
251}
252
253// Helper functions
254
255fn create_error_string(msg: &str) -> *const c_char {
256    match CString::new(msg) {
257        Ok(s) => s.into_raw() as *const c_char,
258        Err(_) => std::ptr::null(),
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use std::ffi::CString;
266
267    #[test]
268    fn test_version() {
269        unsafe {
270            let version = pforge_version();
271            assert!(!version.is_null());
272            let version_str = CStr::from_ptr(version).to_str().unwrap();
273            assert!(version_str.starts_with("0.1"));
274        }
275    }
276
277    #[test]
278    fn test_init() {
279        // Note: This test may fail if run after other tests that initialize
280        // the global registry. In practice, init should only be called once.
281        let result = unsafe { pforge_init() };
282        // Either succeeds (0) or already initialized (-1)
283        assert!(result == 0 || result == -1);
284    }
285
286    #[test]
287    fn test_is_initialized() {
288        // After init runs in other tests, registry should be initialized
289        // Verify the value is exactly 0 or 1 (not just any non-zero)
290        let status = pforge_is_initialized();
291        assert!(
292            status == 0 || status == 1,
293            "Expected 0 or 1, got {}",
294            status
295        );
296
297        // If we've initialized, verify it returns 1 specifically
298        if GLOBAL_REGISTRY.get().is_some() {
299            assert_eq!(
300                pforge_is_initialized(),
301                1,
302                "Should return 1 when initialized"
303            );
304        }
305    }
306
307    #[test]
308    fn test_is_initialized_returns_one_after_init() {
309        // First ensure init is called
310        let _ = unsafe { pforge_init() };
311        // After init, must return exactly 1
312        assert_eq!(pforge_is_initialized(), 1);
313    }
314
315    #[test]
316    fn test_create_error_string() {
317        let msg = "Test error message";
318        let ptr = create_error_string(msg);
319        assert!(
320            !ptr.is_null(),
321            "create_error_string should return non-null pointer"
322        );
323
324        // Verify the string content
325        unsafe {
326            let c_str = CStr::from_ptr(ptr);
327            let str_slice = c_str.to_str().unwrap();
328            assert_eq!(str_slice, msg);
329            // Clean up
330            let _ = CString::from_raw(ptr as *mut c_char);
331        }
332    }
333
334    #[test]
335    fn test_create_error_string_with_null_byte() {
336        // String with embedded null byte should return null pointer
337        let msg = "Error\0with null";
338        let ptr = create_error_string(msg);
339        assert!(
340            ptr.is_null(),
341            "Should return null for string with embedded null byte"
342        );
343    }
344
345    #[test]
346    fn test_execute_handler_null_safety() {
347        unsafe {
348            // Null handler name
349            let result = pforge_execute_handler(std::ptr::null(), std::ptr::null(), 0);
350            assert_eq!(result.code, -1);
351            pforge_free_result(result);
352        }
353    }
354
355    #[test]
356    fn test_execute_handler_fallback() {
357        unsafe {
358            let handler_name = CString::new("test_handler").unwrap();
359            let input = b"{}";
360
361            let result = pforge_execute_handler(handler_name.as_ptr(), input.as_ptr(), input.len());
362
363            // Should succeed with fallback response
364            assert_eq!(result.code, 0);
365            assert!(!result.data.is_null());
366            assert!(result.data_len > 0);
367
368            // Parse result
369            let data_slice = slice::from_raw_parts(result.data, result.data_len);
370            let response: serde_json::Value = serde_json::from_slice(data_slice).unwrap();
371            assert_eq!(response["handler"], "test_handler");
372            assert_eq!(response["status"], "ok");
373
374            pforge_free_result(result);
375        }
376    }
377
378    #[test]
379    fn test_register_handler_null_name() {
380        unsafe {
381            let result = pforge_register_handler(std::ptr::null(), std::ptr::null_mut());
382            assert_eq!(result, -1);
383        }
384    }
385}