1use 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
27pub 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
41fn 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
48fn 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[no_mangle]
452pub extern "C" fn soil_debug_live_values() -> usize {
453 crate::value::debug_live_values()
454}
455
456#[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}