
A minimal PE mapper that loads DLLs straight from memory and calls into a clean plugin interface, no LoadLibrary needed.
A minimal reflective PE mapper with a hot-swappable plugin interface for Windows x64. Maps a DLL straight from a byte buffer in memory, resolves its exports, and calls into a clean IPlugin ABI without touching LoadLibrary. Companion code for the blog post on reflective loaders and modular plugin architectures.
See the full writeup: Using Reflective Loaders to Replace LoadLibrary for Hot-Swappable Modules in C++
# Configure
cmake -B build
# Build
cmake --build build --config Release
# The build produces:
# build/Release/cmdplugin.dll (example plugin)
# build/Release/harness.exe (test harness)
# Map cmdplugin.dll from disk, run "dir C:\"
harness.exe cmdplugin.dll "dir C:\"
# Default: loads cmdplugin.dll, runs "whoami"
harness.exe
The harness reads the DLL into a byte buffer, maps it with the reflective loader, resolves the plugin exports, and dispatches the command through the IPlugin interface.
ReflectivePluginLoader/
├── CMakeLists.txt # Root build
├── Include/
│ ├── IPlugin.h # Plugin ABI (TaskApi, IPlugin, helpers)
│ └── ReflectiveLoaderEngine.h # PE mapper + export resolver
├── Modules/
│ └── CmdPlugin/
│ ├── cmdplugin.cpp # Example: command execution plugin
│ └── CMakeLists.txt
├── Testing/
│ ├── main.cpp # Test harness (loads DLL from file)
│ └── CMakeLists.txt
└── Tools/
└── file2hex.py # Convert a DLL to a C byte array
Modules/.IPlugin.h and implement the IPlugin interface:#include "IPlugin.h"
class MyPlugin : public IPlugin {
public:
void init() const override { /* setup */ }
void execute(TaskApi* task) const override { /* do work */ }
void cleanup() const override { /* teardown */ }
};
Implement the five exported functions (create_plugin, destroy_plugin, plugin_init, plugin_exec, plugin_cleanup) using HeapAlloc/placement new for CRT-safe cross-module allocation.
Add a CMakeLists.txt and register it in the root CMakeLists.txt with add_subdirectory().
The host doesn't need to know what your module does internally. It maps, resolves, calls init -> execute -> cleanup, and moves on.
The mapper handles the basics and deliberately stops there:
If a module needs any of those, either extend the mapper or reject the module early with a clear error. Silent half-support is the worst failure mode.