#![allow(clippy::missing_safety_doc)]
use crate::engine::{CancelHandle, Delta, Engine, EngineOptions, Flow, Reply, SamplingParams, StopReason, SystemPrompt};
use anyhow::{Context, Result, anyhow, bail};
use std::cell::RefCell;
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Mutex;
pub type OxiTokenFn = Option<unsafe extern "C" fn(user: *mut c_void, text: *const c_char, len: usize, thinking: c_int) -> c_int>;
pub type OxiProgressFn =
Option<unsafe extern "C" fn(user: *mut c_void, file: *const c_char, done: u64, total: u64, index: c_int, count: c_int) -> c_int>;
pub struct OxiEngine {
cancel: CancelHandle,
engine: Mutex<Engine>,
}
thread_local! {
static LAST_ERROR: RefCell<CString> = RefCell::new(CString::default());
}
fn set_error(msg: String) {
let c = CString::new(msg.replace('\0', " ")).unwrap_or_default();
LAST_ERROR.with(|l| *l.borrow_mut() = c);
}
fn guard<T>(err: T, f: impl FnOnce() -> Result<T>) -> T {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(v)) => v,
Ok(Err(e)) => {
set_error(format!("{e:#}"));
err
}
Err(p) => {
let msg = p
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| p.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".into());
set_error(format!("internal error (panic): {msg}"));
err
}
}
}
unsafe fn str_arg<'a>(p: *const c_char, what: &str) -> Result<&'a str> {
if p.is_null() {
bail!("{what} is NULL");
}
unsafe { CStr::from_ptr(p) }
.to_str()
.with_context(|| format!("{what} is not UTF-8"))
}
unsafe fn opt_str_arg<'a>(p: *const c_char, what: &str) -> Result<Option<&'a str>> {
if p.is_null() {
Ok(None)
} else {
unsafe { str_arg(p, what) }.map(Some)
}
}
fn out_string(s: String) -> *mut c_char {
CString::new(s.replace('\0', " ")).unwrap_or_default().into_raw()
}
unsafe fn engine<'a>(e: *const OxiEngine) -> Result<&'a OxiEngine> {
unsafe { e.as_ref() }.context("engine is NULL")
}
#[unsafe(no_mangle)]
pub extern "C" fn oxi_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
}
#[unsafe(no_mangle)]
pub extern "C" fn oxi_last_error() -> *const c_char {
LAST_ERROR.with(|l| l.borrow().as_ptr())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_string_free(s: *mut c_char) {
if !s.is_null() {
drop(unsafe { CString::from_raw(s) });
}
}
fn parse_options(text: Option<&str>) -> Result<EngineOptions> {
let mut o = EngineOptions::default();
for item in text.unwrap_or("").split(['\n', ';']) {
let item = item.trim();
if item.is_empty() || item.starts_with('#') {
continue;
}
let (k, v) = item.split_once('=').with_context(|| format!("option {item:?} is not key=value"))?;
o.set(k.trim(), v.trim())?;
}
Ok(o)
}
fn with_progress<T>(progress: OxiProgressFn, user: *mut c_void, f: impl FnOnce() -> Result<T>) -> Result<T> {
#[cfg(feature = "hf")]
if let Some(cb) = progress {
let mut hook = |p: &crate::hf::Progress| -> bool {
let file = CString::new(p.file).unwrap_or_default();
let r = unsafe {
cb(
user,
file.as_ptr(),
p.done,
p.total.unwrap_or(0),
p.index as c_int,
p.count as c_int,
)
};
r == 0
};
return crate::hf::with_progress(&mut hook, f);
}
let _ = (progress, user);
f()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_load(
model: *const c_char,
options: *const c_char,
progress: OxiProgressFn,
user: *mut c_void,
) -> *mut OxiEngine {
guard(std::ptr::null_mut(), || {
let model = unsafe { str_arg(model, "model") }?;
let opts = parse_options(unsafe { opt_str_arg(options, "options") }?)?;
let e = with_progress(progress, user, || Engine::load(model, &opts))?;
Ok(Box::into_raw(Box::new(OxiEngine {
cancel: e.cancel_handle(),
engine: Mutex::new(e),
})))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_download(
spec: *const c_char,
cache_dir: *const c_char,
progress: OxiProgressFn,
user: *mut c_void,
) -> *mut c_char {
guard(std::ptr::null_mut(), || {
let spec = unsafe { str_arg(spec, "spec") }?;
let cache = unsafe { opt_str_arg(cache_dir, "cache_dir") }?.map(std::path::PathBuf::from);
let p = with_progress(progress, user, || crate::engine::resolve_model(spec, cache.as_deref()))?;
Ok(out_string(p.to_string_lossy().into_owned()))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_free(e: *mut OxiEngine) {
if !e.is_null() {
guard((), || {
drop(unsafe { Box::from_raw(e) });
Ok(())
})
}
}
fn lock(e: &OxiEngine) -> std::sync::MutexGuard<'_, Engine> {
e.engine.lock().unwrap_or_else(|p| p.into_inner())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_set(e: *mut OxiEngine, key: *const c_char, value: *const c_char) -> c_int {
guard(-1, || {
let e = unsafe { engine(e) }?;
let key = unsafe { str_arg(key, "key") }?;
let value = unsafe { opt_str_arg(value, "value") }?;
let mut g = lock(e);
let v = value.unwrap_or("");
let num = |what: &str| -> Result<f64> { v.parse().map_err(|_| anyhow!("{key}: expected {what}, got {v:?}")) };
let mut s: SamplingParams = g.sampling();
match key {
"system" => {
g.config_mut().system = match value {
None => SystemPrompt::Default,
Some("") => SystemPrompt::None,
Some(t) => SystemPrompt::Text(t.to_string()),
}
}
"think" => g.config_mut().think = num("0 or 1")? != 0.0,
"max_tokens" => g.config_mut().max_tokens = num("a count")? as usize,
"temperature" => s.temperature = num("a number")? as f32,
"top_k" => s.top_k = num("a count")? as usize,
"top_p" => s.top_p = num("a number")? as f32,
"seed" => s.seed = v.parse().map_err(|_| anyhow!("seed: expected an integer, got {v:?}"))?,
_ => bail!("unknown setting {key:?} (system, think, max_tokens, temperature, top_k, top_p, seed)"),
}
if s != g.sampling() {
g.set_sampling(s);
}
Ok(0)
})
}
fn finish_code(r: &Reply) -> c_int {
match r.finish {
StopReason::Stop => 0,
StopReason::Length => 1,
StopReason::Cancelled => 2,
}
}
fn run(
e: *mut OxiEngine,
text: *const c_char,
cb: OxiTokenFn,
user: *mut c_void,
f: impl FnOnce(&mut Engine, &str, &mut dyn FnMut(&Delta) -> Flow) -> Result<Reply>,
) -> c_int {
guard(-1, || {
let e = unsafe { engine(e) }?;
let text = unsafe { str_arg(text, "text") }?;
let mut g = lock(e);
let mut on_delta = |d: &Delta| -> Flow {
match cb {
Some(cb) if !d.text.is_empty() => {
let r = unsafe { cb(user, d.text.as_ptr() as *const c_char, d.text.len(), d.thinking as c_int) };
if r != 0 { Flow::Stop } else { Flow::Continue }
}
_ => Flow::Continue,
}
};
let reply = f(&mut g, text, &mut on_delta)?;
Ok(finish_code(&reply))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_send(e: *mut OxiEngine, text: *const c_char, cb: OxiTokenFn, user: *mut c_void) -> c_int {
run(e, text, cb, user, |g, t, d| g.send(t, d))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_complete(e: *mut OxiEngine, text: *const c_char, cb: OxiTokenFn, user: *mut c_void) -> c_int {
run(e, text, cb, user, |g, t, d| g.complete(t, d))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_reset(e: *mut OxiEngine) -> c_int {
guard(-1, || {
lock(unsafe { engine(e) }?).reset();
Ok(0)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_cancel(e: *const OxiEngine) {
if let Some(e) = unsafe { e.as_ref() } {
e.cancel.cancel();
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_info_json(e: *mut OxiEngine) -> *mut c_char {
guard(std::ptr::null_mut(), || {
let g = lock(unsafe { engine(e) }?);
let i = g.info();
let v = serde_json::json!({
"path": i.path.to_string_lossy(),
"model_type": i.model_type,
"family": format!("{:?}", i.family),
"layers": i.layers,
"hidden_size": i.hidden_size,
"vocab_size": i.vocab_size,
"quant": i.quant.as_str(),
"format": i.format,
"checkpoint_bytes": i.checkpoint_bytes,
"streamed_bytes": i.streamed_bytes,
"device": i.device,
"threads": i.threads,
"mmap": i.mmap,
"speculative": format!("{:?}", i.speculative),
"load_secs": i.load_secs,
"coreml": i.coreml.as_ref().map(|c| serde_json::json!({
"units": c.units.as_str(),
"prefill": c.prefill.as_str(),
"architecture": c.architecture,
"context_length": c.context_length,
"batch_size": c.batch_size,
"chunks": c.chunks,
"recurrent": c.recurrent,
})),
});
Ok(out_string(v.to_string()))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxi_engine_stats_json(e: *mut OxiEngine) -> *mut c_char {
guard(std::ptr::null_mut(), || {
let g = lock(unsafe { engine(e) }?);
let s = g.stats();
let v = serde_json::json!({
"prompt_tokens": s.prompt_tokens,
"prefill_secs": s.prefill_secs,
"prefill_tok_s": s.prefill_tok_s(),
"generated_tokens": s.generated_tokens,
"decode_secs": s.decode_secs,
"decode_tok_s": s.decode_tok_s(),
"forwards": s.forwards,
"drafted": s.drafted,
"accepted": s.accepted,
"acceptance": s.acceptance(),
"context_tokens": s.context_tokens,
});
Ok(out_string(v.to_string()))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn oxi_devices() -> *mut c_char {
guard(std::ptr::null_mut(), || Ok(out_string(crate::platform::detect().report())))
}