From 2f8ff39a8d95b95288875d92abb74b1428713906 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Fri, 16 Jun 2023 09:15:34 -0700 Subject: Add plugin library. --- plugin/src/plugin.c | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 plugin/src/plugin.c (limited to 'plugin/src') diff --git a/plugin/src/plugin.c b/plugin/src/plugin.c new file mode 100644 index 0000000..f65132f --- /dev/null +++ b/plugin/src/plugin.c @@ -0,0 +1,250 @@ +#include "plugin.h" + +#include "cstring.h" +#include "list.h" +#include "log/log.h" // TODO: Use the error library instead. Move it to clib. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// Watching for IN_CREATE leads the plugin engine to try to reload a plugin's +// shared library before the compiler has fully written to it. +static const int WATCH_MASK = IN_CLOSE_WRITE; + +typedef struct Plugin { + void* handle; // First member so that Plugin can be cast to handle. + void* state; // Plugin's internal state. + bool reloaded; // Whether the plugin has been reloaded state needs to be + // re-created. + PluginEngine* eng; // So that the public API can do stuff with just a Plugin*. + mstring filename; +} Plugin; + +DEF_LIST(Plugin); + +typedef struct PluginEngine { + int inotify_instance; + int dir_watch; // inotify watch on the plugins directory. + Plugin_list plugins; + mstring plugins_dir; +} PluginEngine; + +// ----------------------------------------------------------------------------- +// Plugin. +// ----------------------------------------------------------------------------- + +static mstring plugin_lib_name(const Plugin* plugin) { + return mstring_concat( + mstring_make("lib"), mstring_concat_cstr(plugin->filename, ".so")); +} + +static mstring plugin_lib_path(const Plugin* plugin) { + return mstring_concat(plugin->eng->plugins_dir, plugin_lib_name(plugin)); +} + +static bool load_library(Plugin* plugin) { + assert(plugin); + assert(plugin->eng); + + // Handle reloading a previously-loaded library. + if (plugin->handle) { + dlclose(plugin->handle); + plugin->handle = 0; + } + + const mstring lib = plugin_lib_path(plugin); + + // If the plugin fails to load, make sure to keep the plugin's old handle to + // handle the error gracefully. This handles reload failures, specifically. + void* handle = 0; + if ((handle = dlopen(mstring_cstr(&lib), RTLD_NOW))) { + LOGD("Plugin [%s] loaded successfully", mstring_cstr(&plugin->filename)); + plugin->handle = handle; + return true; + } else { + LOGE("dlopen() failed: %s", dlerror()); + } + + return false; +} + +static void destroy_plugin(Plugin* plugin) { + if (plugin) { + if (plugin->handle) { + dlclose(plugin->handle); + plugin->handle = 0; + } + if (plugin->state) { + free(plugin->state); + plugin->state = 0; + } + } +} + +Plugin* load_plugin(PluginEngine* eng, const char* filename) { + assert(eng); + assert(filename); + + Plugin plugin = (Plugin){.eng = eng, .filename = mstring_make(filename)}; + + if (!load_library(&plugin)) { + return 0; + } + + list_push(eng->plugins, plugin); + return &eng->plugins.head->val; +} + +void delete_plugin(Plugin** pPlugin) { + assert(pPlugin); + Plugin* plugin = *pPlugin; + if (plugin) { + assert(plugin->eng); + destroy_plugin(plugin); + list_remove_ptr(plugin->eng->plugins, plugin); + *pPlugin = 0; + } +} + +static void delete_plugin_state(Plugin* plugin) { + if (plugin->state) { + free(plugin->state); + plugin->state = 0; + } +} + +void set_plugin_state(Plugin* plugin, void* state) { + assert(plugin); + delete_plugin_state(plugin); + plugin->state = state; +} + +void* get_plugin_state(Plugin* plugin) { + assert(plugin); + return plugin->state; +} + +bool plugin_reloaded(Plugin* plugin) { + assert(plugin); + const bool reloaded = plugin->reloaded; + plugin->reloaded = false; + return reloaded; +} + +// ----------------------------------------------------------------------------- +// Plugin Engine. +// ----------------------------------------------------------------------------- + +PluginEngine* new_plugin_engine(const PluginEngineDesc* desc) { + PluginEngine* eng = 0; + + if (!(eng = calloc(1, sizeof(PluginEngine)))) { + goto cleanup; + } + eng->plugins = make_list(Plugin); + eng->plugins_dir = mstring_concat_cstr(mstring_make(desc->plugins_dir), "/"); + + LOGD("Watch plugins directory: %s", mstring_cstr(&eng->plugins_dir)); + + if ((eng->inotify_instance = inotify_init()) == -1) { + LOGE("Failed to create inotify instance"); + goto cleanup; + } + if ((eng->dir_watch = inotify_add_watch( + eng->inotify_instance, mstring_cstr(&eng->plugins_dir), + WATCH_MASK)) == -1) { + LOGE("Failed to watch directory: %s", mstring_cstr(&eng->plugins_dir)); + goto cleanup; + } + + return eng; + +cleanup: + delete_plugin_engine(&eng); + return 0; +} + +void delete_plugin_engine(PluginEngine** pEng) { + assert(pEng); + PluginEngine* eng = *pEng; + if (eng) { + list_foreach_mut(eng->plugins, { destroy_plugin(value); }); + del_list(eng->plugins); + if (eng->dir_watch != -1) { + inotify_rm_watch(eng->dir_watch, eng->inotify_instance); + close(eng->dir_watch); + eng->dir_watch = 0; + } + if (eng->inotify_instance != -1) { + close(eng->inotify_instance); + } + free(eng); + *pEng = 0; + } +} + +void plugin_engine_update(PluginEngine* eng) { + assert(eng); + + struct pollfd pollfds[1] = { + {eng->inotify_instance, POLLIN, 0} + }; + + int ret = 0; + while ((ret = poll(pollfds, 1, 0)) != 0) { + if (ret > 0) { + const struct pollfd* pfd = &pollfds[0]; + if (pfd->revents & POLLIN) { + // inotify instances don't like to be partially read, and the events, + // when watching a directory, have a variable-length file name. + uint8_t buf[sizeof(struct inotify_event) + NAME_MAX + 1] = {0}; + ssize_t length = read(eng->inotify_instance, &buf, sizeof(buf)); + if (length == -1) { + LOGE( + "read() on inotify instance failed with error [%d]: %s", errno, + strerror(errno)); + break; + } + const uint8_t* next = buf; + const uint8_t* end = buf + sizeof(buf); + while (next < end) { + const struct inotify_event* event = (const struct inotify_event*)next; + if (event->mask & WATCH_MASK) { + if (event->wd == eng->dir_watch) { + if (event->len > 0) { + // Name does not include directory, e.g., libfoo.so + const mstring file = mstring_make(event->name); + list_foreach_mut(eng->plugins, { + Plugin* plugin = value; + if (mstring_eq(file, plugin_lib_name(plugin))) { + if (load_library(plugin)) { + plugin->reloaded = true; + } + break; + } + }); + } + } + } + next += sizeof(struct inotify_event) + event->len; + } + } + if ((pfd->revents & POLLERR) || (pfd->revents & POLLHUP) || + (pfd->revents & POLLNVAL)) { + LOGE("inotify instance is in a bad state"); + break; + } + } else if (ret == -1) { + LOGE("poll() failed with error [%d]: %s", errno, strerror(errno)); + break; + } + } +} -- cgit v1.2.3