use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::ptr;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::InferenceError;
const AVAILABILITY_CACHE_TTL: Duration = Duration::from_secs(5);
#[cfg(car_fm_swift_built)]
extern "C" {
fn car_fm_is_available() -> c_int;
fn car_fm_free_string(ptr: *mut c_char);
fn car_fm_generate(
prompt: *const c_char,
instructions: *const c_char,
max_tokens: i32,
temperature: f64,
out_text: *mut *mut c_char,
out_err: *mut *mut c_char,
) -> c_int;
fn car_fm_generate_stream(
prompt: *const c_char,
instructions: *const c_char,
max_tokens: i32,
temperature: f64,
callback: extern "C" fn(token: *const c_char, state: *mut c_void) -> c_int,
state: *mut c_void,
out_err: *mut *mut c_char,
) -> c_int;
}
#[cfg(not(car_fm_swift_built))]
mod swift_stubs {
use super::{c_char, c_int, c_void};
pub(super) unsafe fn car_fm_is_available() -> c_int {
0
}
pub(super) unsafe fn car_fm_free_string(_ptr: *mut c_char) {}
pub(super) unsafe fn car_fm_generate(
_prompt: *const c_char,
_instructions: *const c_char,
_max_tokens: i32,
_temperature: f64,
_out_text: *mut *mut c_char,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_generate called without the Swift bridge")
}
pub(super) unsafe fn car_fm_generate_stream(
_prompt: *const c_char,
_instructions: *const c_char,
_max_tokens: i32,
_temperature: f64,
_callback: extern "C" fn(token: *const c_char, state: *mut c_void) -> c_int,
_state: *mut c_void,
_out_err: *mut *mut c_char,
) -> c_int {
unreachable!("car_fm_generate_stream called without the Swift bridge")
}
}
#[cfg(not(car_fm_swift_built))]
use swift_stubs::{
car_fm_free_string, car_fm_generate, car_fm_generate_stream, car_fm_is_available,
};
pub fn is_available() -> bool {
static CACHE: Mutex<Option<(Instant, bool)>> = Mutex::new(None);
let now = Instant::now();
let mut guard = match CACHE.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if let Some((stamped, value)) = *guard {
if now.duration_since(stamped) < AVAILABILITY_CACHE_TTL {
return value;
}
}
let value = unsafe { car_fm_is_available() != 0 };
*guard = Some((now, value));
value
}
pub fn generate(
prompt: &str,
instructions: Option<&str>,
max_tokens: u32,
temperature: f32,
) -> Result<String, InferenceError> {
if !is_available() {
return Err(unavailable_error());
}
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instr_c = match instructions {
Some(s) if !s.is_empty() => Some(CString::new(s).map_err(|e| {
InferenceError::InferenceFailed(format!("instructions have interior NUL: {e}"))
})?),
_ => None,
};
let mut out_text: *mut c_char = ptr::null_mut();
let mut out_err: *mut c_char = ptr::null_mut();
let rc = unsafe {
car_fm_generate(
prompt_c.as_ptr(),
instr_c.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
max_tokens.min(i32::MAX as u32) as i32,
temperature as f64,
&mut out_text as *mut *mut c_char,
&mut out_err as *mut *mut c_char,
)
};
if rc != 0 {
return Err(InferenceError::InferenceFailed(consume_swift_string(
out_err,
)));
}
Ok(consume_swift_string(out_text))
}
pub struct StreamCallback<'a> {
on_delta: Box<dyn FnMut(&str) -> bool + Send + 'a>,
}
impl<'a> StreamCallback<'a> {
pub fn new<F>(on_delta: F) -> Self
where
F: FnMut(&str) -> bool + Send + 'a,
{
Self {
on_delta: Box::new(on_delta),
}
}
}
extern "C" fn stream_trampoline(token: *const c_char, state: *mut c_void) -> c_int {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if state.is_null() {
return 1;
}
let cb = unsafe { &mut *(state as *mut StreamCallback) };
let s = if token.is_null() {
""
} else {
match unsafe { CStr::from_ptr(token) }.to_str() {
Ok(s) => s,
Err(_) => return 1,
}
};
if (cb.on_delta)(s) {
0 } else {
1 }
}));
match result {
Ok(rc) => rc,
Err(_) => 1, }
}
pub fn stream(
prompt: &str,
instructions: Option<&str>,
max_tokens: u32,
temperature: f32,
mut callback: StreamCallback<'_>,
) -> Result<(), InferenceError> {
if !is_available() {
return Err(unavailable_error());
}
let prompt_c = CString::new(prompt)
.map_err(|e| InferenceError::InferenceFailed(format!("prompt has interior NUL: {e}")))?;
let instr_c = match instructions {
Some(s) if !s.is_empty() => Some(CString::new(s).map_err(|e| {
InferenceError::InferenceFailed(format!("instructions have interior NUL: {e}"))
})?),
_ => None,
};
let mut out_err: *mut c_char = ptr::null_mut();
let state: *mut c_void = &mut callback as *mut StreamCallback as *mut c_void;
let rc = unsafe {
car_fm_generate_stream(
prompt_c.as_ptr(),
instr_c.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
max_tokens.min(i32::MAX as u32) as i32,
temperature as f64,
stream_trampoline,
state,
&mut out_err as *mut *mut c_char,
)
};
if rc != 0 {
return Err(InferenceError::InferenceFailed(consume_swift_string(
out_err,
)));
}
Ok(())
}
fn consume_swift_string(ptr: *mut c_char) -> String {
if ptr.is_null() {
return String::new();
}
let s = unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
unsafe { car_fm_free_string(ptr) };
s
}
fn unavailable_error() -> InferenceError {
InferenceError::UnsupportedMode {
mode: "apple-foundation-models",
backend: "foundation-models",
reason: "FoundationModels framework reports unavailable on this host. Requires macOS 26+ \
on Apple Silicon with Apple Intelligence enabled. Falling through to the next \
router candidate.",
}
}