ballistics_engine/bridge/
ffi.rs1use std::ffi::{c_char, CStr, CString};
15
16use super::{bridge_call, BridgeErrorCode, BRIDGE_API_VERSION};
17
18fn respond(json: String) -> *mut c_char {
19 CString::new(json)
23 .unwrap_or_else(|_| {
24 CString::new(format!(
25 r#"{{"ok":false,"api_version":{BRIDGE_API_VERSION},"error":{{"code":"internal_error","message":"response contained an interior NUL"}}}}"#
26 ))
27 .expect("fallback envelope contains no NUL")
28 })
29 .into_raw()
30}
31
32fn error_envelope(code: BridgeErrorCode, message: &str) -> String {
33 let code = serde_json::to_string(&code).unwrap_or_else(|_| "\"internal_error\"".into());
36 format!(
37 r#"{{"ok":false,"api_version":{BRIDGE_API_VERSION},"error":{{"code":{code},"message":"{message}"}}}}"#
38 )
39}
40
41#[no_mangle]
47pub unsafe extern "C" fn ballistics_bridge_call(request_json: *const c_char) -> *mut c_char {
48 if request_json.is_null() {
49 return respond(error_envelope(
50 BridgeErrorCode::InvalidJson,
51 "request pointer is NULL",
52 ));
53 }
54 let bytes = CStr::from_ptr(request_json).to_bytes();
55 call_with_bytes(bytes)
56}
57
58#[no_mangle]
64pub unsafe extern "C" fn ballistics_bridge_call_n(
65 request: *const u8,
66 len: usize,
67) -> *mut c_char {
68 if request.is_null() {
69 return respond(error_envelope(
70 BridgeErrorCode::InvalidJson,
71 "request pointer is NULL",
72 ));
73 }
74 let bytes = std::slice::from_raw_parts(request, len);
75 call_with_bytes(bytes)
76}
77
78fn call_with_bytes(bytes: &[u8]) -> *mut c_char {
79 match std::str::from_utf8(bytes) {
80 Ok(text) => respond(bridge_call(text)),
81 Err(_) => respond(error_envelope(
82 BridgeErrorCode::InvalidJson,
83 "request is not valid UTF-8",
84 )),
85 }
86}
87
88#[no_mangle]
95pub unsafe extern "C" fn ballistics_bridge_free(response: *mut c_char) {
96 if !response.is_null() {
97 drop(CString::from_raw(response));
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use std::ffi::CString;
105
106 fn roundtrip(request: &str) -> serde_json::Value {
107 let c_request = CString::new(request).unwrap();
108 let raw = unsafe { ballistics_bridge_call(c_request.as_ptr()) };
109 assert!(!raw.is_null(), "bridge must never return NULL");
110 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
111 unsafe { ballistics_bridge_free(raw) };
112 serde_json::from_str(&text).expect("bridge output is JSON")
113 }
114
115 #[test]
116 fn c_abi_smoke_meta_version() {
117 let out = roundtrip(r#"{"api_version":1,"command":"meta.version"}"#);
118 assert_eq!(out["ok"], true);
119 assert_eq!(out["result"]["engine_version"], env!("CARGO_PKG_VERSION"));
120 }
121
122 #[test]
123 fn null_request_is_an_envelope() {
124 let raw = unsafe { ballistics_bridge_call(std::ptr::null()) };
125 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
126 unsafe { ballistics_bridge_free(raw) };
127 let out: serde_json::Value = serde_json::from_str(&text).unwrap();
128 assert_eq!(out["ok"], false);
129 assert_eq!(out["error"]["code"], "invalid_json");
130 }
131
132 #[test]
133 fn length_variant_handles_non_terminated_buffers() {
134 let payload = br#"{"api_version":1,"command":"meta.version"}"#;
135 let raw = unsafe { ballistics_bridge_call_n(payload.as_ptr(), payload.len()) };
136 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
137 unsafe { ballistics_bridge_free(raw) };
138 let out: serde_json::Value = serde_json::from_str(&text).unwrap();
139 assert_eq!(out["ok"], true);
140 }
141
142 #[test]
143 fn invalid_utf8_is_an_envelope() {
144 let payload = [0xFFu8, 0xFE, 0x00];
145 let raw = unsafe { ballistics_bridge_call_n(payload.as_ptr(), payload.len()) };
146 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
147 unsafe { ballistics_bridge_free(raw) };
148 let out: serde_json::Value = serde_json::from_str(&text).unwrap();
149 assert_eq!(out["error"]["code"], "invalid_json");
150 }
151
152 #[test]
153 fn free_null_is_a_no_op() {
154 unsafe { ballistics_bridge_free(std::ptr::null_mut()) };
155 }
156}