Source Engine plugins in C and other languages

December 10, 2023

Source Engine plugins are usually written in C++, the language of the Source Engine. The SDK is in C++, and the plugin interface is very C++ specific. However, this isn’t stopping anyone from writing plugins in other languages anyway. This article will provide an explanation of how to do just that, along with explaining what we are doing as well as how to interact with the engine generally. I will be using C to explain things as we go along, as much of the syntax is similar to C++ and C syntax is something that most people already know. I will also assume that you also know how to build a shared library using whatever build system you prefer.

The Interface Factory

Source is a fairly modular engine, consisting of a good number of different binaries, most of them located in the bin folder of your game installation. They talk to each other through what are called interfaces. To accomplish this, each shared library exports a function called CreateInterface with the following signature:

void *CreateInterface(const char*, int*);

This function function takes two arguments: the name of the interface, which usually consists of the name followed by a version number, and a pointer to write the optional return code. This is almost always a null pointer as you can just as easily just check if the return value is null.

This function returns pointers to interface objects, which are how the dynamically loaded objects export their functionality to the rest of the engine.

Now what exactly is an interface?

In the source engine, an interface is a singleton instance of a C++ abstract class. This means that it has member functions implemented virtually, which is crucial for how the interface works. Virtual functions are implemented Virtual functions are implemented by adding a pointer to a table of virtual functions before the data members of the class.

If you are looking at the engine code, the interface classes will usually start with an I, and the actual object returned will usually be of a class with the same name, but with a C instead of the I.

Here’s what a basic interface would look like for our purposes:

struct IExampleInterface {
    void **vtable;
    ...other entries here
}

Or, you could describe the vtable with a struct. This is useful to know exactly what function is being called in the code.

struct IExampleInterface {
    void *struct ExampleInterface_vtable;
};
struct ExampleInterface_vtable {
    void (__thiscall *do_something)(void *this);
}

What does this have to do with plugins?

A plugin acts the same way as any other engine binary. When the engine loads a plugin, it calls the plugin’s CreateInterface function asking for a specific interface, and from that function we will return our implementation of that interface to handle common game events.

That being said, let’s look at the plugin interface.

abstract_class IServerPluginCallbacks
{
public:
    virtual bool Load(CreateInterfaceFn interfaceFactory,
            CreateInterfaceFn gameServerFactory);
    virtual void Unload(void);
    virtual void Pause(void);
    virtual void UnPause(void);
    virtual const char *GetPluginDescription(void);      
    virtual void LevelInit(char const *pMapName);
    virtual void ServerActivate(edict_t *pEdictList, int edictCount, int clientMax);
    virtual void GameFrame(bool simulating);
    virtual void LevelShutdown(void);
    virtual void ClientActive(edict_t *pEntity);
    virtual void ClientDisconnect(edict_t *pEntity);
    virtual void ClientPutInServer(edict_t *pEntity, char const *playername);
    virtual void SetCommandClient(int index);
    virtual void ClientSettingsChanged(edict_t *pEdict);
    virtual PLUGIN_RESULT ClientConnect(bool *bAllowConnect, edict_t *pEntity,
            const char *pszName, const char *pszAddress, char *reject,
            int maxrejectlen);
    virtual PLUGIN_RESULT ClientCommand(edict_t *pEntity, const CCommand &args);
    virtual PLUGIN_RESULT NetworkIDValidated(const char *pszUserName,
            const char *pszNetworkID);
    // Added with version 2 of the interface.
    virtual void OnQueryCvarValueFinished(QueryCvarCookie_t iCookie,
            edict_t *pPlayerEntity, EQueryCvarValueStatus eStatus, 
            const char *pCvarName, const char *pCvarValue);
    // added with version 3 of the interface.
    virtual void OnEdictAllocated( edict_t *edict );
    virtual void OnEdictFreed( const edict_t *edict  ); 
};

Realistically, most plugins do not have any meaningful implementation for most of these functions, but we do need to populate the vtable with real functions so the game does not crash when it tries to call them. Let’s go!

Writing our bare plugin

Keep in mind that nearly every Source Engine game is 32-bit, so when building the plugin make sure you are building a 32-bit shared object.

On Windows, you must use clang or gcc to compile the plugin, as Microsoft’s MSVC compiler does not support the thiscall calling convention on C functions. Clang is reccomended over gcc though since it works a lot better on Windows.

To use thiscall on Windows and the default cdecl on Linux, we can define a macro:

#ifdef WIN32
#define VCALLCONV __attribute__((thiscall))
#else
#define VCALLCONV
#endif

Calling conventions are different schemes for how to pass and return arguments to functions in the generated assembly. They are outside the scope of this article.

Anyway, first write stub functions for each of the functions in the class above. Keep in mind you can use a void pointer to take a pointer of any type in without crashing. PLUGIN_RESULT, EQueryCvarValueStatus, and QueryCvarCookie_t are all ints. Return 0 from the functions that return PLUGIN_RESULT.

Here’s what a few of the functions should look like:

bool VCALLCONV Load(void *this,
                    CreateInterfaceFn ifacefactory,
                    CreateInterfaceFn gameserverfactory) {
    return false; // returns false when the plugin has loaded successfully
}

char VCALLCONV *GetPluginDescription(void *this) {
    return "Example plugin";
}

Next, let’s make our vtable and ‘class’ object.

struct CMyExamplePlugin {
    void *const *const vtable;
}

static const void *vtable[] = {
    (void*)&Load,
    (void*)&Unload,
    ...etc // follow the order in the class definition above
}

struct CMyExamplePlugin plugin_object = { vtable };

Keep in mind that some functions are different in newer engine branches, but this setup should work for anything except Portal 2 or Alien Swarm as far as I know.

The Interface Factory, Reprise

Now it is time to implement our own CreateInterface. Most Source games use interface version 3, so we will write our function like this.

void *CreateInterface(const char *name, int *ret) {
    if (!strcmp(name, "ISERVERPLUGINCALLBACKS003")) {
        if (ret) *ret = 0;
        return &plugin_object;
    }
    if (ret) *ret = 1;
    return 0;
}

On Windows, this function needs to be marked as __declspec(dllexport) This can be done with a few preprocessor directives again if you want to be cross platform.

Once you build your plugin, you can put the binary in the game directory. This is the folder next to the actually engine executable, like hl2 or portal. You can then load it with the plugin_load command.