use std::ffi::{c_char, CStr, CString};
use super::{bridge_call, BridgeErrorCode, BRIDGE_API_VERSION};
fn respond(json: String) -> *mut c_char {
CString::new(json)
.unwrap_or_else(|_| {
CString::new(format!(
r#"{{"ok":false,"api_version":{BRIDGE_API_VERSION},"error":{{"code":"internal_error","message":"response contained an interior NUL"}}}}"#
))
.expect("fallback envelope contains no NUL")
})
.into_raw()
}
fn error_envelope(code: BridgeErrorCode, message: &str) -> String {
let code = serde_json::to_string(&code).unwrap_or_else(|_| "\"internal_error\"".into());
format!(
r#"{{"ok":false,"api_version":{BRIDGE_API_VERSION},"error":{{"code":{code},"message":"{message}"}}}}"#
)
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_bridge_call(request_json: *const c_char) -> *mut c_char {
if request_json.is_null() {
return respond(error_envelope(
BridgeErrorCode::InvalidJson,
"request pointer is NULL",
));
}
let bytes = CStr::from_ptr(request_json).to_bytes();
call_with_bytes(bytes)
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_bridge_call_n(
request: *const u8,
len: usize,
) -> *mut c_char {
if request.is_null() {
return respond(error_envelope(
BridgeErrorCode::InvalidJson,
"request pointer is NULL",
));
}
let bytes = std::slice::from_raw_parts(request, len);
call_with_bytes(bytes)
}
fn call_with_bytes(bytes: &[u8]) -> *mut c_char {
match std::str::from_utf8(bytes) {
Ok(text) => respond(bridge_call(text)),
Err(_) => respond(error_envelope(
BridgeErrorCode::InvalidJson,
"request is not valid UTF-8",
)),
}
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_bridge_free(response: *mut c_char) {
if !response.is_null() {
drop(CString::from_raw(response));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
fn roundtrip(request: &str) -> serde_json::Value {
let c_request = CString::new(request).unwrap();
let raw = unsafe { ballistics_bridge_call(c_request.as_ptr()) };
assert!(!raw.is_null(), "bridge must never return NULL");
let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
unsafe { ballistics_bridge_free(raw) };
serde_json::from_str(&text).expect("bridge output is JSON")
}
#[test]
fn c_abi_smoke_meta_version() {
let out = roundtrip(r#"{"api_version":1,"command":"meta.version"}"#);
assert_eq!(out["ok"], true);
assert_eq!(out["result"]["engine_version"], env!("CARGO_PKG_VERSION"));
}
#[test]
fn null_request_is_an_envelope() {
let raw = unsafe { ballistics_bridge_call(std::ptr::null()) };
let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
unsafe { ballistics_bridge_free(raw) };
let out: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(out["ok"], false);
assert_eq!(out["error"]["code"], "invalid_json");
}
#[test]
fn length_variant_handles_non_terminated_buffers() {
let payload = br#"{"api_version":1,"command":"meta.version"}"#;
let raw = unsafe { ballistics_bridge_call_n(payload.as_ptr(), payload.len()) };
let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
unsafe { ballistics_bridge_free(raw) };
let out: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(out["ok"], true);
}
#[test]
fn invalid_utf8_is_an_envelope() {
let payload = [0xFFu8, 0xFE, 0x00];
let raw = unsafe { ballistics_bridge_call_n(payload.as_ptr(), payload.len()) };
let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
unsafe { ballistics_bridge_free(raw) };
let out: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(out["error"]["code"], "invalid_json");
}
#[test]
fn free_null_is_a_no_op() {
unsafe { ballistics_bridge_free(std::ptr::null_mut()) };
}
}