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>, } 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> { 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 }; 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> { 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 }; 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(&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 }; 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); } }; } } #[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); } }