https://github.com/speedrun-program/load_extender
This is the previous post: UPDATE #1: Lengthening the time it takes to access files using function hooking
I came back to this to try to improve it. I'm using std::unique_ptr instead of std::string_view and vector in the hash map to reduce memory per key. I don't include std::string anymore because I was barely using std::string features. I didn't use the filesystem header, but I did use std::to_chars to make the file finding function more simple.
I still tried to reduce the executable size because, even though the easyhook DLLs are big, they seem like they're an independent thing and could just be replaced with something smaller if it's made available. If I increased the size by ~50% when it could be avoided easily, it would irritate me, so I didn't use the filesystem header.
I tried to improve hash map lookup speed, hash map memory usage, files_and_delays.txt reading speed, and exe file size.
To compile this on Windows, you need to install EasyHook.
load_extender_injector.cpp (only used on Windows because LD_PRELOAD and DYLD_INSERT_LIBRARIES can be used on Linux and Mac OS)
#include <tchar.h>
#include <iostream>
#include <format>
#include <climits>
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>
#include <Windows.h>
void getExitInput()
{
int ch = 0;
for (; ch != '\n'; ch = std::getchar());
std::printf("Press Enter to exit\n");
ch = std::getchar();
}
int _tmain(int argc, _TCHAR* argv[])
{
WCHAR* dllToInject32 = nullptr;
WCHAR* dllToInject64 = nullptr;
_TCHAR* applicationName = argv[0];
DWORD binaryType = 0;
BOOL getBinaryTypeResult = GetBinaryType(applicationName, &binaryType);
if (getBinaryTypeResult == 0 || (binaryType != 0 && binaryType != 6))
{
std::printf("ERROR: This exe wasn't identified as 32-bit or as 64-bit\n");
getExitInput();
return EXIT_FAILURE;
}
else if (binaryType == 0)
{
dllToInject32 = (WCHAR*)L"load_extender_32.dll";
}
else
{
dllToInject64 = (WCHAR*)L"load_extender_64.dll";
}
std::printf("Enter the process Id: ");
DWORD pid = 0;
std::cin >> pid;
NTSTATUS errorCode = RhInjectLibrary(
pid, // The process to inject into
0, // ThreadId to wake up upon injection
EASYHOOK_INJECT_DEFAULT,
dllToInject32, // 32-bit
dllToInject64, // 64-bit
nullptr, // data to send to injected DLL entry point
0 // size of data to send
);
if (errorCode != 0)
{
std::printf("RhInjectLibrary failed with error code = %d\n", errorCode);
PWCHAR errorMessage = RtlGetLastErrorString();
std::printf("%ls\n", errorMessage);
getExitInput();
return EXIT_FAILURE;
}
std::printf("Library injected successfully.\n");
getExitInput();
return EXIT_SUCCESS;
}
load_extender.cpp
#include <climits>
#include <mutex>
#include <thread>
#include <vector>
#include <memory>
#include <cstring>
#include <charconv>
#include <exception>
#include <string_view>
#include <unordered_map>
#ifdef _WIN32
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>
#include <Windows.h>
using wcharOrChar = wchar_t; // file paths are UTF-16LE on Windows
using svType = std::wstring_view;
#define cmpFunction std::wcscmp
#else
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <dlfcn.h>
using wcharOrChar = char;
using svType = std::string_view;
#define cmpFunction std::strcmp
#endif
using uPtrType = std::unique_ptr<wcharOrChar[]>;
using vectorType = std::vector<wcharOrChar>;
// using multiple cpp files made exe bigger, so definitions are in this header
#include "shared.h"
static MapAndMutex mapAndMutexObject;
#ifdef _WIN32
static NTSTATUS WINAPI NtCreateFileHook(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength)
{
const wchar_t* path = (const wchar_t*)(ObjectAttributes->ObjectName->Buffer);
int pathEndIndex = (ObjectAttributes->ObjectName->Length) / sizeof(wchar_t);
int filenameIndex = pathEndIndex;
for (; filenameIndex >= 0 && path[filenameIndex] != '\\'; filenameIndex--);
filenameIndex++; // moving past '\\' character or to 0 if no '\\' was found
auto it = mapAndMutexObject.fileMap.find(
svType(path + filenameIndex,
(size_t)pathEndIndex - filenameIndex)
);
if (it != mapAndMutexObject.fileMap.end())
{
mapAndMutexObject.delayFile(it->second);
}
return NtCreateFile(
FileHandle,
DesiredAccess,
ObjectAttributes,
IoStatusBlock,
AllocationSize,
FileAttributes,
ShareAccess,
CreateDisposition,
CreateOptions,
EaBuffer,
EaLength
);
}
extern "C" void __declspec(dllexport) __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO * inRemoteInfo);
void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* inRemoteInfo)
{
HOOK_TRACE_INFO hHook1 = { nullptr };
HMODULE moduleHandle = GetModuleHandle(TEXT("ntdll"));
if (moduleHandle)
{
LhInstallHook(
GetProcAddress(moduleHandle, "NtCreateFile"),
NtCreateFileHook,
nullptr,
&hHook1
);
}
ULONG ACLEntries[1] = { 0 };
LhSetExclusiveACL(ACLEntries, 1, &hHook1);
}
#else
static auto originalFopen = reinterpret_cast<FILE * (*)(const char* path, const char* mode)>(dlsym(RTLD_NEXT, "fopen"));
FILE* fopen(const char* path, const char* mode)
{
int filenameIndex = -1;
int pathEndIndex = 0;
for (; path[pathEndIndex] != '\0'; pathEndIndex++)
{
if (path[pathEndIndex] == '/')
{
filenameIndex = pathEndIndex;
}
}
filenameIndex++; // moving past '/' character or to 0 if no '/' was found
auto it = mapAndMutexObject.fileMap.find(
svType(path + filenameIndex,
(size_t)pathEndIndex - filenameIndex)
);
if (it != mapAndMutexObject.fileMap.end())
{
delayFile(it->second);
}
return original_fopen(path, mode);
}
#endif
shared.h
#ifndef DEBUG
#define stdprintf(...) (0)
#endif
bool checkIfFileExists(const char* filename)
{
FILE* fcheck = nullptr;
try
{
bool exists = false;
#ifdef _WIN32
fopen_s(&fcheck, filename, "rb");
#else
fcheck = std::fopen(filename, "rb");
#endif
if (fcheck)
{
std::fclose(fcheck);
fcheck = nullptr;
exists = true;
}
return exists;
}
catch (char const* e) // making sure fcheck gets closed if an error happens somehow
{
if (fcheck)
{
std::fclose(fcheck);
}
throw std::runtime_error(e);
}
}
// char name[16 + 10 + 4 + 1] starts as "files_and_delays1.txt"
// 16: "files_and_delays"
// 10: number part
// 4: ".txt"
// 1: '\0'
void findCorrectFileName(char* name)
{
unsigned int fileNumber = 1;
while (checkIfFileExists(name))
{
fileNumber++;
auto [dottxt, ec] = std::to_chars(name + 16, name + 26, fileNumber);
if (ec == std::errc::value_too_large || fileNumber == 0) // this probably won't ever happen
{
throw std::runtime_error("too many files_and_delays files");
}
dottxt[0] = '.'; dottxt[1] = 't'; dottxt[2] = 'x'; dottxt[3] = 't';
}
fileNumber--;
auto [dottxt, _] = std::to_chars(name + 16, name + 26, fileNumber);
// dottxt[4] might not be '\0'. for example, fileNumber decrements from 100 to 99
dottxt[0] = '.'; dottxt[1] = 't'; dottxt[2] = 'x'; dottxt[3] = 't'; dottxt[4] = '\0';
}
class FileHelper
{
public:
FileHelper(const FileHelper& fhelper) = delete;
FileHelper& operator=(FileHelper other) = delete;
FileHelper(FileHelper&&) = delete;
FileHelper& operator=(FileHelper&&) = delete;
FileHelper(const char* filename)
{
#ifdef _WIN32
if (fopen_s(&_f, filename, "rb") != 0 || !_f)
#else
if (!(_f = std::fopen(filePath, "rb")))
#endif
{
stdprintf("ERROR: %s\n", filename);
throw std::runtime_error("FileHelper fopen failure in constructor");
}
}
~FileHelper()
{
if (_f)
{
std::fclose(_f);
}
}
bool getCharacter(wcharOrChar& ch)
{
if (_bufferPosition == _charactersRead)
{
_bufferPosition = 0;
_charactersRead = (int)std::fread(_buffer.data(), sizeof(wcharOrChar), _buffer.size(), _f);
if (!_charactersRead)
{
return false;
}
}
ch = _buffer.at(_bufferPosition);
_bufferPosition++;
return true;
}
void resetFile() // used in tests
{
if (std::fseek(_f, 0, SEEK_SET) != 0)
{
throw std::runtime_error("FileHelper fseek failure in resetFile");
}
_bufferPosition = 0;
_charactersRead = 0;
}
private:
FILE* _f = nullptr;
std::vector<wcharOrChar> _buffer = std::vector<wcharOrChar>(8192 / sizeof(wcharOrChar));
int _bufferPosition = 0;
int _charactersRead = 0;
};
struct MapValue
{
// delays[0] == -1 says to reset all MapValue.position to 0
// delays ending with -1 says to reset at the end
// delays ending with -2 says to NOT reset at the end
std::unique_ptr<int[]> delays;
size_t position = 0;
size_t fullResetCheckNumber = 0;
MapValue(std::vector<int>& delaysVector)
{
delays = std::make_unique_for_overwrite<int[]>(delaysVector.size());
std::memcpy(delays.get(), delaysVector.data(), delaysVector.size() * sizeof(int));
}
};
struct KeyCmp
{
using is_transparent = void;
bool operator()(const uPtrType& cStr1, const uPtrType& cStr2) const
{
return cmpFunction(cStr1.get(), cStr2.get()) == 0;
}
bool operator()(const uPtrType& cStr, const svType sv) const
{
return cmpFunction(cStr.get(), sv.data()) == 0;
}
bool operator()(const svType sv, const uPtrType& cStr) const
{
return cmpFunction(sv.data(), cStr.get()) == 0;
}
};
struct KeyHash
{
using is_transparent = void;
size_t operator()(const svType sv) const
{
return _hashObject(sv);
}
size_t operator()(const uPtrType& cStr) const
{
// if they add an easy way to do it, change this so it doesn't need to find the c-string length
return _hashObject(svType(cStr.get()));
}
private:
std::hash<svType> _hashObject = std::hash<svType>();
};
using myMapType = std::unordered_map<uPtrType, MapValue, KeyHash, KeyCmp>;
class MapAndMutex
{
public:
std::mutex mutexForMap;
myMapType fileMap;
MapAndMutex()
{
try
{
// intAsChars used in fillDelaysVector but made here so it doesn't need to be remade repeatedly
std::vector<char> intAsChars;
intAsChars.reserve(10);
intAsChars.push_back('0'); // empty vector causes std::errc::invalid_argument
vectorType keyVector;
std::vector<int> delaysVector;
char name[16 + 10 + 4 + 1] = "files_and_delays.txt";
// 16: "files_and_delays"
// 10: number part
// 4: ".txt"
// 1: '\0'
if (checkIfFileExists("files_and_delays0.txt"))
{
char* nameEnd = name + 16;
nameEnd[0] = '1'; nameEnd[1] = '.'; nameEnd[2] = 't'; nameEnd[3] = 'x'; nameEnd[4] = 't';
findCorrectFileName(name);
}
FileHelper fhelper(name);
#ifdef _WIN32
wchar_t byteOrderMark = '\0';
if (!fhelper.getCharacter(byteOrderMark))
{
stdprintf(
"files_and_delays.txt byte order mark is missing\n\
make sure files_and_delays.txt is saved as UTF-16 LE\n"
);
}
else if (byteOrderMark != 0xFEFF) // not 0xFFFE due to how wchar_t is read
{
stdprintf(
"files_and_delays.txt byte order mark isn't marked as UTF-16 LE\n\
make sure files_and_delays.txt is saved as UTF-16 LE\n"
);
}
#endif
while (addMapPair(fileMap, keyVector, delaysVector, fhelper, intAsChars));
}
catch (const std::runtime_error& e)
{
char const* fixC4101Warning = e.what();
stdprintf("%s\n", fixC4101Warning);
fileMap.clear(); // clear map so failure is more obvious
}
}
bool addMapPair(myMapType& fileMap, vectorType& keyVector, std::vector<int>& delaysVector, FileHelper& fhelper, std::vector<char> intAsChars)
{
keyVector.clear();
delaysVector.clear();
wcharOrChar ch = '\0';
bool stripWhitespace = false;
bool textRemaining = fhelper.getCharacter(ch);
if (ch == '/')
{
stripWhitespace = true;
textRemaining = fhelper.getCharacter(ch);
}
else if (ch == ' ' || ch == '\f' || ch == '\r' || ch == '\t' || ch == '\v')
{
// don't include starting whitespace
for (
textRemaining = fhelper.getCharacter(ch);
textRemaining && (ch == ' ' || ch == '\f' || ch == '\r' || ch == '\t' || ch == '\v');
textRemaining = fhelper.getCharacter(ch));
}
while (ch != '\n' && ch != '/' && textRemaining)
{
keyVector.push_back(ch);
textRemaining = fhelper.getCharacter(ch);
}
// don't include ending whitespace
if (!stripWhitespace)
{
while (!keyVector.empty())
{
wcharOrChar ch = keyVector.back();
if (ch == ' ' || ch == '\f' || ch == '\r' || ch == '\t' || ch == '\v')
{
keyVector.pop_back();
}
else
{
break;
}
}
}
if (textRemaining && ch == '/') // line didn't end abruptly
{
fillDelaysVector(textRemaining, delaysVector, fhelper, intAsChars);
if (!keyVector.empty() && !delaysVector.empty())
{
if (delaysVector.back() != -1)
{
delaysVector.push_back(-2);
}
keyVector.push_back('\0');
uPtrType keyPtr = std::make_unique_for_overwrite<wcharOrChar[]>(keyVector.size());
std::memcpy(keyPtr.get(), keyVector.data(), keyVector.size() * sizeof(wcharOrChar));
fileMap.emplace(std::move(keyPtr), MapValue(delaysVector));
}
}
return textRemaining;
}
// the -2 at the end is added in addMapPair when there isn't already a -1
void fillDelaysVector(bool& textRemaining, std::vector<int>& delaysVector, FileHelper& fhelper, std::vector<char> intAsChars)
{
wcharOrChar ch = '\0';
int delay = 0;
for (
textRemaining = fhelper.getCharacter(ch);
ch != '\n' && textRemaining;
textRemaining = fhelper.getCharacter(ch))
{
if (ch >= '0' && ch <= '9')
{
intAsChars.push_back((char)ch);
}
else if (ch == '-')
{
delaysVector.push_back(-1);
break;
}
else if (ch == '/')
{
auto [ptr, ec] = std::from_chars(intAsChars.data(), intAsChars.data() + intAsChars.size(), delay);
if (ec == std::errc::result_out_of_range)
{
throw std::runtime_error("delays can't be larger than INT_MAX");
}
delaysVector.push_back(delay);
intAsChars.clear();
intAsChars.push_back('0'); // empty vector causes std::errc::invalid_argument
}
}
if (delaysVector.empty() || delaysVector.back() != -1)
{
if (intAsChars.size() > 1)
{
auto [ptr, ec] = std::from_chars(intAsChars.data(), intAsChars.data() + intAsChars.size(), delay);
if (ec == std::errc::result_out_of_range)
{
throw std::runtime_error("delays can't be larger than INT_MAX");
}
delaysVector.push_back(delay);
}
}
// make sure to go to end of line
for (; ch != '\n' && textRemaining; textRemaining = fhelper.getCharacter(ch));
}
void delayFile(MapValue& fileMapValue)
{
#ifndef DEBUG // this needs to be reset in the test, so it's a global variable instead
size_t fullResetCount = 0;
#endif
stdprintf("fullResetCount: %zu\n", fullResetCount);
int delay = 0;
{
std::lock_guard<std::mutex> mutexForMapLock(mutexForMap);
if (fileMapValue.fullResetCheckNumber < fullResetCount)
{
fileMapValue.position = 0;
fileMapValue.fullResetCheckNumber = fullResetCount;
stdprintf("this delay sequence reset due to prior full reset\n");
}
if (fileMapValue.delays[0] == -1)
{
if (fullResetCount == SIZE_MAX) // this probably won't ever happen
{
fullResetCount = 0;
for (auto& [uPtrType, MapValue] : fileMap)
{
MapValue.fullResetCheckNumber = 0;
}
stdprintf("fullResetCount reset\n");
}
fullResetCount++;
stdprintf("fullResetCount set to %zu, all sequences will be reset\n", fullResetCount);
}
else if (fileMapValue.delays[fileMapValue.position] == -1)
{
fileMapValue.position = 0;
stdprintf("this delay sequence reset\n");
}
else if (fileMapValue.delays[fileMapValue.position] == -2)
{
stdprintf("delay sequence already finished\n");
}
if (fileMapValue.delays[fileMapValue.position] > 0)
{
delay = fileMapValue.delays[fileMapValue.position];
fileMapValue.position++;
}
stdprintf("delay is %d millisecond(s)\n\n", delay);
}
if (delay > 0)
{
#ifndef DEBUG
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
#endif
}
}
};
load_extender_test.cpp
#include <climits>
#include <mutex>
#include <vector>
#include <memory>
#include <cstring>
#include <charconv>
#include <exception>
#include <string_view>
#include <unordered_map>
#ifdef _WIN32
using wcharOrChar = wchar_t; // file paths are UTF-16LE on Windows
using svType = std::wstring_view;
#define cmpFunction std::wcscmp
#else
using wcharOrChar = char;
using svType = std::string_view;
#define cmpFunction std::strcmp
#endif
using uPtrType = std::unique_ptr<wcharOrChar[]>;
using vectorType = std::vector<wcharOrChar>;
#define DEBUG
#define stdprintf std::printf
size_t fullResetCount = 0;
// using multiple cpp files made exe bigger, so definitions are in this header
#include "shared.h"
void windowsHookFunction(MapAndMutex& mapAndMutexObject, vectorType& path)
{
if (!path.empty() && path.back() == '\r')
{
path.pop_back();
}
path.push_back('\0');
const wcharOrChar* pathPtr = path.data();
// in the actual program, pathEndIndex will always be less than INT_MAX
int pathEndIndex = path.size() < INT_MAX ? (int)path.size() : INT_MAX;
int filenameIndex = pathEndIndex;
for (; filenameIndex >= 0 && pathPtr[filenameIndex] != '\\'; filenameIndex--);
filenameIndex++; // moving past '\\' character or to 0 if no '\\' was found
auto it = mapAndMutexObject.fileMap.find(
svType(pathPtr + filenameIndex,
(size_t)pathEndIndex - filenameIndex)
);
if (it != mapAndMutexObject.fileMap.end())
{
#ifdef _WIN32
std::printf("\n%ls found in map\n", path.data());
#else
std::printf("\n%s found in map\n", path.c_str());
#endif
mapAndMutexObject.delayFile(it->second);
}
else
{
#ifdef _WIN32
std::printf("\n%ls not found in map\n", path.data());
#else
std::printf("\n%s not found in map\n", path.c_str());
#endif
}
}
void unixHookFunction(MapAndMutex& mapAndMutexObject, vectorType& path)
{
if (!path.empty() && path.back() == '\r')
{
path.pop_back();
}
path.push_back('\0');
const wcharOrChar* pathPtr = path.data();
int filenameIndex = -1;
int pathEndIndex = 0;
// in the actual program, pathEndIndex will always be less than INT_MAX
for (; pathPtr[pathEndIndex] != '\0' && pathEndIndex < INT_MAX; pathEndIndex++)
{
if (pathPtr[pathEndIndex] == '/')
{
filenameIndex = pathEndIndex;
}
}
filenameIndex++; // moving past '/' character or to 0 if no '/' was found
auto it = mapAndMutexObject.fileMap.find(
svType(pathPtr + filenameIndex,
(size_t)pathEndIndex - filenameIndex)
);
if (it != mapAndMutexObject.fileMap.end())
{
#ifdef _WIN32
std::printf("\n%ls found in map\n", path.data());
#else
std::printf("\n%s found in map\n", path.data());
#endif
mapAndMutexObject.delayFile(it->second);
}
else
{
#ifdef _WIN32
std::printf("\n%ls not found in map\n", path.data());
#else
std::printf("\n%s not found in map\n", path.data());
#endif
}
}
void printMap(const myMapType& fileMap)
{
for (auto& [key, value] : fileMap)
{
std::printf(
#ifdef _WIN32
"%ls : fullResetCheckNumber %zu : position %zu : ",
#else
"%s : fullResetCheckNumber %zu : position %zu : ",
#endif
key.get(),
value.fullResetCheckNumber,
value.position
);
size_t delaysPosition = 0;
for (; value.delays[delaysPosition] >= 0; delaysPosition++)
{
std::printf("%d / ", value.delays[delaysPosition]);
}
if (value.delays[delaysPosition] == -1)
{
std::printf(delaysPosition == 0 ? "RESET ALL" : "RESET");
}
std::printf("\n");
}
}
void testFunctions(MapAndMutex& mapAndMutexObject, FileHelper& fhelper, bool testUnix)
{
vectorType path;
wcharOrChar ch = '\0';
while (fhelper.getCharacter(ch))
{
if (ch == '\n')
{
if (testUnix)
{
unixHookFunction(mapAndMutexObject, path);
}
else
{
windowsHookFunction(mapAndMutexObject, path);
}
printMap(mapAndMutexObject.fileMap);
path.clear();
}
else
{
path.push_back(ch);
}
}
if (testUnix)
{
unixHookFunction(mapAndMutexObject, path);
}
else
{
windowsHookFunction(mapAndMutexObject, path);
}
}
void testInputs(MapAndMutex& mapAndMutexObject)
{
FileHelper fhelper("test_input.txt");
wcharOrChar byteOrderMark = '\0';
#ifdef _WIN32
if (!fhelper.getCharacter(byteOrderMark))
{
std::printf(
"test_input.txt byte order mark is missing\n\
save test_input.txt as UTF-16 LE\n\n"
);
return;
}
else if (byteOrderMark != 0xFEFF) // not 0xFFFE due to how wchar_t is read
{
std::printf(
"test_input.txt byte order mark isn't marked as UTF-16 LE\n\
make sure files_and_delays.txt is saved as UTF-16 LE\n\n"
);
}
#endif
std::printf("\ntesting UNIX\n\n - - - - - - - - - -\n\n");
printMap(mapAndMutexObject.fileMap);
testFunctions(mapAndMutexObject, fhelper, true);
printMap(mapAndMutexObject.fileMap);
// resetting things to test windows version of fake hook function
fullResetCount = 0;
fhelper.resetFile();
#ifdef _WIN32
fhelper.getCharacter(byteOrderMark);
#endif
for (auto& [key, value] : mapAndMutexObject.fileMap)
{
value.position = 0;
value.fullResetCheckNumber = 0;
}
std::printf("\n\ntesting Windows\n\n - - - - - - - - - -\n\n");
printMap(mapAndMutexObject.fileMap);
testFunctions(mapAndMutexObject, fhelper, false);
printMap(mapAndMutexObject.fileMap);
}
int main()
{
try
{
std::printf("\ntest start\n\n");
MapAndMutex mapAndMutexObject;
testInputs(mapAndMutexObject);
}
catch (const std::runtime_error& e)
{
std::printf("%s", e.what());
return EXIT_FAILURE;
}
std::printf("\ntest finished, press Enter to exit\n");
char ch = getchar();
return EXIT_SUCCESS;
}