oden/oden-js/src/class.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

309 lines
9.4 KiB
Rust

use crate::{ContextRef, Error, Result, Value, ValueRef, ValueResult};
use oden_js_sys as sys;
use std::cell::{Ref, RefCell, RefMut};
use std::ffi::CString;
use std::fmt;
use std::sync::Mutex;
pub struct ClassID {
name: &'static str,
value: Mutex<Option<sys::JSClassID>>,
}
impl ClassID {
pub const fn new(name: &'static str) -> Self {
ClassID {
name,
value: Mutex::new(None),
}
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn get(&self) -> sys::JSClassID {
let mut locked = self.value.lock().unwrap();
match *locked {
None => {
let id = unsafe {
let mut id: sys::JSClassID = 0;
sys::JS_NewClassID(&mut id);
id
};
*locked = Some(id);
id
}
Some(id) => id,
}
}
}
impl fmt::Debug for ClassID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id_str = {
let locked = self.value.lock().unwrap();
match *locked {
None => format!("none"),
Some(id) => format!("{id}"),
}
};
f.debug_struct("ClassID")
.field("id", &id_str)
.field("name", &self.name)
.finish()
}
}
/// Implement the `Class` trait on rust types that you want to embed into
/// JavaScript values.
pub trait Class: Sized {
fn class_id() -> &'static ClassID;
fn into_value(self, context: &ContextRef) -> ValueResult {
let class_id = Self::class_id();
// Check to see if the class is registered with the runtime. If not,
// register it.
if !context.is_registered_class(class_id) {
let class_name = match CString::new(class_id.name()) {
Ok(cs) => Ok(cs),
Err(_) => Err(Error::UnexpectedNul),
}?;
let class_def = sys::JSClassDef {
class_name: class_name.as_ptr(),
finalizer: Some(Self::finalizer),
gc_mark: if Self::needs_mark() {
Some(Self::gc_mark)
} else {
None
},
call: None,
exotic: std::ptr::null_mut(),
};
if 0 != unsafe {
sys::JS_NewClass(sys::JS_GetRuntime(context.ctx), class_id.get(), &class_def)
} {
return Err(Error::TooManyClasses);
}
}
// Check to see if the class proto is registered. We always register
// the prototype as an object, so if quickjs says the prototype is
// NULL then we can go ahead and register our own object.
unsafe {
let pval =
Value::from_raw(sys::JS_GetClassProto(context.ctx, class_id.get()), &context);
if pval.is_null() {
let proto = Self::prototype(context)?;
sys::JS_DupValue(context.ctx, proto.val);
sys::JS_SetClassProto(context.ctx, class_id.get(), proto.val);
}
}
// Now that the class is registered and the prototype is registered
// we can just ask quickjs to create the object for us, referencing
// the existing prototype.
let val = context.check_exception(unsafe {
sys::JS_NewObjectClass(context.ctx, class_id.get() as i32)
})?;
// Jam the value onto the heap, and thence into the opaque slot of
// the JS value. We'll be sure to free the box when the value is
// destroyed, because of the finalizer that we register in the class
// description above.
let b = Box::new(RefCell::new(self));
unsafe {
sys::JS_SetOpaque(val.val, Box::into_raw(b) as *mut _);
}
Ok(val)
}
fn try_from_value_mut<'a>(value: &'a ValueRef) -> Result<RefMut<'a, Self>> {
let class = Self::class_id();
// SAFETY: value.val is known to be valid, from the ValueRef.
let ptr = unsafe { sys::JS_GetOpaque(value.val, class.get()) as *const RefCell<Self> };
if ptr.is_null() {
return Err(Error::WrongClass(class.name().to_string()));
}
// SAFETY: The pointer will live as long as the value, which we have
// a reference to and whose lifetime we propagate into the
// reference to Self.
Ok(unsafe { ptr.as_ref() }
.expect("already checked for null")
.borrow_mut())
}
fn from_value_mut<'a>(value: &'a ValueRef) -> RefMut<'a, Self> {
Self::try_from_value_mut(value).expect("Wrong type for value")
}
fn try_from_value<'a>(value: &'a ValueRef) -> Result<Ref<'a, Self>> {
let class = Self::class_id();
// SAFETY: value.val is known to be valid, from the ValueRef.
let ptr = unsafe { sys::JS_GetOpaque(value.val, class.get()) as *const RefCell<Self> };
if ptr.is_null() {
return Err(Error::WrongClass(class.name().to_string()));
}
// SAFETY: The pointer will live as long as the value, which we have
// a reference to and whose lifetime we propagate into the
// reference to Self.
Ok(unsafe { ptr.as_ref() }
.expect("already checked for null")
.borrow())
}
fn from_value<'a>(value: &'a ValueRef) -> Ref<'a, Self> {
match Self::try_from_value(value) {
Ok(r) => r,
Err(_) => panic!(
"Expected a {} but got a value {:?}",
Self::class_id().name(),
value
),
}
}
fn prototype(context: &ContextRef) -> ValueResult {
context.new_object()
}
/// If your class contains a Value or ValueRef, `needs_mark` must return
/// true and you must implement `mark`. (See the documentation for `mark`
/// for more information.)
///
/// This method returns `true` by default. If you know that you do not
/// contain any Value or ValueRefs then you may override this to return
/// `false`, as an optimization.
fn needs_mark() -> bool {
true
}
/// If your class contians a Value or ValueRef then you *must* implement
/// this method and return `true` from `needs_mark`. Call the provided
/// closure with each `ValueRef` that you store, so that the garbage
/// collector won't collect it.
fn mark<F>(&self, _mark: F)
where
F: Fn(&ValueRef) -> (),
{
// By default, nothing.
}
unsafe extern "C" fn gc_mark(
_rt: *mut sys::JSRuntime,
val: sys::JSValue,
mark_func: sys::JS_MarkFunc,
) {
let class = Self::class_id();
// SAFETY: value.val is known to be valid, from the ValueRef.
let ptr = unsafe { sys::JS_GetOpaque(val, class.get()) as *const RefCell<Self> };
if !ptr.is_null() {
return;
}
// SAFETY: The pointer will live as long as the value, which we have
// a reference to and whose lifetime we propagate into the
// reference to Self.
let self_ref = unsafe { ptr.as_ref() }
.expect("already checked for null")
.borrow();
self_ref.mark(|v| sys::JS_MarkValue(_rt, v.val, mark_func));
}
unsafe extern "C" fn finalizer(_rt: *mut sys::JSRuntime, val: sys::JSValue) {
unsafe {
let opaque = sys::JS_GetOpaque(val, Self::class_id().get());
if !opaque.is_null() {
// Just let the system drop it here.
let _ = Box::from_raw(opaque as *mut RefCell<Self>);
}
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Context, Runtime};
#[derive(Debug)]
struct X {
x: i32,
}
impl Class for X {
fn class_id() -> &'static ClassID {
static ID: ClassID = ClassID::new("X");
&ID
}
}
#[derive(Debug)]
struct Y {
y: i64,
}
impl Class for Y {
fn class_id() -> &'static ClassID {
static ID: ClassID = ClassID::new("Y");
&ID
}
}
#[test]
fn class_ids_distinct() {
assert_ne!(X::class_id().get(), Y::class_id().get());
}
#[test]
fn round_trips() {
let rt = Runtime::new();
let ctx = Context::new(rt);
let x = X { x: 76 };
let val = x.into_value(&ctx).expect("Unable to create value!");
let r = X::try_from_value(&val).expect("Should be able to get an X back out!");
assert_eq!(r.x, 76);
}
#[test]
fn double_create() {
let rt = Runtime::new();
let ctx = Context::new(rt);
let x = X { x: 76 };
let _v1 = x.into_value(&ctx).expect("Unable to create value!");
let x = X { x: 76 };
let _v2 = x.into_value(&ctx).expect("Unable to create value!");
}
#[test]
fn bad_class_id() {
let rt = Runtime::new();
let ctx = Context::new(rt);
let y = Y { y: 110 };
let val = y.into_value(&ctx).expect("Unable to create value!");
match X::try_from_value(&val) {
Err(Error::WrongClass(_)) => (),
Err(e) => assert!(false, "Got an error but of the wrong type: {e}"),
Ok(_) => assert!(false, "Expected to have an error when I unwrap"),
};
let r = Y::try_from_value(&val).expect("Should be able to get a Y back out!");
assert_eq!(r.y, 110);
}
}