Last active
June 27, 2025 09:31
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "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" | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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