309 lines
9.5 KiB
Rust
309 lines
9.5 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<'r>(self, context: &ContextRef<'r>) -> ValueResult<'r> {
|
|
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<'r>(value: &ValueRef<'r>) -> Result<'r, RefMut<'r, 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<'r>(value: &ValueRef<'r>) -> RefMut<'r, Self> {
|
|
Self::try_from_value_mut(value).expect("Wrong type for value")
|
|
}
|
|
|
|
fn try_from_value<'r>(value: &ValueRef<'r>) -> Result<'r, Ref<'r, 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<'r>(value: &ValueRef<'r>) -> Ref<'r, 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<'r>(context: &ContextRef<'r>) -> ValueResult<'r> {
|
|
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: for<'r> Fn(&ValueRef<'r>) -> (),
|
|
{
|
|
// 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);
|
|
}
|
|
}
|