#[cfg(applefm_bridge)]
mod real {
use std::ffi::{CStr, CString, c_char, c_void};
unsafe extern "C" {
fn afm_availability() -> *mut c_char;
fn afm_session_create(config_json: *const c_char) -> *mut c_char;
fn afm_session_respond(session: u64, request_json: *const c_char) -> *mut c_char;
fn afm_session_respond_stream(
session: u64,
request_json: *const c_char,
on_event: Option<unsafe extern "C" fn(*mut c_void, *const c_char)>,
context: *mut c_void,
);
fn afm_session_free(session: u64);
fn afm_prewarm(session: u64);
fn afm_string_free(ptr: *mut c_char);
}
unsafe fn take_bridge_string(ptr: *mut c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
unsafe {
let s = CStr::from_ptr(ptr).to_string_lossy().into_owned();
afm_string_free(ptr);
Some(s)
}
}
fn null_reply() -> String {
r#"{"error":{"kind":"internal","message":"bridge returned null"}}"#.to_owned()
}
fn nul_byte_reply() -> String {
r#"{"error":{"kind":"decode","message":"request contained a NUL byte"}}"#.to_owned()
}
pub fn availability_json() -> String {
unsafe { take_bridge_string(afm_availability()) }
.unwrap_or_else(|| r#"{"available":false,"reason":"bridge returned null"}"#.to_owned())
}
pub fn session_create(config_json: &str) -> String {
let Ok(config) = CString::new(config_json) else {
return nul_byte_reply();
};
unsafe { take_bridge_string(afm_session_create(config.as_ptr())) }
.unwrap_or_else(null_reply)
}
pub fn session_respond(session: u64, request_json: &str) -> String {
let Ok(request) = CString::new(request_json) else {
return nul_byte_reply();
};
unsafe { take_bridge_string(afm_session_respond(session, request.as_ptr())) }
.unwrap_or_else(null_reply)
}
pub fn session_stream(session: u64, request_json: &str, on_event: impl FnMut(&str) + Send) {
let mut on_event: Box<dyn FnMut(&str) + Send> = Box::new(on_event);
let Ok(request) = CString::new(request_json) else {
on_event(
r#"{"type":"error","error":{"kind":"decode","message":"request contained a NUL byte"}}"#,
);
return;
};
unsafe extern "C" fn trampoline(context: *mut c_void, event: *const c_char) {
if context.is_null() || event.is_null() {
return;
}
unsafe {
let on_event = &mut *context.cast::<Box<dyn FnMut(&str) + Send>>();
let event = CStr::from_ptr(event).to_string_lossy();
on_event(&event);
}
}
let context = (&raw mut on_event).cast::<c_void>();
unsafe { afm_session_respond_stream(session, request.as_ptr(), Some(trampoline), context) }
}
pub fn session_free(session: u64) {
unsafe { afm_session_free(session) }
}
pub fn prewarm(session: u64) {
unsafe { afm_prewarm(session) }
}
}
#[cfg(not(applefm_bridge))]
mod stub {
const STUB_REASON: &str = "chat-applefm was built without the Swift bridge (non-macOS target, docs build, or APPLEFM_SKIP_BRIDGE set)";
pub fn availability_json() -> String {
format!(r#"{{"available":false,"reason":"{STUB_REASON}"}}"#)
}
pub fn session_create(_config_json: &str) -> String {
format!(r#"{{"error":{{"kind":"unavailable","message":"{STUB_REASON}"}}}}"#)
}
pub fn session_respond(_session: u64, _request_json: &str) -> String {
format!(r#"{{"error":{{"kind":"unavailable","message":"{STUB_REASON}"}}}}"#)
}
pub fn session_stream(
_session: u64,
_request_json: &str,
mut on_event: impl FnMut(&str) + Send,
) {
on_event(&format!(
r#"{{"type":"error","error":{{"kind":"unavailable","message":"{STUB_REASON}"}}}}"#
));
}
pub fn session_free(_session: u64) {}
pub fn prewarm(_session: u64) {}
}
#[cfg(applefm_bridge)]
pub(crate) use real::{
availability_json, prewarm, session_create, session_free, session_respond, session_stream,
};
#[cfg(not(applefm_bridge))]
pub(crate) use stub::{
availability_json, prewarm, session_create, session_free, session_respond, session_stream,
};