silicera_runtime/ffi.rs
1//! Experimental C ABI surface for embedding Silicera-runtime in C/C++ hosts.
2//!
3//! Enabled with feature `c-abi`. All `unsafe` is isolated here. Rust callers
4//! should prefer the safe `Dispatcher` API.
5//!
6//! # Symbols
7//!
8//! | Symbol | Role |
9//! |--------|------|
10//! | `silicera_init` | Process/library init (idempotent) |
11//! | `silicera_profile_load` | Load `.hnep` into an opaque handle |
12//! | `silicera_variant_select` | Size → variant name (NUL-terminated into caller buffer) |
13//! | `silicera_profile_free` | Release handle |
14//!
15//! Status codes: `0` ok, negative = error (`-1` null, `-2` I/O/parse, `-3` buffer).
16
17use std::ffi::{CStr, CString};
18use std::os::raw::{c_char, c_int, c_ulonglong};
19use std::path::Path;
20use std::ptr;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::Mutex;
23
24use crate::{Dispatcher, MismatchPolicy};
25
26static INIT: AtomicBool = AtomicBool::new(false);
27
28struct ProfileHandle {
29 dispatcher: Dispatcher,
30}
31
32/// Opaque profile handle for C callers.
33pub struct SiliceraProfile {
34 inner: Mutex<ProfileHandle>,
35}
36
37/// Initialize the Silicera C ABI (idempotent). Returns `0`.
38#[no_mangle]
39pub extern "C" fn silicera_init() -> c_int {
40 INIT.store(true, Ordering::SeqCst);
41 0
42}
43
44/// Load an HNEP profile from a UTF-8 path.
45///
46/// On success writes a non-null `*out_profile` that must be freed with
47/// [`silicera_profile_free`]. `strict_machine != 0` uses [`MismatchPolicy::StrictMachine`].
48///
49/// # Safety
50///
51/// `path` must be a valid NUL-terminated UTF-8 C string (or null → `-1`).
52/// `out_profile` must be a valid writable pointer.
53#[no_mangle]
54pub unsafe extern "C" fn silicera_profile_load(
55 path: *const c_char,
56 strict_machine: c_int,
57 out_profile: *mut *mut SiliceraProfile,
58) -> c_int {
59 if path.is_null() || out_profile.is_null() {
60 return -1;
61 }
62 // SAFETY: caller guarantees NUL-terminated C string.
63 let cstr = unsafe { CStr::from_ptr(path) };
64 let path_str = match cstr.to_str() {
65 Ok(s) => s,
66 Err(_) => return -2,
67 };
68 let policy = if strict_machine != 0 {
69 MismatchPolicy::StrictMachine
70 } else {
71 MismatchPolicy::FallbackBaseline
72 };
73 let dispatcher = match Dispatcher::open(Path::new(path_str), policy) {
74 Ok(d) => d,
75 Err(_) => return -2,
76 };
77 let boxed = Box::new(SiliceraProfile {
78 inner: Mutex::new(ProfileHandle { dispatcher }),
79 });
80 // SAFETY: caller guarantees out_profile is writable.
81 unsafe {
82 *out_profile = Box::into_raw(boxed);
83 }
84 0
85}
86
87/// Select a variant name for `size_bytes` into `buf` (NUL-terminated).
88///
89/// Writes at most `buf_len` bytes including the trailing NUL.
90///
91/// # Safety
92///
93/// `profile` must be a handle from [`silicera_profile_load`] (or null → `-1`).
94/// `buf` must point to a writable buffer of `buf_len` bytes when `buf_len > 0`.
95#[no_mangle]
96pub unsafe extern "C" fn silicera_variant_select(
97 profile: *mut SiliceraProfile,
98 size_bytes: c_ulonglong,
99 buf: *mut c_char,
100 buf_len: usize,
101) -> c_int {
102 if profile.is_null() || buf.is_null() || buf_len == 0 {
103 return -1;
104 }
105 // SAFETY: handle from silicera_profile_load; not freed yet.
106 let handle = unsafe { &*profile };
107 let name = {
108 let g = match handle.inner.lock() {
109 Ok(g) => g,
110 Err(_) => return -2,
111 };
112 g.dispatcher.size(size_bytes).to_string()
113 };
114 let cstr = match CString::new(name) {
115 Ok(c) => c,
116 Err(_) => return -2,
117 };
118 let bytes = cstr.as_bytes_with_nul();
119 if bytes.len() > buf_len {
120 return -3;
121 }
122 // SAFETY: buf has buf_len writable bytes.
123 unsafe {
124 ptr::copy_nonoverlapping(bytes.as_ptr() as *const c_char, buf, bytes.len());
125 }
126 0
127}
128
129/// Free a profile handle from [`silicera_profile_load`].
130///
131/// # Safety
132///
133/// `profile` must be null or a unique handle from `silicera_profile_load`.
134/// Double-free is undefined.
135#[no_mangle]
136pub unsafe extern "C" fn silicera_profile_free(profile: *mut SiliceraProfile) {
137 if profile.is_null() {
138 return;
139 }
140 // SAFETY: unique ownership from Box::into_raw in load.
141 unsafe {
142 drop(Box::from_raw(profile));
143 }
144}