[oden] Grab my incomplete QuickJS wrapper
This commit is contained in:
parent
aa70df41a3
commit
898b1fe129
114 changed files with 244181 additions and 0 deletions
309
oden-js/src/class.rs
Normal file
309
oden-js/src/class.rs
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue