Skip to content

Instantly share code, notes, and snippets.

@waldnercharles
Last active March 26, 2026 17:17
Show Gist options
  • Select an option

  • Save waldnercharles/fd506cf5127e6cc822a7a9505a83bccd to your computer and use it in GitHub Desktop.

Select an option

Save waldnercharles/fd506cf5127e6cc822a7a9505a83bccd to your computer and use it in GitHub Desktop.
A quick and dirty hot-reloader
#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;
}
#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
#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