Hi, all!
The past years i've spent many, many hours trying to implement compile- and
runtime-type-safe conversions from JS to Native space and back. For a long
time i used external std::maps to hold information needed for tracing the
type of a given (void*) (the one v8 holds). However, there's been an answer
staring me in the face all along which i've missed entirely. Since this
approach is very generic, easy, and self-contained (i.e. not dependent on
anyone else's code), i thought i'd post it...
When we bind natives, we typically reserve 1 internal field in the
ObjectTemplate and store the native there in the constructor. i have never
yet encountered a use case which would benefit from more than one internal
field. Until today. We can use a second internal field to hold an arbitrary
opaque value (unique per wrapped type) to identify the type held in that
object. For example:
Here's our type ID:
static const int MyTypeID = 0; // value is irrelevant
We can use an arbitrary type, as long as MyTypeID's address is unique per
wrapped type.
During construction we:
obj->SetPointerInInternalField( 0, (void *)&MyTypeID ); // cast is
unfortunate but necessary
obj->SetPointerInInternalField( 1, myNewNative );
During extraction (some call it unwrapping) we:
if( &MyTypeID != obj->GetPointerInInternalField(0) ) {
... this is not one of ours. Fail the conversion now...
}
This is easy to genericize in templates and functors. Here's the complete
code (and docs) for a functor which converts a v8::Handle<v8::Value> to (T*)
using this type-safe approach...
/**
A concrete JSToNative implementation (to be used as a base class)
which does a type-safe conversion from JS to (T*).
T is the bound native type. A pointer of this type is expected
in the object-internal field specified by ObjectFieldIndex.
Each value to be converted is checked for an internal field
at the index specified by TypeIdFieldIndex. If (and only if)
that field contains the value TypeID is the conversion
considered legal. If they match but the native value stored
in field ObjectFieldIndex is NOT-a-T then results are undefined.
Note that the TypeID value is compared by pointer address, not
by its string contents (which may legally be NULL).
*/
template <typename T,
char const * & TypeID,
int InternalFieldCount = 2,
int TypeIdFieldIndex = 0,
int ObjectFieldIndex = 1,
bool SearchPrototypeChain = false>
struct JSToNative_ObjectWithInternalFieldsTypeSafe
{
private:
typedef char AssertIndexRanges
[(InternalFieldCount>=2)
&& (TypeIdFieldIndex != ObjectFieldIndex)
&& (ObjectFieldIndex>=0)
&& (ObjectFieldIndex>0)
&& (ObjectFieldIndex < InternalFieldCount)
? 1 : -1];
public:
typedef T * ResultType;
/**
If h is-a Object and it meets the requirements described
in this class' documentation, then this function returns
the result of static_cast<ResultType>(void*), using the (void*)
extracted from the Object. If the requirements are not met
then NULL is returned.
See the class docs for more details on the requirements
of the passed-in handle.
If SearchPrototypeChain is true and the object does not
contain a native then the prototype chain is searched
recursively. This is generally only required when bound
types are subclassed from JS code.
*/
ResultType operator()( v8::Handle<v8::Value> const & h ) const
{
if( h.IsEmpty() || ! h->IsObject() ) return NULL;
else
{
void const * tid = NULL;
void * ext = NULL;
v8::Handle<v8::Value> proto(h);
while( !ext && !proto.IsEmpty() && proto->IsObject() )
{
v8::Local<v8::Object> const & obj( v8::Object::Cast(
*proto ) );
tid = (obj->InternalFieldCount() != InternalFieldCount)
? NULL
: obj->GetPointerFromInternalField( TypeIdFieldIndex
);
ext = (tid == TypeID)
? obj->GetPointerFromInternalField( ObjectFieldIndex
)
: NULL;
if( ! ext )
{
if( ! SearchPrototypeChain ) break;
else proto = obj->GetPrototype();
}
}
return ext ? static_cast<ResultType>(ext) : NULL;
}
}
};
Note that i've made the TypeID template type (char const *&) instead of
(void const *). This is for reasons internal to my library (i store the
JS-side names of classes there). It could be changed to (void const *) but
because we cannot cast pointers in template parameter lists, a (void const
*) template parameter is rarely useful in my experience. For those who don't
know this: using string literals as template parameters does not work in an
intuitive manner but we can use (char const *&) to store strings in
templates at compile-time, e.g.:
struct Strings {
static char const * MyClassName;
};
char const * Strings::MyClassName = "MyClass";
typedef JSToNative_ObjectWithInternalFieldsTypeSafe<MyType,Strings::MyClassName>
Converter; // this is legal
Note, however, that file-local static vars cannot be used as template
parameters, which is why we generally need (for templating purposes) a
structure to put our static strings in.
Anyway...
Happy Hacking!
--
----- stephan beal
http://wanderinghorse.net/home/stephan/
--
v8-users mailing list
[email protected]
http://groups.google.com/group/v8-users