Bindings is the bridge that allows client JavaScript code to read/modify DOM objects. In order to better understand what this means, let's start with a simple example from the V8 embedder's guide, and build up to Cobalt bindings in their entirety.
Suppose we are working with a native application that deals with Point
structure, representing two coordinates as integers. Now suppose that we also wanted to allow for the application to be scripted in JavaScript, meaning that JavaScript code is capable of manipulating our project specific Point
struct.
struct Point { int x; int y; };
This is accomplished by using embedder only native APIs to both
Point
s, by putting a pointer to the Point
an object represents inside of a native-only internal field.In V8, this would look something like:
Local<ObjectTemplate> point_template = ObjectTemplate::New(isolate); point_template->SetInternalFieldCount(1); point_template.SetAccessor(String::NewFromUtf8(isolate, "x"), GetPointX, SetPointX); point_template.SetAccessor(String::NewFromUtf8(isolate, "y"), GetPointY, SetPointY); Point* p = new Point{0, 0}; Local<Object> obj = point_template->NewInstance(); obj->SetInternalField(0, External::New(isolate, p)); void GetPointX(Local<String> property, const PropertyCallbackInfo<Value>& info) { Local<Object> self = info.Holder(); Local<External> wrap = Local<External>::Cast(self->GetInternalField(0)); void* ptr = wrap->Value(); int value = static_cast<Point*>(ptr)->x; info.GetReturnValue().Set(value); } // void GetPointY(...
In the above example, we first create an ObjectTemplate
. This is a structure that can later be fed into V8 that lets it know the “native shape” of the point object. In particular, the SetAccessor
lines are the ones that tell it to call into our GetPointX
function when the property “x” is accessed on a point object. After V8 calls into the GetPointX
function, we find the associated native Point
object, get its x field, convert that value to a JavaScript integer, and then pass that back to JavaScript as the return value. Note that in SpiderMonkey this process is conceptually identical to V8, however with slightly different APIs.
That pattern, of intercept, convert from JavaScript to native, perform native operations, and then convert the native result back to JavaScript, is the essence of bindings. Of course, it is far more complicated, in that there are many different types, and more operations that must be performed. It is important however, to keep this general pattern in mind.
How do we scale up from what we saw before into full Cobalt bindings? The first problem that must be addressed is what should be exposed to JavaScript within each target. Instead of manually writing an ObjectTemplate for each object we want to expose to JavaScript, we write an IDL file. Then, these IDL files are collected by the IDL compiler, and then combined with a jinja2 template file, that will generate C++ header and source files for each IDL.
The jinja2 template file is responsible for
The details behind each of these responsibilities are discussed in the Web IDL spec, which has opinions on how to map Web IDL defined concepts that you see in the IDL files, to precise language bindings behavior. The first section discusses this mapping without a particular language in mind, however thankfully after that there is a section about ECMAScript bindings in particular.
The majority of the template file is about translating what the spec says in the ECMAScript bindings section of that spec, and calling the appropriate native APIs given a particular interface context. So for example, when we see
// document.idl // ... HTMLCollection getElementsByTagName(DOMString localName); // ...
in an idl file, our IDL compiler will render the jinja template (relevant sections shown below)
// interface.cc.template // … {%- for operation in operations + static_operations %} void {{operation.idl_name}}{{overload.overload_index}}( const v8::FunctionCallbackInfo<v8::Value>& info) { {{ function_implementation(overload) -}} } {% macro function_implementation(operation) %} v8::Isolate* isolate = info.GetIsolate(); {% if operation.is_static %} {{ static_function_prologue() }} // ... {% for operation in operations + static_operations %} v8::Local<v8::String> name = NewInternalString(isolate, "{{operation.idl_name}}"); v8::Local<v8::FunctionTemplate> method_template = v8::FunctionTemplate::New(isolate, {{operation.idl_name}}{{"Static" if operation.is_static else ""}}Method); method_template->RemovePrototype(); method_template->SetLength({{operation.length}}); {% if operation.is_static %} function_template-> {% elif operation.is_unforgeable or is_global_interface %} instance_template-> {% else %} prototype_template-> {% endif %} Set(name, method_template);
and produce the following native code
void getElementsByTagNameMethod(const v8::FunctionCallbackInfo<v8::Value>& info) { v8::Isolate* isolate = info.GetIsolate(); v8::Local<v8::Object> object = info.Holder(); V8cGlobalEnvironment* global_environment = V8cGlobalEnvironment::GetFromIsolate(isolate); WrapperFactory* wrapper_factory = global_environment->wrapper_factory(); if (!WrapperPrivate::HasWrapperPrivate(object) || !V8cDocument::GetTemplate(isolate)->HasInstance(object)) { V8cExceptionState exception(isolate); exception.SetSimpleException(script::kDoesNotImplementInterface); return; } // The name of the property is the identifier. v8::Local<v8::String> name = NewInternalString( isolate, "getElementsByTagName"); // ...
script/ implements various utility functions, abstraction classes, and wrapper classes that are both used directly by Cobalt, and to support bindings. The majority of these are either interfaces to abstract over important engine native APIs (such as forcing garbage collection, or getting heap statistics), or interfaces to allow Cobalt to interact with ECMAScript defined types that are implemented by the JavaScript engine, such as promises.
JavaScriptEngine is an abstract isolated instance of the JavaScript engine. It corresponds directly to the type v8::Isolate. These types are about everything that has to do with a JavaScript before the global object gets involved, so things like the heap, and builtin functions. As this class owns the JavaScript heap, Cobalt must go through it when Cobalt wants to interact with the JavaScript heap, for things such as forcing garbage collection (which is used to lower memory footprint during suspend), reporting extra memory usage implied by the engine (so for example, when a JavaScript object being alive means that xhr data that lives in native Cobalt buffers is kept alive), and gathering information about the size of the heap (used in APIs such as window.performance.memory). Additionally, having a JavaScriptEngine is a prerequisite for creating a GlobalEnvironment, which is required for JavaScript execution.
GlobalEnvironment is an abstract JavaScript execution context, which effectively means the global object itself, as well as things that are very closely related to the global object (such as script evaluation). It corresponds to type v8::Context. Also note that in the case of Cobalt, there is only one GlobalEnvironment per JavaScriptEngine, because we don't support features that would require two (such as iframes). Cobalt will use this class when it wants to do things such as evaluate scripts, inspect execution state (such as getting a stack trace), or interact with garbage collection. Implementations of this class are also responsible for setting up the global environment
Additionally, it contains interfaces for JavaScript (both ES6 and Web IDL) types that Cobalt DOM code needs to interact with, such as array buffers, callback functions, and promises. One worthy of special attention is ValueHandle, which represents an opaque JavaScript value (that could be anything), which can be used when interactions with an object is not the goal, but rather just holding onto it, and then likely passing it to someone else.
Each of these JavaScript value interface types when used by Cobalt must get held in a special wrapper type, called ScriptValue. ScriptValue provides a common type to feed into additional ScriptValue wrapper types (ScriptValue::Reference and script::Handle), that manage the lifetime of the underlying JavaScript value held in the ScriptValue. This is done because the JavaScript object itself is owned by the JavaScript engine, so management of its lifetime must be done through the native engine APIs, however Cobalt cannot access those directly.
Then, we designate a specific area of code to be our engine specific implementation of the interfaces established in script. We commit to an engine at gyp time, and then based on that, select the appropriate set of files. This is the only area of Cobalt code (except for bindings/*/$engine, which is essentially more of the same stuff) that is allowed to include engine specific headers (files in v8/). Maintaining this abstraction has been useful throughout our multiple JavaScript engine migrations over the years.
A large portion of script/$engine is filling in the types discussed in the previous section. So V8cEngine implements JavaScriptEngine by wrapping a v8::Isolate, and V8cGlobalEnvironment implements GlobalEnvironment by wrapping a v8::Context. Note that these files are actually quite a bit bigger than just being thin wrappers over the V8 types, as they have more work to do in addition to just implementing their script interfaces, such as maintaining state necessary for bindings (interface objects need to be owned somewhere), serving as a bridge between the Isolate, and dealing with garbage collection interaction (the engine specific script::Tracer is implemented near them).
JavaScript value interface type implementations follow the pattern of creating a concrete implementation that wraps an appropriate v8::Value. They will also typically have a Create function if it makes sense to build them in Cobalt. In the case where they don't have a Create function (such as ValueHandle), the only way to gain access to one in Cobalt is to receive it from bindings code.
Another important area in this module are utility functions that exist solely for bindings. In particular, in the conversion helpers are implemented here, which is a giant file that implements functions to convert back and forth between native Cobalt types and JavaScript values. These conversion helper functions get called in the native callback implementation for the getters, setters, and functions that we saw at the beginning of this doc (so the stuff that would go inside of GetPointX). Because these conversion helpers primarily exist for parts of bindings defined by Web IDL, they're on the more complex side (because Web IDL allows for many conversion details to be configurable, such as whether null objects are accepted, or whether integral types should be clamped or not), however they are also used throughout common script/ when it makes sense.
Finally, another critical thing that takes place in script/$engine is what we call WrapperPrivate (in both SpiderMonkey and V8). This is the special type that goes inside of the native only internal object field that was discussed in the beginning of this doc. WrapperPrivate is a bit more complicated however, as it has to both bridge between Cobalt DOM garbage collection and the JavaScript engine's garbage collection for more details on this), and allow for Cobalt to possibly manipulate garbage collection from the JavaScript side as well (as in add JavaScript objects to the root set of reachable objects).
Bindings test is tested in isolation via the bindings_test target. Because bindings is the bridge from the JavaScript engine to the DOM, bindings test works by having an entirely separate set of IDL files that contain minimal internal logic, and are meant to stress Web IDL features. This is accomplished by parameterizing what IDL files should get compiled at gyp time. All other parts of the bindings build pipeline (such as the IDL compiler and jinja templates) are shared between bindings_test and cobalt entirely. Note that bindings_test lives above the script/ interface, so no engine specific APIs can be used within the tests.
Additionally, when it is convenient to implement a test entirely within JavaScript, certain script/ and bindings/ features are tested within layout_tests and web_platform_tests (see for example, platform-object-user-properties-survive-gc.html). These serve as higher level, more end-to-end tests, that are good for testing more complex examples that also involve Cobalt's usage of script/.