User Functions in SourceMod, Part 2 of 2

Last week, we saw that “callfuncs” and “forwards” from AMX Mod X were literally combined into one system. We also saw that eliminating the distinction between “single” and “multi” forwards allows for much more flexible coding. Today we’ll take a look at the other side to calling functions: dynamic (also called “fake”) natives.

Dynamic natives were added to make IPC easier for AMX Mod X scripters. They have a few hugely important benefits over forwards and callfuncs:

  • Plugins can act as modules. As a demonstration of this, the “amxmod_compat” plugin for AMX Mod X implements many AMX Mod natives merely as a script.
  • It’s faster and safer than callfuncs. The compiler handles all parameter checking and pushing for you.
  • It’s easier than callfuncs. You don’t have to implement a complex stock that finds the plugin, finds the function, pushes all its parameters, and then returns the result.

That said, how do they work? For AMX Mod X and SourceMod, respectively, any native must implement the following prototypes:

typedef cell (*AMX_NATIVE)(AMX *, const cell *);
typedef cell_t (*SPVM_NATIVE_FUNC)(IPluginContext *, const cell_t *params);

However, how do we make a native with this prototype, such that it knows which plugin and function to call? This is tricky. Technically, all we know about the native, while we’re inside the actual function, is the native index we’re using in the calling plugin. From there we can get its name, and with the name, we could look it up in a table of dynamic natives. Unfortunately, name lookup is slow. Another possible solution is making a secondary lookup table for each plugin, that says “native #X is dynamic native #Y in plugin #Z.” However, we opted for something more crafty, which uses less memory and less computation time.

Both AMX Mod X and SourceMod dynamically assemble the fake native function such that it knows exactly what to do. Basically, the native looks like this in assembly:

;cell_t FakeNativeRouter(IPluginContext *pContext, cell_t *params, void *pData);
extern FakeNativeRouter
 
;(IPluginContext *, cell_t *)
push DWORD_PTR    ;Push the fake native ID/pointer
push [esp+12]     ;Push the "params" array
push [esp+12]     ;Push the context pointer
call FakeNativeRouter
add esp, 12
ret

Once assembled, the new address is given to plugins as the native pointer. When called, it invokes FakeNativeRouter, which is the real meat of the solution. In steps, it:

  1. Decodes the index/pointer back to information about the plugin and function.
  2. Pushes the parameters and plugins onto a global stack.
  3. Calls the function in the plugin, where natives get parameter information from the global stack.
  4. The plugin returns to Core, and Core returns to the JIT/VM.

This is the optimal, albeit more complicated solution. The natives, as mentioned earlier, are nearly identical between AMX Mod X and SourceMod. The difference is in how the dynamic function is constructed.

AMX Mod X implements the dynamic native construction through an external assembly object.

  1. amxx_DynaInit is called. This caches the FakeNativeRouter address.
  2. amxx_DynaSize is called. This tells Core how many bytes are required for the memory buffer for building the function.
  3. amxx_DynaMake is called. This copies a dynamic native function “template” into the given executable buffer. It then modifies a “push” call such that it contains the dynamic native’s ID.
  4. The router function then receives an ID, which is an index into a vector of dynamic natives. From there, it knows how to execute into the plugin.

SourceMod’s system is much cleaner.

  1. CreateFakeNative is called in the JIT/VM. This handles executable memory allocation, casting, relocation, and function building. The JIT internally has macros for building functions in assembly, so it is both optimized and much more readable than the NASM version, which must copy and modify a function template.
  2. The router function receives a pointer, which it casts back into a structure that has all the information it needs. The reason it uses a pointer instead of an ID is subtle: since dynamic natives can be removed at runtime (plugin unloading), a simple vector does not suffice for efficiency or memory growth.

As you can see, dynamic natives are implemented with a rather hacky solution. On the other hand, they’re highly optimized and require very little additional memory to store.

Leave a Reply

You must be logged in to post a comment.