Natives and forwards
Sharing code between plugins — natives a C++ plugin or a script provides, libraries and optional natives, global and private forwards, and calling functions of other plugins.
Plugins share code the way SourceMod plugins do: one plugin provides natives that others declare and call, and announces itself as a library they can test for; it tells them about things through forwards. A native can come from a C++ plugin or from a script, and the calling script cannot tell the difference.
In the include set, SourceMod's free natives are static methods:
CreateNative is Source2ModNatives.CreateNative, Call_PushCell is
Source2ModCall.PushCell (source2mod/functions).
Providing a native
The same two natives, Sample_Add(int a, int b) and
Sample_Echo(char[] buffer, int maxlen, const char[] fmt, any ...), and the
library sample, from either language:
#include "source2mod/ISource2ModNatives.h"
static ISource2ModNatives* g_pNatives = nullptr;
// Sample_Add(int a, int b): params[0] is the count, the arguments follow.
static cell_t Native_Add(SourcePawn::IPluginContext* ctx, const cell_t* params)
{
return params[1] + params[2];
}
// Sample_Echo(char[] buffer, int maxlen, const char[] fmt, any ...): the
// core's formatter, then into the script's buffer.
static cell_t Native_Echo(SourcePawn::IPluginContext* ctx, const cell_t* params)
{
char szBuffer[256];
size_t written = 0;
if (!g_pNatives->FormatString(ctx, params, 3, szBuffer, sizeof(szBuffer), &written))
return 0; // the error is reported to the script already
ctx->StringToLocalUTF8(params[1], params[2], szBuffer, &written);
return static_cast<cell_t>(written);
}
static const sp_nativeinfo_t g_Natives[] = {
{ "Sample_Add", Native_Add },
{ "Sample_Echo", Native_Echo },
{ nullptr, nullptr },
};
bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS();
g_pNatives = (ISource2ModNatives*)g_Source2ModAPI->Source2ModFactory(SOURCE2MOD_NATIVES_INTERFACE, nullptr, nullptr);
if (g_pNatives)
{
g_pNatives->AddNatives(g_PluginID, g_Natives);
g_pNatives->RegisterLibrary(g_PluginID, "sample");
}
return true;
}public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
Source2ModNatives.CreateNative("Sample_Add", Native_Add);
Source2ModNatives.CreateNative("Sample_Echo", Native_Echo);
Source2ModNatives.RegisterLibrary("sample");
return APLRes_Success;
}
// Sample_Add(int a, int b): parameters are numbered from 1.
public any Native_Add(Handle plugin, int numParams)
{
return Source2ModNatives.GetCell(1) + Source2ModNatives.GetCell(2);
}
// Sample_Echo(char[] buffer, int maxlen, const char[] fmt, any ...): the
// format is parameter 3, its arguments start at 4, the result goes to 1.
public any Native_Echo(Handle plugin, int numParams)
{
int written;
Source2ModNatives.FormatString(1, 3, 4, Source2ModNatives.GetCell(2), written);
return written;
}From C++
ISource2ModNatives is what a SourceMod
extension does for its scripts. It is not filled in by
SOURCE2MOD_SAVEVARS(): ask the factory for SOURCE2MOD_NATIVES_INTERFACE.
Including the header brings the SourcePawn VM headers (sp_vm_api.h, the
SDK's public/sourcepawn) with it.
A native is SourcePawn's own SPVM_NATIVE_FUNC: the calling script's
IPluginContext and its parameters, params[0] being their count. Strings,
references and arrays go through the context (LocalToString,
StringToLocalUTF8, LocalToPhysAddr), errors through
ctx->ThrowNativeError(...), exactly as in a SourceMod extension.
| Method | |
|---|---|
AddNatives(owner, natives) | Registers a { nullptr, nullptr }-terminated list (copied). A name another live plugin already registered is refused and logged; the rest still go in. |
RegisterLibrary(owner, name) | What LibraryExists() and OnLibraryAdded() see. Several plugins may register the same library; it goes when the last of them unloads. |
LibraryExists(name) | Whether any plugin, script or native, registered it. |
GetScriptPlugin(ctx) | The plugin id of the script that called the native, 0 for none. |
FormatString(ctx, params, fmtParam, buffer, maxlen, written) | SourceMod's Format() over the native's own parameters: the format at params[fmtParam], its arguments after it. false after reporting the error to the script. |
CreateHandleType(owner, name, destroy) | A handle type of your own; destroy gets the object when its last handle is closed. |
CreateHandle(type, object, ctx) | A new handle, owned by the calling script and closed when it unloads. |
ReadHandle(ctx, handle, type) | The object behind a handle; nullptr after reporting "Invalid Handle" to the script. |
CloseHandle(handle) | Closes a handle, whoever owns it. |
cpp_sample (section 12 in samples/cs2/cpp_sample/src/plugin.cpp) also
hands out a handle of its own type, Sample_CreateCounter, that a script
closes with delete. sp_sample uses all of it from the other side.
When the C++ plugin unloads, its natives become unbound — a script calling
one gets an error — its libraries are removed (OnLibraryRemoved), and its
handle types are destroyed together with every handle of them.
From a script
Source2ModNatives.CreateNative(name, function) makes a native; create them
in AskPluginLoad2, before the plugin's own OnPluginStart, together with
RegisterLibrary. The function gets the calling plugin's handle and the parameter count, and reads
the parameters with Source2ModNatives, numbered from 1:
| SourceMod | Source2Mod | |
|---|---|---|
GetNativeCell(n) | Source2ModNatives.GetCell(n) | A cell: int, bool, float, handle, entity |
GetNativeCellRef(n) / SetNativeCellRef(n, v) | GetCellRef / SetCellRef | A by-reference cell |
GetNativeStringLength(n, len) | GetStringLength | To size a buffer |
GetNativeString(n, buf, max) / SetNativeString(n, s, max) | GetString / SetString | |
GetNativeArray(n, arr, size) / SetNativeArray(n, arr, size) | GetArray / SetArray | Always by reference |
GetNativeFunction(n) | GetFunction | A callback the caller passed |
IsNativeParamNullString(n) / IsNativeParamNullVector(n) | IsParamNullString / IsParamNullVector | NULL_STRING / NULL_VECTOR passed |
FormatNativeString(...) | FormatString(out, fmt, vararg, outLen, written) | Only fmt_param: the format must be a parameter, its arguments right after it |
ThrowNativeError(code, fmt, ...) | ThrowError(code, fmt, ...) | The error lands in the calling plugin |
// native void MyPlugin_Greet(const char[] name, char[] out, int maxlen);
public any Native_Greet(Handle plugin, int numParams)
{
char name[64], out[128];
Source2ModNatives.GetString(1, name, sizeof(name));
Source2ModApi.Format(out, sizeof(out), "Hello, %s!", name);
Source2ModNatives.SetString(2, out, Source2ModNatives.GetCell(3));
return 0;
}
// native void MyPlugin_Double(int &value, int values[3]);
public any Native_Double(Handle plugin, int numParams)
{
Source2ModNatives.SetCellRef(1, Source2ModNatives.GetCellRef(1) * 2);
int values[3];
Source2ModNatives.GetArray(2, values, sizeof(values));
for (int i = 0; i < sizeof(values); i++)
values[i] *= 2;
Source2ModNatives.SetArray(2, values, sizeof(values));
return 0;
}
// native void MyPlugin_SetPoints(CPlayerSlot slot, int points);
public any Native_SetPoints(Handle plugin, int numParams)
{
CPlayerSlot slot = Source2ModNatives.GetCell(1);
if (!slot.IsValid())
return Source2ModNatives.ThrowError(SP_ERROR_NATIVE, "Invalid player slot %d", slot.Get());
// ...
return 0;
}A native name belongs to one plugin at a time: creating one that another live plugin owns is an error.
Using another plugin's natives
A plugin that provides natives ships an include with their declarations,
like a SourceMod extension or library does. cpp_sample ships
scripting/include/cpp_sample.inc; for the sample library above:
#if defined _sample_included
#endinput
#endif
#define _sample_included
/** a + b. */
native int Sample_Add(int a, int b);
/** Formats into buffer. @return Bytes written */
native int Sample_Echo(char[] buffer, int maxlen, const char[] fmt, any ...);
public SharedPlugin __pl_sample =
{
name = "sample",
file = "sample.smx",
required = 0,
};
#if !defined REQUIRE_PLUGIN
public void __pl_sample_SetNTVOptional()
{
MarkNativeAsOptional("Sample_Add");
MarkNativeAsOptional("Sample_Echo");
}
#endifRequired natives
A script whose natives are not all there does not start. It is not given up
on: as soon as a plugin registers natives — a C++ plugin through
AddNatives, a script through CreateNative — the scripts waiting for
natives are loaded again, on the next frame. Load order between the
provider and its users therefore does not matter.
Optional natives
#undef REQUIRE_PLUGIN before the include makes its natives optional: the
core calls every __pl_<name>_SetNTVOptional / __ext_<name>_SetNTVOptional
the script has, which marks them with MarkNativeAsOptional. The script
loads without the provider and asks whether the library is there before
calling; calling a missing native is a run-time error.
#include <source2mod/api>
#undef REQUIRE_PLUGIN
#include <sample>
bool g_hasSample;
public void OnPluginStart()
{
g_hasSample = Source2ModNatives.LibraryExists("sample");
Source2ModCommands.RegisterConCommand("add", Cmd_Add);
}
public void OnLibraryAdded(const char[] name)
{
if (Source2ModUtils.Equal(name, "sample"))
g_hasSample = true;
}
public void OnLibraryRemoved(const char[] name)
{
if (Source2ModUtils.Equal(name, "sample"))
g_hasSample = false;
}
public void Cmd_Add(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
if (!g_hasSample)
{
Source2ModCommands.ReplyToCommand(context, "The sample plugin is not loaded.");
return;
}
char text[64];
Sample_Echo(text, sizeof(text), "2 + 3 = %d", Sample_Add(2, 3));
Source2ModCommands.ReplyToCommand(context, "%s", text);
}When the provider unloads, its natives turn into errors and
OnLibraryRemoved fires; when it comes back, the natives are bound again
and OnLibraryAdded fires. LibraryExists is meant for start-up; follow
changes with the two forwards.
Forwards
Global forwards
A GlobalForward calls the public function of its name in every plugin that
has one — SourceMod's CreateGlobalForward. The provider creates it once
and fires it with Source2ModCall:
GlobalForward g_fwdOnPoints;
public void OnPluginStart()
{
// forward Action MyPlugin_OnPoints(CPlayerSlot slot, int &points);
g_fwdOnPoints = new GlobalForward("MyPlugin_OnPoints", ET_Hook, Param_Cell, Param_CellByRef);
}
// Asks every plugin first; false when one of them blocked it.
bool GivePoints(CPlayerSlot slot, int points)
{
Action result = Plugin_Continue;
Source2ModCall.StartForward(g_fwdOnPoints);
Source2ModCall.PushCell(slot);
Source2ModCall.PushCellRef(points);
if (Source2ModCall.Finish(result) != SP_ERROR_NONE || result >= Plugin_Handled)
return false;
// ... give `points` -- changed when a plugin answered Plugin_Changed
return true;
}Any plugin listens by declaring the function — put the forward line in
your include:
public Action MyPlugin_OnPoints(CPlayerSlot slot, int &points)
{
if (points > 100)
return Plugin_Handled; // blocked
points *= 2;
return Plugin_Changed;
}ExecType | The call returns |
|---|---|
ET_Ignore | 0; the functions' return values are ignored |
ET_Single | The last function's value |
ET_Event | The highest Action; a Plugin_Stop does not stop the others |
ET_Hook | The highest Action; Plugin_Stop stops the chain |
FunctionCount says how many functions a forward would call.
Private forwards
A PrivateForward calls only the functions added to it — the shape for a
"call me when …" native that takes a callback:
PrivateForward g_fwdRoundWon;
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
// native void MyPlugin_OnRoundWon(MyPlugin_RoundWonCallback callback);
Source2ModNatives.CreateNative("MyPlugin_OnRoundWon", Native_OnRoundWon);
return APLRes_Success;
}
public void OnPluginStart()
{
// typedef MyPlugin_RoundWonCallback = function void (int team);
g_fwdRoundWon = new PrivateForward(ET_Ignore, Param_Cell);
}
public any Native_OnRoundWon(Handle plugin, int numParams)
{
g_fwdRoundWon.AddFunction(plugin, Source2ModNatives.GetFunction(1));
return 0;
}
void RoundWon(int team)
{
Source2ModCall.StartForward(g_fwdRoundWon);
Source2ModCall.PushCell(team);
Source2ModCall.Finish();
}RemoveFunction and RemoveAllFunctions take functions off again; a
plugin's functions go by themselves when it unloads. Close a forward with
delete.
Calling one function
Source2ModCall.StartFunction(plugin, function) calls a single function —
in the calling plugin (INVALID_HANDLE) or, with the plugin handle a native
got, back in the plugin that called it. GetFunctionByName finds a public
function by name:
public int Tripled(int value)
{
return value * 3;
}
public void OnPluginStart()
{
Function fn = Source2ModCall.GetFunctionByName(INVALID_HANDLE, "Tripled");
int result;
Source2ModCall.StartFunction(INVALID_HANDLE, fn);
Source2ModCall.PushCell(5);
if (Source2ModCall.Finish(result) == SP_ERROR_NONE)
Source2ModApi.Log("Tripled(5) = %d", result);
}| SourceMod | Source2Mod |
|---|---|
Call_StartForward, Call_StartFunction | Source2ModCall.StartForward, StartFunction |
Call_PushCell, Call_PushCellRef, Call_PushFloat, Call_PushFloatRef | PushCell, PushCellRef, PushFloat, PushFloatRef |
Call_PushString, Call_PushStringEx, Call_PushArray, Call_PushArrayEx | PushString, PushStringEx, PushArray, PushArrayEx |
Call_PushNullString, Call_PushNullVector | PushNullString, PushNullVector |
Call_Finish, Call_Cancel | Finish, Cancel |
GetFunctionByName | Source2ModCall.GetFunctionByName |
CreateGlobalForward, CreateForward | new GlobalForward(...), new PrivateForward(...) |
AddToForward, RemoveFromForward | PrivateForward.AddFunction, RemoveFunction |
RegPluginLibrary, LibraryExists | Source2ModNatives.RegisterLibrary, LibraryExists |
GetMyHandle, GetPluginFilename | Source2ModPlugins.GetMyHandle, GetFilename |
SetFailState, ThrowError | Source2ModPlugins.SetFailState, ThrowError |
Finish returns SP_ERROR_NONE or the error the call ended with — a
function that threw, a native that refused.
Entity handles
Entities in scripts are entity handles, not indexes — NULL_ENTITY, IsValid, why an entity is a negative number, converting to and from indexes and slots, and what happens to a handle whose entity is gone.
Raw hooks
Source2ModHook — hooking any game function from a script, described at runtime; where the address comes from, parameter types, this, pre and post, actions and return values, stack arguments, and when a game hook or a forward is the better choice.