|
BindingFunctions
Binding near-arbitrary functions to JS objects.
IntroductionThe 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 frameworkThe general properties of this framework are:
Header filesThe 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 FunctionsFor 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 FunctionsFor 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 inheritanceTODO: 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:
The (char const *) ProblemThe 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:
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:
Binding to Static or Member VariablesThis 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 bindingsAs 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:
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 TricksDiscardable or non-convertible return typesThe 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. ExamplesHere 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