Creating Natives (SourceMod Scripting)

From AlliedModders Wiki
Revision as of 10:30, 16 September 2012 by FaTony (talk | contribs) (Variable Arguments)
Jump to: navigation, search

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.

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(num1, 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 functions.inc we can see that this follows the prototype:

functag NativeCall public(Handle:plugin, numParams);

Thus, we have a function like:

public Native_My_AddNumbers(Handle:plugin, 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.

public Native_My_AddNumbers(Handle:plugin, numParams)
{
   /* Retrieve the first parameter we receive */
   new num1 = GetNativeCell(1);
   /* Retrieve the second parameter we receive */
   new Float:num2 = Float:GetNativeCell(2);
   /* Add them together */
   new Float:value = num1 + num2;
   /* Return the value */
   return _:value;
}

You should note four 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 change the tag to Float manual. 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 tag (i.e., no tag at all).

Similarly, the third detail is that we cannot simply put return value or return FloatRound(value). The reason is that our native is supposed to be returning a float. If we return value, we'll get a tag mismatch. If we return FloatRound(value), we'll be returning an integer when a float is expected. The solution is to change the tag to the default tag, which will retain the value but remove the tag warning.

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, String:error[], 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:

public Native_AltCloseHandle(Handle:plugin, numParams)
{
   new Handle:hndl = 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 StringToUpper(String:str[]);

An implementation of this is very easy, because of dynamic arrays:

public Native_StringToUpper(Handle:plugin, numParams)
{
   new len;
   GetNativeStringLength(1, len);
 
   if (len <= 0)
   {
      return;
   }
 
   new String:str[len + 1];
   GetNativeString(1, str, len + 1);
 
   for (new 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 SumArray(const array[], size);

An implementation of this could look like:

public Native_SumArray(Handle:plugin, numParams)
{
   new size = GetNativeCell(2);
   if (size < 1)
   {
      return 0;
   }
 
   new array[size], total;
   GetNativeArray(1, array, size);
 
   for (new 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 SumParams(num, ...);

An implementation of this would look like:

public Native_SumParams(Handle:plugin, numParams)
{
   new base = GetNativeCell(1);
   for (new 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 DebugPrint(const String:fmt[], any:...);

This could be easily implemented as:

native DebugPrint(Handle:plugin, numParams)
{
   decl String:buffer[1024], 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:

public MyFunction(Handle:plugin, numParams)
{
   new client = GetNativeCell(1)
   if (client < 1 || client > GetMaxClients())
   {
      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 */
}