MetaEng – Part II

How does MetaEng unify calling across multiple scripting languages? How can it seamlessly tell multiple virtual machines to execute the same function with the same parameters?

Answer – not easily. Consider the following problem: The host application (SourceMod) wants to forward two parameters: a string and an integer. It must be sent to a C script, an AMX virtual machine, and a JavaScript context; because of this, we are restricted to only primitive data types. A JSstring is not a C string, which is not an AMX cell string. Even at the basic string we have total incompatibility. Furthermore, look at the calling methods:

C method: Push the integer, then the string pointer. Make the call, realign the stack pointer.
AMX VM: Allocate the string in the heap, copy it, push the string’s DAT offset and the integer, make the call. Reset the heap pointer.
JavaScript VM: Allocate and copy the string into the GC, then push the GC object and the integer. Make the call.

These methods are all quite different, and since an IForward can have a list of random scripts to call, it gets pretty messy. MetaEng solves this by creating a generic calling convention called Param_Blk. A Param_Blk is a block of raw memory formatted into 1+n*2 slots. Each slot is 4 or 8 bytes depending on the architecture (sizeof(long)).

Param_Blk[0] = Number of parameters
Param_Blk[n] = Pointer to data
Param_Blk[n+1] = Flags describing data type

So for our example above, MetaEng will create:

pb[0] = 2
pb[1] = (const char *)str
pb[2] = Param_String
pb[3] = (int *)&number;
pb[4] = Param_Int

MetaEng will then ask each IScript container for a way to call the public function. A dummy class called CFuncObject is used to store information on how to call a function. MetaEng will simply pass this value back into the scripting engine – meaning an AMX function index, JS function object, and C physical pointer are all under one abstract type. Example:

CFuncObject *cfo = script->GetFunction("name");

Then, each IScript is told to execute the CFuncObject and the resulting Param_Blk. Each IScript implementation in the MetaEngine is responsible for decoding the Param_Blk correctly. Admittedly, it’s not nice, and requires some ugly casting. Example:

int result = script->CallFunction(cfo, pb);

However, the _real problem is when slightly more advanced data structures are used. Take, for example, an array. A string is easy to pass because the null terminator marks the size… but each engine handles an array differently:

C: Just pass the pointer
AMX: Must be internally allocated and then freed
JS: Must be copied into the GC

So, this code would not work (given IScript *script)

int array[] = {1,2,3,4,5,6,7,8,9};
//Param_Ref flag means "reference as an array"
IForward *fwd = g_ForwardMngr.SingleForward(script, "my_callback", Param_Int|Param_Ref);
g_ForwardMngr.SimpleExec(fwd, array);

Nor would this work:
g_ForwardMngr.SimpleExec(fwd, array, 9);

… Because then the vararg decoding would get rather annoying. Instead, there’s a different level of control for fine-tuning the calling convention:

fwd->SetParam(0, array);
fwd->PrepareArray(0, 9);

Note that SimpleExec is a varargs wrapper for SetParam()s+Execute()

PrepareArray() solves this problem. For each script in the call list, it tells it to first prepare the array internally, then return a replacement parameter to pass instead. While C doesn’t need this, the AMX and JS VM return private data allocations that have to be passed instead.

There are probably a thousand different solutions to this problem – for the time being, I thought this one would be plenty effective. It allows for greater flexibility and control.

Tune in next time for a discussion about how C/C++ plugins are considered executable scripts and what happens when a MetaEng Context (IScript) is destroyed/unloaded.

Leave a Reply

You must be logged in to post a comment.