[RFC] Spidermonkey Binding Abstraction Layer

RFC for an Abstraction layer of Spidermonkey bindings:

Abstract:

The spidermonkey bindings give a bridge between Javascript and C++ in cocos2d-js. Historically, in cocos2d-x, we have generated the bindings via the binding generator. This automatically maps C++ functions and constructs to equivalent JavaScript APIs. However, the binding generator doesn’t cover every need of cocos2d-x, and there is large amount of manually written and maintained bindings. Maintaining different versions of these bindings has made upgrading spidermonkey difficult, and puts a large burden on the cocos2d-x team. The purpose of the RFC is to propose a abstraction layer that will make maintaining Spidermonkey easier for future developers and engine users.

Content:

The high level goals of this Abstraction layer should be to allow creation, binding, and manipulation of C++ objects from JavaScript, and vice versa. It will need to be able to properly handle rooting and tracing in an agnostic way across multiple VM implementation. Ideally, this layer will not be tied to cocos2d-x, and instead be generic and usable in other projects, for any other embedders. This layer should be easy to target for the binding generator, with minimal and efficient code to bind and manipulate objects.

Object Structure:

The base type for manipulating values in JavaScript will be a JSValue*. This JSValue* will rarely be used directly, and will never be stored in a class/struct by itself. Instead, users will deal with two constructs, a Local<JSValue*> or a Persistent<JSValue*>. A Local<JSValue*> will never be stored on the heap, and will only exist on the stack. While the Local<JSValue*> is on the stack, the underlying JSValue* will not be GC’d. Different Local<JSValue*>'s can point to the same underlying JSValue* object.

By contrast, a Persistent<JSValue*> can exist on the heap. However, Persistent<JSValue*> will act as a unique_ptr to the underlying JSValue*. However, in practice it will act almost like a shared_ptr<JSValue*>, however it will not do any explicit reference counting. Instead, it will use the underlying JS API’s to make sure that as long as a Persistent<JSValue*> exists for a given JSValue*, that JSValue* will not be GC’d. If the VM is reset, all JSValues will be flushed, and all Persistent<JSValue>s will be invalidated. In order to prevent this, the VM will maintain a counter that will be incremented every restart. At creation time, all Persistent<JSValue*> will mark the current VM restart counter. In each member method call of Persistent<JSValue*>, we will compare the Persistent<JSValue*>'s restart counter to the current restart value.
Example:

template<typename T>
Persistent::foo()
{
    // Not run in Release builds
	DEBUG_ASSERT(this->restartCounter == ScriptingCore::getInstance()->getCurrentRestartCounter());
	// .. do stuff
}

A JSValue* represents any representable value in JavaScript. This includes objects, arrays, numbers, booleans, strings, etc. In C++, each of these JavaScript types will be represented by a corresponding type. For example, a JSValue* can be converted into a JSString*, if the JSValue* is a string. This will be done via a constructor call, wrapped in a factory function.

Local<JSValue*> myValue = getValueFromSomewhere(); // get a JSValue from somewhere.
Local<JSString*> mystr = myValue.toString();
std::string cppString = mystr.value(); 

Local’s and Persistent’s can be transferred into standard C++ data structures via a uniform call to .value(), or an explicit call to valueAsUnorderedMap, or valueAsString, etc.

While a JSValue* has an underlying type. Casting a JSValue* to be a type that is not should assert in a debug build. This RFC will not define the runtime behavior of this case in release builds.

JSValue* will have own void* that can be used by the API user to store arbitrary data. When setting this data, the user can specific a custom callback to be fired when the JSValue* is destroyed, to handle the cleanup of the data of the void*

Interpreter and Threading:

All JSValues* will be owned by a JSInterpreter*. A JSInterpreter* is owned by a singular thread, and can only be accessed by this one thread. Multiple threads may have independent JSInterpreters, which will have a definite API for transferring objects between one another. JSValue’s may NOT be shared between JSInterpreter*, they may only be transferred or copied via the previously mentioned API. That API will be defined in another RFC.

A JSInterpreter* will be responsible for binding JS functions to native functions. JSInterpreter* will have an API that will bind a key to a C++ define function. This will look something like


void jsb_myFunction(JSArguments& arguments)
{
	std::vector<JSValue*> values = arguments.getArgs();
	Local<JSValue*> retVal = Local<JSValue*>::From(JSString::New("Hello World"));
	arguments.rval().set(retVal);
}



Local<JSValue*> myJSValue = /* ... */;
JSInterpreter* inter = getInterpreter();
inter->bind(myJSValue, "myFunction", jsb_myFunction);

A JSInterpreter* will also be responsible for executing adhoc scripts. It will have an API similar to ScriptingCore in this respect.

Header Mocks:

Here are my drafts for the Headers for the JSInterpreter and JSValue/JSString/JSObject classes. They are not yet complete, but I feel they are complete enough to share them with the community for feedback.

JSInterpreter.h:


class ImplementationData; // This will be defined per VM Implementation
class JSValue;
class JSArguments;

class JSInterpreter 
{
public:
	static JSInterpreter* New();

	JSInterpreter();
	JSInterpreter(const JSInterpreter&) = delete;
	JSInterpreter(JSInterpreter&&);
	~JSInterpreter();

	JSInterpreter& operator=(const JSInterpreter&) = delete;
	JSInterpreter& operator=(JSInterpreter&&);

	bool Bind(Local<JSValue*> object, const std::string& name funcName, void(*nativeFunc)(JSArguments&));
	bool ExecuteFunctionWithOwner(Local<JSValue*> owner, const std::string& name, JSArguments& args);
	bool EvalString(const std::string& js);
	bool ExecuteScript(const std::string& path);

	void Restart();

private:
	ImplementationData implData;
}

JSValue.h


class JSValueImplementationData; // Unique per type of VM, i.e. Spidermonkey vs v8
class JSValue
{
public:
	JSValue();
	static Local<JSValue*> New(nullptr_t);
	static Local<JSValue*> New(int);
	static Local<JSValue*> New(double);
	// etc.

	static Local<JSValue*> From(Local<JSString*>);
	static Local<JSValue*> From(Local<JSObject*>);
	// etc.

	static Local<JSValue*> JSNull(); //sentinal value
	static Local<JSValue*> JSUndefined();

	bool isUndefined() const;
	bool isNull() const;
	bool isUndefinedOrNull() const;
	bool isArray() const;
	bool isFunction() const;
	bool isObject() const;
	// etc, I won't write all of these...

	Local<JSString*> toString();
	Local<JSObject*> toObject();
	Local<JSInteger*> toInterger();
	// etc...

private:
	JSInterpreter* owner;
	JSValueImplementationData implData;
}

class JSString
{
	static Local<JSString*> New(std::string);
	static Local<JSString*> NewUtf8(uint16_t* buffer, size_t len);
	// Etc.

	static Local<JSString*> Empty(); // Empty string sentinal value

	size_t length() const;
	std::string value() const; // Copy of string
	std::unique_ptr<uint16_t> valueUtf8() const; // copy of data as unique_ptr

	JSString* Cast(Local<JSValue*>); // Will cast a JSValue* representing a double/int/etc to a JSString*
	// etc cast functions
}

class JSObject
{
	// Basic Object manipulation
	bool Delete(const std::string& propName);
	bool Set(const std::string& propName, Local<JSValue*> propValue);
	Local<JSValue*> Get(const std::string& propName);
	bool HasOwnProperty(const std::string& propName);

	std::vector<Local<JSValue*>> GetProperties();
}

// Need to define for JSInteger, JSNumber, JSArray, etc.

I am interesting in feedback from the entire community here. I would be very thankful if any core members, especially @pandamicro would comment on what is presented here.

Edit: Things that will still need to be addressed:
GC/Finalize callback functions
JSAutoCompartment/Isolate/Context attachment