#![allow(clippy::missing_const_for_fn, clippy::unnecessary_wraps)]
use core::ffi::c_void;
use core::mem::MaybeUninit;
use std::cell::RefCell;
use generic_lang_api::{
FfiReturn, FfiStatus, FfiStr, GENERIC_PLUGIN_ABI_VERSION, GenericValue, Host, HostApi,
PluginError, ValueKind,
};
fn blob(first: u64) -> GenericValue {
GenericValue {
opaque: [
MaybeUninit::new(first),
MaybeUninit::new(0),
MaybeUninit::new(0),
MaybeUninit::new(0),
],
}
}
unsafe fn limb0(value: GenericValue) -> u64 {
unsafe { value.opaque[0].assume_init() }
}
fn add(host: &mut Host, args: &[GenericValue]) -> Result<GenericValue, PluginError> {
let (Some(a), Some(b)) = (host.as_int(args[0]), host.as_int(args[1])) else {
return Err(host.type_error("add expects two integers"));
};
Ok(host.make_int(a + b))
}
fn fail(host: &mut Host, _args: &[GenericValue]) -> Result<GenericValue, PluginError> {
Err(host.index_error("out of bounds"))
}
fn explode(_host: &mut Host, _args: &[GenericValue]) -> Result<GenericValue, PluginError> {
panic!("kaboom");
}
fn forward_fatal(_host: &mut Host, _args: &[GenericValue]) -> Result<GenericValue, PluginError> {
Err(PluginError::Fatal)
}
fn explode_any(_host: &mut Host, _args: &[GenericValue]) -> Result<GenericValue, PluginError> {
std::panic::panic_any(42);
}
fn sum_any(host: &mut Host, args: &[GenericValue]) -> Result<GenericValue, PluginError> {
let mut sum = 0;
for arg in args {
let Some(value) = host.as_int(*arg) else {
return Err(host.type_error("sum_any expects integers"));
};
sum += value;
}
Ok(host.make_int(sum))
}
fn widget_method(
host: &mut Host,
_this: GenericValue,
_args: &[GenericValue],
) -> Result<GenericValue, PluginError> {
Ok(host.make_nil())
}
extern "C" fn widget_drop(_ptr: *mut c_void) {}
extern "C" fn widget_traverse(
_ptr: *mut c_void,
_visit: generic_lang_api::PluginVisitFn,
_visit_ctx: *mut c_void,
) -> i32 {
0
}
fn make_seven(_host: &mut Host) -> Result<GenericValue, PluginError> {
Ok(blob(7))
}
fn value_fail(host: &mut Host) -> Result<GenericValue, PluginError> {
Err(host.type_error("value creation failed"))
}
generic_lang_api::export_module![
value("seven", make_seven),
value("broken", value_fail),
("add", &[2], add),
("fail", &[0], fail),
("explode", &[0], explode),
("forward_fatal", &[0], forward_fatal),
("explode_any", &[0], explode_any),
("sum_any", &[1, 3], sum_any),
class("Widget") {
("__init__", &[0], widget_method),
("poke", &[0, 1], widget_method),
drop: widget_drop,
traverse: widget_traverse,
},
class("Plain") {
("ping", &[0], widget_method),
},
];
thread_local! {
static INTERNED: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
#[allow(clippy::cast_possible_wrap)]
extern "C" fn mock_int_get(_ctx: *mut c_void, value: GenericValue, out: *mut i64) -> bool {
unsafe { *out = limb0(value) as i64 };
true
}
#[allow(clippy::cast_sign_loss)]
extern "C" fn mock_int_new(_ctx: *mut c_void, value: i64) -> GenericValue {
blob(value as u64)
}
extern "C" fn mock_string_new(_ctx: *mut c_void, value: FfiStr) -> FfiReturn {
let s = unsafe {
core::str::from_utf8_unchecked(core::slice::from_raw_parts(value.ptr, value.len))
};
INTERNED.with_borrow_mut(|strings| strings.push(s.to_owned()));
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_builtin_get(_ctx: *mut c_void, name: FfiStr) -> FfiReturn {
let s =
unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(name.ptr, name.len)) };
INTERNED.with_borrow_mut(|strings| strings.push(s.to_owned()));
FfiReturn {
status: 0,
value: blob(name.len as u64),
}
}
extern "C" fn mock_exception_new(
_ctx: *mut c_void,
class: GenericValue,
message: FfiStr,
) -> FfiReturn {
let s = unsafe {
core::str::from_utf8_unchecked(core::slice::from_raw_parts(message.ptr, message.len))
};
INTERNED.with_borrow_mut(|strings| strings.push(s.to_owned()));
FfiReturn {
status: 0,
value: class,
}
}
extern "C" fn mock_value_kind(_ctx: *mut c_void, _value: GenericValue) -> u32 {
ValueKind::Int as u32
}
extern "C" fn mock_bool_get(_ctx: *mut c_void, _value: GenericValue, _out: *mut bool) -> bool {
false
}
extern "C" fn mock_float_get(_ctx: *mut c_void, _value: GenericValue, _out: *mut f64) -> bool {
false
}
extern "C" fn mock_string_get(_ctx: *mut c_void, _value: GenericValue, _out: *mut FfiStr) -> bool {
false
}
extern "C" fn mock_list_len(_ctx: *mut c_void, _value: GenericValue, _out: *mut usize) -> bool {
false
}
extern "C" fn mock_list_get(_ctx: *mut c_void, _value: GenericValue, _index: usize) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_nil_new(_ctx: *mut c_void) -> GenericValue {
blob(0)
}
extern "C" fn mock_bool_new(_ctx: *mut c_void, _value: bool) -> GenericValue {
blob(0)
}
extern "C" fn mock_float_new(_ctx: *mut c_void, _value: f64) -> GenericValue {
blob(0)
}
extern "C" fn mock_list_new(_ctx: *mut c_void) -> GenericValue {
blob(0)
}
extern "C" fn mock_list_push(
_ctx: *mut c_void,
_list: GenericValue,
_item: GenericValue,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_value_display(_ctx: *mut c_void, _value: GenericValue) -> GenericValue {
blob(0)
}
extern "C" fn mock_call_value(
_ctx: *mut c_void,
_callee: GenericValue,
_args: *const GenericValue,
_nargs: usize,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_invoke_method(
_ctx: *mut c_void,
_receiver: GenericValue,
_name: FfiStr,
_args: *const GenericValue,
_nargs: usize,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_value_str(_ctx: *mut c_void, _value: GenericValue) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_root(_ctx: *mut c_void, _value: GenericValue) {}
extern "C" fn mock_unroot(_ctx: *mut c_void, _n: usize) {}
extern "C" fn mock_tuple_len(_ctx: *mut c_void, _value: GenericValue, _out: *mut usize) -> bool {
false
}
extern "C" fn mock_tuple_get(_ctx: *mut c_void, _value: GenericValue, _index: usize) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_container_len(
_ctx: *mut c_void,
_value: GenericValue,
_out: *mut usize,
) -> bool {
false
}
extern "C" fn mock_attr_get(
_ctx: *mut c_void,
_receiver: GenericValue,
_name: FfiStr,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_attr_set(
_ctx: *mut c_void,
_receiver: GenericValue,
_name: FfiStr,
_value: GenericValue,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_attr_has(
_ctx: *mut c_void,
_receiver: GenericValue,
_name: FfiStr,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_list_set(
_ctx: *mut c_void,
_list: GenericValue,
_index: usize,
_value: GenericValue,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_value_binary(
_ctx: *mut c_void,
_a: GenericValue,
_b: GenericValue,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_dict_set(
_ctx: *mut c_void,
_dict: GenericValue,
_key: GenericValue,
_value: GenericValue,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_value_unary(_ctx: *mut c_void, _value: GenericValue) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_is_instance(
_ctx: *mut c_void,
_value: GenericValue,
_class: GenericValue,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
fn mock_host_api() -> HostApi {
HostApi {
abi_version: GENERIC_PLUGIN_ABI_VERSION,
ctx: core::ptr::null_mut(),
value_kind: mock_value_kind,
bool_get: mock_bool_get,
int_get: mock_int_get,
float_get: mock_float_get,
string_get: mock_string_get,
list_len: mock_list_len,
list_get: mock_list_get,
tuple_len: mock_tuple_len,
tuple_get: mock_tuple_get,
dict_len: mock_container_len,
set_len: mock_container_len,
builtin_get: mock_builtin_get,
is_instance: mock_is_instance,
class_of: mock_value_unary,
attr_get: mock_attr_get,
attr_set: mock_attr_set,
attr_has: mock_attr_has,
nil_new: mock_nil_new,
bool_new: mock_bool_new,
int_new: mock_int_new,
float_new: mock_float_new,
string_new: mock_string_new,
list_new: mock_list_new,
list_push: mock_list_push,
list_set: mock_list_set,
exception_new: mock_exception_new,
value_display: mock_value_display,
call_value: mock_call_value,
invoke_method: mock_invoke_method,
value_str: mock_value_str,
dict_get: mock_value_binary,
dict_set: mock_dict_set,
dict_contains: mock_value_binary,
set_add: mock_value_binary,
set_contains: mock_value_binary,
value_truthy: mock_value_unary,
value_equals: mock_value_binary,
value_hash: mock_value_unary,
root: mock_root,
unroot: mock_unroot,
instance_set_opaque: mock_instance_set_opaque,
instance_get_opaque: mock_instance_get_opaque,
}
}
extern "C" fn mock_instance_set_opaque(
_ctx: *mut c_void,
_receiver: GenericValue,
_ptr: *mut c_void,
) -> FfiReturn {
FfiReturn {
status: 0,
value: blob(0),
}
}
extern "C" fn mock_instance_get_opaque(_ctx: *mut c_void, _receiver: GenericValue) -> *mut c_void {
core::ptr::null_mut()
}
fn last_interned() -> String {
INTERNED.with_borrow(|strings| strings.last().cloned().unwrap_or_default())
}
#[test]
fn descriptor_contents() {
let desc = generic_plugin_init();
assert!(!desc.is_null());
assert_eq!(desc, generic_plugin_init());
let desc = unsafe { &*desc };
assert_eq!(desc.abi_version, GENERIC_PLUGIN_ABI_VERSION);
assert_eq!(desc.functions_len, 6);
let functions = unsafe { core::slice::from_raw_parts(desc.functions, desc.functions_len) };
let names: Vec<&str> = functions
.iter()
.map(|f| {
unsafe {
core::str::from_utf8_unchecked(core::slice::from_raw_parts(f.name.ptr, f.name.len))
}
})
.collect();
assert_eq!(
names,
[
"add",
"fail",
"explode",
"forward_fatal",
"explode_any",
"sum_any"
]
);
let expected_arities: [&[u8]; 6] = [&[2], &[0], &[0], &[0], &[0], &[1, 3]];
for (function, expected) in functions.iter().zip(expected_arities) {
let arities =
unsafe { core::slice::from_raw_parts(function.arities, function.arities_len) };
assert_eq!(arities, expected);
}
assert!(functions.iter().all(|f| f.fun.is_some()));
}
#[test]
fn value_descriptor_contents_and_creators() {
let desc = unsafe { &*generic_plugin_init() };
assert_eq!(desc.values_len, 2);
let values = unsafe { core::slice::from_raw_parts(desc.values, desc.values_len) };
let names: Vec<&str> = values.iter().map(|v| unsafe { ffi_str(v.name) }).collect();
assert_eq!(names, ["seven", "broken"]);
assert!(values.iter().all(|v| v.fun.is_some()));
let api = mock_host_api();
let ret = values[0].fun.expect("checked non-null above")(&raw const api);
assert_eq!(ret.status, FfiStatus::Ok as u32);
assert_eq!(unsafe { limb0(ret.value) }, 7);
let ret = values[1].fun.expect("checked non-null above")(&raw const api);
assert_eq!(ret.status, FfiStatus::Exception as u32);
}
unsafe fn ffi_str(s: FfiStr) -> &'static str {
unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(s.ptr, s.len)) }
}
#[test]
fn class_descriptor_contents() {
let desc = unsafe { &*generic_plugin_init() };
assert_eq!(desc.classes_len, 2);
let classes = unsafe { core::slice::from_raw_parts(desc.classes, desc.classes_len) };
let widget = &classes[0];
assert_eq!(unsafe { ffi_str(widget.name) }, "Widget");
assert_eq!(widget.methods_len, 2);
assert!(widget.drop.is_some());
assert!(widget.traverse.is_some());
let methods = unsafe { core::slice::from_raw_parts(widget.methods, widget.methods_len) };
let method_names: Vec<&str> = methods.iter().map(|m| unsafe { ffi_str(m.name) }).collect();
assert_eq!(method_names, ["__init__", "poke"]);
let poke_arities =
unsafe { core::slice::from_raw_parts(methods[1].arities, methods[1].arities_len) };
assert_eq!(poke_arities, &[0, 1]);
assert!(methods.iter().all(|m| m.fun.is_some()));
let plain = &classes[1];
assert_eq!(unsafe { ffi_str(plain.name) }, "Plain");
assert_eq!(plain.methods_len, 1);
assert!(plain.drop.is_none());
assert!(plain.traverse.is_none());
}
fn call_exported(index: usize, args: &[GenericValue]) -> FfiReturn {
let desc = unsafe { &*generic_plugin_init() };
let functions = unsafe { core::slice::from_raw_parts(desc.functions, desc.functions_len) };
let api = mock_host_api();
let fun = functions[index]
.fun
.expect("export_module! emits non-null function pointers");
fun(&raw const api, args.as_ptr(), args.len())
}
#[test]
fn multi_arity_export_runs_at_each_declared_arity() {
let ret = call_exported(5, &[blob(40), blob(1), blob(1)]);
assert_eq!(ret.status, 0);
assert_eq!(unsafe { limb0(ret.value) }, 42);
let ret = call_exported(5, &[blob(7)]);
assert_eq!(ret.status, 0);
assert_eq!(unsafe { limb0(ret.value) }, 7);
}
#[test]
fn ok_path() {
let args = [blob(19), blob(23)];
let ret = call_exported(0, &args);
assert_eq!(ret.status, 0);
assert_eq!(unsafe { limb0(ret.value) }, 42);
}
#[test]
fn typed_error_path() {
let ret = call_exported(1, &[]);
assert_eq!(ret.status, FfiStatus::Exception as u32);
assert_eq!(unsafe { limb0(ret.value) }, "IndexError".len() as u64);
let interned = INTERNED.with_borrow(Clone::clone);
assert_eq!(
&interned[interned.len() - 2..],
["IndexError", "out of bounds"]
);
}
extern "C" fn mock_string_get_null_ptr(
_ctx: *mut c_void,
_value: GenericValue,
out: *mut FfiStr,
) -> bool {
unsafe { *out = FfiStr::null() };
true
}
#[test]
fn as_str_rejects_a_null_pointer() {
let mut api = mock_host_api();
api.string_get = mock_string_get_null_ptr;
let host = Host::new(&api);
assert_eq!(host.as_str(blob(0)), None);
}
#[test]
fn panic_becomes_exception() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let ret = call_exported(2, &[]);
std::panic::set_hook(previous);
assert_eq!(ret.status, FfiStatus::Exception as u32);
assert_eq!(unsafe { limb0(ret.value) }, "Exception".len() as u64);
assert_eq!(last_interned(), "panic: kaboom");
}
#[test]
fn fatal_forwards_unchanged() {
let ret = call_exported(3, &[]);
assert_eq!(ret.status, FfiStatus::Fatal as u32);
assert_eq!(ret.status, 99);
}
#[test]
fn non_string_panic_payload_gets_the_fallback_message() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let ret = call_exported(4, &[]);
std::panic::set_hook(previous);
assert_eq!(ret.status, FfiStatus::Exception as u32);
assert_eq!(last_interned(), "panic: plugin function panicked");
}
#[test]
fn null_args_with_zero_nargs_is_accepted() {
let desc = unsafe { &*generic_plugin_init() };
let functions = unsafe { core::slice::from_raw_parts(desc.functions, desc.functions_len) };
let api = mock_host_api();
let fun = functions[1]
.fun
.expect("export_module! emits non-null function pointers");
let ret = fun(&raw const api, core::ptr::null(), 0);
assert_eq!(ret.status, FfiStatus::Exception as u32);
}
extern "C" fn mock_call_value_unknown_status(
_ctx: *mut c_void,
_callee: GenericValue,
_args: *const GenericValue,
_nargs: usize,
) -> FfiReturn {
FfiReturn {
status: 7,
value: blob(0),
}
}
#[test]
fn unknown_host_status_is_a_protocol_violation_exception() {
let mut api = mock_host_api();
api.call_value = mock_call_value_unknown_status;
let mut host = Host::new(&api);
let result = host.call(blob(0), &[]);
assert!(matches!(result, Err(PluginError::Exception(_))));
assert_eq!(last_interned(), "host callback returned unknown status 7");
}
extern "C" fn mock_builtin_get_unknown_status(_ctx: *mut c_void, _name: FfiStr) -> FfiReturn {
FfiReturn {
status: 7,
value: blob(0),
}
}
#[test]
fn unknown_status_during_error_construction_does_not_recurse() {
let mut api = mock_host_api();
api.call_value = mock_call_value_unknown_status;
api.builtin_get = mock_builtin_get_unknown_status;
let mut host = Host::new(&api);
let result = host.call(blob(0), &[]);
assert!(matches!(result, Err(PluginError::Exception(_))));
}
extern "C" fn mock_builtin_get_fatal(_ctx: *mut c_void, _name: FfiStr) -> FfiReturn {
FfiReturn {
status: FfiStatus::Fatal as u32,
value: blob(0),
}
}
#[test]
fn fatal_during_error_construction_stays_fatal() {
let mut api = mock_host_api();
api.builtin_get = mock_builtin_get_fatal;
let host = Host::new(&api);
assert!(matches!(host.type_error("boom"), PluginError::Fatal));
}