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 std::ffi::{CStr, CString};
7use std::os::raw::{c_char, c_int};
8use std::slice;
9
10/// Opaque handle to a handler context
11#[repr(C)]
12pub struct HandlerContext {
13    _private: [u8; 0],
14}
15
16/// Result structure for FFI calls
17#[repr(C)]
18pub struct FfiResult {
19    /// 0 = success, non-zero = error code
20    pub code: c_int,
21    /// Pointer to result data (JSON bytes)
22    pub data: *mut u8,
23    /// Length of result data
24    pub data_len: usize,
25    /// Error message (null if success)
26    pub error: *const c_char,
27}
28
29/// Execute a handler by name with JSON input
30///
31/// # Safety
32/// - `handler_name` must be a valid null-terminated string
33/// - `input_json` must be a valid pointer to JSON bytes
34/// - `input_len` must be the correct length of input data
35/// - Caller must free result data with `pforge_free_result`
36#[no_mangle]
37pub unsafe extern "C" fn pforge_execute_handler(
38    handler_name: *const c_char,
39    input_json: *const u8,
40    input_len: usize,
41) -> FfiResult {
42    // Validate inputs
43    if handler_name.is_null() || input_json.is_null() {
44        return FfiResult {
45            code: -1,
46            data: std::ptr::null_mut(),
47            data_len: 0,
48            error: create_error_string("Null pointer provided"),
49        };
50    }
51
52    // Convert handler name
53    let name = match CStr::from_ptr(handler_name).to_str() {
54        Ok(s) => s,
55        Err(_) => {
56            return FfiResult {
57                code: -2,
58                data: std::ptr::null_mut(),
59                data_len: 0,
60                error: create_error_string("Invalid UTF-8 in handler name"),
61            }
62        }
63    };
64
65    // Get input bytes
66    let _input = slice::from_raw_parts(input_json, input_len);
67
68    // TODO: Actually dispatch to handler registry
69    // For now, return a simple echo response
70    let response = serde_json::json!({
71        "handler": name,
72        "input_size": input_len,
73        "status": "ok"
74    });
75
76    match serde_json::to_vec(&response) {
77        Ok(data) => {
78            let mut boxed = data.into_boxed_slice();
79            let data_ptr = boxed.as_mut_ptr();
80            let data_len = boxed.len();
81            // SAFETY: Transfer ownership to C caller. Memory will be freed via pforge_free_result.
82            // This is the correct pattern for FFI memory management.
83            #[allow(clippy::mem_forget)]
84            std::mem::forget(boxed);
85
86            FfiResult {
87                code: 0,
88                data: data_ptr,
89                data_len,
90                error: std::ptr::null(),
91            }
92        }
93        Err(e) => FfiResult {
94            code: -3,
95            data: std::ptr::null_mut(),
96            data_len: 0,
97            error: create_error_string(&format!("Serialization error: {}", e)),
98        },
99    }
100}
101
102/// Free result data allocated by pforge
103///
104/// # Safety
105/// - Must only be called once per FfiResult
106/// - `result` must have been returned from pforge_execute_handler
107#[no_mangle]
108pub unsafe extern "C" fn pforge_free_result(result: FfiResult) {
109    if !result.data.is_null() && result.data_len > 0 {
110        let _ = Vec::from_raw_parts(result.data, result.data_len, result.data_len);
111    }
112    if !result.error.is_null() {
113        let _ = CString::from_raw(result.error as *mut c_char);
114    }
115}
116
117/// Get the pforge version
118///
119/// # Safety
120/// - Returned string is valid for program lifetime
121#[no_mangle]
122pub unsafe extern "C" fn pforge_version() -> *const c_char {
123    static VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
124    VERSION.as_ptr() as *const c_char
125}
126
127// Helper functions
128
129fn create_error_string(msg: &str) -> *const c_char {
130    match CString::new(msg) {
131        Ok(s) => s.into_raw() as *const c_char,
132        Err(_) => std::ptr::null(),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use std::ffi::CString;
140
141    #[test]
142    fn test_version() {
143        unsafe {
144            let version = pforge_version();
145            assert!(!version.is_null());
146            let version_str = CStr::from_ptr(version).to_str().unwrap();
147            assert!(version_str.starts_with("0.1"));
148        }
149    }
150
151    #[test]
152    fn test_execute_handler_null_safety() {
153        unsafe {
154            // Null handler name
155            let result = pforge_execute_handler(std::ptr::null(), std::ptr::null(), 0);
156            assert_eq!(result.code, -1);
157            pforge_free_result(result);
158        }
159    }
160
161    #[test]
162    fn test_execute_handler_success() {
163        unsafe {
164            let handler_name = CString::new("test_handler").unwrap();
165            let input = b"{}";
166
167            let result = pforge_execute_handler(handler_name.as_ptr(), input.as_ptr(), input.len());
168
169            assert_eq!(result.code, 0);
170            assert!(!result.data.is_null());
171            assert!(result.data_len > 0);
172
173            // Parse result
174            let data_slice = slice::from_raw_parts(result.data, result.data_len);
175            let response: serde_json::Value = serde_json::from_slice(data_slice).unwrap();
176            assert_eq!(response["handler"], "test_handler");
177            assert_eq!(response["status"], "ok");
178
179            pforge_free_result(result);
180        }
181    }
182}