use std::ffi::{c_char, c_int, c_void, CStr};
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
pub const CODE_ABI_VERSION: u32 = 1;
pub const CODE_VALUE_SLOT_SIZE: usize = 80;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeTag {
Number,
Str,
Bool,
Null,
Array,
Object,
}
#[repr(C)]
pub struct CodeValue {
pub tag: CodeTag,
pub heap: c_int,
pub number: f64,
pub str: *const c_char,
pub boolean: c_int,
pub items: *mut c_void,
pub keys: *mut *const c_char,
pub len: i64,
}
impl CodeValue {
pub fn zeroed() -> Self {
unsafe { std::mem::zeroed() }
}
}
impl Default for CodeValue {
fn default() -> Self {
Self::zeroed()
}
}
#[repr(C)]
pub struct CodeVarList {
pub count: i64,
pub names: *const *const c_char,
pub values: *mut CodeValue,
}
unsafe impl Send for CodeValue {}
unsafe impl Sync for CodeValue {}
unsafe impl Send for CodeVarList {}
unsafe impl Sync for CodeVarList {}
extern "C" {
fn code_number(out: *mut CodeValue, n: f64);
fn code_str(out: *mut CodeValue, s: *const c_char);
fn code_bool(out: *mut CodeValue, b: c_int);
fn code_null(out: *mut CodeValue);
fn code_array(out: *mut CodeValue, items: *mut c_void, len: i64);
fn code_object(out: *mut CodeValue, keys: *mut *const c_char, values: *mut c_void, len: i64);
fn code_copy(out: *mut CodeValue, src: *const CodeValue);
fn code_retain(v: *const CodeValue);
fn code_values_equal(a: *const CodeValue, b: *const CodeValue) -> c_int;
fn code_runtime_error(message: *const c_char) -> !;
#[cfg(feature = "shared-module")]
fn code_native_vendored_release(v: *mut CodeValue);
}
#[cfg(not(feature = "shared-module"))]
extern "C" {
fn code_release(v: *mut CodeValue);
}
#[cfg(feature = "shared-module")]
#[no_mangle]
pub unsafe extern "C" fn code_release(v: *mut CodeValue) {
code_native_vendored_release(v)
}
pub fn slot_at(base: *mut c_void, index: i64) -> *mut CodeValue {
(base as *mut u8).wrapping_offset(index as isize * CODE_VALUE_SLOT_SIZE as isize)
as *mut CodeValue
}
fn cstr(s: &str) -> std::ffi::CString {
std::ffi::CString::new(s).unwrap_or_else(|_| std::ffi::CString::new("<invalid-utf8>").unwrap())
}
pub fn number(out: &mut CodeValue, n: f64) {
unsafe { code_number(out, n) }
}
pub fn borrowed_str(out: &mut CodeValue, s: &'static CStr) {
unsafe { code_str(out, s.as_ptr()) }
}
pub fn owned_str(out: &mut CodeValue, s: &str) {
let c = cstr(s);
unsafe { code_str(out, c.as_ptr()) }
std::mem::forget(c);
}
pub fn boolean(out: &mut CodeValue, b: bool) {
unsafe { code_bool(out, b as c_int) }
}
pub fn null(out: &mut CodeValue) {
unsafe { code_null(out) }
}
pub fn release(v: &mut CodeValue) {
unsafe { code_release(v) }
}
pub fn copy(out: &mut CodeValue, src: &CodeValue) {
unsafe { code_copy(out, src) }
}
pub fn retain(v: &CodeValue) {
unsafe { code_retain(v) }
}
pub fn field(out: &mut CodeValue, obj: &CodeValue, name: &str) {
match find_field(obj, name) {
Some(value) => copy(out, value),
None => null(out),
}
}
pub fn index(out: &mut CodeValue, arr: &CodeValue, i: &CodeValue) {
match arr.tag {
CodeTag::Array => {
let n = if i.tag == CodeTag::Number {
i.number
} else {
f64::NAN
};
let whole = n as i64;
if whole as f64 == n && whole >= 0 && whole < arr.len {
copy(out, unsafe { &*slot_at(arr.items, whole) });
} else {
null(out);
}
}
CodeTag::Object => match read_str(i).and_then(|key| find_field(arr, key)) {
Some(value) => copy(out, value),
None => null(out),
},
_ => null(out),
}
}
pub fn values_equal(a: &CodeValue, b: &CodeValue) -> bool {
unsafe { code_values_equal(a, b) != 0 }
}
#[deprecated(
since = "1.1.0",
note = "a module may not end the application; return `exception(out, source, message)` instead"
)]
pub fn runtime_error(message: &str) -> ! {
let c = cstr(message);
unsafe { code_runtime_error(c.as_ptr()) }
}
pub struct SlotBuffer {
buf: Vec<u8>,
len: i64,
}
impl SlotBuffer {
pub fn new(count: usize) -> Self {
Self {
buf: vec![0u8; count * CODE_VALUE_SLOT_SIZE],
len: count as i64,
}
}
pub fn slot_mut(&mut self, index: i64) -> &mut CodeValue {
debug_assert!(index >= 0 && index < self.len);
unsafe { &mut *slot_at(self.buf.as_mut_ptr() as *mut c_void, index) }
}
fn as_items_ptr(&mut self) -> *mut c_void {
self.buf.as_mut_ptr() as *mut c_void
}
pub fn release_all(&mut self) {
for i in 0..self.len {
unsafe { code_release(slot_at(self.buf.as_mut_ptr() as *mut c_void, i)) }
}
}
}
pub fn array(out: &mut CodeValue, elems: &mut SlotBuffer) {
unsafe { code_array(out, elems.as_items_ptr(), elems.len) }
}
pub fn object(out: &mut CodeValue, keys: &[&'static CStr], values: &mut SlotBuffer) {
debug_assert_eq!(keys.len() as i64, values.len);
let mut key_ptrs: Vec<*const c_char> = keys.iter().map(|k| k.as_ptr()).collect();
unsafe {
code_object(
out,
key_ptrs.as_mut_ptr(),
values.as_items_ptr(),
values.len,
)
}
}
pub fn find_field<'a>(v: &'a CodeValue, name: &str) -> Option<&'a CodeValue> {
if v.tag != CodeTag::Object || v.keys.is_null() {
return None;
}
for i in 0..v.len {
let key = unsafe { *v.keys.offset(i as isize) };
if key.is_null() {
continue;
}
let key_str = unsafe { CStr::from_ptr(key) };
if key_str.to_bytes() == name.as_bytes() {
return Some(unsafe { &*slot_at(v.items, i) });
}
}
None
}
pub fn read_str(v: &CodeValue) -> Option<&str> {
if v.tag != CodeTag::Str || v.str.is_null() {
return None;
}
unsafe { CStr::from_ptr(v.str) }.to_str().ok()
}
pub fn read_number(v: &CodeValue) -> Option<f64> {
(v.tag == CodeTag::Number).then_some(v.number)
}
pub fn read_bool(v: &CodeValue) -> Option<bool> {
(v.tag == CodeTag::Bool).then_some(v.boolean != 0)
}
pub fn read_field_str<'a>(v: &'a CodeValue, name: &str) -> Option<&'a str> {
read_str(find_field(v, name)?)
}
pub fn read_field_number(v: &CodeValue, name: &str) -> Option<f64> {
read_number(find_field(v, name)?)
}
pub fn read_field_bool(v: &CodeValue, name: &str) -> Option<bool> {
read_bool(find_field(v, name)?)
}
pub fn array_elems(v: &CodeValue) -> impl Iterator<Item = &CodeValue> {
let (items, len) = if v.tag == CodeTag::Array {
(v.items, v.len)
} else {
(std::ptr::null_mut(), 0)
};
(0..len).map(move |i| unsafe { &*slot_at(items, i) })
}
pub fn make_result(
out: &mut CodeValue,
class_name: &'static CStr,
fill: impl FnOnce(&mut CodeValue),
) {
let mut value = CodeValue::zeroed();
fill(&mut value);
let mut buf = SlotBuffer::new(2);
borrowed_str(buf.slot_mut(0), class_name);
unsafe { code_copy(buf.slot_mut(1), &value) };
object(out, &[c"_class", c"value"], &mut buf);
buf.release_all();
release(&mut value);
}
pub type CodeEmitFn = unsafe extern "C" fn(queue: *mut c_void, value: *const CodeValue);
pub static INBOUND_QUEUE: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
pub static INBOUND_EMIT: AtomicUsize = AtomicUsize::new(0);
pub fn store_inbound(queue: *mut c_void, emit: CodeEmitFn) {
INBOUND_QUEUE.store(queue, Ordering::Release);
INBOUND_EMIT.store(emit as usize, Ordering::Release);
}
#[macro_export]
macro_rules! declare_inbound {
($name:ident) => {
#[no_mangle]
pub unsafe extern "C" fn $name(queue: *mut ::std::ffi::c_void, emit: $crate::CodeEmitFn) {
$crate::store_inbound(queue, emit);
}
};
() => {
#[no_mangle]
pub unsafe extern "C" fn code_module_set_inbound(
queue: *mut ::std::ffi::c_void,
emit: $crate::CodeEmitFn,
) {
$crate::store_inbound(queue, emit);
}
};
}
#[macro_export]
macro_rules! declare_inbound_reply {
($name:ident, $handler:path) => {
#[no_mangle]
pub unsafe extern "C" fn $name(
particle: *const $crate::CodeValue,
result: *const $crate::CodeValue,
) {
if particle.is_null() || result.is_null() {
return;
}
$handler(&*particle, &*result);
}
};
($handler:path) => {
#[no_mangle]
pub unsafe extern "C" fn code_module_inbound_reply(
particle: *const $crate::CodeValue,
result: *const $crate::CodeValue,
) {
if particle.is_null() || result.is_null() {
return;
}
$handler(&*particle, &*result);
}
};
}
pub fn emit_inbound(value: &CodeValue) -> bool {
let emit = INBOUND_EMIT.load(Ordering::Acquire);
if emit == 0 {
return false;
}
let queue = INBOUND_QUEUE.load(Ordering::Acquire);
let emit: CodeEmitFn = unsafe { std::mem::transmute::<usize, CodeEmitFn>(emit) };
unsafe { emit(queue, value) };
true
}
pub fn exception(out: &mut CodeValue, source: &str, message: &str) {
let mut inner = CodeValue::zeroed();
null(&mut inner);
exception_wrapping(out, source, message, &inner);
release(&mut inner);
}
pub fn exception_wrapping(out: &mut CodeValue, source: &str, message: &str, inner: &CodeValue) {
let mut buf = SlotBuffer::new(4);
borrowed_str(buf.slot_mut(0), c"Exception");
owned_str(buf.slot_mut(1), source);
owned_str(buf.slot_mut(2), message);
copy(buf.slot_mut(3), inner);
object(
out,
&[c"_class", c"source", c"message", c"innerException"],
&mut buf,
);
buf.release_all();
}
pub fn guarded(out: &mut CodeValue, source: &str, body: impl FnOnce(&mut CodeValue)) {
let slot: *mut CodeValue = out;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
body(unsafe { &mut *slot })
}));
let Err(payload) = result else {
return;
};
let detail = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "panicked".to_string());
exception(
unsafe { &mut *slot },
source,
&format!("module panicked: {detail}"),
);
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_object(out: &mut CodeValue) {
let mut values = SlotBuffer::new(2);
number(values.slot_mut(0), 1.0);
owned_str(values.slot_mut(1), "hi");
object(out, &[c"a", c"s"], &mut values);
values.release_all();
}
fn sample_array(out: &mut CodeValue) {
let mut items = SlotBuffer::new(3);
for (i, v) in [10.0, 20.0, 30.0].into_iter().enumerate() {
number(items.slot_mut(i as i64), v);
}
array(out, &mut items);
items.release_all();
}
fn tag_of(f: impl FnOnce(&mut CodeValue)) -> CodeTag {
let mut out = CodeValue::zeroed();
f(&mut out);
let tag = out.tag;
release(&mut out);
tag
}
#[test]
fn field_reads_a_present_member() {
let mut obj = CodeValue::zeroed();
sample_object(&mut obj);
let mut out = CodeValue::zeroed();
field(&mut out, &obj, "s");
assert_eq!(read_str(&out), Some("hi"));
release(&mut out);
release(&mut obj);
}
#[test]
fn field_answers_null_for_an_absent_member() {
let mut obj = CodeValue::zeroed();
sample_object(&mut obj);
assert_eq!(tag_of(|out| field(out, &obj, "nope")), CodeTag::Null);
release(&mut obj);
}
#[test]
fn field_answers_null_on_a_non_object() {
let mut n = CodeValue::zeroed();
number(&mut n, 42.0);
assert_eq!(tag_of(|out| field(out, &n, "anything")), CodeTag::Null);
release(&mut n);
}
#[test]
fn index_reads_an_array_element() {
let mut arr = CodeValue::zeroed();
sample_array(&mut arr);
let mut i = CodeValue::zeroed();
number(&mut i, 1.0);
let mut out = CodeValue::zeroed();
index(&mut out, &arr, &i);
assert_eq!(read_number(&out), Some(20.0));
release(&mut out);
release(&mut arr);
}
#[test]
fn index_answers_null_for_every_kind_of_miss() {
let mut arr = CodeValue::zeroed();
sample_array(&mut arr);
for probe in [99.0, -1.0, 1.5] {
let mut i = CodeValue::zeroed();
number(&mut i, probe);
assert_eq!(
tag_of(|out| index(out, &arr, &i)),
CodeTag::Null,
"index {probe} should be null"
);
}
let mut key = CodeValue::zeroed();
owned_str(&mut key, "0");
assert_eq!(tag_of(|out| index(out, &arr, &key)), CodeTag::Null);
release(&mut key);
release(&mut arr);
}
#[test]
fn index_reads_an_object_by_string_key() {
let mut obj = CodeValue::zeroed();
sample_object(&mut obj);
let mut key = CodeValue::zeroed();
owned_str(&mut key, "a");
let mut out = CodeValue::zeroed();
index(&mut out, &obj, &key);
assert_eq!(read_number(&out), Some(1.0));
release(&mut out);
release(&mut key);
release(&mut obj);
}
#[test]
fn index_answers_null_on_a_non_container() {
let mut n = CodeValue::zeroed();
number(&mut n, 42.0);
let mut i = CodeValue::zeroed();
number(&mut i, 0.0);
assert_eq!(tag_of(|out| index(out, &n, &i)), CodeTag::Null);
release(&mut n);
}
}