use crate::{
function::Params,
qjs::{self},
value::Constructor,
Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value,
};
use std::{hash::Hash, marker::PhantomData, mem, ops::Deref, ptr::NonNull};
mod cell;
mod trace;
pub(crate) mod ffi;
pub use cell::{
Borrow, BorrowMut, JsCell, Mutability, OwnedBorrow, OwnedBorrowMut, Readable, Writable,
};
use ffi::{ClassCell, VTable};
pub use trace::{Trace, Tracer};
#[doc(hidden)]
pub mod impl_;
pub trait JsClass<'js>: Trace<'js> + JsLifetime<'js> + Sized {
const NAME: &'static str;
const CALLABLE: bool = false;
type Mutable: Mutability;
fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
Object::new(ctx.clone()).map(Some)
}
fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>>;
fn call<'a>(this: &JsCell<'js, Self>, params: Params<'a, 'js>) -> Result<Value<'js>> {
let _ = this;
Ok(Value::new_undefined(params.ctx().clone()))
}
}
#[repr(transparent)]
pub struct Class<'js, C: JsClass<'js>>(pub(crate) Object<'js>, PhantomData<C>);
impl<'js, C: JsClass<'js>> Clone for Class<'js, C> {
fn clone(&self) -> Self {
Class(self.0.clone(), PhantomData)
}
}
impl<'js, C: JsClass<'js>> PartialEq for Class<'js, C> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<'js, C: JsClass<'js>> Eq for Class<'js, C> {}
impl<'js, C: JsClass<'js>> Hash for Class<'js, C> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state)
}
}
unsafe impl<'js, C> JsLifetime<'js> for Class<'js, C>
where
C: JsClass<'js> + JsLifetime<'js>,
for<'to> C::Changed<'to>: JsClass<'to>,
{
type Changed<'to> = Class<'to, C::Changed<'to>>;
}
impl<'js, C: JsClass<'js>> Deref for Class<'js, C> {
type Target = Object<'js>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'js, C: JsClass<'js>> Class<'js, C> {
pub fn instance(ctx: Ctx<'js>, value: C) -> Result<Class<'js, C>> {
let id = unsafe {
if C::CALLABLE {
ctx.get_opaque().get_callable_id()
} else {
ctx.get_opaque().get_class_id()
}
};
let prototype = Self::prototype(&ctx)?;
let prototype = prototype.map(|x| x.as_js_value()).unwrap_or(qjs::JS_NULL);
let val = unsafe {
ctx.handle_exception(qjs::JS_NewObjectProtoClass(ctx.as_ptr(), prototype, id))?
};
let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
Ok(Self(
unsafe { Object::from_js_value(ctx, val) },
PhantomData,
))
}
pub fn instance_proto(value: C, proto: Object<'js>) -> Result<Class<'js, C>> {
let id = unsafe {
if C::CALLABLE {
proto.ctx().get_opaque().get_callable_id()
} else {
proto.ctx().get_opaque().get_class_id()
}
};
let val = unsafe {
proto.ctx.handle_exception(qjs::JS_NewObjectProtoClass(
proto.ctx().as_ptr(),
proto.0.as_js_value(),
id,
))?
};
let ptr = Box::into_raw(Box::new(ClassCell::new(value)));
unsafe { qjs::JS_SetOpaque(val, ptr.cast()) };
Ok(Self(
unsafe { Object::from_js_value(proto.ctx.clone(), val) },
PhantomData,
))
}
pub fn prototype(ctx: &Ctx<'js>) -> Result<Option<Object<'js>>> {
unsafe { ctx.get_opaque().get_or_insert_prototype::<C>(ctx) }
}
pub fn create_constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
C::constructor(ctx)
}
pub fn define(object: &Object<'js>) -> Result<()> {
if let Some(constructor) = Self::create_constructor(object.ctx())? {
object.set(C::NAME, constructor)?;
}
Ok(())
}
#[inline]
pub(crate) fn get_class_cell<'a>(&self) -> &'a ClassCell<JsCell<'js, C>> {
unsafe { self.get_class_ptr().as_ref() }
}
#[inline]
pub fn get_cell<'a>(&self) -> &'a JsCell<'js, C> {
&self.get_class_cell().data
}
#[inline]
pub fn borrow<'a>(&'a self) -> Borrow<'a, 'js, C> {
self.get_cell().borrow()
}
#[inline]
pub fn borrow_mut<'a>(&'a self) -> BorrowMut<'a, 'js, C> {
self.get_cell().borrow_mut()
}
#[inline]
pub fn try_borrow<'a>(&'a self) -> Result<Borrow<'a, 'js, C>> {
self.get_cell().try_borrow().map_err(Error::ClassBorrow)
}
#[inline]
pub fn try_borrow_mut<'a>(&'a self) -> Result<BorrowMut<'a, 'js, C>> {
self.get_cell().try_borrow_mut().map_err(Error::ClassBorrow)
}
#[inline]
pub(crate) fn get_class_ptr(&self) -> NonNull<ClassCell<JsCell<'js, C>>> {
let id = unsafe {
if C::CALLABLE {
self.ctx.get_opaque().get_callable_id()
} else {
self.ctx.get_opaque().get_class_id()
}
};
let ptr = unsafe { qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0 .0.as_js_value(), id) };
NonNull::new(ptr.cast()).expect("invalid class object, object didn't have opaque value")
}
#[inline]
pub fn into_inner(self) -> Object<'js> {
self.0
}
#[inline]
pub fn as_inner(&self) -> &Object<'js> {
&self.0
}
#[inline]
pub fn from_value(value: &Value<'js>) -> Result<Self> {
if let Some(cls) = value.as_object().and_then(Self::from_object) {
return Ok(cls);
}
Err(Error::FromJs {
from: value.type_name(),
to: C::NAME,
message: None,
})
}
#[inline]
pub fn into_value(self) -> Value<'js> {
self.0.into_value()
}
#[inline]
pub fn from_object(object: &Object<'js>) -> Option<Self> {
object.into_class().ok()
}
}
impl<'js> Object<'js> {
pub fn instance_of<C: JsClass<'js>>(&self) -> bool {
let id = unsafe {
if C::CALLABLE {
self.ctx.get_opaque().get_callable_id()
} else {
self.ctx.get_opaque().get_class_id()
}
};
let Some(x) = NonNull::new(unsafe {
qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0.as_js_value(), id)
}) else {
return false;
};
let v_table = unsafe { x.cast::<ClassCell<()>>().as_ref().v_table };
if std::ptr::eq(v_table, VTable::get::<C>()) {
return true;
}
v_table.is_of_class::<C>()
}
pub fn into_class<C: JsClass<'js>>(&self) -> std::result::Result<Class<'js, C>, &Self> {
if self.instance_of::<C>() {
Ok(Class(self.clone(), PhantomData))
} else {
Err(self)
}
}
pub fn as_class<C: JsClass<'js>>(&self) -> Option<&Class<'js, C>> {
if self.instance_of::<C>() {
unsafe { Some(mem::transmute::<&Object<'js>, &Class<'js, C>>(self)) }
} else {
None
}
}
}
impl<'js, C: JsClass<'js>> FromJs<'js> for Class<'js, C> {
fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
Self::from_value(&value)
}
}
impl<'js, C: JsClass<'js>> IntoJs<'js> for Class<'js, C> {
fn into_js(self, _ctx: &Ctx<'js>) -> Result<Value<'js>> {
Ok(self.0 .0)
}
}
#[cfg(test)]
mod test {
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use crate::{
class::{JsClass, Readable, Trace, Tracer, Writable},
function::This,
test_with,
value::Constructor,
CatchResultExt, Class, Context, FromJs, Function, IntoJs, JsLifetime, Object, Runtime,
};
#[test]
fn trace() {
pub struct Container<'js> {
inner: Vec<Class<'js, Container<'js>>>,
test: Arc<AtomicBool>,
}
impl<'js> Drop for Container<'js> {
fn drop(&mut self) {
self.test.store(true, Ordering::SeqCst);
}
}
impl<'js> Trace<'js> for Container<'js> {
fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
self.inner.iter().for_each(|x| x.trace(tracer))
}
}
unsafe impl<'js> JsLifetime<'js> for Container<'js> {
type Changed<'to> = Container<'to>;
}
impl<'js> JsClass<'js> for Container<'js> {
const NAME: &'static str = "Container";
type Mutable = Writable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
Ok(Some(Object::new(ctx.clone())?))
}
fn constructor(
_ctx: &crate::Ctx<'js>,
) -> crate::Result<Option<crate::value::Constructor<'js>>> {
Ok(None)
}
}
let rt = Runtime::new().unwrap();
let ctx = Context::full(&rt).unwrap();
let drop_test = Arc::new(AtomicBool::new(false));
ctx.with(|ctx| {
let cls = Class::instance(
ctx.clone(),
Container {
inner: Vec::new(),
test: drop_test.clone(),
},
)
.unwrap();
assert!(cls.instance_of::<Container>());
let cls_clone = cls.clone();
cls.borrow_mut().inner.push(cls_clone);
});
rt.run_gc();
assert!(drop_test.load(Ordering::SeqCst));
ctx.with(|ctx| {
let cls = Class::instance(
ctx.clone(),
Container {
inner: Vec::new(),
test: drop_test.clone(),
},
)
.unwrap();
let cls_clone = cls.clone();
cls.borrow_mut().inner.push(cls_clone);
ctx.globals().set("t", cls).unwrap();
});
}
#[derive(Clone, Copy)]
pub struct Vec3 {
x: f32,
y: f32,
z: f32,
}
impl Vec3 {
pub fn new(x: f32, y: f32, z: f32) -> Self {
Vec3 { x, y, z }
}
pub fn add(self, v: Vec3) -> Self {
Vec3 {
x: self.x + v.x,
y: self.y + v.y,
z: self.z + v.z,
}
}
}
impl<'js> Trace<'js> for Vec3 {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
impl<'js> FromJs<'js> for Vec3 {
fn from_js(ctx: &crate::Ctx<'js>, value: crate::Value<'js>) -> crate::Result<Self> {
Ok(*Class::<Vec3>::from_js(ctx, value)?.try_borrow()?)
}
}
impl<'js> IntoJs<'js> for Vec3 {
fn into_js(self, ctx: &crate::Ctx<'js>) -> crate::Result<crate::Value<'js>> {
Class::instance(ctx.clone(), self).into_js(ctx)
}
}
unsafe impl<'js> JsLifetime<'js> for Vec3 {
type Changed<'to> = Vec3;
}
impl<'js> JsClass<'js> for Vec3 {
const NAME: &'static str = "Vec3";
type Mutable = Writable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<crate::Object<'js>>> {
let proto = Object::new(ctx.clone())?;
let func = Function::new(ctx.clone(), |this: This<Vec3>, other: Vec3| this.add(other))?
.with_name("add")?;
proto.set("add", func)?;
Ok(Some(proto))
}
fn constructor(
ctx: &crate::Ctx<'js>,
) -> crate::Result<Option<crate::value::Constructor<'js>>> {
let constr =
Constructor::new_class::<Vec3, _, _>(ctx.clone(), |x: f32, y: f32, z: f32| {
Vec3::new(x, y, z)
})?;
Ok(Some(constr))
}
}
#[test]
fn constructor() {
test_with(|ctx| {
Class::<Vec3>::define(&ctx.globals()).unwrap();
let v = ctx
.eval::<Vec3, _>(
r"
let a = new Vec3(1,2,3);
let b = new Vec3(4,2,8);
a.add(b)
",
)
.catch(&ctx)
.unwrap();
approx::assert_abs_diff_eq!(v.x, 5.0);
approx::assert_abs_diff_eq!(v.y, 4.0);
approx::assert_abs_diff_eq!(v.z, 11.0);
let name: String = ctx.eval("new Vec3(1,2,3).constructor.name").unwrap();
assert_eq!(name, Vec3::NAME);
})
}
#[test]
fn extend_class() {
test_with(|ctx| {
Class::<Vec3>::define(&ctx.globals()).unwrap();
let v = ctx
.eval::<Vec3, _>(
r"
class Vec4 extends Vec3 {
w = 0;
constructor(x,y,z,w){
super(x,y,z);
this.w
}
}
new Vec4(1,2,3,4);
",
)
.catch(&ctx)
.unwrap();
approx::assert_abs_diff_eq!(v.x, 1.0);
approx::assert_abs_diff_eq!(v.y, 2.0);
approx::assert_abs_diff_eq!(v.z, 3.0);
})
}
#[test]
fn get_prototype() {
pub struct X;
impl<'js> Trace<'js> for X {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
unsafe impl<'js> JsLifetime<'js> for X {
type Changed<'to> = X;
}
impl<'js> JsClass<'js> for X {
const NAME: &'static str = "X";
type Mutable = Readable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
let object = Object::new(ctx.clone())?;
object.set("foo", "bar")?;
Ok(Some(object))
}
fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
Ok(None)
}
}
test_with(|ctx| {
let proto = Class::<X>::prototype(&ctx).unwrap().unwrap();
assert_eq!(proto.get::<_, String>("foo").unwrap(), "bar")
})
}
#[test]
fn generic_types() {
pub struct DebugPrinter<D: std::fmt::Debug> {
d: D,
}
impl<'js, D: std::fmt::Debug> Trace<'js> for DebugPrinter<D> {
fn trace<'a>(&self, _tracer: Tracer<'a, 'js>) {}
}
unsafe impl<'js, D: std::fmt::Debug + 'static> JsLifetime<'js> for DebugPrinter<D> {
type Changed<'to> = DebugPrinter<D>;
}
impl<'js, D: std::fmt::Debug + 'static> JsClass<'js> for DebugPrinter<D> {
const NAME: &'static str = "DebugPrinter";
type Mutable = Readable;
fn prototype(ctx: &crate::Ctx<'js>) -> crate::Result<Option<Object<'js>>> {
let object = Object::new(ctx.clone())?;
object.set(
"to_debug_string",
Function::new(
ctx.clone(),
|this: This<Class<DebugPrinter<D>>>| -> crate::Result<String> {
Ok(format!("{:?}", &this.0.borrow().d))
},
),
)?;
Ok(Some(object))
}
fn constructor(_ctx: &crate::Ctx<'js>) -> crate::Result<Option<Constructor<'js>>> {
Ok(None)
}
}
test_with(|ctx| {
let a = Class::instance(ctx.clone(), DebugPrinter { d: 42usize });
let b = Class::instance(
ctx.clone(),
DebugPrinter {
d: "foo".to_string(),
},
);
ctx.globals().set("a", a).unwrap();
ctx.globals().set("b", b).unwrap();
assert_eq!(
ctx.eval::<String, _>(r#" a.to_debug_string() "#)
.catch(&ctx)
.unwrap(),
"42"
);
assert_eq!(
ctx.eval::<String, _>(r#" b.to_debug_string() "#)
.catch(&ctx)
.unwrap(),
"\"foo\""
);
if ctx
.globals()
.get::<_, Class<DebugPrinter<String>>>("a")
.is_ok()
{
panic!("Conversion should fail")
}
if ctx
.globals()
.get::<_, Class<DebugPrinter<usize>>>("b")
.is_ok()
{
panic!("Conversion should fail")
}
ctx.globals()
.get::<_, Class<DebugPrinter<usize>>>("a")
.unwrap();
ctx.globals()
.get::<_, Class<DebugPrinter<String>>>("b")
.unwrap();
})
}
}