My favorites | Sign in
Project Logo
                
Search
for
Updated Today (11 hours ago) by sgbeal
Labels: Phase-UserDocs, Topic-CPlusPlus
BindingFunctions  
Binding near-arbitrary functions to JS objects.

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:

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:

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

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:

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:

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::bar>

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:

char const * cstring_test( char const * c )
{
    std::cerr << "cstring_test( @"<<(void const *)c<<") ["<<(c ? c : "<NULL>")<<"]\n";
    return c;
}

v8::InvocableCallback cb =
   convert::FunctionForwarder<1>::Invocable<char const *,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 a static variable as a member property to JS the objects:

v8::Handle<v8::ObjectTemplate> proto = ...;
typedef v8::juice::convert::PropertyBinder<MyNativeType> PB;
static int sharedInt = 42;
static double sharedDouble = 42.42;
PB::BindStaticVar<int,&sharedInt>( "sharedInteger", proto );
PB::BindStaticVar<double,&sharedDouble>( "sharedDouble", proto );

The syntax for binding to static member variables is the same, but the var name must be prefixed with the containing class name. e.g. &MyNativeType::sharedInteger.

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);
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:

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.

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
Hosted by Google Code