Creating Natives (SourceMod Scripting)
SourceMod allows plugins to create their own natives that other plugins can use. This is very similar to AMX Mod X's register_native function. It is a powerful inter-plugin communication resource that can greatly enhance the functionality you provide.
Prerequisites: You should have a good grasp of Pawn or SourcePawn scripting, as well as how Tags work.
Note: Most functions herein can be found in the SourceMod SDK, under plugins/include/functions.inc.
Contents
Introduction
Before creating natives, you should always document and specify exactly how they will work. This means writing your "include file" beforehand. Not only does this guide you in writing your natives, but it has the added benefit of completing a large portion of documentation at the same time.
Include File
For our first example, let's start off with a simple native that adds two numbers, a float and a non-float. It might look like this:
/** Double-include prevention */ #if defined _mynatives_included_ #endinput #endif #define _mynatives_included_ /** * Adds two numbers together. * * @param num1 An integer. * @param num2 A float. * @return The float value of the integer and float added together. */ native float My_AddNumbers(int num1, float num2);
The documentation and commenting style above is a SourceMod Development Team convention. While you are not required to follow it for your own code, it may be a good idea to enhance readability (as readers will expect the style to conform).
Creating the Native
To actually create the native, first let's define the native handler itself. From the API reference we can see that there are two prototypes. We will use the second one:
function any (Handle plugin, int numParams)
Thus, we have a function like:
any Native_My_AddNumbers(Handle plugin, int numParams) { }
Note that we don't use the same function name as we did in our include file. This is to prevent a possible name clash. Otherwise, two different symbols (one a native, another a function) could have the same name, and that's not allowed. We'll see later how we bind the native to its actual name.
Next, we need to implement our native.
any Native_My_AddNumbers(Handle plugin, int numParams) { /* Retrieve the first parameter we receive */ int num1 = GetNativeCell(1); /* Retrieve the second parameter we receive */ float num2 = view_as<float>(GetNativeCell(2)); /* Add them together */ float value = num1 + num2; /* Return the value */ return value; }
You should note three important details. The first is that we have to manually grab the parameters -- they're not easily sent via arguments. This is for technical reasons that go beyond the scope of this document. That means you have to know exactly how to grab each parameter. For our example, we know that both parameters are simply cells, and thus we can use GetNativeCell.
However, the second detail is that there is no GetNativeCellFloat. This is because a float is still a cell; the data is just interpreted differently. Thus, we cast the type to float manually. This is different from calling float(GetNativeCell(2)). In fact, that would not work; the float() call would interpret the cell as an integer, when in fact it's already a float, just with the wrong type (i.e., no type at all).
Lastly, we did not need to verify numParams. The compiler will always send the correct amount of parameters - the user can't mess this up in any easy way. Only if you use variadic arguments, or add parameters and need to support older binaries, do you need to handle numParams variations.
Registering the Native
Natives must be registered in AskPluginLoad2. Although they can be registered elsewhere, only this function guarantees that all other plugins will see your native. The definition for this forward is in sourcemod.inc.
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { CreateNative("My_AddNumbers", Native_My_AddNumbers); return APLRes_Success; }
Now, any plugin will be able to use our native as if it were from a Core or a C++ extension.
Usage
By-Reference
Sometimes, it may be desirable to return data through by-reference parameters. For example, observe the following native prototype:
/** * Closes a Handle, and sets the variable to INVALID_HANDLE by reference. * * @param hndl Handle to close and clear. * @return Anything that CloseHandle() returns. */ native bool AltCloseHandle(Handle &hndl);
An implementation of this would look like:
int Native_AltCloseHandle(Handle plugin, int numParams) { Handle hndl = view_as<Handle>(GetNativeCellRef(1)); SetNativeCellRef(1, 0); /* Zero out the variable by reference */ return CloseHandle(hndl); }
Now, we have a native that is "safer" than a normal CloseHandle, as it ensures we don't hold onto invalid Handles.
Strings
Strings can be retrieved and altered via dynamic natives. Observe the following native prototype:
/** * Converts a string to upper case. * * @param str String to convert. * @noreturn */ native void StringToUpper(char[] str);
An implementation of this is very easy, because of dynamic arrays:
int Native_StringToUpper(Handle plugin, int numParams) { int len; GetNativeStringLength(1, len); if (len <= 0) { return; } char[] str = new char[len + 1]; GetNativeString(1, str, len + 1); for (int i = 0; i < len; i++) { /* 5th bit specifies case in ASCII -- * this is not UTF-8 compatible. */ if ((str[i] & (1 << 5)) == (1 << 5)) { str[i] &= ~(1 << 5) } } SetNativeString(1, str, len+1, false); }
Arrays
Working with arrays is very similar to strings. For example, observe the following native prototype:
/** * Sums an array of numbers and returns the sum. * * @param array Array of numbers to sum. * @param size Size of array. * @return Sum of the array elements. */ native int SumArray(const int[] array, int size);
An implementation of this could look like:
int Native_SumArray(Handle plugin, int numParams) { int size = GetNativeCell(2); if (size < 1) { return 0; } int[] array = new int[size]; int total; GetNativeArray(1, array, size); for (int i = 0; i < size; i++) { total += array[i]; } return total; }
Similarly, there is a SetNativeArray, which is used to alter array contents.
Variable Arguments
Variable arguments are usually used when dealing with format class functions, however it is also possible to use them for general functionality. The only trick is that unlike normal parameters, every variable argument parameter is passed by-reference. This adds no change for arrays and strings, but for floats, handles, numbers, or anything else that fits in a cell, GetNativeCellRef must be used instead.
As an example, let's convert our function from above to use variable arguments:
/** * Sums a list of numbers. * * @param num First number to use as a base. * @param ... Any number of additional numbers to add. * @return Sum of all numbers passed. */ native int SumParams(int num, ...);
An implementation of this would look like:
int Native_SumParams(Handle plugin, int numParams) { int base = GetNativeCell(1); for (int i = 2; i <= numParams; i++) { base += GetNativeCellRef(i); } return base; }
Format Functions
It is also possible to create your own format class functions. This topic is much more complicated than the previous ones, as the native string formatter allows for many different configurations. This is mainly for optimization. Note that a formatting routine deals with two buffers: the input format string, and the output buffer. FormatNativeString, by default, uses parameter indexes directly so you don't have to copy or locally store any of these buffers.
The most common usage of this is to accept a user-formatted string and to not
/** * Prints a message with a [DEBUG] prefix to the server. * * @param fmt Format string. * @param ... Format arguments. */ native void DebugPrint(const char[] fmt, any ...);
This could be easily implemented as:
native int DebugPrint(Handle plugin, int numParams) { char buffer[1024]; int written; FormatNativeString(0, /* Use an output buffer */ 1, /* Format param */ 2, /* Format argument #1 */ sizeof(buffer), /* Size of output buffer */ written, /* Store # of written bytes */ buffer /* Use our buffer */ ); PrintToServer("[DEBUG] %s", buffer); }
Throwing Errors
Often, your native will encounter an unrecoverable error, and you wish to trigger a fatal exception. This is equivalent to IPluginContext::ThrowNativeError, and will halt the current function execution in the plugin. The debugger will be invoked and a backtrace will be printed out (if the plugin is in debug mode). Although the current function is cancelled, the plugin will continue to operate normally afterward.
For example, let's say we have a native that operates on clients. We want to throw an error if we get a client index that is invalid or disconnected. We could do this like so:
int MyFunction(Handle plugin, int numParams) { int client = GetNativeCell(1); if (client < 1 || client > MaxClients) { return ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index (%d)", client); } if (!IsClientConnected(client)) { return ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not connected", client); } /* Code */ }
Creating Natives for Methodmaps
Declaring natives for methodmaps is similar to other natives, with a few requirements:
- The CreateNative string argument is in the format Tag.Function.
- The function is declared as a "member" of the methodmap.
- The magic "this" parameter is implicitly passed in first.
Here's an example of a shared plugin and its corresponding include file:
/* sharedplugin.sp */ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { CreateNative("Number.PrintToClient", Native_PrintNumberToClient); return APLRes_Success; } int Native_PrintNumberToClient(Handle plugin, int numParams) { int number = GetNativeCell(1); int client = GetNativeCell(2); PrintToChat(client, "%d", number); return; } /* sharedplugin.inc */ methodmap Number { public Number(int number) { return view_as<Number>(number); } public native void PrintToClient(int client); }