Skip to main content

soil_rt/
capi.rs

1//! The C ABI (impl plan §4 step 8). Verbatim rules: no global state
2//! (`SoilRuntime*` is the instance); explicit init/teardown; no
3//! unwinding across the boundary — every entry point is wrapped in
4//! `catch_unwind`, and a caught panic becomes a `SoilError` of kind
5//! `Internal`.
6//!
7//! Threading: a runtime and every handle created under it belong to one
8//! thread (non-atomic refcounts by resolved decision). Parallel hosts
9//! run one runtime per thread.
10//!
11//! Ownership: constructors return owned handles; every input handle is
12//! *borrowed* (internally refcount-bumped as needed), so the caller
13//! frees exactly what it was returned — values with `soil_value_free`,
14//! strings with `soil_string_free`, errors with `soil_error_free`.
15//! Closures are not constructible over this ABI yet (`soil_closure_new`
16//! arrives with compiled code, plan 05).
17
18use std::ffi::{c_char, c_int, CStr, CString};
19use std::panic::{catch_unwind, AssertUnwindSafe};
20use std::ptr;
21
22use crate::descriptor::{TypeBody, TypeId};
23use crate::error::{PanicKind, SoilError};
24use crate::value::{Record, Ref, SumVal, Value};
25use crate::{descriptor_json, json, ops, Runtime};
26
27/// Opaque C-side names. `SoilRuntime*` is a `Runtime`, `SoilValue*` a
28/// `Value`, `SoilError*` a `SoilError`; the internal layouts are not
29/// part of the ABI.
30pub struct SoilRuntime(Runtime);
31
32fn internal(payload: Box<dyn std::any::Any + Send>) -> SoilError {
33    let message = payload
34        .downcast_ref::<&str>()
35        .map(|s| s.to_string())
36        .or_else(|| payload.downcast_ref::<String>().cloned())
37        .unwrap_or_else(|| "runtime panic".to_string());
38    SoilError::new(PanicKind::Internal, message)
39}
40
41/// Store `err` through the out-parameter, if one was provided.
42fn put_err(out: *mut *mut SoilError, err: SoilError) {
43    if !out.is_null() {
44        unsafe { *out = Box::into_raw(Box::new(err)) };
45    }
46}
47
48/// Run `body` with unwinds caught; on success return its value, on
49/// error store the error and return `fail`.
50fn guard<T>(
51    out_err: *mut *mut SoilError,
52    fail: T,
53    body: impl FnOnce() -> Result<T, SoilError>,
54) -> T {
55    match catch_unwind(AssertUnwindSafe(body)) {
56        Ok(Ok(value)) => value,
57        Ok(Err(err)) => {
58            put_err(out_err, err);
59            fail
60        }
61        Err(payload) => {
62            put_err(out_err, internal(payload));
63            fail
64        }
65    }
66}
67
68unsafe fn cstr<'a>(ptr: *const c_char) -> Result<&'a str, SoilError> {
69    if ptr.is_null() {
70        return Err(SoilError::new(PanicKind::CapiMisuse, "null string"));
71    }
72    unsafe { CStr::from_ptr(ptr) }
73        .to_str()
74        .map_err(|_| SoilError::new(PanicKind::CapiMisuse, "string is not valid UTF-8"))
75}
76
77unsafe fn rt_ref<'a>(runtime: *mut SoilRuntime) -> Result<&'a mut Runtime, SoilError> {
78    unsafe { runtime.as_mut() }
79        .map(|r| &mut r.0)
80        .ok_or_else(|| SoilError::new(PanicKind::CapiMisuse, "null runtime"))
81}
82
83unsafe fn value_ref<'a>(value: *const Value) -> Result<&'a Value, SoilError> {
84    unsafe { value.as_ref() }.ok_or_else(|| SoilError::new(PanicKind::CapiMisuse, "null value"))
85}
86
87fn owned(value: Value) -> *mut Value {
88    Box::into_raw(Box::new(value))
89}
90
91/// Create a runtime instance. Never fails; returns null only on an
92/// internal panic.
93#[no_mangle]
94pub extern "C" fn soil_init() -> *mut SoilRuntime {
95    catch_unwind(|| Box::into_raw(Box::new(SoilRuntime(Runtime::new())))).unwrap_or(ptr::null_mut())
96}
97
98/// Tear down a runtime. Values created under it must already be freed.
99///
100/// # Safety
101/// `runtime` must be a pointer returned by `soil_init`, not yet torn
102/// down.
103#[no_mangle]
104pub unsafe extern "C" fn soil_teardown(runtime: *mut SoilRuntime) {
105    if !runtime.is_null() {
106        let _ = catch_unwind(AssertUnwindSafe(|| drop(unsafe { Box::from_raw(runtime) })));
107    }
108}
109
110/// Register types from descriptor JSON (see `descriptor_json`). Returns
111/// 0 on success, nonzero with `*err` set on failure.
112///
113/// # Safety
114/// `runtime` is a live runtime; `desc_json` is a NUL-terminated UTF-8
115/// string; `err` is null or a valid out-pointer.
116#[no_mangle]
117pub unsafe extern "C" fn soil_register_types(
118    runtime: *mut SoilRuntime,
119    desc_json: *const c_char,
120    err: *mut *mut SoilError,
121) -> c_int {
122    guard(err, 1, || {
123        let rt = unsafe { rt_ref(runtime) }?;
124        let json = unsafe { cstr(desc_json) }?;
125        descriptor_json::load_descriptors(rt, json)?;
126        Ok(0)
127    })
128}
129
130/// Look up a type id by name. Returns `UINT32_MAX` if unknown.
131///
132/// # Safety
133/// `runtime` is a live runtime; `name` is a NUL-terminated UTF-8 string.
134#[no_mangle]
135pub unsafe extern "C" fn soil_type_lookup(runtime: *mut SoilRuntime, name: *const c_char) -> u32 {
136    guard(ptr::null_mut(), u32::MAX, || {
137        let rt = unsafe { rt_ref(runtime) }?;
138        let name = unsafe { cstr(name) }?;
139        Ok(rt.registry.lookup(name).map_or(u32::MAX, TypeId::index))
140    })
141}
142
143#[no_mangle]
144pub extern "C" fn soil_i64_new(n: i64) -> *mut Value {
145    catch_unwind(|| owned(Value::I64(n))).unwrap_or(ptr::null_mut())
146}
147
148#[no_mangle]
149pub extern "C" fn soil_f64_new(x: f64) -> *mut Value {
150    catch_unwind(|| owned(Value::F64(x))).unwrap_or(ptr::null_mut())
151}
152
153#[no_mangle]
154pub extern "C" fn soil_unit_new() -> *mut Value {
155    catch_unwind(|| owned(Value::Unit)).unwrap_or(ptr::null_mut())
156}
157
158/// # Safety
159/// `s` is a NUL-terminated UTF-8 string; `err` is null or valid.
160#[no_mangle]
161pub unsafe extern "C" fn soil_utf8_new(s: *const c_char, err: *mut *mut SoilError) -> *mut Value {
162    guard(err, ptr::null_mut(), || {
163        let s = unsafe { cstr(s) }?;
164        Ok(owned(Value::Utf8(Ref::from(s))))
165    })
166}
167
168/// Build a record from its **non-ignored** fields in declaration order;
169/// `ignored` fields are refilled from their defaults, exactly as in
170/// JSON decode (micro-pin §8.6). Field values are borrowed.
171///
172/// # Safety
173/// `runtime` is live; `fields` points to `n` valid value handles; `err`
174/// is null or valid.
175#[no_mangle]
176pub unsafe extern "C" fn soil_record_new(
177    runtime: *mut SoilRuntime,
178    type_id: u32,
179    fields: *const *const Value,
180    n: usize,
181    err: *mut *mut SoilError,
182) -> *mut Value {
183    guard(err, ptr::null_mut(), || {
184        let rt = unsafe { rt_ref(runtime) }?;
185        let id = TypeId::from_index(type_id);
186        let desc = rt.registry.get(id)?;
187        let TypeBody::Record(field_descs) = &desc.body else {
188            return Err(SoilError::new(
189                PanicKind::CapiMisuse,
190                format!("type {} is not a record", desc.name),
191            ));
192        };
193        let expected = field_descs.iter().filter(|f| f.ignored.is_none()).count();
194        if n != expected {
195            return Err(SoilError::new(
196                PanicKind::CapiMisuse,
197                format!(
198                    "record {} takes {expected} non-ignored fields, got {n}",
199                    desc.name
200                ),
201            ));
202        }
203        let inputs = if n == 0 {
204            &[]
205        } else if fields.is_null() {
206            return Err(SoilError::new(PanicKind::CapiMisuse, "null field array"));
207        } else {
208            unsafe { std::slice::from_raw_parts(fields, n) }
209        };
210        let mut supplied = Vec::with_capacity(n);
211        for &field in inputs {
212            supplied.push(unsafe { value_ref(field) }?.clone());
213        }
214        let mut values = Vec::with_capacity(field_descs.len());
215        let mut next = 0usize;
216        for field_desc in field_descs {
217            match &field_desc.ignored {
218                None => {
219                    values.push(supplied[next].clone());
220                    next += 1;
221                }
222                Some(default) => values.push(default.call(&supplied)?),
223            }
224        }
225        Ok(owned(Value::Record(Ref::new(Record {
226            type_id: id,
227            fields: values.into_boxed_slice(),
228        }))))
229    })
230}
231
232/// Build a sum value by variant name. `payload` is null for nullary
233/// variants, borrowed otherwise.
234///
235/// # Safety
236/// `runtime` is live; `variant` is a NUL-terminated UTF-8 string;
237/// `payload` is null or a valid value handle; `err` is null or valid.
238#[no_mangle]
239pub unsafe extern "C" fn soil_sum_new(
240    runtime: *mut SoilRuntime,
241    type_id: u32,
242    variant: *const c_char,
243    payload: *const Value,
244    err: *mut *mut SoilError,
245) -> *mut Value {
246    guard(err, ptr::null_mut(), || {
247        let rt = unsafe { rt_ref(runtime) }?;
248        let id = TypeId::from_index(type_id);
249        let desc = rt.registry.get(id)?;
250        let variant_name = unsafe { cstr(variant) }?;
251        let TypeBody::Sum(variants) = &desc.body else {
252            return Err(SoilError::new(
253                PanicKind::CapiMisuse,
254                format!("type {} is not a sum", desc.name),
255            ));
256        };
257        let Some(index) = variants.iter().position(|v| v.name == variant_name) else {
258            return Err(SoilError::new(
259                PanicKind::CapiMisuse,
260                format!("type {} has no variant {variant_name}", desc.name),
261            ));
262        };
263        let payload_value = match (&variants[index].payload, payload.is_null()) {
264            (Some(_), false) => Some(unsafe { value_ref(payload) }?.clone()),
265            (None, true) => None,
266            _ => {
267                return Err(SoilError::new(
268                    PanicKind::CapiMisuse,
269                    format!("variant {variant_name} payload mismatch"),
270                ));
271            }
272        };
273        Ok(owned(Value::Sum(Ref::new(SumVal {
274            type_id: id,
275            variant: u32::try_from(index).expect("validated at registration"),
276            payload: payload_value,
277        }))))
278    })
279}
280
281/// Build a list from borrowed elements.
282///
283/// # Safety
284/// `items` points to `n` valid value handles; `err` is null or valid.
285#[no_mangle]
286pub unsafe extern "C" fn soil_list_new(
287    items: *const *const Value,
288    n: usize,
289    err: *mut *mut SoilError,
290) -> *mut Value {
291    guard(err, ptr::null_mut(), || {
292        let inputs = if n == 0 {
293            &[]
294        } else if items.is_null() {
295            return Err(SoilError::new(PanicKind::CapiMisuse, "null item array"));
296        } else {
297            unsafe { std::slice::from_raw_parts(items, n) }
298        };
299        let mut values = Vec::with_capacity(n);
300        for &item in inputs {
301            values.push(unsafe { value_ref(item) }?.clone());
302        }
303        Ok(owned(Value::List(Ref::new(values))))
304    })
305}
306
307/// # Safety
308/// `value` is a valid handle.
309#[no_mangle]
310pub unsafe extern "C" fn soil_value_clone(value: *const Value) -> *mut Value {
311    guard(ptr::null_mut(), ptr::null_mut(), || {
312        Ok(owned(unsafe { value_ref(value) }?.clone()))
313    })
314}
315
316/// # Safety
317/// `value` was returned by this ABI and is freed exactly once.
318#[no_mangle]
319pub unsafe extern "C" fn soil_value_free(value: *mut Value) {
320    if !value.is_null() {
321        let _ = catch_unwind(AssertUnwindSafe(|| drop(unsafe { Box::from_raw(value) })));
322    }
323}
324
325/// Read an `I64` out of a value handle. Returns 0 on success.
326///
327/// # Safety
328/// All pointers valid; `out` is a valid out-pointer.
329#[no_mangle]
330pub unsafe extern "C" fn soil_i64_get(
331    value: *const Value,
332    out: *mut i64,
333    err: *mut *mut SoilError,
334) -> c_int {
335    guard(err, 1, || match unsafe { value_ref(value) }? {
336        Value::I64(n) => {
337            unsafe { *out = *n };
338            Ok(0)
339        }
340        _ => Err(SoilError::new(PanicKind::TypeError, "value is not an I64")),
341    })
342}
343
344/// Derived structural equality (design §3.7). Returns 1, 0, or -1 with
345/// `*err` set.
346///
347/// # Safety
348/// `runtime` is live; `a` and `b` are valid handles; `err` null or valid.
349#[no_mangle]
350pub unsafe extern "C" fn soil_eq(
351    runtime: *mut SoilRuntime,
352    a: *const Value,
353    b: *const Value,
354    err: *mut *mut SoilError,
355) -> c_int {
356    guard(err, -1, || {
357        let rt = unsafe { rt_ref(runtime) }?;
358        let equal = ops::eq(rt, unsafe { value_ref(a) }?, unsafe { value_ref(b) }?)?;
359        Ok(c_int::from(equal))
360    })
361}
362
363/// Canonical JSON of a value — `show` (design §3.7). Returns an owned
364/// NUL-terminated string; free with `soil_string_free`.
365///
366/// # Safety
367/// `runtime` is live; `value` is a valid handle; `err` null or valid.
368#[no_mangle]
369pub unsafe extern "C" fn soil_show(
370    runtime: *mut SoilRuntime,
371    value: *const Value,
372    err: *mut *mut SoilError,
373) -> *mut c_char {
374    guard(err, ptr::null_mut(), || {
375        let rt = unsafe { rt_ref(runtime) }?;
376        let encoded = json::encode(rt, unsafe { value_ref(value) }?)?;
377        CString::new(encoded)
378            .map(CString::into_raw)
379            .map_err(|_| SoilError::new(PanicKind::Internal, "canonical JSON contained NUL"))
380    })
381}
382
383/// Type-directed decode — `parse` (design §3.7). `shape_json` is a
384/// shape in the descriptor-JSON encoding (`{"tag": "Named", "value":
385/// "Doc"}`, `{"tag": "List", "value": {"tag": "I64"}}`, …).
386///
387/// # Safety
388/// `runtime` is live; both strings are NUL-terminated UTF-8; `err` null
389/// or valid.
390#[no_mangle]
391pub unsafe extern "C" fn soil_decode(
392    runtime: *mut SoilRuntime,
393    shape_json: *const c_char,
394    value_json: *const c_char,
395    err: *mut *mut SoilError,
396) -> *mut Value {
397    guard(err, ptr::null_mut(), || {
398        let rt = unsafe { rt_ref(runtime) }?;
399        let shape = descriptor_json::parse_shape_json(rt, unsafe { cstr(shape_json) }?)?;
400        Ok(owned(json::decode(rt, &shape, unsafe {
401            cstr(value_json)
402        }?)?))
403    })
404}
405
406/// # Safety
407/// `s` was returned by `soil_show` and is freed exactly once.
408#[no_mangle]
409pub unsafe extern "C" fn soil_string_free(s: *mut c_char) {
410    if !s.is_null() {
411        let _ = catch_unwind(AssertUnwindSafe(|| drop(unsafe { CString::from_raw(s) })));
412    }
413}
414
415/// # Safety
416/// `err` is a valid error handle.
417#[no_mangle]
418pub unsafe extern "C" fn soil_error_message(err: *const SoilError) -> *mut c_char {
419    guard(ptr::null_mut(), ptr::null_mut(), || {
420        let err = unsafe { err.as_ref() }
421            .ok_or_else(|| SoilError::new(PanicKind::CapiMisuse, "null error"))?;
422        CString::new(err.to_string())
423            .map(CString::into_raw)
424            .map_err(|_| SoilError::new(PanicKind::Internal, "error message contained NUL"))
425    })
426}
427
428/// The `PanicKind` as a stable small integer (order of declaration).
429///
430/// # Safety
431/// `err` is a valid error handle.
432#[no_mangle]
433pub unsafe extern "C" fn soil_error_kind(err: *const SoilError) -> c_int {
434    match unsafe { err.as_ref() } {
435        None => -1,
436        Some(e) => e.kind as c_int,
437    }
438}
439
440/// # Safety
441/// `err` was returned through an out-parameter and is freed exactly once.
442#[no_mangle]
443pub unsafe extern "C" fn soil_error_free(err: *mut SoilError) {
444    if !err.is_null() {
445        let _ = catch_unwind(AssertUnwindSafe(|| drop(unsafe { Box::from_raw(err) })));
446    }
447}
448
449/// Live runtime allocations on this thread — debug builds only (always
450/// 0 in release). The leak-check hook.
451#[no_mangle]
452pub extern "C" fn soil_debug_live_values() -> usize {
453    crate::value::debug_live_values()
454}
455
456/// The trivial `main` wrapper (design §3.10): init, hand the runtime to
457/// the host's entry function, tear down, return its exit code.
458#[no_mangle]
459pub extern "C" fn soil_main(entry: Option<extern "C" fn(*mut SoilRuntime) -> c_int>) -> c_int {
460    let Some(entry) = entry else { return 2 };
461    let runtime = soil_init();
462    if runtime.is_null() {
463        return 2;
464    }
465    let code = entry(runtime);
466    unsafe { soil_teardown(runtime) };
467    code
468}