Devoloping builtins in C++
Adding custom builtins
Adding builtins is as simple as calling add_builtin in the importing project’s CMakeLists.txt.
Say you want to add a builtin defined in the file my-builtin.cpp, like so:
// The extension API is automatically on the include path for builtins.
#include "extension-api.h"
// The namespace name must match the name passed to `add_builtin` in the CMakeLists.txt
namespace my_project::my_builtin {
bool install(api::Engine* engine) {
printf("installing my-builtin\n");
return true;
}
} // namespace my_builtin
This file can now be included in the runtime’s builtins like so:
add_builtin(my_project::my_builtin SRC my-builtin.cpp)
If your builtin requires multiple .cpp files, you can pass all of them to add_builtin as values
for the SRC argument.
Writing builtins
Rooting
important
SpiderMonkey has a moving GC, it is very important that it knows about each and every pointer to a GC thing in the system. SpiderMonkey’s rooting API tries to make this task as simple as possible.
It is highly recommended that readers review the SpiderMonkey
documentation on rooting before proceeding with this section. For
convenience, here is a brief recap of the main rooting rules from the referenced page:
- Use
JS::Rooted<T>typedefs for local variables on the stack. - Use
JS::Handle<T>typedefs for function parameters. - Use
JS::MutableHandle<T>typedefs for function out-parameters. - Use an implicit cast from
JS::Rooted<T>to get a JS::Handle. - Use an explicit address-of-operator on
JS::Rooted<T>to get a JS::MutableHandle. - Return raw pointers from functions.
- Use
JS::Rooted<T>fields when possible for aggregates, otherwise use an AutoRooter. - Use
JS::Heap<T>members for heap data. Note: Heapare not “rooted”: they must be traced! - Do not use
JS::Rooted<T>,JS::Handleor JS::MutableHandle on the heap. - Do not use
JS::Rooted<T>for function parameters. - Use
JS::PersistentRooted<T>for things that are alive until the process exits (or until you manually delete thePersistentRootedfor a reason not based on GC finalization.)
StarlingMonkey builtins API
The builtin API provided by StarlingMonkey is built on top of SpiderMonkey’s
jsapi, Mozilla’s C++ interface for embedding JavaScript. It simplifies the
creation and management of native JavaScript classes and objects by abstracting common patterns and
boilerplate code required by the underlying SpiderMonkey engine.
See also SpiderMonkey documentation on Custom Objects.
This section explains how to implement a native JavaScript class using the StarlingMonkey framework. We’ll use the following JavaScript class as an example:
class MyClass {
constructor(a, b) {
this._a = a;
this._b = b;
}
get prop() {
return 42;
}
method() {
return this.a + this.b;
}
static get static_prop() {
return "static";
}
static static_method(a, b) {
return a + b;
}
}
Step 1: Define the Header File (my_class.h)
Create a header file to declare the native class and its methods/properties.
#ifndef BUILTINS_MY_CLASS_H
#define BUILTINS_MY_CLASS_H
#include "builtin.h"
namespace builtins {
namespace my_class {
class MyClass : public BuiltinImpl<MyClass> {
// Instance methods
static bool method(JSContext *cx, unsigned argc, JS::Value *vp);
static bool prop_get(JSContext *cx, unsigned argc, JS::Value *vp);
// Static methods
static bool static_method(JSContext *cx, unsigned argc, JS::Value *vp);
static bool static_prop_get(JSContext *cx, unsigned argc, JS::Value *vp);
public:
enum Slots : uint8_t { SlotA, SlotB, Count };
static constexpr const char *class_name = "MyClass";
static constexpr unsigned ctor_length = 2;
static const JSFunctionSpec static_methods[];
static const JSPropertySpec static_properties[];
static const JSFunctionSpec methods[];
static const JSPropertySpec properties[];
static bool init_class(JSContext *cx, HandleObject global);
static bool constructor(JSContext *cx, unsigned argc, Value *vp);
};
bool install(api::Engine *engine);
} // namespace my_class
} // namespace builtins
#endif // BUILTINS_MY_CLASS_H
Step 2: Implement the Class (my_class.cpp)
#include "my_class.h"
namespace builtins {
namespace my_class {
const JSFunctionSpec MyClass::methods[] = {
JS_FN("method", MyClass::method, 0, JSPROP_ENUMERATE),
JS_FS_END,
};
const JSPropertySpec MyClass::properties[] = {
JS_PSG("prop", MyClass::prop_get, JSPROP_ENUMERATE),
JS_STRING_SYM_PS(toStringTag, "MyClass", JSPROP_READONLY),
JS_PS_END,
};
const JSFunctionSpec MyClass::static_methods[] = {
JS_FN("static_method", MyClass::static_method, 2, JSPROP_ENUMERATE),
JS_FS_END,
};
const JSPropertySpec MyClass::static_properties[] = {
JS_PSG("static_prop", MyClass::static_prop_get, JSPROP_ENUMERATE),
JS_PS_END,
};
// Constructor implementation
bool MyClass::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
CTOR_HEADER("MyClass", 2);
RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args));
if (!self) {
return false;
}
SetReservedSlot(self, Slots::SlotA, args.get(0));
SetReservedSlot(self, Slots::SlotB, args.get(1));
args.rval().setObject(*self);
return true;
}
// Instance method implementation
bool MyClass::method(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);
double a, b;
if (!JS::ToNumber(cx, JS::GetReservedSlot(self, Slots::SlotA), &a) ||
!JS::ToNumber(cx, JS::GetReservedSlot(self, Slots::SlotB), &b)) {
return false;
}
args.rval().setNumber(a + b);
return true;
}
// Instance property getter implementation
bool MyClass::prop_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0);
args.rval().setInt32(42);
return true;
}
// Static method implementation
bool MyClass::static_method(JSContext *cx, unsigned argc, JS::Value *vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.requireAtLeast(cx, "static_method", 2)) {
return false;
}
double a, b;
if (!JS::ToNumber(cx, args.get(0), &a) ||
!JS::ToNumber(cx, args.get(1), &b)) {
return false;
}
args.rval().setNumber(a + b);
return true;
}
// Static property getter implementation
bool MyClass::static_prop_get(JSContext *cx, unsigned argc, JS::Value *vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setString(JS_NewStringCopyZ(cx, "static"));
return true;
}
// Class initialization
bool MyClass::init_class(JSContext *cx, JS::HandleObject global) {
return init_class_impl(cx, global);
}
bool install(api::Engine *engine) {
return MyClass::init_class(engine->cx(), engine->global());
}
} // namespace my_class
} // namespace builtins
Deriving from BuiltinImpl automatically creates a JSClass definition using parameters provided
by the implementation:
Explanation of StarlingMonkey API Usage
static constexpr JSClass class_{
Impl::class_name,
JSCLASS_HAS_RESERVED_SLOTS(static_cast<uint32_t>(Impl::Slots::Count)) | class_flags,
&class_ops,
};
-
SetReservedSlotandGetReservedSlot:- Used to store and retrieve internal state associated with the JavaScript object.
-
JSFunctionSpecandJSPropertySpecarrays:- Define instance/static methods and properties exposed to JavaScript.
StarlingMonkey provides macros and helper functions to simplify native class implementation:
-
CTOR_HEADER(name, required_argc):- Initializes constructor arguments.
- Ensures the constructor is called with the
newkeyword. - Checks the minimum number of arguments.
-
METHOD_HEADER(required_argc):- Initializes method arguments.
- Ensures the receiver (
this) is an instance of the correct class. - Checks the minimum number of arguments.
-
is_instance(JSObject *obj):- Used to verify if object passed is instance of builtin class or subclass.
- Used often as assertion in class methods to ensure expected memory layout, for example:
JSString *MyObject::my_method(JSObject *self) { MOZ_ASSERT(is_instance(self)); return JS::GetReservedSlot(self, Slots::MySlot).toString(); }
note
Registering the Class with StarlingMonkey Engine
When you use add_builtin in your CMakeLists.txt file, it automatically adds your builtin to the
builtins.inclfile, which is then processed to ensure your install function is
called during engine initialization.
Providing a custom host API implementation
The host-apis directory can contain implementations of the host API for different
versions of WASI—or in theory any other host interface. Those can be selected by setting the
HOST_API environment variable to the name of one of the directories. Currently, implementations in terms of wasi-0.2.0, wasi-0.2.2, and wasi-0.2.3 are provided, with the latter used by default.
To provide a custom host API implementation, you can set HOST_API to the (absolute) path of a
directory containing that implementation. As is done in the implementations for wasi-0.2.{2,3}, it’s possible to extend existing implementations instead of duplicating any shared implementation.