oden/oden-js/src/callback.rs
John Doty 9f808cea31 [oden] The big lifetime removal
It turns out that rust can't really reason about the relationship
between the runtime lifetime and the context lifetime in a way that is
actually usable. This removes the lifetime stuff in favor of reference
counting the runtime itself, via a block that we embed in the
pointer. This, I think, it the least worst option here.
2023-06-19 08:28:26 -07:00

169 lines
5.3 KiB
Rust

use crate::{Class, ClassID, ContextRef, Error, ValueRef, ValueResult};
use oden_js_sys as sys;
use std::ffi::{c_int, CString};
use std::panic::catch_unwind;
pub trait Callback: Fn(&ContextRef, &ValueRef, &[&ValueRef]) -> ValueResult {}
impl<T> Callback for T where T: Fn(&ContextRef, &ValueRef, &[&ValueRef]) -> ValueResult {}
struct CallbackObject<T: Callback> {
callback: T,
}
impl<T: Callback> CallbackObject<T> {
fn new(callback: T, context: &ContextRef) -> ValueResult {
let obj = CallbackObject { callback };
obj.into_value(context)
}
}
impl<T: Callback> Class for CallbackObject<T> {
fn class_id() -> &'static ClassID {
static ID: ClassID = ClassID::new("CallbackObject");
&ID
}
}
fn throw_string(context: &ContextRef, message: String) -> sys::JSValue {
let ctx = context.ctx;
match context.new_string(&message) {
Ok(e) => unsafe {
// Because context.new_string yields an owned Value, and will
// clean it up on the way out, we need to explicitly DupValue a
// reference for the `Throw` to own.
let err = sys::JS_NewError(ctx);
if sys::JS_ValueGetTag(err) == sys::JS_TAG_EXCEPTION {
// GIVE UP; this is out of memory anyway things probably went
// wrong because of that.
return err;
}
sys::JS_DupValue(ctx, e.val); // SetProperty takes ownership.
let prop = CString::new("message").unwrap();
if sys::JS_SetPropertyStr(ctx, err, prop.as_ptr(), e.val) == -1 {
// Also an out of memory but we need to free the error object
// on our way out.
sys::JS_FreeValue(ctx, err);
return sys::JS_MakeException(); // JS_EXCEPTION
}
sys::JS_Throw(ctx, err)
},
Err(_) => unsafe {
sys::JS_Throw(
ctx,
sys::JS_NewString(ctx, "Errors within errors: embedded nulls in the description of the error that occurred".as_bytes().as_ptr() as *const i8),
)
},
}
}
fn callback_impl<F>(
ctx: *mut sys::JSContext,
_this: sys::JSValue,
argc: c_int,
argv: *mut sys::JSValue,
_magic: c_int,
data: *mut sys::JSValue,
) -> sys::JSValue
where
F: Callback,
{
let context: ContextRef = ContextRef::from_raw(ctx);
let this: ValueRef = ValueRef::from_raw(_this);
let mut actual_args = Vec::new();
unsafe {
for arg in std::slice::from_raw_parts(argv, argc.try_into().unwrap()) {
actual_args.push(ValueRef::from_raw(*arg));
}
}
let mut args = Vec::new();
for aa in &actual_args {
args.push(aa);
}
// Grab the callback that we stashed in the value.
let data_ref = unsafe { ValueRef::from_raw(*data) };
let result = match CallbackObject::<F>::try_from_value(&data_ref) {
Ok(closure) => (closure.callback)(&context, &this, &args),
Err(e) => Err(e),
};
match result {
Ok(v) => {
// `result` is owned; make sure to dup the value on the way out. Ideally
// we would just have a `take` or something on `Value` but I don't quite
// know how to implement it correctly.
unsafe {
let ret = &v.val;
sys::JS_DupValue(ctx, *ret);
*ret
}
}
Err(Error::Exception(e)) => unsafe {
// If we returned `Error::Exception` then we're propagating an
// exception through the JS stack, just flip it.
let exc = &e.val;
sys::JS_DupValue(ctx, *exc);
sys::JS_Throw(ctx, *exc)
},
Err(err) => throw_string(&context, err.to_string()),
}
}
unsafe extern "C" fn trampoline<F>(
ctx: *mut sys::JSContext,
_this: sys::JSValue,
argc: c_int,
argv: *mut sys::JSValue,
_magic: c_int,
data: *mut sys::JSValue,
) -> sys::JSValue
where
F: Callback,
{
match catch_unwind(|| callback_impl::<F>(ctx, _this, argc, argv, _magic, data)) {
Ok(r) => r,
Err(e) => {
let message = if let Some(e) = e.downcast_ref::<&'static str>() {
format!("{e}")
} else {
format!("Unknown error")
};
let context: ContextRef = ContextRef::from_raw(ctx);
throw_string(&context, message)
}
}
}
/// Get an unsafe pointer to the trampoline for a closure. We need a generic
/// function here to intuit the type of the closure, otherwise otherwise
/// there's nothing we can put in the `<>` for `trampoline`. This also
/// ensures that the rust compiler actually generates the trampoline.
fn get_trampoline<F>(_closure: &F) -> sys::JSCFunctionData
where
F: Callback,
{
Some(trampoline::<F>)
}
/// Construct a new value that wraps a closure.
pub(crate) fn new_fn<F>(ctx: &ContextRef, func: F) -> sys::JSValue
where
F: Callback,
{
let closure = func;
let callback = get_trampoline(&closure);
let value = CallbackObject::new(closure, ctx).expect("Unable to create");
unsafe {
// Just some silly pointer magic; they want an array but we only have
// one value so.
let mut val = value.val;
sys::JS_NewCFunctionData(ctx.ctx, callback, 0, 0, 1, &mut val)
}
}