Browse Source

Feature native class objects (`NativeObject` and `Class` traits) (#627)

pull/667/head
Halid Odat 4 years ago committed by GitHub
parent
commit
ad162e09e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 150
      boa/examples/classes.rs
  2. 2
      boa/src/builtins/function/mod.rs
  3. 15
      boa/src/builtins/json/mod.rs
  4. 78
      boa/src/builtins/object/internal_methods.rs
  5. 301
      boa/src/builtins/object/mod.rs
  6. 4
      boa/src/builtins/property/attribute/mod.rs
  7. 2
      boa/src/builtins/property/mod.rs
  8. 35
      boa/src/exec/mod.rs
  9. 2
      boa/src/lib.rs

150
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<Value> {
// 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::<Person>() {
// 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<Self> {
// 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::<Person>() {
// 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::<Person>().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();
}

2
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 {

15
boa/src/builtins/json/mod.rs

@ -81,18 +81,19 @@ impl Json {
holder: &mut Value,
key: &PropertyKey,
) -> Result<Value> {
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) => {}
}

78
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(&current, &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(&current, &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
///

301
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<T: Any + Debug + Trace> 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<Self>;
/// 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<Value>
where
Self: Sized;
}
impl<T: Class> ClassConstructor for T {
fn raw_constructor(this: &Value, args: &[Value], ctx: &mut Interpreter) -> Result<Value>
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<T>(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<N>(&mut self, name: N, length: usize, function: NativeFunction)
where
N: Into<String>,
{
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<N>(&mut self, name: N, length: usize, function: NativeFunction)
where
N: Into<String>,
{
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<K, V>(&mut self, key: K, value: V, attribute: Attribute)
where
K: Into<PropertyKey>,
V: Into<Value>,
{
// 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<K, V>(&mut self, key: K, value: V, attribute: Attribute)
where
K: Into<PropertyKey>,
V: Into<Value>,
{
// 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<Value, Value>),
@ -77,6 +289,7 @@ pub enum ObjectData {
Ordinary,
Date(Date),
Global,
NativeObject(Box<dyn NativeObject>),
}
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<Self, ()> {
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<T>(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<T>(&self) -> bool
where
T: NativeObject,
{
use std::ops::Deref;
match self.data {
ObjectData::NativeObject(ref object) => object.deref().as_any().is::<T>(),
_ => false,
}
}
/// Downcast a reference to the object,
/// if the object is type native object type `T`.
pub fn downcast_ref<T>(&self) -> Option<&T>
where
T: NativeObject,
{
use std::ops::Deref;
match self.data {
ObjectData::NativeObject(ref object) => object.deref().as_any().downcast_ref::<T>(),
_ => None,
}
}
/// Downcast a mutable reference to the object,
/// if the object is type native object type `T`.
pub fn downcast_mut<T>(&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::<T>()
}
_ => None,
}
}
}
/// Create a new object.
pub fn make_object(_: &Value, args: &[Value], ctx: &mut Interpreter) -> Result<Value> {
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] )`

4
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;
}
}

2
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,

35
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::<MyClass>();
/// ```
pub fn register_global_class<T>(&mut self) -> Result<()>
where
T: Class,
{
let mut class_builder = ClassBuilder::new::<T>(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 {

2
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<T> = StdResult<T, Value>;

Loading…
Cancel
Save