Native Methods in Tamarin

Many of the native support libraries in Tamarin are written in ActionScript. Classes that come with the language such as Math, Object, String are partially written in ActionScript and are collectively known as the Builtin types. You can see all the definitions in core/*.as. (e.g. core/Math.as) However, the rest of the VM is written in C. The next question is, how are those methods declared/implemented in AS3 actually integrated with the rest of the VM? To explain this, lets use the example of the AS3 function Math::abs().

Math.as
package
{
[native(cls="MathClass", instance="double", methods="auto")]
public final class Math
{
public native static function abs (x:Number) :Number
}
}

The "final" operator means it is illegal to subclass Math. This also means that Math.abs() can be "early bound", or resolved immediately without a runtime lookup. This early binding is done by the ASC compiler. I'm not sure what ASC does, but the builtin.abc already has which method on what object to call in the bytecode. However, this information has to somehow be implemented in Tamarin. That's where nativegen.py comes in!

Nativegen.py is the glue between the ABC and the C files required by Tamarin itself. NativeGen.py reads the builtin.abc file and outputs C files used within Tamarin. All the files that have the comment "machine generated" such as builtin.cpp are emitted by NativeGen. Builtin.cpp contains a bunch of "thunks", or trampoline functions used during method invocation. When a native method is called, the AS3 method actually calls a C thunk, which then finally calls the requested method. Therefore, Tamarin has a mapping from ActionScript 3 method->C thunk. This map is created in builtin.cpp via Macros:

Builtin.cpp
AVMTHUNK_BEGIN_NATIVE_METHODS(builtin)
AVMTHUNK_NATIVE_FUNCTION(native_script_function_isXMLName, Toplevel::isXMLName)
AVMTHUNK_NATIVE_METHOD(Math_abs, MathClass::abs)
......
AVMTHUNK_END_NATIVE_METHODS()

Now let's reconstruct the macros and see what's being built for our example of the builtin map and the MathClass::abs method:

NativeFunction.h
#define AVMTHUNK_BEGIN_NATIVE_METHODS(NAME) \
static const NativeMethodInfo NAME##_methodEntries[] = {
//static const NativeMethodInfo builtin__methodEntries[] = {

Everything below is going to be wrapped in this array of method entries. Lets see how the method definition expands:

Step 1:
AVMTHUNK_NATIVE_METHOD(Math_abs, MathClass::abs)

#define AVMTHUNK_NATIVE_METHOD(METHID, IMPL) \
_AVMTHUNK_NATIVE_METHOD(ScriptObject, METHID, IMPL)

_AVMTHUNK_NATIVE_METHOD(ScriptObject, Math_abs, MathClass::abs)

Step 2:
#define _AVMTHUNK_NATIVE_METHOD(CLS, METHID, IMPL) \
{ { _NATIVE_METHOD_CAST_PTR(CLS, &IMPL) }, (AvmThunkNativeThunker)avmplus::NativeID::METHID##_thunk, avmplus::NativeID::METHID },

{ { _NATIVE_METHOD_CAST_PTR(ScriptObject, &MathClass::abs) }, (AvmThunkNativeThunker)avmplus::NativeID::Math_abs_thunk, avmplus::Math_abs) }

Step 3:
#define _NATIVE_METHOD_CAST_PTR(CLS, PTR) \
reinterpret_cast<AvmThunkNativeMethodHandler>((void(CLS::*)())(PTR))

// reinterpret_cast allows anything to be cast to anything!
// http://msdn.microsoft.com/en-us/library/e0w9f63b(VS.80).aspx

{ { reinterpret_cast<AvmThunkNativeMethodHandler>((void(ScriptObject::*)())(&MathClass::abs)) }, (AvmThunkNativeThunker)avmplus::NativeID::Math_abs_thunk, avmplus::Math_abs) }


Finally we close off the array:

#define AVMTHUNK_END_NATIVE_METHODS() \
{ { NULL }, NULL, -1 } };

When all the macros are expanded, we get the following:

static const NativeMethodInfo NAMEbuiltin_methodEntries[] { // BEGIN_NATIVE_METHODS
{ { reinterpret_cast<AvmThunkNativeMethodHandler>((void(ScriptObject::*)())(&MathClass::abs)) }, (AvmThunkNativeThunker)avmplus::NativeID::Math_abs_thunk, avmplus::Math_abs) }
{ { NULL }, NULL, -1 } // END_NATIVE_METHOds
};

Why is MathClass cast to a ScriptObject? Let's look at the inheritance chain of MathClass:

MathClass.h
class MathClass : public ClassClosure
class ClassClosure : public ScriptObject

Finally the mapping of avmplus::NativeID's to their thunks is created during the startup phase of Tamarin:

avmcore.cpp
void AvmCore::initBuiltinPool()
{
builtinPool = AVM_INIT_BUILTIN_ABC(builtin, this);
}

#define AVM_INIT_BUILTIN_ABC(MAPNAME, CORE) \
avmplus::NativeID::initBuiltinABC_##MAPNAME((CORE), (CORE)->builtinDomain, NULL)

PoolObject* initBuiltinABC_##NAME(AvmCore* core, Domain* domain, const List<Stringp, LIST_RCObjects>* includes) { \
NativeInitializer ninit(core, \
avmplus::NativeID::NAME##_abc_data, \
avmplus::NativeID::NAME##_abc_length, \
avmplus::NativeID::NAME##_abc_method_count, \
avmplus::NativeID::NAME##_abc_class_count); \
ninit.fillInClasses(NAME##_classEntries); \
ninit.fillInMethods(NAME##_methodEntries); \ fill in the builtin methodEntries
return ninit.parseBuiltinABC(domain, includes); \
}


Now we have one big mapping of ActionScript methods to their native thunks. We also have a mapping of the method MathClass::abs() to this AvmThunkNativeThunker thing. If we look at Math_abs_thunk we see:

builtin.h
#define Math_abs_thunk builtin_d2d_od_thunk

Finally, this means anytime we call Math.abs() in ActionScript, the builin_d2d_od_thunk() is executed. How does the NativeGen.py know which thunker to call? I think it is based on the method signature. It groups similar method signatures together into the same thunker. When the Math.abs() is finally executed, the thunker is called first. The thunker then finally resolves the actual Math.abs() method implemented and executes it. If we actually look at the generated thunks, we get something like this:

builtin.cpp
// Date_private__setTime
// Math_cos
// Math_ceil
// Math_acos
// Math_abs
// Math_atan
// Math_asin
// Math_exp
// Math_log
// Math_round
// Math_tan
// Math_sin
// Math_sqrt
// Math_floor
double builtin_d2d_od_thunk(AvmMethodEnv env, uint32_t argc, AvmBox* argv)
{
enum {
argoff0 = 0
, argoff1 = argoff0 + AvmThunkArgSize_AvmObject
};
(void)argc;
typedef AvmRetType_double (AvmObjectT::*FuncType)(double);
const FuncType func = reinterpret_cast<FuncType>(AVMTHUNK_GET_METHOD_HANDLER(env));

// func will point to Math_abs
return (*(AvmThunkUnbox_AvmReceiver(AvmObject, argv[argoff0])).*(func))( // Call Math::abs()
AvmThunkUnbox_double(argv[argoff1])
);
}


Finally we can get the absolute number:

MathClass.cpp
double MathClass::abs(double x)
{
return MathUtils::abs(x);
}