Writing Extensions
SourceMod's Extension System is a powerful way to greatly enhance the flexibility of your SourceMod plugins. You can take full advantage of SourceMod's Core, using the interfaces it provides, to create C++ plugins, plugin events, plugin native functions, and shared interfaces. Extensions can also hook into Metamod:Source.
This article goes into the details of creating a SourceMod Extension. Knowledge of C++ is required.
Contents
SDK Structure
First, download the SourceMod SDK from the SourceMod website. The directory structure looks like this:
- extensions
- geoip/ - Source code to the GeoIP extension
- mysql/ - Source code to the MySQL extension
- threader/ - Source code to the Threader extension
- plugins/ - Source code to all of SourceMod's plugins
- include/ - The include files which document plugin API
- public/ - Interface files for SourceMod's Core Interfaces
- extensions/ - Interfaces that are provided by extensions
- sample_ext/ - The Sample Extension SDK
- sourcepawn - The include/interface files for SourcePawn
- sourcepawn/
- compiler/ - The SourcePawn Compiler
Starting an Extension
For simplicity, this article will assume you are using the SDK base files. These are:
- smsdk_config.h - Configuration settings (we will be editing this)
- smsdk_ext.h - Header for SDK wrappers (usually never needs to be edited)
- Note: Sometimes, this may be updated by the SourceMod Dev Team. Using a newest version is recommended.'
- smsdk_ext.cpp - Source for SDK wrappers (usually never needs to be edited)
- Note: Sometimes, this may be updated by the SourceMod Dev Team. Using a newest version is recommended.'
- extension.h - User file for main extension header
- extension.cpp - User file for main extension code
The extension.* files are not technically part of the SDK. However, they are provided to give users a starting point and some documentation.
Note: For help setting up Visual Studio, please see Setting up Visual Studio.
The first step of creating your extension is to configure it. Open the smsdk_config.h file and edit each of the following defines. Use the information below to guide you.
- SMEXT_CONF_NAME - The general name for your Extension (should be short).
- SMEXT_CONF_DESCRIPTION - A short description of what your extension does.
- SMEXT_CONF_VERSION - A version number string. By convention, SourceMod uses the four set build number notation. I.e. for 1.2.3.4:
- 1 is the "Major" release version.
- 2 is the "Minor" release version.
- 3 is the "Maintenance" or "Revision" version.
- 4 is the "Build," usually an internal source control number.
- SMEXT_CONF_AUTHOR - Your name (or whoever/whatever authored the plugin).
- SMEXT_CONF_URL - The URL/homepage of this extension.
- SMEXT_CONF_LOGTAG - The logtag your extension will use for SourceMod logging. By convention, try to keep this under ten characters or so, and to make it all caps.
- SMEXT_CONF_LICENSE - Right now, the SourceMod Dev Team mandates that 3rd party extensions must be under an Open Source license. Putting a license abbreviation or very short message here is appropriate. For more information, see SourceMod License.
- SMEXT_CONF_DATESTRING - You do not need to modify this.
- SMEXT_CONF_METAMOD - If your plugin requires using SourceHook or Metamod:Source, uncomment this line.
Next, you may want to change the name of the default class that extension.h declares. To do this...
- Open extension.h and change "Sample" in this line:
class Sample : public SDKExtension
- Open extension.cpp and change the two lines to correspond to your new class name. You can also change the global singleton that's declared, although you cannot remove the SMEXT_LINK line (this lets SourceMod load your extension).
Visual Studio Users
Make sure you change the name of your .dll file. The naming convention for SourceMod extensions is name.ext.dll, where name is a short name for your extension. You are free to change this, but it is highly recommended that you keep the .ext.dll in tact.
You can do this by going to Project -> Properties -> Configuration Properties -> Linker -> General. Set the Output File option appropriately.
Linux Users
Simply edit the Makefile and change the BINARY line near the top. Make sure you do not add any trailing spaces at the end of the name, or the build won't come out right.
You should now be able to compile a blank extension!
Creating Native Functions
Most of the time, an extension exists to add "native functions" to the plugin system. Native functions are simply the functions that plugins can use in SourceMod. They are coded in C++ and have a short prototype declaration in an include file.
Prototyping
The first step to creating a native is to spec it, or prototype it. This is a simple but often time consuming process, but if you get in the habit of it, your documentation will be much better. Let's first create a very simply native. Here is a prototype for it, which we will place in sample.inc:
/** * Returns the square of a number. * * @param num Number to square. * @return The square of num. */ native SquareNumber(num);
The native keyword tells the compiler that this function exists in an outside source. The comment style is similar to doxygen or Javadoc, and is preferred by the SourceMod Development Team.
Implementing
Now that our function is documented, it's time to create it! Each native function is bound to a C++ function. The prototype for this function looks lile:
cell_t Function(IPluginContext *pContext, const cell_t *params);
A quick explanation of these types:
- cell_t - This is a 32bit signed integer, the generic data type for Pawn.
- IPluginContext - This interface provides Virtual Machine functions for retrieving or modifying memory in the plugin.
- params - This is the "stack" of parameters that the plugin passed. It is an array from [0..N] where 0 contains N.
- For example, if one parameter was passed, and the parameter is 25:
- params[0] == 1
- params[1] == 25
- Even though it tells you how many parameters are passed, you do not need to verify this number. The compiler will always pass the correct number of parameters in. The only time you need to verify it is for backwards compatibility or variable parameter lists, which will be discussed later.
- For example, if one parameter was passed, and the parameter is 25:
Now, let's write our native function:
cell_t SquareNumber(IPluginContext *pContext, const cell_t *params) { cell_t number = params[1]; return number * number; }
Binding
Now, the final step is to bind our native. Natives are bound in native lists. Each list must be terminated by a NULL bind. Example:
const sp_nativeinfo_t MyNatives[] = { {"SquareNumber", SquareNumber}, {NULL, NULL}, };
The first column/member is a string containing the name you've assigned to the native. The second column/member is the C++ function you're binding to. Here, both contain the same name, but it is certainly possible to bind a function to any valid Pawn function name, and likewise, you can bind one function to more than one name.
Lastly, you must tell Core about your native list. There are two places that are good to do this. Whichever you choose, you must uncomment the function in extension.h and implement it in extension.cpp.
- SDK_OnLoad - This is called when your extension is first loaded. If you do not require any outside interfaces, it is safe to add natives here.
- SDK_OnAllLoaded - This is called when all extensions have been loaded. This is generally a better place to add natives.
Let's say we choose SDK_OnAllLoaded, we'd then have code like this:
void Sample::SDK_OnAllLoaded() { g_pShareSys->AddNatives(myself, MyNatives); }
Note: myself is a global variable declared in the SDK. It is a pointer that identifies your extension.
For more information on writing natives, see Natives (SourceMod Development).
Creating Events/Forwards
Events are an integral part to SourceMod. An Event is formally called a Forward. Forwards are functions inside a plugin which are not called by the plugin itself, but are triggered from an external source. For example, OnClientConnect is a forward which is triggered when a player connects to a server.
There are two major types of forwards:
- Managed: This forward is global and has a name. To use this forward, the function must be declared as public inside the user's script. Any plugin having this public function will receive the event.
- Unmanaged: This forward is private in nature. For example, the HookUserMessage native lets users specify a function to be called when a specific user message is sent. This is done with an unmanaged forward, and adding functions to this forward is not automatic. In general, function calls for unmanaged forwards are not public, although they can be.
Managed Forwards
Let's say we want to create a forward that will be called when a player uses say chat. For simplicity, let's assume you already have a function that tells you when a player says say chat. We will just be creating the forward for it.
As we did before, first let's prototype our global forward. In our include file, we add this:
/** * @brief Called whenever a client says somethign in chat. * * @param client Index of the client. * @param text String containing the say text. * @return Pl_Handled to block text from printing, Pl_Continue otherwise. */ forward ResultType:OnPlayerSayChat(client, const String:text[]);
The forward keyword tells the compiler that any function having this name must be declared exactly the same. In a plugin, this would
The next step is to declare your forward somewhere at the top of your extension.cpp file. This will look like:
IForward *g_pSayChat = NULL
Next, re-using our code from above, let's create this forward in SDK_OnAllLoaded:
void Sample::SDK_OnAllLoaded() { g_pShareSys->AddNatives(myself, MyNatives); g_pSayChat = g_pForwards->CreateForward(ET_Event, 2, NULL, Param_Cell, Param_String); }
Explanation of parameters:
- ET_Event - Since forwards can execute multiple functions in multiple scripts, this is one of the ways of specifying what to do with the return value of each function. ET_Event means "return the highest value, do not allow stops." A stop refers to Pl_Stop, which lets a plugin block the rest of the event chain.
- 2 - This is the number of parameters our forward will have.
- NULL - Not used in our example. This lets you pass parameters by array rather than variable arguments.
- Param_Cell - The first parameter is a cell (integer).
- Param_String - The second parameter is a string.
Now, we write a quick function in our module to call this forward:
/** Fires the say chat event in plugins. Returns true to allow the text, false to block. */ bool FireChatEvent(int client, const char *text) { if (!g_pSayChat) { return true; } cell_t result = 0; g_pSayChat->PushCell(client); g_pSayChat->PushString(text); g_pSayChat->Execute(&result); if (result == Pl_Handled) { return false; } return true; }
As you can see, this was pretty simple. Each parameter is "pushed" one at a time, in ascending order. First we push the client index as a cell, then the text as a string. Once done, we execute, and the value will be returned by reference.
Lastly, there is a caveat. Unlike natives, SourceMod does not automatically clean up forwards for us when are done. We have to make sure we destroy them. Uncomment SDK_OnUnload from extension.h and implement it like so:
void Sample::SDK_OnUnload() { g_pForwards->ReleaseForward(g_pSayChat); }
Unmanaged Forwards
Now things get a bit more complicated. However, unmanaged forwards are essentially the same -- they just give you more flexibility. Let's consider the managed example with a new twisty. We want to let users add and remove hooks from their plugins, rather than having one global forward. The standard way to do this is with function IDs.
Example prototype:
funcenum SayChatHook { forward(client, const String:text[]); } native AddSayChatHook(SayChatHook:hook); native RemoveSayChatHook(SayChatHook:hook);
The funcenum syntax lets you define all the valid prototypes for your hook. In this example, the only valid method is a function declared like this (only MyHook can be changed):
MyHook(client, const String:text[])
How do we make such a beast? The first step is setting up our forward.
IChangeableForward *g_pSayChat = NULL; /* ... */ void Sample::SDK_OnAllLoaded() { g_pSayChat = g_pForwards->CreateForwardEx(NULL, ET_Hook, 2, NULL, Param_Cell, Param_String); }
Notice two big difference. We now use IChangeableForward instead of IForward, and we use CreateForwardEx instead. The initial first parameter specifies a name for our forward - we're going to ignore this.
Now, we need to create our natives. These will be fairly simple:
cell_t AddSayChatHook(IPluginContext *pContext, const cell_t *params) { g_pSayChat->AddFunction(pContext, static_cast<funcid_t>(params[1])); return 1; } cell_t RemoveSayChatHook(IPluginContext *pContext, const cell_t *params) { IPluginFunction *pFunction = pContext->GetFunctionById(static_cast<funcid_t>(params[1])); g_pSayChat->RemoveFunction(pFunction); return 1; }
Note: These natives must be added -- that step is removed here and explained earlier.
You do not need to worry about a plugin unloading - Core will automatically remove the functions for you. Since an IChangeableForward is also an IForward, the FireChatEvent function from earlier does not need to change. We're done!
Creating Interfaces
Do you want your extension to share an interface? If so, first you should create an interface file. This file should contain everything your interface needs - this way other authors can easily reference it. Your interface must inherit the SMInterface class. It must also declare two global macros that uniquely identify your interface. It must also implement two functions: GetInterfaceName and GetInterfaceVersion.
The two defines must start with SMINTERFACE_ and end with _NAME and _VERSION respectively. The first must be a string and the second must be an unsigned integer. This integer represents your interface's version number. While you are free to use it however you please, by default it should be linearly increased. If you make breaking changes or change the versioning scheme, you must overload the IsVersionCompat function in SMInterface.
Example:
#ifndef _INCLUDE_MYINTERFACE_FILE_H_ #define _INCLUDE_MYINTERFACE_FILE_H_ #include <IShareSys.h> #define SMINTERFACE_MYINTERFACE_NAME "IMyInterface" #define SMINTERFACE_MYINTERFACE_VERSION 1 class IMyInterface : public SourceMod::SMInterface { public: virtual const char *GetInterfaceName() { return SMINTERFACE_MYINTERFACE_NAME; } virtual unsigned int GetInterfaceVersion() { return SMINTERFACE_MYINTERFACE_VERSION; } public: /** * @brief This function does nothing. */ virtual void DoNothingAtAll() =0; }; #endif //_INCLUDE_MYINTERFACE_FILE_H_
Notice, of course, that our interface is pure virtual. This means it must be implemented in the extension. Assuming you know C++, this should be fairly straight forward, so let's skip right to how to add this interface to the SourceMod shared system:
bool Sample::SDK_OnLoad(char *error, size_t err_max, bool late) { g_pShareSys->AddInterface(myself, &g_MyInterface); return true; }
Note: We do this in SDK_OnLoad() instead of SDK_OnAllLoaded(). Otherwise, an extension might try to search for your interface during SDK_OnAllLoaded(), and it might fail depending on the load order.
That simple? Yup! Now other extensions can use your interface.
Using Interfaces/Other Extensions
There are a variety of interfaces that are available, but not loaded by default in the Extension SDK. These interfaces can be retrieved and used in your extension.
Core Interfaces
If they are provided by Core, getting them is very simple. For example, code to use the IPluginManager interface might look like this:
#include <IPluginSys.h> IPluginManager *g_pPlugins = NULL; bool Sample::SDK_OnLoad(char *error, size_t err_max, bool late) { SM_GET_IFACE(PLUGINSYSTEM, g_pPlugins); return true; }
Where did we get PLUGINSYSTEM from? Observe the two lines in IPluginSys.h:
#define SMINTERFACE_PLUGINSYSTEM_NAME "IPluginManager" #define SMINTERFACE_PLUGINSYSTEM_VERSION 1
The SM_GET_IFACE macro uses the text in between the SMINTERFACE_ and the _NAME characters. It also uses the version for compatibility checking. If it can't find the interface, it automatically prints to the error buffer and returns false.
External Interfaces
The situation changes if your extension requires another extension, since extensions may load in a completely random order. The first step is to mark the other extension as a dependency. Let's say you want to use the IThreader.h interfaces from threader.ext.dll. First, we declare it as such:
bool Sample::SDK_OnLoad(char *error, size_t err_max, bool late) { g_pShareSys->AddDependency(myself, "thread.ext", true, true); return true; }
The two boolean parameters to AddDependency mean, respectively: "try to automatically load this extension" and "this extension cannot work without the dependency."
Second, we query for the interface in SDK_OnAllLoaded. Since we don't know when thread.ext will actually be loaded, we have to wait until everything is definitely loaded.
IThreader *g_pThreader = NULL; /* ... */ void Sample::SDK_OnAllLoaded() { SM_GET_IFACE(THREADER, g_pThreader); }
Third, we need to find a way to fail if this interface was never found. The way to do this is by uncommenting QueryRunning from extension.h and implementing it:
bool Sample::QueryRunning(char *error, size_t err_max) { SM_CHECK_IFACE(THREADER, g_pThreader); return false; }
When SourceMod queries your extension, the macro above will either vaildate the pointer or return false. If it returns false, SourceMod marks your extension as failed.
Caveats
NULL Interfaces
There are a few ways that external interfaces can make your code complicated. The first is a simple recommendation. Don't assume interfaces will be okay. For example, say we want to create a thread once we get the interface. Our code might look like this:
void Sample::SDK_OnAllLoaded() { SM_GET_IFACE(THREADER, g_pThreader); if (QueryRunning(NULL, 0)) { g_pThreader->MakeThread(/* stuff */); } }
Note that we query ourself in order to find if it's safe to continue executing code. Preventing crashes becaues a user is missing an extension is a good idea. This is especially important if your extension continues to do things like hook events or add listeners. Make sure you are only doing this if you have finished your sanity checks.
Dynamic Unloading
The second major caveat is that extensions can be dynamically unloaded. If you specified yourself as having a dependency and that each dependency is required, you will have no problem. Your extension will be unloaded before the interface is removed, and you can clean up memory/hooks in your SDK_OnUnload() code. There are two situations where this is a different story.
Optional Interfaces
The first situation is if you are not requiring an extension that contains an interface. Now, when the extension unloads, your extension will not be unloaded first. This creates a small problem, as you must clean up without being unloaded. There are two functions you can implement in your extension class to resolve this, both documented in IExtensionSys.h:
- QueryInterfaceDrop - Allows you to request unloading when a specific interface is unloaded. This is the default behaviour for all interfaces. If you want to block this functionality, continue reading.
- NotifyInterfaceDrop - This is called when an interface is dropped. If your plugin will not be unloaded, you can use this to clean up any resources on a specific inteface being removed.
Circular Dependencies
The second situation is a bit more complicated. It is possible for two extensions to have circular dependencies. For example:
- Extension "A" requires extension "B."
- Extension "B" requires extension "A."
If either extension is unloaded, the opposite extension is unloaded normally. The first extension then receives the interface drop callbacks. In this situation, it is essential that the NotifyInterfaceDrop function be implemented and used with the circular interface. Otherwise, you risk crashing when your extension unloads (or at the very least, leaking memory). Since the actual drop order is undefined, it means both extensions must implement this function to be safe.