Last active
March 26, 2026 17:17
-
-
Save waldnercharles/fd506cf5127e6cc822a7a9505a83bccd to your computer and use it in GitHub Desktop.
A quick and dirty hot-reloader
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 "plugin.h" | |
| #include <stdio.h> | |
| int main(int argc, char *argv[]) | |
| { | |
| (void)argc; | |
| Plugin plugin = { 0 }; | |
| if (!plugin_open(&plugin, argv[0], "my_cool_plugin")) { | |
| return 1; | |
| } | |
| while (!plugin_update(&plugin)) { | |
| // Keep on updating! | |
| } | |
| plugin_close(&plugin); | |
| return 0; | |
| } |
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 "plugin.h" | |
| static int on_load(PluginCtx *ctx) { return 0; } | |
| static int on_step(PluginCtx *ctx) { return 0; } // Return non-zero to exit | |
| static int on_close(PluginCtx *ctx) { return 0; } | |
| PLUGIN_ENTRY(on_load, on_step, NULL, on_close) // You could also just write your own plugin_entry. | |
| // It can be named whatever you want with #define PLUGIN_ENTRY_SYM |
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
| #ifndef PLUGIN_H | |
| #define PLUGIN_H | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #if defined(_WIN32) | |
| #define PLUGIN_EXPORT __declspec(dllexport) | |
| #define PLUGIN_EXT ".dll" | |
| #elif defined(__APPLE__) | |
| #define PLUGIN_EXPORT __attribute__((visibility("default"))) | |
| #define PLUGIN_EXT ".dylib" | |
| #elif defined(__linux__) | |
| #define PLUGIN_EXPORT __attribute__((visibility("default"))) | |
| #define PLUGIN_EXT ".so" | |
| #else | |
| #error "Unsupported platform" | |
| #endif | |
| /* --- Plugin interface ---------------------------------------------------- */ | |
| typedef enum | |
| { | |
| PLUGIN_LOAD, | |
| PLUGIN_STEP, | |
| PLUGIN_UNLOAD, | |
| PLUGIN_CLOSE, | |
| } PluginOp; | |
| typedef struct | |
| { | |
| void *userdata; | |
| unsigned int version; | |
| } PluginCtx; | |
| typedef int (*PluginEntryFn)(PluginCtx *ctx, PluginOp op); | |
| /* --- Plugin entry point macro -------------------------------------------- */ | |
| static int plugin_noop_(PluginCtx *ctx) | |
| { | |
| (void)ctx; | |
| return 0; | |
| } | |
| /* clang-format off */ | |
| #define PLUGIN_ENTRY(load, step, unload, close) \ | |
| PLUGIN_EXPORT int plugin_entry(PluginCtx *ctx, PluginOp op) \ | |
| { \ | |
| switch (op) { \ | |
| case PLUGIN_LOAD: return ((load) ? (load) : plugin_noop_)(ctx); \ | |
| case PLUGIN_STEP: return ((step) ? (step) : plugin_noop_)(ctx); \ | |
| case PLUGIN_UNLOAD: return ((unload) ? (unload) : plugin_noop_)(ctx); \ | |
| case PLUGIN_CLOSE: return ((close) ? (close) : plugin_noop_)(ctx); \ | |
| } \ | |
| return 0; \ | |
| } | |
| /* clang-format on */ | |
| /* --- Host plugin management ---------------------------------------------- */ | |
| #ifndef PLUGIN_STATIC | |
| #ifndef PLUGIN_ENTRY_SYM | |
| #define PLUGIN_ENTRY_SYM "plugin_entry" | |
| #endif | |
| #include <limits.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <time.h> | |
| static const char *plugin_exe_dir_(const char *argv0) | |
| { | |
| static char buf[PATH_MAX]; | |
| if (!realpath(argv0, buf)) return "."; | |
| char *slash = strrchr(buf, '/'); | |
| if (slash) *slash = '\0'; | |
| return buf; | |
| } | |
| #if defined(_WIN32) | |
| #define WIN32_LEAN_AND_MEAN | |
| #include <sys/stat.h> | |
| #include <windows.h> | |
| static time_t plugin_mtime_(const char *path) | |
| { | |
| struct _stat st; | |
| if (_stat(path, &st) != 0) return 0; | |
| return st.st_mtime; | |
| } | |
| static void *plugin_dlopen_(const char *path) | |
| { | |
| return (void *)LoadLibraryA(path); | |
| } | |
| static void plugin_dlclose_(void *handle) | |
| { | |
| if (!handle) return; | |
| FreeLibrary((HMODULE)handle); | |
| } | |
| static void *plugin_dlsym_(void *handle, const char *name) | |
| { | |
| return (void *)GetProcAddress((HMODULE)handle, name); | |
| } | |
| static const char *plugin_dlerror_() | |
| { | |
| static char buf[256]; | |
| FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), 0, buf, sizeof(buf), NULL); | |
| return buf; | |
| } | |
| #else | |
| #include <dlfcn.h> | |
| #include <sys/stat.h> | |
| static time_t plugin_mtime_(const char *path) | |
| { | |
| struct stat st; | |
| if (stat(path, &st) != 0) return 0; | |
| return st.st_mtime; | |
| } | |
| static void *plugin_dlopen_(const char *path) | |
| { | |
| return dlopen(path, RTLD_NOW); | |
| } | |
| static void plugin_dlclose_(void *handle) | |
| { | |
| if (!handle) return; | |
| dlclose(handle); | |
| } | |
| static void *plugin_dlsym_(void *handle, const char *name) | |
| { | |
| return dlsym(handle, name); | |
| } | |
| static const char *plugin_dlerror_() | |
| { | |
| return dlerror(); | |
| } | |
| #endif | |
| static bool plugin_copy_file_(const char *src, const char *dst) | |
| { | |
| FILE *in = fopen(src, "rb"); | |
| if (!in) return false; | |
| FILE *out = fopen(dst, "wb"); | |
| if (!out) { | |
| fclose(in); | |
| return false; | |
| } | |
| char buf[4096]; | |
| int n; | |
| while ((n = (int)fread(buf, 1, sizeof(buf), in)) > 0) { | |
| if ((int)fwrite(buf, 1, n, out) != n) { | |
| fclose(in); | |
| fclose(out); | |
| return false; | |
| } | |
| } | |
| fclose(in); | |
| fclose(out); | |
| return true; | |
| } | |
| typedef struct | |
| { | |
| PluginCtx ctx; | |
| PluginEntryFn entry; | |
| void *handle; | |
| void *prev_handle; | |
| char path[1024]; | |
| char live_path[1024]; | |
| time_t last_mtime; | |
| } Plugin; | |
| static bool plugin_open(Plugin *plugin, const char *argv0, const char *name) | |
| { | |
| const char *dir = plugin_exe_dir_(argv0); | |
| snprintf(plugin->path, sizeof(plugin->path), "%s/%s" PLUGIN_EXT, dir, name); | |
| plugin->ctx.version = 1; | |
| snprintf( | |
| plugin->live_path, | |
| sizeof(plugin->live_path), | |
| "%s.%d", | |
| plugin->path, | |
| plugin->ctx.version | |
| ); | |
| if (!plugin_copy_file_(plugin->path, plugin->live_path)) { | |
| fprintf(stderr, "[hot_reload] failed to copy '%s'\n", plugin->path); | |
| return false; | |
| } | |
| plugin->handle = plugin_dlopen_(plugin->live_path); | |
| if (!plugin->handle) { | |
| fprintf(stderr, "[hot_reload] failed to load '%s': %s\n", plugin->live_path, plugin_dlerror_()); | |
| return false; | |
| } | |
| plugin->prev_handle = NULL; | |
| plugin->last_mtime = plugin_mtime_(plugin->path); | |
| plugin->entry = (PluginEntryFn)plugin_dlsym_(plugin->handle, PLUGIN_ENTRY_SYM); | |
| if (!plugin->entry) { | |
| fprintf(stderr, "[hot_reload] missing plugin_entry symbol\n"); | |
| plugin_dlclose_(plugin->handle); | |
| plugin->handle = NULL; | |
| return false; | |
| } | |
| return plugin->entry(&plugin->ctx, PLUGIN_LOAD) == 0; | |
| } | |
| static int plugin_update(Plugin *plugin) | |
| { | |
| time_t mtime = plugin_mtime_(plugin->path); | |
| if (mtime != plugin->last_mtime && mtime != 0) { | |
| plugin->entry(&plugin->ctx, PLUGIN_UNLOAD); | |
| // Use a unique path each reload — macOS dlopen caches by path | |
| plugin->ctx.version++; | |
| snprintf( | |
| plugin->live_path, | |
| sizeof(plugin->live_path), | |
| "%s.%d", | |
| plugin->path, | |
| plugin->ctx.version | |
| ); | |
| if (!plugin_copy_file_(plugin->path, plugin->live_path)) { | |
| fprintf(stderr, "[hot_reload] failed to copy '%s'\n", plugin->path); | |
| } else { | |
| void *new_handle = plugin_dlopen_(plugin->live_path); | |
| if (!new_handle) { | |
| fprintf( | |
| stderr, | |
| "[hot_reload] failed to reload '%s': %s\n", | |
| plugin->live_path, | |
| plugin_dlerror_() | |
| ); | |
| } else { | |
| plugin_dlclose_(plugin->prev_handle); | |
| plugin->prev_handle = plugin->handle; | |
| plugin->handle = new_handle; | |
| plugin->last_mtime = mtime; | |
| PluginEntryFn new_entry = (PluginEntryFn) | |
| plugin_dlsym_(plugin->handle, PLUGIN_ENTRY_SYM); | |
| if (new_entry) { | |
| plugin->entry = new_entry; | |
| plugin->entry(&plugin->ctx, PLUGIN_LOAD); | |
| fprintf(stderr, "[hot_reload] reloaded (v%u)\n", plugin->ctx.version); | |
| } else { | |
| fprintf(stderr, "[hot_reload] missing plugin_entry after reload\n"); | |
| } | |
| } | |
| } | |
| } | |
| return plugin->entry(&plugin->ctx, PLUGIN_STEP); | |
| } | |
| static void plugin_close(Plugin *plugin) | |
| { | |
| plugin->entry(&plugin->ctx, PLUGIN_CLOSE); | |
| plugin_dlclose_(plugin->handle); | |
| plugin_dlclose_(plugin->prev_handle); | |
| plugin->handle = NULL; | |
| plugin->prev_handle = NULL; | |
| } | |
| #else | |
| typedef struct | |
| { | |
| PluginCtx ctx; | |
| PluginEntryFn entry; | |
| } Plugin; | |
| static bool plugin_open(Plugin *plugin, const char *argv0, const char *name) | |
| { | |
| (void)argv0; | |
| (void)name; | |
| extern int plugin_entry(PluginCtx * ctx, PluginOp op); | |
| plugin->entry = plugin_entry; | |
| plugin->ctx.version = 1; | |
| return plugin->entry(&plugin->ctx, PLUGIN_OP_LOAD) == 0; | |
| } | |
| static int plugin_update(Plugin *plugin) | |
| { | |
| return plugin->entry(&plugin->ctx, PLUGIN_OP_STEP); | |
| } | |
| static void plugin_close(Plugin *plugin) | |
| { | |
| plugin->entry(&plugin->ctx, PLUGIN_OP_CLOSE); | |
| } | |
| #endif | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment