Difference between revisions of "Natives (SourceMod Development)"
m |
(Updating pawn examples to newer syntax) |
||
(One intermediate revision by one other user not shown) | |||
Line 19: | Line 19: | ||
* @return Decremented number. | * @return Decremented number. | ||
*/ | */ | ||
− | native Decrement(num);</pawn> | + | native int Decrement(int num);</pawn> |
How would we use this native? Since <tt>num</tt> is passed as a value, it cannot change in the native code. This means we have to use the return value as such: | How would we use this native? Since <tt>num</tt> is passed as a value, it cannot change in the native code. This means we have to use the return value as such: | ||
<pawn>//Decrements a number 5 times | <pawn>//Decrements a number 5 times | ||
− | Example(num) | + | int Example(int num) |
{ | { | ||
num = Decrement(num); | num = Decrement(num); | ||
Line 43: | Line 43: | ||
* @noreturn | * @noreturn | ||
*/ | */ | ||
− | native Decrement(&num);</pawn> | + | native int Decrement(int& num);</pawn> |
Note the ampersand ('&') before the parameter name -- this specifies that it is passed by reference. Now, let's see how this would look in our script: | Note the ampersand ('&') before the parameter name -- this specifies that it is passed by reference. Now, let's see how this would look in our script: | ||
− | <pawn>Example(num) | + | <pawn> |
+ | int Example(int num) | ||
{ | { | ||
Decrement(num); | Decrement(num); | ||
Line 59: | Line 60: | ||
In this example, <tt>Decrement</tt> is acting on the <tt>num</tt> variable ''itself'', not a copy of it. Thus, num will decrease 5 times. In fact, scripts can even do this internally. We can shorten the example even more: | In this example, <tt>Decrement</tt> is acting on the <tt>num</tt> variable ''itself'', not a copy of it. Thus, num will decrease 5 times. In fact, scripts can even do this internally. We can shorten the example even more: | ||
− | <pawn>Example(&num) | + | <pawn>int Example(int& num) |
{ | { | ||
Decrement(num); | Decrement(num); | ||
Line 69: | Line 70: | ||
==When to use By Ref== | ==When to use By Ref== | ||
− | There is a misconception that passing by reference is always better | + | There is a misconception that passing by reference is always better than by value. After all, copying data sounds excessive. However, by value in SourcePawn only works on 32bit values, and thus copying the value is inherent to the processor, and trivial. |
On the other hand, passing by reference is slightly more expensive. First, the compiler has to generate a little extra code to compute the local address of the variable. Second, the native code itself has to translate the local address to a ''real virtual address'' (native memory). | On the other hand, passing by reference is slightly more expensive. First, the compiler has to generate a little extra code to compute the local address of the variable. Second, the native code itself has to translate the local address to a ''real virtual address'' (native memory). | ||
Line 86: | Line 87: | ||
* @return Computed exponent result as a Float. | * @return Computed exponent result as a Float. | ||
*/ | */ | ||
− | native | + | native float FloatIntPower(float fNum, int exp);</pawn> |
An implementation of this native might look like: | An implementation of this native might look like: | ||
Line 107: | Line 108: | ||
==By Reference== | ==By Reference== | ||
By reference is a little more tricky. Let's first implement our <tt>Decrement</tt> native from earlier. A refresher: | By reference is a little more tricky. Let's first implement our <tt>Decrement</tt> native from earlier. A refresher: | ||
− | <pawn>native Decrement(&num);</pawn> | + | <pawn>native void Decrement(int& num);</pawn> |
If we try to use our above code, <tt>params[1]</tt> will no longer hold a value. Instead, it holds a ''local address'' in the plugin. We must use the <tt>LocalToPhysAddr</tt> function to translate this. It takes in a local address and returns back a physical pointer, which we can then modify. This will modify the value in the script. | If we try to use our above code, <tt>params[1]</tt> will no longer hold a value. Instead, it holds a ''local address'' in the plugin. We must use the <tt>LocalToPhysAddr</tt> function to translate this. It takes in a local address and returns back a physical pointer, which we can then modify. This will modify the value in the script. | ||
Line 134: | Line 135: | ||
* @noreturn | * @noreturn | ||
*/ | */ | ||
− | native Decrement(& | + | native void Decrement(float& fNum, int decamt); |
</pawn> | </pawn> | ||
Line 170: | Line 171: | ||
* @return Average number as a Float. | * @return Average number as a Float. | ||
*/ | */ | ||
− | native | + | native float Average(int[] array, int num);</pawn> |
Usage might look like: | Usage might look like: | ||
− | <pawn>Example() | + | <pawn>float Example() |
{ | { | ||
− | + | int numbers[5] = {5, 6, 1, 3, 8}; | |
return Average(numbers, 5); | return Average(numbers, 5); | ||
}</pawn> | }</pawn> | ||
Line 212: | Line 213: | ||
* @noreturn | * @noreturn | ||
*/ | */ | ||
− | native AddVectors( | + | native void AddVectors(float r[3], const float v1[3], const float v2[3]);</pawn> |
The implementation is straightforward: | The implementation is straightforward: | ||
Line 238: | Line 239: | ||
Note that we only store the result after we have computed the input, rather than store directly. This is a bit more work, but is good practice in case users re-use data inputs, and risk overwriting inputs as they are written. For example: | Note that we only store the result after we have computed the input, rather than store directly. This is a bit more work, but is good practice in case users re-use data inputs, and risk overwriting inputs as they are written. For example: | ||
− | <pawn> | + | <pawn>float origin[3]; |
AddVectors(origin, origin, origin);</pawn> | AddVectors(origin, origin, origin);</pawn> | ||
Line 255: | Line 256: | ||
* @return Number of bytes in the string. | * @return Number of bytes in the string. | ||
*/ | */ | ||
− | native strlen(const | + | native int strlen(const char[] string);</pawn> |
Implementation: | Implementation: | ||
Line 276: | Line 277: | ||
* @noreturn | * @noreturn | ||
*/ | */ | ||
− | native StringCopy( | + | native void StringCopy(char[] dest, int length, const char[] source);</pawn> |
Implementation using <tt>memmove</tt> for safety: | Implementation using <tt>memmove</tt> for safety: | ||
Line 324: | Line 325: | ||
==Default Arguments== | ==Default Arguments== | ||
Any type of parameter can have a default argument. For example, here is a native where every argument has a default parameter: | Any type of parameter can have a default argument. For example, here is a native where every argument has a default parameter: | ||
− | <pawn>native RandomStuff(a=0, | + | <pawn>native void RandomStuff(int a=0, float b=1.0, int& c=0, const char[] d="", int e[3] = {0,1,2});</pawn> |
− | Note that even by reference parameters can have default arguments, as shown above. This does not change the code; it just means the address will contain the default value. | + | Note that even by-reference parameters can have default arguments, as shown above. This does not change the code; it just means the address will contain the default value. |
Scripts can force default values. Example: | Scripts can force default values. Example: | ||
− | <pawn>native RandomStuff(a, b, c=0, d=0); | + | <pawn>native void RandomStuff(int a, int b, int c=0, int d=0); |
− | Example() | + | void Example() |
{ | { | ||
RandomStuff(1, 2, _, d); | RandomStuff(1, 2, _, d); | ||
Line 340: | Line 341: | ||
==Variable Arguments== | ==Variable Arguments== | ||
Variable arguments means any number of arguments can be passed. An example of this looks like: | Variable arguments means any number of arguments can be passed. An example of this looks like: | ||
− | <pawn>native FormatText(const | + | <pawn>native FormatText(const char[] format, any ...);</pawn> |
− | + | The <tt>...</tt> characters mean any number of parameters can follow. | |
'''There is one important note about variable arguments. They are always passed by reference.''' This means if you have a function which supports variable arguments, and an integer is passed, you will need to use <tt>LocalToPhysAddr</tt> as required with by reference parameters. | '''There is one important note about variable arguments. They are always passed by reference.''' This means if you have a function which supports variable arguments, and an integer is passed, you will need to use <tt>LocalToPhysAddr</tt> as required with by reference parameters. | ||
Line 350: | Line 351: | ||
For example, consider the following native: | For example, consider the following native: | ||
− | <pawn>native DoSomething(index);</pawn> | + | <pawn>native void DoSomething(int index);</pawn> |
Let's say that this native exists in plugins for six months. After that, you decide to add a new parameter. You want two conditions to be true after you release this update: | Let's say that this native exists in plugins for six months. After that, you decide to add a new parameter. You want two conditions to be true after you release this update: | ||
Line 357: | Line 358: | ||
The second condition is solved by using default arguments. You should choose a value that will mimic the old functionality of the native. | The second condition is solved by using default arguments. You should choose a value that will mimic the old functionality of the native. | ||
− | <pawn>native DoSomething(index, newparam = 0);</pawn> | + | <pawn>native void DoSomething(int index, int newparam = 0);</pawn> |
Next, when changing the implementation, you should detect whether <tt>params[0]</tt> contains ''one parameter'' or ''two parameters''. If it only contains one, you know to use the old version. If it contains two, you know to use the later version and accept the second parameter. | Next, when changing the implementation, you should detect whether <tt>params[0]</tt> contains ''one parameter'' or ''two parameters''. If it only contains one, you know to use the old version. If it contains two, you know to use the later version and accept the second parameter. |
Latest revision as of 09:54, 10 March 2021
Natives are functions which are available to plugins via Core itself, or a C++ extension. They are called "natives" because they must be translated via a native interface. This article explains the various parameter passing conventions in SourcePawn, as well as how to use them in your own natives.
To understand the contents of this article, you will need to read Creating Natives, the Native section in the introductory article on creating extensions.
In this article, float refers to the C/C++ data type, and Float (note capital 'F') refers to the SourcePawn tag.
Contents
By Value versus By Reference
There are two ways to pass integers/Floats to native implementations. They are by value (ByVal) and by reference (ByRef). ByVal means that a copy of the value is passed to the native. This is the default behaviour. ByRef means a reference is passed to the native, and this reference points to the value. Both will be explained below.
Note that arrays and strings, as will be explained later, are always passed by reference. This is because they are usually larger structures, and passing them ByVal would require copying their data, wasting CPU cycles.
By Value
Passing by value is the default behaviour for primitive data types (integers, Floats, or any tag that's cell-based). Data is treated normally as raw, copied input. Observe the following native:
/** * Returns the number decremented by one. * * @param num Number to decrement. * @return Decremented number. */ native int Decrement(int num);
How would we use this native? Since num is passed as a value, it cannot change in the native code. This means we have to use the return value as such:
//Decrements a number 5 times int Example(int num) { num = Decrement(num); num = Decrement(num); num = Decrement(num); num = Decrement(num); num = Decrement(num); return num; }
By Reference
Let's reuse the above example to use reference passing. Observe the new native below:
/** * Subtracts one from the given number, by reference. * * @param num Number to subtract, by reference. * @noreturn */ native int Decrement(int& num);
Note the ampersand ('&') before the parameter name -- this specifies that it is passed by reference. Now, let's see how this would look in our script:
int Example(int num) { Decrement(num); Decrement(num); Decrement(num); Decrement(num); Decrement(num); return num; }
In this example, Decrement is acting on the num variable itself, not a copy of it. Thus, num will decrease 5 times. In fact, scripts can even do this internally. We can shorten the example even more:
int Example(int& num) { Decrement(num); Decrement(num); Decrement(num); Decrement(num); Decrement(num); }
When to use By Ref
There is a misconception that passing by reference is always better than by value. After all, copying data sounds excessive. However, by value in SourcePawn only works on 32bit values, and thus copying the value is inherent to the processor, and trivial.
On the other hand, passing by reference is slightly more expensive. First, the compiler has to generate a little extra code to compute the local address of the variable. Second, the native code itself has to translate the local address to a real virtual address (native memory).
It is a good idea to only use by reference when you need it. A common usage is when you need to return more than one piece of data, and you can't fit it into the return value of your native or function. In this case, by reference is ideal.
Integers/Floats
By Value
Natives based purely on by-value floating point or integer input are generally the easiest to make. Let's take a simple function which takes in a Float and an integer, and returns a Float:
/** * Raises a number to an integer power. * * @param fNum Base number (Float). * @param exp Exponent (integer). * @return Computed exponent result as a Float. */ native float FloatIntPower(float fNum, int exp);
An implementation of this native might look like:
#include <stdlib.h> static cell_t sm_FloatIntPower(IPluginContext *pContext, const cell_t *params) { float f1 = sp_ctof(params[1]); int num = params[2]; float result = (float)pow(f1, (double)f2); return sp_ftoc(result); }
Integers, as we saw in the introduction, are accessed from the parameter stack normally. Floats, however, must be reinterpret casted from a cell_t to float. Two inline functions are provided for this:
- sp_ctof: Convert a cell_t to a float.
- sp_ftoc: Convert a float to a cell_t.
By Reference
By reference is a little more tricky. Let's first implement our Decrement native from earlier. A refresher:
native void Decrement(int& num);
If we try to use our above code, params[1] will no longer hold a value. Instead, it holds a local address in the plugin. We must use the LocalToPhysAddr function to translate this. It takes in a local address and returns back a physical pointer, which we can then modify. This will modify the value in the script.
static cell_t sm_Decrement(IPluginContext *pContext, const cell_t *params) { cell_t *addr; /* Translate the address. */ pContext->LocalToPhysAddr(params[1], &addr); /* Decrement the number */ *addr -= 1; /* We have to return something, even if the plugin doesn't use the value */ return 1; }
Note: LocalToPhysAddr can return an error code. For all practical purposes, this will never error, unless there is some memory or corruption issue, so checking it isn't necessary.
This works the same way for floats. Let's say we change our native to this:
/** * Decrements a Float by an integer, and stores the result by reference. * * @param fNum Float number to decrement (by ref). * @param decamt Amount to decrement by. * @noreturn */ native void Decrement(float& fNum, int decamt);
This example is fairly contrived -- our native simply performs a subtract operation. But let's look at the implementation:
static cell_t sm_Decrement(IPluginContext *pContext, const cell_t *params) { cell_t *addr; /* Translate the address. */ pContext->LocalToPhysAddr(params[1], &addr); /* Get the value */ float val = sp_ctof(*addr); /* Decrement */ val -= params[2]; /* Store back */ *addr = sp_ftoc(val); /* We have to return something, even if the plugin doesn't use the value */ return 1; }
Note that even though the first parameter was passed by reference, the second was not, and is accessed normally.
Arrays
Basic Arrays
Arrays are always passed by reference. The first example is an array which contains a list of numbers to sum. Observe the native:
/** * Averages an array of numbers. * * @param array Array of numbers to average. * @param num Number of slots in the array. * @return Average number as a Float. */ native float Average(int[] array, int num);
Usage might look like:
float Example() { int numbers[5] = {5, 6, 1, 3, 8}; return Average(numbers, 5); }
The implementation, like the by ref examples above, requires LocalToPhysAddr:
static cell_t sm_Average(IPluginContext *pContext, cell_t *params) { cell_t *array; float sum = 0.0f, average; if (params[2] < 1) { /* 0 works without sp_ftoc() */ return 0; } pContext->LocalToPhysAddr(params[1], &array); for (cell_t i=0; i<params[2]; i++) { sum += array[i]; } average = sum / params[2]; return sp_ftoc(average); }
Float Arrays
This works similarly for Float arrays. Let's take Vectors an example, with the following native:
/** * Adds two vectors together. * * @param r Array to store the result in. * @param v1 First vector to add. * @param v2 Second vector to add. * @noreturn */ native void AddVectors(float r[3], const float v1[3], const float v2[3]);
The implementation is straightforward:
static cell_t sm_AddVectors(IPluginContext *pContext, cell_t *params) { cell_t *v1, *v2; float result[3]; pContext->LocalToPhysAddr(params[2], &v1); pContext->LocalToPhysAddr(params[3], &v2); result[0] = sp_ctof(v1[0]) + sp_ctof(v2[0]); result[1] = sp_ctof(v1[1]) + sp_ctof(v2[1]); result[2] = sp_ctof(v1[2]) + sp_ctof(v2[2]); cell_t *r; pContext->LocalToPhysAddr(params[1], &r); r[0] = sp_ftoc(result[0]); r[1] = sp_ftoc(result[1]); r[2] = sp_ftoc(result[2]); return 1; }
Note that we only store the result after we have computed the input, rather than store directly. This is a bit more work, but is good practice in case users re-use data inputs, and risk overwriting inputs as they are written. For example:
float origin[3]; AddVectors(origin, origin, origin);
Strings
Strings are just like arrays, in that they are passed by reference. The only difference is that each character is stored in a byte, not a 32bit cell_t. However, to make coding a bit easier for you, the coder, there are separate functions.
- LocalToString: Converts a local address to a physical string address.
- StringToLocal: Copies a physical string into a local address buffer.
- StringToLocalUTF8: Same as StringToLocal, but only copies the maximum amount possible without cutting off any multi byte characters.
First, let's take an easy example: the infamous strlen.
/** * Returns the length of a string. * * @param string String to calculate. * @return Number of bytes in the string. */ native int strlen(const char[] string);
Implementation:
static cell_t sm_StrLen(IPluginContext *pContext, cell_t *params) { char *str; pContext->LocalToString(params[1], &str); return strlen(str); }
Now, let's say we want to modify the string. There is an important note here: since the string is passed by reference, when you modify the string, it is also modified in the plugin. This is a huge difference from AMX Mod X, where strings were essentially by value in most cases, and you had to copy results back.
Thus, if we wanted to copy a new result into str in the above implementation, it would be very easy. If we were implementing strcpy(), we would have to use memmove() to make sure there are no overlaps. Let's do this:
/** * Copies one string onto another. * * @param dest Destination buffer to copy to. * @param length Maximum length of the buffer. * @param source Source to copy from. * @noreturn */ native void StringCopy(char[] dest, int length, const char[] source);
Implementation using memmove for safety:
static cell_t sm_StringCopy(IPluginContext *pContext, cell_t *params) { char *dest, *src; size_t len; pContext->LocalToString(params[1], &dest); pContext->LocalToString(params[3], &src); /* Perform bounds checking */ len = strlen(src); if (len >= params[2]) { len = params[2] - 1; } else { len = params[2]; } /* Copy */ memmove(dest, src, len); dest[len] = '\0'; return 1; }
We can also use StringToLocal, which would requires a temporary buffer for the original string:
static cell_t sm_StringCopy(IPluginContext *pContext, cell_t *params) { char *src; char buffer[2048]; size_t len; pContext->LocalToString(params[3], &src); snprintf(buffer, sizeof(buffer), "%s", src); pContext->StringToLocal(params[1], params[2], buffer); return 1; }
Generally, you will be using StringToLocal for setting strings from outside sources, because it calculates addresses and overflows for you. However, you must make sure that you are not overwriting memory as you are reading it, or else programmers who are using certain types of array slicing may find themselves getting unexpected results.
Advanced
Default Arguments
Any type of parameter can have a default argument. For example, here is a native where every argument has a default parameter:
native void RandomStuff(int a=0, float b=1.0, int& c=0, const char[] d="", int e[3] = {0,1,2});
Note that even by-reference parameters can have default arguments, as shown above. This does not change the code; it just means the address will contain the default value.
Scripts can force default values. Example:
native void RandomStuff(int a, int b, int c=0, int d=0); void Example() { RandomStuff(1, 2, _, d); }
The underscore ('_') means "use the default value for this argument."
Variable Arguments
Variable arguments means any number of arguments can be passed. An example of this looks like:
native FormatText(const char[] format, any ...);
The ... characters mean any number of parameters can follow.
There is one important note about variable arguments. They are always passed by reference. This means if you have a function which supports variable arguments, and an integer is passed, you will need to use LocalToPhysAddr as required with by reference parameters.
Argument Counts/Backwards Compatibility
In native handlers, the params array has a 0th index which stores the number of parameters it contains. This is useful for both Default Arguments and Variable Arguments.
For example, consider the following native:
native void DoSomething(int index);
Let's say that this native exists in plugins for six months. After that, you decide to add a new parameter. You want two conditions to be true after you release this update:
- Old plugins in binary form should still work on newer installations.
- Old plugins should still compile fine on the new API.
The second condition is solved by using default arguments. You should choose a value that will mimic the old functionality of the native.
native void DoSomething(int index, int newparam = 0);
Next, when changing the implementation, you should detect whether params[0] contains one parameter or two parameters. If it only contains one, you know to use the old version. If it contains two, you know to use the later version and accept the second parameter.
Similarly, you can use the params[0] count to detect how many arguments were passed to a variable argument native. This is used primarily in Format Class Functions for parameter-count validation.
Throwing Errors/Invoking the Debugger
Often, you will write a native and realize that you need to tell the plugin that a serious error has occurred. A good example of this is if you are writing a function which requires a player index, but that index is beyond the reasonable bounds for the maximum players in the server.
In this case, it is a good idea to throw a runtime error. This is an error that halts the plugin and invokes a call trace from the debugger. The halt is not permanent, but it does break the execution flow of the current callback.
There are two ways to throw a native error: ThrowNativeErrorEx() or ThrowNativeError(). The former lets you specify a detailed error code from sp_vm_types.h. The latter is a helper function for generic errors, and it returns 0, allowing you to return with the function itself. For example:
static cell_t GetPlayerFrags(IPluginContext *pContext, const cell_t *params) { if (params[1] < 0 || params[1] > maxplayers) { return pContext->ThrowNativeError("Invalid client index: %d", params[1]); } /* ... rest of code ... */ }
Don't worry about including extra information, such as your native name or the module name. The debugger knows all of this information and will decide what to print to the user.