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:

plugin.cpp
#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;
}

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:

SourceModSource2Mod
GetNativeCell(n)Source2ModNatives.GetCell(n)A cell: int, bool, float, handle, entity
GetNativeCellRef(n) / SetNativeCellRef(n, v)GetCellRef / SetCellRefA by-reference cell
GetNativeStringLength(n, len)GetStringLengthTo size a buffer
GetNativeString(n, buf, max) / SetNativeString(n, s, max)GetString / SetString
GetNativeArray(n, arr, size) / SetNativeArray(n, arr, size)GetArray / SetArrayAlways by reference
GetNativeFunction(n)GetFunctionA callback the caller passed
IsNativeParamNullString(n) / IsNativeParamNullVector(n)IsParamNullString / IsParamNullVectorNULL_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
plugin.sp
// 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:

include/sample.inc
#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");
}
#endif

Required 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.

plugin.sp
#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:

plugin.sp
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:

plugin.sp
public Action MyPlugin_OnPoints(CPlayerSlot slot, int &points)
{
	if (points > 100)
		return Plugin_Handled;      // blocked

	points *= 2;
	return Plugin_Changed;
}
ExecTypeThe call returns
ET_Ignore0; the functions' return values are ignored
ET_SingleThe last function's value
ET_EventThe highest Action; a Plugin_Stop does not stop the others
ET_HookThe 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:

plugin.sp
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:

plugin.sp
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);
}
SourceModSource2Mod
Call_StartForward, Call_StartFunctionSource2ModCall.StartForward, StartFunction
Call_PushCell, Call_PushCellRef, Call_PushFloat, Call_PushFloatRefPushCell, PushCellRef, PushFloat, PushFloatRef
Call_PushString, Call_PushStringEx, Call_PushArray, Call_PushArrayExPushString, PushStringEx, PushArray, PushArrayEx
Call_PushNullString, Call_PushNullVectorPushNullString, PushNullVector
Call_Finish, Call_CancelFinish, Cancel
GetFunctionByNameSource2ModCall.GetFunctionByName
CreateGlobalForward, CreateForwardnew GlobalForward(...), new PrivateForward(...)
AddToForward, RemoveFromForwardPrivateForward.AddFunction, RemoveFunction
RegPluginLibrary, LibraryExistsSource2ModNatives.RegisterLibrary, LibraryExists
GetMyHandle, GetPluginFilenameSource2ModPlugins.GetMyHandle, GetFilename
SetFailState, ThrowErrorSource2ModPlugins.SetFailState, ThrowError

Finish returns SP_ERROR_NONE or the error the call ended with — a function that threw, a native that refused.

On this page