My favorites | Sign in
Project Hosting will be READ-ONLY Thursday at 3:00pm UTC for up to 3 hours for network maintenance.
Project Home Downloads Wiki Issues Source
Search
for
BindingFunctions  
Binding near-arbitrary functions to JS objects.
Phase-UserDocs, Topic-CPlusPlus
Updated Jul 23, 2011 by sgbeal@gmail.com

See also: V8Convert, which provides a standalone distribution of an improved version of the API described below.

Introduction

The function forwarding API is an extension of the type conversion API, and it will help to first understand the concepts described on that page before continuing with this topic.

Most of this API was derived from functionality in the ClassBinder code, and may be familiar to anyone who's used that code. This code is, however, more generic, and can be used in conjunction with arbitrary class-binding approaches (e.g. the newer ClassWrap API).

The function binding API works using a very generic principal: convert arbitrary functions to v8::InvocationCallback instances, where v8::InvocationCallback is a v8 typedef which looks like:

    typedef v8::Handle<v8::Value> (*InvocationCallback)( v8::Arguments const & );

That is the signature of all "normal" JS callback functions, though there are a few special-case exceptions, like binding accessors functions to JS properties (those routines have different signatures).

In the general case, we bind functions implementing the InvocationCallback interface to a JS object like this:

jsObj->Set( String::New("foo"),
            FunctionTemplate::New( MyInvocationCallback )->GetFunction() );

(though macros are often used to shorten that somewhat.)

This framework allows us to convert nearly arbitrary functions to InvocationCallback functions, which in turn allows us to bind those functions using the v8-standard approach shown above.

An example of what a binding looks like, here we bind the Unix-standard sleep() function to an arbitrary JS object:

jsObj->Set( String::New("sleep"), FunctionTemplate::New(
      convert::FunctionForwarder<1>::Invocable<unsigned int,unsigned int,::sleep>
    )->GetFunction() );

That strange FunctionForwarder<1> bit literally creates a v8::InvocationCallback function which forwards one unsigned int argument to sleep() and returns an unsigned int value. If any of the conversions are illegal (e.g. the function returns a double instead of an unsigned int), a compile-time error will be generated.

Properties of this framework

The general properties of this framework are:

  • It can bind native free functions and member functions to JS object functions.
  • It can bind static variables and member variables to JS properties.
  • Binding is done with compile-time type safety.
  • Function bindings generate v8::InvocationCallback functions, and thus require no extra internal objects to hold the Native-to-JS function mapping (as is the case in most binding frameworks).
  • Any convertible function can be bound to any JS object. This makes it trivial to add many standard C functions, or common library functions, to JS.
  • Template specializations can be used to customize certain aspects of it. The most notable example is that a JSToNative<T> and NativeToJS<T> specializations must normally be created to add automatic conversion support for custom JS/Native-bound T types.
  • The library supports, by default, functions taking up to 10 arguments. That said, all generators taking more than 0 arguments are generated by a shell script, and we can generate specializations for any given number of arguments.

Header files

The required header files:

The code is all in the v8::juice::convert namespace.

Tip: Aside from those two files and a few script-generated files which they use, this code is standalone. It is independent of other v8-juice code and can easily be extracted for use in unrelated projects.

Free Functions

For a free (non-member) function to be bindable they must meet these requirements:

  • All argument types must be convertible from JS to Native objects using v8::juice::convert::JSToNative<T>.
  • All return types must be void or convertible from Native to JS using v8::juice::convert::NativeToJS<T>.

Out of the box this supports at least the following argument/return types:

  • The standard numeric types (int, double, bool, short, etc.).
  • std::string
  • std::list, std::vector, and std::map, where the container types may contain any other convertible types (nested arbitrarily deep).

It is normally easy to add conversions support for custom types, but argument types which are known to be problematic, or are otherwise not directly supported:

  • (void *). This cannot work for the general case. v8 allows us to do it, but dereferencing the object in JS will cause a crash, and the 100% lack of type-safety would eventually lead to crashes when the objects travel between JS and native and someone passes the wrong (void*) somewhere.
  • ([const] char *) can be used, but have some notable limitations. See below for more details.

Here are examples of functions which can be bound using this framework, including the template required to construct a v8::InvocationCallback function from them.

void foo();
// ^^^ FunctionForwarder<0>::Invocable<void,foo>

int bar( double );
// ^^^ FunctionForwarder<1>::Invocable<int,double,bar>

typedef std::map< int, std::string > MapT;
MapT getMap();
// ^^^ FunctionForwarder<0>::Invocable<MapT,getMap>

typedef std::list< MapT > ListT;
ListT getMapList();
// ^^^ FunctionForwarder<0>::Invocable<ListT,getMapList>

T * getNative(); // IFF CastToJS<T>() works!
// ^^^ FunctionForwarder<0>::Invocable<T*,getNative>

void setNative(T *); // IFF CastFromJS<T>() works!
// ^^^ FunctionForwarder<1>::Invocable<void,T*,setNative>

(As you may have guessed, the numeric template argument is the number of arguments in the bound function's signature.)

Member Functions

For a given native class T, we can bind T::foo() (i.e. member functions) to a JS object provided the following requirements are met:

  • The JS object the function is bound to must be convertible to a (T*) using CastFromJS<T>().
  • The member meets the same basic requirements as for function bindings (see above).
  • The member may not return a non-const reference, but this is a bug and will hopefully be resolved. (TODO: check this! i think that was fixed by treating it like a pointer conversion and throwing on a null pointer.)
  • The member may not have a throw(...) clause (it might work, but it's untested).
  • The member may not be variadic (i.e. taking ellipse (...) arguments).

Bound member functions may be const or return void and they may be templates.

For example, the following member functions are legal for binding purposes:

void T::foo();
// ^^^ MemFuncForwarder<0>::Invocable<T,void,&T::foo>

size_t T::bar() const;
// ^^^ MemFuncForwarder<0>::Invocable<T, size_t,&T::bar>

typedef std::vector<std::string> StringVec;
StringVec T::split( std::string const & ) const;
// ^^^ MemFuncForwarder<1>::Invocable<T, StringVec, std::string const &,&T::split>

template <typename Y> Y T::add( Y y1, Y y2 ) const; // IFF CastTo/FromJS<Y>() works
// ^^^ e.g. MemFuncForwarder<2>::Invocable<T,int,int,int,&T::add<int> >

Foo * T::getBuddy(); // IFF CastToJS<Foo>() works
// ^^^ MemFuncForwarder<0>::Invocable<T,Foo*,&T::getBuddy>

void T::setBuddy(Foo *); // IFF CastFromJS<Foo>() works
// ^^^ MemFuncForwarder<1>::Invocable<T,void,Foo*,&T::setBuddy>

Regarding CastToJS<T> and CastFromJS<T>(): in short, CastFromJS() can be done fairly generically, but CastToJS() requires some sort of underlying binding mechanism from which we can map a native pointer to its JS object representation. Those bits come in the form of a higher-level class binding mechanism (like the ClassWrap API).

The MemFuncForwarder class has a counterpart, TMemFuncForwarder, which is identical in every way except that it is templatized at the class level instead of the function level. For example:

//Assume we have:
int T::func();

typedef MemFuncForwarder<0> MF;
typedef TMemFuncForwarder<T,0> TMF;

v8::InvocationCallback IC;
// These are equivalent:
IC = MF::Invocable<T,int,&T::func>
IC = TMF::Invocable<int,&T::func>

Limitations vis-a-vis inheritance

TODO: explain this in gross detail. There are workarounds for some aspects, but they require support from a more specific class-binding mechanism (e.g. ClassWrap).

TODO: Some points to discuss:

  • How virtual method lookup across JS/Native inheritance does not come for free, and requires fairly detailed support from a higher-level framework. This is implemented in ClassWrap, for example.
  • How JS-triggered destruction of bound objects needs to be sure and pick up the proper native destructor function in the face of inheritance.
  • How to bind to inherited native functions. e.g. Class B inherits from native A, and we want to bind an inherited A::*func to a B object. (How DO we do this? i think adding using func to the inherited interface might be enough, but that needs to be tested.)

The (char const *) Problem

The JSToNative API cannot convert JS handles to (char const *) because the lifetime of the converted value expires when the JSToNative conversion returns, meaning the caller gets a pointer which refers to undefined memory.

However, as of 20091121 (commit r1092) the function binding framework supports C-style string arguments with the following limitations:

  • The C-strings must be NULL or null-terminated. It cannot be used with binary data.
  • The bound function must not hold the pointer it is passed after it returns. It must either consume or ignore the input, and keep no copy of the pointer itself.
  • The internal conversion process uses v8::String::Utf8Value, and "should" be safe for use with functions accepting ASCII or UTF8 input. Behaviour with any other encodings is undefined.
  • If passed a JS null or undefined, it converts to a literal NULL, otherwise it will convert to a string (though possibly empty). This is significant for functions which treat NULL different than an empty string (like strlen(), which does not like NULL).

This support allows us to convert functions to v8::InvocableCallback functions like:

std::string cstring_test( char const * c )
{
    std::cerr << "cstring_test( @"<<(void const *)c<<") ["<<(c ? c : "<NULL>")<<"]\n";
    return c ? c : "";
    /* we don't return (char const *), and c, because c's lifetime
       is undefined as soon as this function returns!
    */
}

v8::InvocationCallback cb =
   convert::FunctionForwarder<1>::Invocable<std::string,char const *,cstring_test>;

If we bind that to JS with the name cstr:

var jstr = "Hi, world!";
jstr = cstr(jstr); print(jstr);
jstr = cstr(undefined); print(jstr);
jstr = cstr(null); print(jstr);
jstr = cstr("Bye, world!"); print(jstr);

The output looks something like:

cstring_test( @0x984acbc) [Hi, world!]
Hi, world!
cstring_test( @0) [<NULL>]
null
cstring_test( @0) [<NULL>]
null
cstring_test( @0x984acbc) [Bye, world!]
Bye, world!

When implementing custom function forwarders and you need to pass a C-style string, you have at least these options:

  • Use a proxy class for the conversion. Conventionally we use std::string for this, and use str.c_str() to fetch the C-style string.
  • Use convert::ArgCaster, paying very careful attention to the ArgCaster API documentation, to do the conversion.
  • "Manually" do the conversion with a v8::String::AsciiValue or v8::String::Utf8Value.

Binding to Static or Member Variables

This framework can also bind JS member properties to static or member variables. It does so by using templates to create proxies for the v8::AccessorGetter and v8::AccessorSetter interfaces. Because of this, it is only possible to bind members or static vars at the class (JS prototype) level, and not the level of the individual object. As a side-effect of that, we can only bind statics to JS objects for which CastFromJS<T>() works (that feature is normally provided by a higher-level class creation mechanism like ClassBinder).

As an example, assume we are creating a new JS class in C++. Assume the JS class is bound to the native class MyNativeType, and JSToNative<MyNativeType> has been specialized specialized so that CastFromJS<MyNativeType>() will work as expected. To bind members (static or not) or non-member static variable as a member properties to JS the objects:

// Static members we want to bind:
int MyNativeType::sharedInt = 42;
double MyNativeType::sharedDouble = 42.42;

// Prototype object to bind them to:
v8::Handle<v8::ObjectTemplate> proto = ...;

// Convenience typedef:
typedef v8::juice::convert::PropertyBinder<MyNativeType> PB;

// Bind the members to JS:
PB::BindStaticVar<int,&sharedInt,&MyNativeType::sharedInt>( "sharedInteger", proto );
PB::BindStaticVar<double,&sharedDouble,&MyNativeType::sharedDouble>( "sharedDouble", proto );

// Or bind to non-member statics:
static int staticInt = 42;
static double staticDouble = 42.42;
PB::BindStaticVar<int,&staticInt>( "staticInt", proto );
PB::BindStaticVar<double,&staticDouble>( "staticDouble", proto );

// Or bind to non-static members:
PB::BindMemVar<int,&MyNativeType::anIntMember>( "anIntMember", proto );
PB::BindMemVar<double,&MyNativeType::aDoubleMember>( "aDoubleMember", proto );

Now the following JS code will access the native integer shown above:

var m = new MyNativeType();
print( m.sharedInteger ); // == 42
++m.sharedInteger;
print( m.sharedInteger ); // == 43

So far i have no found a way (in the v8 API) to bind these to a constructor, such that MyNativeType.sharedInteger will work (however, MyNativeType.prototype.sharedInteger does). Likewise, i have found no way to bind these selectively to individual objects.

Creating overloaded JS bindings

As of r889 it is possible to bind a JS function to multiple native functions (including member funcs), such that the argument count will determine which one gets called. This is a fairly advanced feature and requires some knowledge of topics like "type lists".

The list of requirements for overload binding:

  • No two native overloads may take the same number of arguments.
  • When resolving overloads, the first bound func which matches the given argument count exactly will be called.
  • If the overload is a v8::InvocationCallback function it can take any number of arguments (this requires a trick shown below).

For example's sake, assume we have these functions which we want to bind to a single overloaded JS method:

bool BoundNative::overload();
int BoundNative_overload( int i );
double BoundNative_overload( int i, double d );
v8::Handle<v8::Value> BoundNative_overload( v8::Arguments const & argv );

Notice the mix of members and non-members, and one implements the v8::InvocationCallback interface.

Now let's bind them to a JS object:

v8::InvocationCallback IC = 
 convert::OverloadInvocables< tmp::TypeList<
      convert::InvocableMemFunc0<BoundNative,bool,&BoundNative::overload>,
      convert::InvocableFunction1<int,int,BoundNative_overload>,
      convert::InvocableFunction2<double,int,double,BoundNative_overload>,
      convert::InvocableCallback<-1, BoundNative_overload>
    > >::Invocable
myObject->Set( String::New("overloaded"),
               FunctionTemplate::New( IC )->GetFunction() );

Note that the OverloadInvocables type has a TypeList as its first argument. Type lists are an advanced C++ topic covered in gross detail in Modern C++ Design. They allow us to build up lists of types. In this case, each type in the list must provide the following definitions:

static const int Arity = NumberOfArgs;
static v8::Handle<v8::Value> Invocable( v8::Arguments const & argv );

Not coincidentally, that is the interface used by much of the function binding code, so this allows us to pass the various function binder helpers.

In the case of InvocableCallback<> (a proxy class for a v8::InvocationCallback) the Arity value may be less than 0, which tells the overload resolver to use that overload regardless of the number of arguments. Thus this can be used to catch any unresolved calls.

Now in JS code:

myobj.overload(); // calls: bool BoundNative::overload()
myobj.overload(13); // calls: int BoundNative_overload(int)
myobj.overload(13, 17.7); // calls: double BoundNative_overload(int,double)
myobj.overload(1,2,3,4); // calls: Handle<Value> BoundNative_overload(Arguments)

Tips and Tricks

Discardable or non-convertible return types

The FunctionForwarder and MemFuncForwarder classes each have a function called InvocableVoid which works just like Invocable (it generates a v8::InvocationCallback), but it does not try to convert the return type. This can be useful in the following cases:

  • When there is no legal Native-to-JS conversion for the return type.
  • When you do not want to instantiate the template for such a conversion (e.g. to avoid a compile error, or to avoid a semantic error).
  • Any time you want the JS version of the function to return undefined to the caller.

It is not necessary (though it is legal) to use InvocableVoid instead of Invokable when the return type is actually void, as function overloads take care of dispatching FunctionForwarder<N>::Invocable<void,...> and MemFuncForwarder<N>::Invocable<T,void,...> via InvocableVoid.

Getting the native this pointer inside non-member bound functions

When binding non-member v8::InvocationCallback functions to JS objects, the functions can get access to the native this pointer if they are called via the binding framework. From inside the bound function:

  BoundNative * self = convert::JSToNative<BoundNative>( arguments.This() );

If that returns non-null then the function was called in the context of a BoundNative object.

Beware of this

If references to bound member functions or variables are given to other objects of different types, accessing them might invoke an exception because the internal dispatching mechanism cannot find the associated native this object. For example:

var b = new BoundNative();
var o = {};
o.func = b.someBoundFunction;
o.func(); // will throw an exception

Examples

Here are some a complete example of what the binding process itself looks like:

Search those files for "Instance()" to find the relevant parts.

Here's some JS code related to the above C++ code:


Sign in to add a comment
Powered by Google Project Hosting