MetaEng – Part III

What’s up with C++ plugins? Are they native code or what, and how does MetaEng work with them? Also, what happens when MetaEng Contexts (IScripts) are destroyed or unloaded? Wouldn’t all of the registered forwards cause a crash?

Well, C++ plugins are easy to explain: they’re just DLLs being wrapped around an IScript implementation. Their bytecode execution and API is handled directly by the operating system. In fact, the code for MetaEng-C is almost trivial, as IScripts map to system calls fairly well (Load==LoadLibrary, GetFunction==GetProcAddress). Unfortunately, it is inherently impossible to do variadic argument pushing in C/C++, so Param_Blk decoding must be done at the assembly level.

The stdarg/va_arg stuff in the standard library deals with retrieving already pushed arguments, but for reasons that probably have to do with weird syntax or non-usefulness, you can’t say “I have a variable number of parameters, and I’d like to just throw them into a function pointer.” I had numerous arguments with ex-developer malex about this, and he came up with several solutions involving pure C (such as passing va_list pointers or doing struct memory hacks), but the end result is that the safest way to do it is to use native machine code.

That is part of the reason that Param_Blk structure is so cryptic – it was designed around the first MetaEngine being able to decode a list of parameters, in memory, using assembly, and construct the required stack frame to call the function. The only difficulty here (which malex would not let me forget) is that each processor and each operating system has a different method of calling. x86 was a simple list of push operations, but the alpha and especially AMD64 ports required an annoying decoding routine.

Nonetheless, they work, and in theory these functions could be used to port languages like “Angel Script” to other CPUs, which (if I recall correcty) is x86 specific because it decodes strings as C calling conventions and uses assembly to bind them to bytecode. Oops, getting off track here…

Now, let’s take a breather from the low level stuff and move back to the overall architecture of MetaEng. Admin-Mod, AMX Mod, and AMX Mod X did not allow you to remove plugins during runtime. Why?

The reason is that it’s a pain. If you unload a plugin, every forward that contains callbacks into the script must be told the callback no longer exists. Otherwise, events will still be called in the deleted script, causing a crash. To get around this is a lot of coding for something that may have little to no use. Yet SourceMod supports it. How does it deal with this, and why bother?

There are two ways to ensure safety when creating MetaEng callbacks. The first is to always use IVPluginManager::RegisterForward(), which creates a global callback registered for every plugin that has it. This will cache your forward inside the plugin manager, so when the plugin manager unloads or loads a plugin, the forward will be automatically updated. It will never be freed by the PluginManager– you’re responsible for this — but you can be assured that if your IForward conforms, unloaded scripts will be filtered out automatically.

However, this is only used for registering global forwards, not specific script callbacks (a single function in a single IScript context). If you’re registering specific callbacks, there’s another process – based on Managers. If you are registering single forwards yourself, you will need to keep track of them. Our example class will be IVMessageManager, which (for the sake of argument) is meant to let plugins register callbacks for client messages. Our manager will be keeping a list of all the assigned forwards.

class IVMessageManager : public CInterface
//...
class CMessageManager : public IVMessageManager, public IUnloadable
{
public:
    void RemoveScript(IScript *); //go through and clean m_Forwards of IScripts
private:
    std::list<iforward *> m_Forwards;
};    
//...
CMessageManager g_MsgMngr;
g_PluginMngr->AddUnloadable(dynamic_cast<IUnloadable *>(&g_MsgMngr));

This will register your manager class as an IUnloadable, which must implement at least one function: RemoveScript(IScript *). RemoveScript() is called every time a script context is about to be destroyed and provides you with a nice callback for making sure you’ve removed all instances of it.

Why bother? Quite simply, so you can unload and reload a plugin for beta testing. Other than that, there aren’t too many reasons you would need this functionality, since a map change resyncs the plugin list in SourceMod.

That’s all for this MetaEng update – next time we’ll cover how Natives work (calling C/C++ host functions from scripts).

Leave a Reply

You must be logged in to post a comment.