User Functions in SourceMod, Part 1 of 2

Inter-plugin communication was one of the most important features of AMX Mod X, and thus it was equally as important in SourceMod.

IPC started in AMX Mod X with the “callfunc” system, which we thought was enough to suffice for all IPC usage. It was implemented by PM OnoTo and appeared in the first AMX Mod X release. Simply put, they allowed you to call any public function in any plugin with three major steps:

  1. Start the call, given a plugin name and a public function.
  2. Push the parameters you want to send.
  3. End the call and get the return value.

The idea behind callfuncs was that plugins could share information between each other. It worked great when you wanted to notify other plugins of certain events. However, that only represented one direction of data flow, owner to listener. The opposite, listener to owner, was much harder with callfuncs. Say you wanted to provide external functionality to plugin B from another plugin A. You had to make an include file with something like:

stock my_function(params)
{
    callfunc_begin("my_real_function", "plugin_A.amxx")
    callfunc_push(params);
    return callfunc_end();
}

Then plugin B would have to call this stock, and hope that plugin A was loaded. To solve this problem, AMX Mod X introduced a three tier function calling system:

  1. “Callfuncs,” the lowest level of function calling. Version 0.1+
  2. “Dynamic Natives,” allowing plugins to create natives. Version 1.5+
  3. “Forwards,” allowing plugins to make event/callback systems more easily, especially with dynamic natives. Version 1.7+

In SourceMod, it’s a bit different. Internally, SourcePawn is object oriented. Callfuncs are equivalent to the IPluginFunction class, and forwards are equivalent to the IForward class. The trick is that they both derive from one type, ICallable, and share the same calling convention. Because of this, “callfuncs” and “forwards” are one and the same in SourceMod, and the methods for calling both are nearly identical, both internally and for plugins.

There is one subtle difference between a forward call and a function. A function is technically a “temporary” object. If the plugin unloads, the function unloads as well. Special care must be taken to remove references to functions that are being unloaded.

On the other hand, forwards are automated collections of functions. Calling the forward calls all functions in its collection. They also intrinsically remove the unloadability complication. Whenever a plugin is unloaded, all forwards are “cleansed” of the removed references. While forwards are more expensive to create and maintain, they are much easier to use and manage, especially since unlike AMX Mod X, they are true collections, and thus you don’t need to worry about storing individual function IDs in arrays. They allow completely arbitrary functions to be packed in.

There is one other subtle difference between SourceMod’s forward system and AMX Mod X’s, which has been touched upon before. In AMX Mod X, there were “multi forwards” and “single forwards.” A multi forward called a function of the same name in every plugin; for example, “plugin_init” was a multi forward. A single forward was simply one function, and a multi forward was a named global function.

In SourceMod, this design is split for usability purposes. IPluginFunction is the “single forward” equivalent, save for it being unsafe to cache. IForward is the “multi forward” equivalent. Derived from that is the new type, IChangeableForward, which lets you modify the internal collection. This important feature allows users to implement per-function hooks without worrying about local memory constraints. An IChangeableForward can act as either a multi forward or a single forward, or both.

As a quick example, AMX Mod X code for per-function events looked like:

#define MAX_FUNCS 80
new g_FuncsNum = 0
new g_FuncArray[MAX_FUNCS]   //Array of single functions
 
AddFunction(const plugin[], const name[])
{
    if (g_FuncsNum >= 80)
        return 0
    g_FuncArray[++g_FuncsNum] = CreateOneForward(plugin, name)
    return 1
}

A SourceMod version might look like:

new Handle:g_Funcs
 
AddFunction(Handle:plugin, Function:func)
{
    if (g_Funcs == INVALID_HANDLE)
        g_Funcs = CreateForward()
    AddToForward(g_Funcs, plugin, func)
}

That covers callfuncs and forwards, which have been combined into one system for SourceMod. On Monday we’ll see how dynamic natives are different from AMX Mod X, and how they are implemented. There’s something special planned for Friday, so stay tuned!

Leave a Reply

You must be logged in to post a comment.