|
CreatingJSClasses
Creating new JS classes from C++ and binding C++ classes to JS
Achtung: the WeakJSClassCreator and ClassBinder APIs described below are considered obsolete, and have been superseded in functionality and flexibility by the ClassWrap API. Several of the v8-juice plugins still use the older APIs (and porting them would require a significant effort), so the older APIs will be supported for the foreseeable future. More specifically: there are no plans to remove them, but new C++/JS bindings should use the ClassWrap API instead of the APIs described below. Achtung #2: this page documents the current state of the library, which might be five minutes or a thousand years newer than any given version found on the downloads page.
IntroductionThe JSClassCreator and WeakJSClassCreator classes are helpers which encapsulate much of the drudge work associated with binding a native class to JS. This page describes how to use them. This API is likely to be continually tweaked, so if you use this API be prepared for changes down the road. See also...
Getting the source codeWhile this code is part of v8-juice, much of it is independent of any other v8-juice library components, which means it be extracted from the v8-juice source tree for use in other v8-based projects. The source files are: The ClassBinder type, described on its own page, extends WeakJSClassCreator to add member function/variable binding, but relies on the v8::juice type conversions API and is therefore not standalone code. JSClassCreatorThe base binding class, JSClassCreator is suitable for creating new JS classes which do not need to link to a specific native instance, or want (or need) to do their own handling binding/unbinding of those instances. The WeakJSClassCreator subclass adds the ability to automatically handle the binding/unbinding of a native object to/from the garbage collection system (in v8 this is called a "weak persistent pointer"). // JS-side constructor for MyClass objects...
Handle<Value> MyClass_ctor( Arguments const & argv );
// In your JS init code:
JSClassCreator binder( "MyClass", // JS class name
targetObject, // "global" object (the class we be installed here)
MyClass_ctor, // constructor callback
1 ); // # of internal fields to reserve
binder.Set( "funcOne", my_callback_func )
.Set( "valOne", String::New("hi there!") )
... more setters ...
.Seal(); // Seal() MUST come last!With the JSClassCreator, the client is responsible for:
(The WeakJSClassCreator does those things for your, but lets start with the basics before we skip to that.) Once that's in place we can do the following from JS: var obj = new MyClass(...); For the most basic cases, that's all that necessary, in terms of C++ setup. However, the most significantly missing feature of MyClass is memory management. v8 provides a callback mechanism with which we can be notified "when a context no longer needs an object." The callback mechanism is, due to v8 design decisions, not 100% reliable (there is never a guaranty that our callback will be called!), which makes it useless as a basis for resource management when destructors of certain types must be called to avoid resource leaks, data corruption, etc. To be clear, when your application exits, the memory used by your bindings will be freed, but if they are not GC'd before that then their destructors will not be called. For most types a missing dtor call is not all that critical, assuming the application is exiting anyway, but for some types it's essential for proper operation. Backing up a bit: the garbage collection problemLet's back up a moment and describe the overall problem: When an object is created in JavaScript, there is normally no client-visible "native" component to that value - it is an opaque type defined by v8, and has no visible association with a native value. That's all fine and good because the JS engine takes care of cleaning up those objects when it no longer needs them. When "binding" native classes to JS, we invariably associate a JS class with an instance of a C++ class (or some other native type - it need not be a class, e.g. it may be an opaque pointer type from a third-party C library). To do such bindings effectively we need at least these components:
In practice there are a few wrinkles in that story, but we will skip over those now for the sake of clarity, and will touch on them again later on. The documentation for v8's weak pointers support is a bit spotty, and one can easily spend hours experimenting before the first attempts at class wrapping work. Or we can use WeakJSClassCreator, which was authored after many hours of painful experimentation and sorting through the vague documentation regarding weak pointers in v8... WeakJSClassCreatorThe WeakJSClassCreator is a JSClassCreator subclass which uses C++ templates to create functions for binding and unbinding JS objects to/from native objects of a given type. It is based on the v8 "weak persistent pointers" concept, and thus its odd name. It is used mostly like JSClassCreator, the notable difference being a template argument. As a template argument it takes a native type on whos behalf it should act. In addition, client code must provide a WeakJSClassCreatorOps<MyType> class template specialization for each bound type. That class is described fully in the API docs and briefly shown here: namespace v8 { namespace juice {
template <>
struct WeakJSClassCreatorOps<MyClass>
{
/** The actual wrapped native type (may differ from the
template arg in some cases!). */
typedef MyClass WrappedType;
/** Number of internal JS Object fields client needs for
his own use. */
enum { ExtraInternalFieldCount = 0 };
/** Constructor function. */
static WrappedType * Ctor( Arguments const & /*argv*/,
std::string & /*exceptionText*/);
/** Destructor function. */
static void Dtor( WrappedType * obj );
/** The JS class name. */
static char const * ClassName() { return "MyClass"; }
};
}} // namespacesAgain, the semantics of the API are documented in full in the source code. In brief:
Now that we have a ctor and a dtor, we've got everything we need. From there we can let WeakJSClassCreator implement the more tedious bits for us: typedef WeakJSClassCreator<MyClass> CC;
CC c( objectToAddClassTo );
c.Set("propertyOne", String::New("Hi, world!"))
.Set("answer", Integer::New(42))
.Set(...)
.Set(...)
.Seal(); // must be the last setter call made on this object.Again, that's all there is to it. Now when we call new MyClass(...) from JS, the following will happen:
The native object can be fetched later by passing a bound JS object to one of the static methods WeakJSClassCreator::GetSelf() or WeakJSClassCreator::GetNative(), both of which work similarly but have very slightly different semantics. The former is expicitly intended to be pass argv.This() from a bound member function, whereas the latter can be passed a Handle<Value> and it will attempt to figure out if it's of the wrapped native type. The second option is useful when passing wrapped objects as arguments, as opposed to them being the this object. Both forms use a type-safe conversion and will not crash if passed a JS reference to an object with a different native type. It is often useful to destroy a bound object from script code. The classic example is a close() member of a Stream or Database class, which should free the underlying device handle. This feature is simple to implement: just call WeakJSClassCreator::DestroyObject() from the appropriate close/destroy routine. Clients can optionally tie in to their own "supplemental" GC (for when v8's GC doesn't kick in) by adding the logic to their WeakJSClassCreatorOps<T>::Ctor() and Dtor() routines. If client code also uses the v8-juice type conversions framework, wrapped types can easily be tied in to the automatic type conversions process (which really simplifies passing around wrapped types to arbitrary bound functions). Since that requires additional v8-juice functionality, and WeakJSClassCreator is intended to be "standalone" v8 code, that feature is covered in more detail on the ClassBinder page. Extra internal fieldsThe WeakJSClassCreatorOps<T> interface defines a constant integer value named ExtraInternalFieldCount, which can normally be left at 0. If a wrapper needs access to more internal fields for client-side use, define ExtraInternalFieldCount to be some other number. In v8 client code, clients conventionally store their native data in the internal fields numbered 0 to (N-1), where N is the number of fields. To avoid interfering with this convention, WeakJSClassCreator stores its internal binding at field number N (that is, one past the last client-defined field). When the class is defined the internal field count is set to (ExtraInternalFieldCount+1) and the wrapper internals use that last position for their own purposes. The client is then free to use slots 0..(N-1), as is conventional. That said, i haven't yet seen any v8 client code which would benefit from having more than one internal data field. Might be handy in some cases, but it would in general seem easier to bind only one native object (i.e. the T type wrapped by WeakJSClassCreator<T>) and store any other bound data in that native object. Real-world examplesThere are some real-world examples of using these types in the v8-juice source tree:
ClassBinderThe ClassBinder class template, described on its own page, extends WeakJSClassCreator type to add the ability to bind member functions and member variables to native types. See that page for more information. Real-world problems and caveatsFor general problems and caveats, see the v8 criticisms page. There are other caveats and gotchas which are associated with certain types of bindings, documented here in the hopes that they might help other developers avoid them. Heirarchical Native Types and Garbage CollectionWhen working with heirarchical native types, we must ensure that JS is aware of the parent/child relationships. If we do not, garbage collection might delete the child or parent before it should. For example, assume we have a Window class which, like all good GUI toolkits, uses parent/child relationships to control ownership of window objects: function createWindows()
{
var parent = new Window();
var child = new Window(parent); // parent now owns child
return parent;
// at this point, JS probably thinks that child is orphaned, and may clean it up.
}If, in the Window ctor, we set up the native parent/child relationship, but do not make JS aware that these is an association, then JS will not know that there is a connection between the parent and child objects. When it comes time to run GC, either the parent or the child need to be deleted (in the above example, it will almost certainly be the child). If the native bindings do not take care to disconnect the native objects when this happens, the next time the parent (or child) tries to do something it may refer to the now-deleted other object and cause a crash. Even if one avoids the dangling pointer problem, not telling JS about the association can lead to child windows mysteriously disappearing at curious times (when GC is run). One workaround for the disappearing child problem is to add a JS Array object to the parent and simply insert the child into it. The array's only purpose is to hold a reference to the child, so that GC will not consider it for destruction (until the parent (and its Array) go away). In cases like these it is, in my experience, necessary to include a few kludges in the native implementation to ensure that objects cannot try to dereference a JS-destroyed object. The ncurses plugin has several such kludges to help protect against premature deletion and prevent dereferencing of a dangling pointer. One simple and somewhat useful heuristic is: in the destructors for the bound types, do not actually destroy the objects if they have native parents, with the assumption that the parent object owns it. This can lead to other problems, however, such as the JS-side child being destroyed and the native one being there (and visible to the client), and the client then wonders why his calls to child.func() all throw with the error "could not find native 'this' object" (that's what the v8-juice binding framework says when the JS/C++ connection has been severed via the JS destruction process). |
Sign in to add a comment