From ad162e09e39818862c10bcfc08bc788c44a85d5d Mon Sep 17 00:00:00 2001 From: Halid Odat Date: Tue, 1 Sep 2020 16:02:04 +0200 Subject: [PATCH] Feature native class objects (`NativeObject` and `Class` traits) (#627) --- boa/examples/classes.rs | 150 ++++++++++ boa/src/builtins/function/mod.rs | 2 +- boa/src/builtins/json/mod.rs | 15 +- boa/src/builtins/object/internal_methods.rs | 78 ++--- boa/src/builtins/object/mod.rs | 301 ++++++++++++++++++-- boa/src/builtins/property/attribute/mod.rs | 4 +- boa/src/builtins/property/mod.rs | 2 +- boa/src/exec/mod.rs | 35 ++- boa/src/lib.rs | 2 + 9 files changed, 511 insertions(+), 78 deletions(-) create mode 100644 boa/examples/classes.rs diff --git a/boa/examples/classes.rs b/boa/examples/classes.rs new file mode 100644 index 00000000000..1ddb74ccd00 --- /dev/null +++ b/boa/examples/classes.rs @@ -0,0 +1,150 @@ +use boa::{ + builtins::{ + object::{Class, ClassBuilder}, + property::Attribute, + value::Value, + }, + exec::Interpreter, + forward_val, + realm::Realm, + Result, +}; + +use gc::{Finalize, Trace}; + +// We create a new struct that is going to represent a person. +// +// We derive `Debug`, `Trace` and `Finalize`, It automatically implements `NativeObject` +// so we can pass it an object in JavaScript. +// +// The fields of the sturct are not accesable by JavaScript unless accessors are created for them. +/// This Represents a Person. +#[derive(Debug, Trace, Finalize)] +struct Person { + /// The name of the person. + name: String, + /// The age of the preson. + age: u32, +} + +// Here we implement a static method for Person that matches the `NativeFunction` signiture. +// +// NOTE: The function does not have to be implemented of Person it can be a free function, +// or any function that matches that signature. +impl Person { + /// This function says hello + fn say_hello(this: &Value, _: &[Value], ctx: &mut Interpreter) -> Result { + // We check if this is an object. + if let Some(object) = this.as_object() { + // If it is we downcast the type to type `Person`. + if let Some(person) = object.downcast_ref::() { + // we print the message to stdout. + println!( + "Hello my name is {}, I'm {} years old", + person.name, + person.age // Here we can access the native rust fields of Person struct. + ); + return Ok(Value::undefined()); + } + } + // If `this` was not an object or the type was not an native object `Person`, + // we throw a `TypeError`. + ctx.throw_type_error("'this' is not a Person object") + } +} + +impl Class for Person { + // we set the binging name of this function to be `"Person"`. + // It does not have to be `"Person"` it can be any string. + const NAME: &'static str = "Person"; + // We set the length to `2` since we accept 2 arguments in the constructor. + // + // This is the same as `Object.length`. + // NOTE: If this is not defiend that the default is `0`. + const LENGTH: usize = 2; + + // This is what is called when we do `new Person()` + fn constructor(_this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result { + // we get the first arguemnt of undefined if the first one is unavalable and call `to_string`. + // + // This is equivalent to `String(arg)`. + let name = args.get(0).cloned().unwrap_or_default().to_string(ctx)?; + // we get the second arguemnt of undefined if the first one is unavalable and call `to_u32`. + // + // This is equivalent to `arg | 0`. + let age = args.get(1).cloned().unwrap_or_default().to_u32(ctx)?; + + // we construct the the native struct `Person` + let person = Person { + name: name.to_string(), + age, + }; + + Ok(person) // and we return it. + } + + /// This is where the object is intitialized. + fn init(class: &mut ClassBuilder) -> Result<()> { + // we add a inheritable method `sayHello` with length `0` the amount of args it takes. + // + // This function is added to `Person.prototype.sayHello()` + class.method("sayHello", 0, Self::say_hello); + // we add a static mathod `is`, and here we use a closure, but it must be converible + // to a NativeFunction. it must not contain state, if it does it will give a compilation error. + // + // This function is added to `Person.is()` + class.static_method("is", 1, |_this, args, _ctx| { + if let Some(arg) = args.get(0) { + if let Some(object) = arg.as_object() { + if object.is::() { + // we check if the object type is `Person` + return Ok(true.into()); // return `true`. + } + } + } + Ok(false.into()) // otherwise `false`. + }); + + // Add a inherited property with the value `10`, with deafault attribute. + // (`READONLY, NON_ENUMERABLE, PERMANENT). + class.property("inheritedProperty", 10, Attribute::default()); + + // Add a static property with the value `"Im a static property"`, with deafault attribute. + // (`WRITABLE, ENUMERABLE, PERMANENT`). + class.static_property( + "staticProperty", + "Im a static property", + Attribute::WRITABLE | Attribute::ENUMERABLE | Attribute::PERMANENT, + ); + + Ok(()) + } +} + +fn main() { + let realm = Realm::create(); + let mut context = Interpreter::new(realm); + + // we register the global class `Person`. + context.register_global_class::().unwrap(); + + forward_val( + &mut context, + r" + let person = new Person('John', 19); + person.sayHello(); + + if (Person.is(person)) { + console.log('person is a Person class instance.'); + } + if (!Person.is('Hello')) { + console.log('\'Hello\' string is not a Person class instance.'); + } + + console.log(Person.staticProperty); + console.log(person.inheritedProperty); + console.log(Person.prototype.inheritedProperty === person.inheritedProperty); + ", + ) + .unwrap(); +} diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index f6e6eb56652..25730235245 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -59,7 +59,7 @@ bitflags! { } impl FunctionFlags { - fn from_parameters(callable: bool, constructable: bool) -> Self { + pub(crate) fn from_parameters(callable: bool, constructable: bool) -> Self { let mut flags = Self::default(); if callable { diff --git a/boa/src/builtins/json/mod.rs b/boa/src/builtins/json/mod.rs index 71f86e50107..d674d18d5b5 100644 --- a/boa/src/builtins/json/mod.rs +++ b/boa/src/builtins/json/mod.rs @@ -81,18 +81,19 @@ impl Json { holder: &mut Value, key: &PropertyKey, ) -> Result { - let mut value = holder.get_field(key.clone()); + let value = holder.get_field(key.clone()); - let obj = value.as_object().as_deref().cloned(); - if let Some(obj) = obj { - for key in obj.keys() { - let v = Self::walk(reviver, ctx, &mut value, &key); + if let Value::Object(ref object) = value { + let keys: Vec<_> = object.borrow().keys().collect(); + + for key in keys { + let v = Self::walk(reviver, ctx, &mut value.clone(), &key); match v { Ok(v) if !v.is_undefined() => { - value.set_field(key.clone(), v); + value.set_field(key, v); } Ok(_) => { - value.remove_property(key.clone()); + value.remove_property(key); } Err(_v) => {} } diff --git a/boa/src/builtins/object/internal_methods.rs b/boa/src/builtins/object/internal_methods.rs index 0595a294abf..8bda40f1ea5 100644 --- a/boa/src/builtins/object/internal_methods.rs +++ b/boa/src/builtins/object/internal_methods.rs @@ -282,45 +282,45 @@ impl Object { }) } - /// `Object.setPropertyOf(obj, prototype)` - /// - /// This method sets the prototype (i.e., the internal `[[Prototype]]` property) - /// of a specified object to another object or `null`. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-setprototypeof-v - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf - pub fn set_prototype_of(&mut self, val: Value) -> bool { - debug_assert!(val.is_object() || val.is_null()); - let current = self.prototype.clone(); - if same_value(¤t, &val) { - return true; - } - if !self.is_extensible() { - return false; - } - let mut p = val.clone(); - let mut done = false; - while !done { - if p.is_null() { - done = true - } else if same_value(&Value::from(self.clone()), &p) { - return false; - } else { - let prototype = p - .as_object() - .expect("prototype should be null or object") - .prototype - .clone(); - p = prototype; - } - } - self.prototype = val; - true - } + // /// `Object.setPropertyOf(obj, prototype)` + // /// + // /// This method sets the prototype (i.e., the internal `[[Prototype]]` property) + // /// of a specified object to another object or `null`. + // /// + // /// More information: + // /// - [ECMAScript reference][spec] + // /// - [MDN documentation][mdn] + // /// + // /// [spec]: https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-setprototypeof-v + // /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf + // pub fn set_prototype_of(&mut self, val: Value) -> bool { + // debug_assert!(val.is_object() || val.is_null()); + // let current = self.prototype.clone(); + // if same_value(¤t, &val) { + // return true; + // } + // if !self.is_extensible() { + // return false; + // } + // let mut p = val.clone(); + // let mut done = false; + // while !done { + // if p.is_null() { + // done = true + // } else if same_value(&Value::from(self.clone()), &p) { + // return false; + // } else { + // let prototype = p + // .as_object() + // .expect("prototype should be null or object") + // .prototype + // .clone(); + // p = prototype; + // } + // } + // self.prototype = val; + // true + // } /// Returns either the prototype or null /// diff --git a/boa/src/builtins/object/mod.rs b/boa/src/builtins/object/mod.rs index ff45ff4c9e8..33dd2815ccf 100644 --- a/boa/src/builtins/object/mod.rs +++ b/boa/src/builtins/object/mod.rs @@ -17,7 +17,7 @@ use crate::{ builtins::{ function::Function, map::ordered_map::OrderedMap, - property::{Property, PropertyKey}, + property::{Attribute, Property, PropertyKey}, value::{RcBigInt, RcString, RcSymbol, Value}, BigInt, Date, RegExp, }, @@ -26,10 +26,13 @@ use crate::{ }; use gc::{Finalize, Trace}; use rustc_hash::FxHashMap; +use std::any::Any; use std::fmt::{Debug, Display, Error, Formatter}; use std::result::Result as StdResult; -use super::function::{make_builtin_fn, make_constructor_fn}; +use super::function::{ + make_builtin_fn, make_constructor_fn, BuiltInFunction, FunctionFlags, NativeFunction, +}; use crate::builtins::value::same_value; mod gcobject; @@ -45,8 +48,217 @@ mod tests; /// Static `prototype`, usually set on constructors as a key to point to their respective prototype object. pub static PROTOTYPE: &str = "prototype"; +/// This trait allows Rust types to be passed around as objects. +/// +/// This is automatically implemented, when a type implements `Debug`, `Any` and `Trace`. +pub trait NativeObject: Debug + Any + Trace { + /// Convert the Rust type which implements `NativeObject` to a `&dyn Any`. + fn as_any(&self) -> &dyn Any; + + /// Convert the Rust type which implements `NativeObject` to a `&mut dyn Any`. + fn as_mut_any(&mut self) -> &mut dyn Any; +} + +impl NativeObject for T { + fn as_any(&self) -> &dyn Any { + self as &dyn Any + } + + fn as_mut_any(&mut self) -> &mut dyn Any { + self as &mut dyn Any + } +} + +/// Native class. +pub trait Class: NativeObject + Sized { + /// The binding name of the object. + const NAME: &'static str; + /// The amount of arguments the class `constructor` takes, default is `0`. + const LENGTH: usize = 0; + /// The attibutes the class will be binded with, default is `writable`, `enumerable`, `configurable`. + const ATTRIBUTE: Attribute = Attribute::all(); + + /// The constructor of the class. + fn constructor(this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result; + + /// Initializes the internals and the methods of the class. + fn init(class: &mut ClassBuilder<'_>) -> Result<()>; +} + +/// This is a wrapper around `Class::constructor` that sets the internal data of a class. +/// +/// This is automatically implemented, when a type implements `Class`. +pub trait ClassConstructor: Class { + fn raw_constructor(this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result + where + Self: Sized; +} + +impl ClassConstructor for T { + fn raw_constructor(this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result + where + Self: Sized, + { + let object_instance = Self::constructor(this, args, ctx)?; + this.set_data(ObjectData::NativeObject(Box::new(object_instance))); + Ok(this.clone()) + } +} + +/// Class builder which allows adding methods and static methods to the class. +#[derive(Debug)] +pub struct ClassBuilder<'context> { + context: &'context mut Interpreter, + object: GcObject, + prototype: GcObject, +} + +impl<'context> ClassBuilder<'context> { + pub(crate) fn new(context: &'context mut Interpreter) -> Self + where + T: ClassConstructor, + { + let global = context.global(); + + let prototype = { + let object_prototype = global.get_field("Object").get_field(PROTOTYPE); + + let object = Object::create(object_prototype); + GcObject::new(object) + }; + // Create the native function + let function = Function::BuiltIn( + BuiltInFunction(T::raw_constructor), + FunctionFlags::CONSTRUCTABLE, + ); + + // Get reference to Function.prototype + // Create the function object and point its instance prototype to Function.prototype + let mut constructor = + Object::function(function, global.get_field("Function").get_field(PROTOTYPE)); + + let length = Property::data_descriptor( + T::LENGTH.into(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, + ); + constructor.insert_property("length", length); + + let name = Property::data_descriptor( + T::NAME.into(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, + ); + constructor.insert_property("name", name); + + let constructor = GcObject::new(constructor); + + prototype + .borrow_mut() + .insert_field("constructor", constructor.clone().into()); + + constructor + .borrow_mut() + .insert_field(PROTOTYPE, prototype.clone().into()); + + Self { + context, + object: constructor, + prototype, + } + } + + pub(crate) fn build(self) -> GcObject { + self.object + } + + /// Add a method to the class. + /// + /// It is added to `prototype`. + pub fn method(&mut self, name: N, length: usize, function: NativeFunction) + where + N: Into, + { + let name = name.into(); + let mut function = Object::function( + Function::BuiltIn(function.into(), FunctionFlags::CALLABLE), + self.context + .global() + .get_field("Function") + .get_field("prototype"), + ); + + function.insert_field("length", Value::from(length)); + function.insert_field("name", Value::from(name.as_str())); + + self.prototype + .borrow_mut() + .insert_field(name, Value::from(function)); + } + + /// Add a static method to the class. + /// + /// It is added to class object itself. + pub fn static_method(&mut self, name: N, length: usize, function: NativeFunction) + where + N: Into, + { + let name = name.into(); + let mut function = Object::function( + Function::BuiltIn(function.into(), FunctionFlags::CALLABLE), + self.context + .global() + .get_field("Function") + .get_field("prototype"), + ); + + function.insert_field("length", Value::from(length)); + function.insert_field("name", Value::from(name.as_str())); + + self.object + .borrow_mut() + .insert_field(name, Value::from(function)); + } + + /// Add a property to the class, with the specified attribute. + /// + /// It is added to `prototype`. + #[inline] + pub fn property(&mut self, key: K, value: V, attribute: Attribute) + where + K: Into, + V: Into, + { + // We bitwise or (`|`) with `Attribute::default()` (`READONLY | NON_ENUMERABLE | PERMANENT`) + // so we dont get an empty attribute. + let property = Property::data_descriptor(value.into(), attribute | Attribute::default()); + self.prototype + .borrow_mut() + .insert_property(key.into(), property); + } + + /// Add a static property to the class, with the specified attribute. + /// + /// It is added to class object itself. + #[inline] + pub fn static_property(&mut self, key: K, value: V, attribute: Attribute) + where + K: Into, + V: Into, + { + // We bitwise or (`|`) with `Attribute::default()` (`READONLY | NON_ENUMERABLE | PERMANENT`) + // so we dont get an empty attribute. + let property = Property::data_descriptor(value.into(), attribute | Attribute::default()); + self.object + .borrow_mut() + .insert_property(key.into(), property); + } + + pub fn context(&mut self) -> &'_ mut Interpreter { + self.context + } +} + /// The internal representation of an JavaScript object. -#[derive(Debug, Trace, Finalize, Clone)] +#[derive(Debug, Trace, Finalize)] pub struct Object { /// The type of the object. pub data: ObjectData, @@ -62,7 +274,7 @@ pub struct Object { } /// Defines the different types of objects. -#[derive(Debug, Trace, Finalize, Clone)] +#[derive(Debug, Trace, Finalize)] pub enum ObjectData { Array, Map(OrderedMap), @@ -77,6 +289,7 @@ pub enum ObjectData { Ordinary, Date(Date), Global, + NativeObject(Box), } impl Display for ObjectData { @@ -98,6 +311,7 @@ impl Display for ObjectData { Self::BigInt(_) => "BigInt", Self::Date(_) => "Date", Self::Global => "Global", + Self::NativeObject(_) => "NativeObject", } ) } @@ -202,21 +416,18 @@ impl Object { } } - /// Converts the `Value` to an `Object` type. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-toobject - pub fn from(value: &Value) -> StdResult { - match *value { - Value::Boolean(a) => Ok(Self::boolean(a)), - Value::Rational(a) => Ok(Self::number(a)), - Value::Integer(a) => Ok(Self::number(f64::from(a))), - Value::String(ref a) => Ok(Self::string(a.clone())), - Value::BigInt(ref bigint) => Ok(Self::bigint(bigint.clone())), - Value::Object(ref obj) => Ok(obj.borrow().clone()), - _ => Err(()), + /// Create a new native object of type `T`. + pub fn native_object(value: T) -> Self + where + T: NativeObject, + { + Self { + data: ObjectData::NativeObject(Box::new(value)), + indexed_properties: FxHashMap::default(), + string_properties: FxHashMap::default(), + symbol_properties: FxHashMap::default(), + prototype: Value::null(), + extensible: true, } } @@ -404,20 +615,62 @@ impl Object { assert!(prototype.is_null() || prototype.is_object()); self.prototype = prototype } + + /// Returns `true` if it holds an Rust type that implements `NativeObject`. + pub fn is_native_object(&self) -> bool { + matches!(self.data, ObjectData::NativeObject(_)) + } + + /// Reeturn `true` if it is a native object and the native type is `T`. + pub fn is(&self) -> bool + where + T: NativeObject, + { + use std::ops::Deref; + match self.data { + ObjectData::NativeObject(ref object) => object.deref().as_any().is::(), + _ => false, + } + } + + /// Downcast a reference to the object, + /// if the object is type native object type `T`. + pub fn downcast_ref(&self) -> Option<&T> + where + T: NativeObject, + { + use std::ops::Deref; + match self.data { + ObjectData::NativeObject(ref object) => object.deref().as_any().downcast_ref::(), + _ => None, + } + } + + /// Downcast a mutable reference to the object, + /// if the object is type native object type `T`. + pub fn downcast_mut(&mut self) -> Option<&mut T> + where + T: NativeObject, + { + use std::ops::DerefMut; + match self.data { + ObjectData::NativeObject(ref mut object) => { + object.deref_mut().as_mut_any().downcast_mut::() + } + _ => None, + } + } } /// Create a new object. pub fn make_object(_: &Value, args: &[Value], ctx: &mut Interpreter) -> Result { if let Some(arg) = args.get(0) { if !arg.is_null_or_undefined() { - return Ok(Value::object(Object::from(arg).unwrap())); + return arg.to_object(ctx); } } - let global = &ctx.realm.global_obj; - - let object = Value::new_object(Some(global)); - Ok(object) + Ok(Value::new_object(Some(ctx.global()))) } /// `Object.create( proto, [propertiesObject] )` diff --git a/boa/src/builtins/property/attribute/mod.rs b/boa/src/builtins/property/attribute/mod.rs index e909a38ad7c..476e262fd4f 100644 --- a/boa/src/builtins/property/attribute/mod.rs +++ b/boa/src/builtins/property/attribute/mod.rs @@ -17,9 +17,6 @@ bitflags! { /// Additionaly there are flags for when the flags are defined. #[derive(Finalize)] pub struct Attribute: u8 { - /// None of the flags are present. - const NONE = 0b0000_0000; - /// The `Writable` attribute decides whether the value associated with the property can be changed or not, from its initial value. const WRITABLE = 0b0000_0011; @@ -46,6 +43,7 @@ bitflags! { /// Is the `Configurable` flag defined. const HAS_CONFIGURABLE = 0b0010_0000; + } } diff --git a/boa/src/builtins/property/mod.rs b/boa/src/builtins/property/mod.rs index f32895e1fc8..4afedf9f94e 100644 --- a/boa/src/builtins/property/mod.rs +++ b/boa/src/builtins/property/mod.rs @@ -72,7 +72,7 @@ impl Property { #[inline] pub fn empty() -> Self { Self { - attribute: Attribute::NONE, + attribute: Attribute::empty(), value: None, get: None, set: None, diff --git a/boa/src/exec/mod.rs b/boa/src/exec/mod.rs index 6400543d1f0..530d01fd5c5 100644 --- a/boa/src/exec/mod.rs +++ b/boa/src/exec/mod.rs @@ -26,8 +26,8 @@ use crate::{ builtins, builtins::{ function::{Function, FunctionFlags, NativeFunction}, - object::{GcObject, Object, ObjectData, PROTOTYPE}, - property::PropertyKey, + object::{Class, ClassBuilder, GcObject, Object, ObjectData, PROTOTYPE}, + property::{Property, PropertyKey}, value::{PreferredType, RcString, RcSymbol, Type, Value}, Console, Symbol, }, @@ -107,7 +107,7 @@ impl Interpreter { /// Retrieves the global object of the `Realm` of this executor. #[inline] - pub(crate) fn global(&self) -> &Value { + pub fn global(&self) -> &Value { &self.realm.global_obj } @@ -356,6 +356,35 @@ impl Interpreter { let object_prototype = self.global().get_field("Object").get_field(PROTOTYPE); GcObject::new(Object::create(object_prototype)) } + + /// Register a global class of type `T`, where `T` implemets `Class`. + /// + /// # Example + /// ```ignore + /// #[derive(Debug, Trace, Finalize)] + /// struct MyClass; + /// + /// impl Class for MyClass { + /// // ... + /// } + /// + /// context.register_global_class::(); + /// ``` + pub fn register_global_class(&mut self) -> Result<()> + where + T: Class, + { + let mut class_builder = ClassBuilder::new::(self); + T::init(&mut class_builder)?; + + let class = class_builder.build(); + let property = Property::data_descriptor(class.into(), T::ATTRIBUTE); + self.global() + .as_object_mut() + .unwrap() + .insert_property(T::NAME, property); + Ok(()) + } } impl Executable for Node { diff --git a/boa/src/lib.rs b/boa/src/lib.rs index 6e3f9f05855..209d88725d2 100644 --- a/boa/src/lib.rs +++ b/boa/src/lib.rs @@ -53,6 +53,8 @@ pub use crate::{ }; use std::result::Result as StdResult; +pub use gc::{custom_trace, unsafe_empty_trace, Finalize, Trace}; + /// The result of a Javascript expression is represented like this so it can succeed (`Ok`) or fail (`Err`) #[must_use] pub type Result = StdResult;