Skip to content

Instantly share code, notes, and snippets.

@Rainyan
Last active June 27, 2025 09:31
Show Gist options
  • Select an option

  • Save Rainyan/66b2bb56a82c860b6d866cd07ebabf56 to your computer and use it in GitHub Desktop.

Select an option

Save Rainyan/66b2bb56a82c860b6d866cd07ebabf56 to your computer and use it in GitHub Desktop.
Call malloc, realloc, free from SourceMod plugins, with optional automatic memory cleanup. Gamedata available only for Neotokyo (2006/ep1 engine, Windows 32bit), but could probably be used for other games too with some modifications. Tested on SM version 1.12. Experimental; use at own risk.
#include <sourcemod>
#include <sdktools>
#pragma semicolon 1
#pragma newdecls required
// SemVer 2.0.0
#define MEMTOOLS_VERSION_MAJOR 0
#define MEMTOOLS_VERSION_MINOR 1
#define MEMTOOLS_VERSION_PATCH 0
#define WORD_SIZE 4
#if WORD_SIZE < 1
#error Invalid WORD_SIZE
#endif
// "This" ECX ptr used for the g_pMemAlloc SDKCall thiscalls.
static Address _this;
// Record of allocated blocks.
StringMap _allocs = null;
// Internal SDKCall handles.
enum {
REALLOC = 0, // (g_pMemAlloc) IMemAlloc::Realloc
FREE, // (g_pMemAlloc) IMemAlloc::Free
FUN_COUNT,
};
static Handle _fun[FUN_COUNT] = { INVALID_HANDLE, ... };
// If you need OnPluginEnd for your own plugin, define the MEM_MANUAL_CLEANUP preprocessor value to disable this code block.
// Note that in such case - and only that case - you *must* call CleanupMemAlloc manually at your OnPluginEnd.
#if !defined(MEM_MANUAL_CLEANUP)
public void OnPluginEnd()
{
CleanupMemAlloc();
}
#endif
// Cleans up all the allocations made by the calling plugin.
// Calling this is optional, as it will be invoked by the OnPluginEnd implementation.
// You may also override the automatic cleanup by defining the MEM_MANUAL_CLEANUP preprocessor value.
// Note that you should *not* call InitMemAlloc again even if using this function manually.
stock void CleanupMemAlloc()
{
if (_allocs == null) {
LogError("Redundant call - did you call this multiple times?");
return;
}
StringMapSnapshot snap = _allocs.Snapshot();
if (snap == null) {
SetFailState("Failed to create hashmap snapshot");
}
for (int i = 0; i < snap.Length; ++i) {
int keyLen = snap.KeyBufferSize(i);
char[] key = new char[keyLen];
if (snap.GetKey(i, key, keyLen) <= 0) {
ThrowError("Failed to retrieve key \"%s\"", key);
}
Address ptr;
if (!_allocs.GetValue(key, ptr)) {
ThrowError("Failed to retrieve ptr value for key \"%s\"", key);
}
Free(ptr, true);
/* // DEBUG
if (_allocs.GetValue(key, ptr)) {
ThrowError("Failed to remove ptr value for key \"%s\"", key);
}
*/
}
delete snap;
}
// This must be called once, and only once, before using the memtools API.
void InitMemAlloc()
{
if (_allocs != null) {
LogError("Redundant call - did you call this multiple times?");
return;
}
_allocs = new StringMap();
GameData gd = LoadGameConfigFile("neotokyo/memtools");
if (gd == INVALID_HANDLE) {
SetFailState("Failed to load GameData");
}
_this = gd.GetAddress("Tier0_g_pMemAlloc");
if (_this == Address_Null) {
SetFailState("Address lookup failed");
}
prepare(gd);
delete gd;
}
// Allocates nSize contiguous bytes on the heap, and returns the Address whence that allocation begins.
// If the "track" argument is true, will record this allocation as originating from calling plugin,
// for the purposes of pairing it with Free calls and the optional automatic memory cleanup.
stock Address Malloc(int nSize, bool track=true)
{
return Realloc(Address_Null, nSize, track);
}
// Malloc aligned to word boundary.
// NOTE: UNTESTED!
// TODO: test
// TODO: aligned free
stock Address MallocAligned(int nSize, int align, bool track=true)
{
if (align <= WORD_SIZE) {
align = WORD_SIZE;
}
else if (align % WORD_SIZE != 0) {
ThrowError("Alignment (%d) must be a multiple of word size (%d)",
align, WORD_SIZE);
}
--align;
Address a = Malloc(WORD_SIZE + align + nSize, track);
Address b = (a + view_as<Address>(WORD_SIZE + align)) & ~view_as<Address>(align);
StoreToAddress(b-view_as<Address>(1), a, NumberType_Int32);
return b;
}
// Allocates nSize contiguous bytes on the heap, and returns the Address whence that allocation begins.
// If it is possible to allocate the bytes at pMem, that address will be used.
// Else, some other address will be used instead.
// Using a pMem address of Address_Null makes this call equivalent to Malloc.
// If the "track" argument is true, will record this allocation as originating from calling plugin,
// for the purposes of pairing it with Free calls and the optional automatic memory cleanup.
stock Address Realloc(Address pMem, int nSize, bool track=true)
{
Address res = SDKCall(_fun[REALLOC], _this, pMem, nSize);
if (res == Address_Null) {
ThrowError("Allocation failed for 0x%x, %d, %d", pMem, nSize, track);
}
if (track) {
char buf[32];
if (0 == addressToString(res, buf, sizeof(buf)))
{
ThrowError("Address to int conversion failed for 0x%x", res);
}
if (!_allocs.SetValue(buf, res, true))
{
ThrowError("Alloc to 0x%x (\"%s\") collided with existing alloc", res, buf);
}
}
return res;
}
// Frees previously allocated memory at Address pMem.
// If "force" equals false, will error if that Address was not allocated by this plugin.
stock void Free(Address pMem, bool force=false)
{
PrintToServer("Freeing: %x", pMem);
if (!force) {
char buf[32];
if (0 == addressToString(pMem, buf, sizeof(buf))) {
ThrowError("Address to int conversion failed for 0x%x", pMem);
}
if (!_allocs.Remove(buf)) {
ThrowError("Memory at 0x%x (\"%s\") was not allocated by this plugin", pMem, buf);
}
}
SDKCall(_fun[FREE], _this, pMem);
}
// Passes a hexadecimal string representation of an Address by reference.
static int addressToString(Address a, char[] out, int maxlen)
{
return Format(out, maxlen, "0x%x", a);
}
// Prepares the SDK calls used to invoke the game's underlying memory functions.
static void prepare(const GameData gd)
{
int n = 0;
Handle tmp;
StartPrepSDKCall(SDKCall_Raw);
PrepSDKCall_SetAddress(_this);
PrepSDKCall_SetVirtual(gd.GetOffset("Realloc"));
PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
tmp = EndPrepSDKCall();
if (tmp == INVALID_HANDLE) {
SetFailState("Failed to prepare SDK call: Realloc");
}
_fun[REALLOC] = tmp;
++n;
StartPrepSDKCall(SDKCall_Raw);
PrepSDKCall_SetAddress(_this);
PrepSDKCall_SetVirtual(gd.GetOffset("Free"));
PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
tmp = EndPrepSDKCall();
if (tmp == INVALID_HANDLE) {
SetFailState("Failed to prepare SDK call: Free");
}
_fun[FREE] = tmp;
++n;
if (n != FUN_COUNT) {
SetFailState("Prepared function count mismatch (%d != %d)", n, FUN_COUNT);
}
}
"Games"
{
"NeotokyoSource"
{
"Signatures"
{
"Sig_HasMalloc"
{
"library" "server"
"windows" "\xA1\x2A\x2A\x2A\x2A\x56\x57\x8B\xF9\x8B\x08\x8B\x11\x8B\x42\x2A\x6A\x48"
}
}
"Addresses"
{
"Tier0_g_pMemAlloc"
{
"signature" "Sig_HasMalloc"
"read" "1"
"read" "0"
"read" "0"
}
}
"Offsets"
{
"Realloc"
{
"windows" "2"
}
"Free"
{
"windows" "3"
}
}
}
}
#include <sourcemod>
#include <sdktools>
#include "nt_mem.inc"
#pragma semicolon 1
#pragma newdecls required
static Address m = Address_Null;
public void OnPluginStart()
{
// Include versioning follows SemVer 2.0.0
PrintToServer("Using memtools version %d.%d.%d",
MEMTOOLS_VERSION_MAJOR,
MEMTOOLS_VERSION_MINOR,
MEMTOOLS_VERSION_PATCH);
// Must be called before using the memtools functionality
InitMemAlloc();
// First allocation. This calls malloc behind the scenes,
// and returns the start Address for that allocation.
m = Malloc(16);
PrintToServer("Malloc at: 0x%x", m);
for (int offset=0; offset<16;++offset)
{
Address a = m + view_as<Address>(offset);
// Store some values to the allocation
StoreToAddress(a, 42+offset, NumberType_Int32);
// And read them
any x = LoadFromAddress(a, NumberType_Int32);
PrintToServer("At 0x%x: %d", a, x);
}
// Allocate some more, reusing original alloc location if possible.
m = Realloc(m, 32);
// A different allocation
Address b = Malloc(123);
// Free the allocated memory at Address b.
// Note that the memtools include implements the OnPluginEnd native,
// so all your allocations are automatically freed at plugin unload.
//
// If you need your own OnPluginEnd implementation or want to manage
// the memory lifetime yourself, you can define the MEM_MANUAL_CLEANUP
// preprocessor define, which will make you responsible for the memory cleanup.
// In such case, you probably want to call the CleanupMemAlloc manually
// at OnPluginEnd to guard against leaks on unexpected plugin unloads
// (even if you plan to call Free manually elsewhere).
//
// You are also allowed to call CleanupMemAlloc manually at any time
// once InitMemAlloc has been called to clean the memory used since,
// for example at OnMapEnd; this holds true even when not defining the
// MEM_MANUAL_CLEANUP override.
Free(b);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment