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/CMakeLists.txt | 16 ++++ plugin/README.md | 12 +++ plugin/include/plugin.h | 66 +++++++++++++ plugin/src/plugin.c | 250 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 344 insertions(+) create mode 100644 plugin/CMakeLists.txt create mode 100644 plugin/README.md create mode 100644 plugin/include/plugin.h create mode 100644 plugin/src/plugin.c diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt new file mode 100644 index 0000000..0cfadc1 --- /dev/null +++ b/plugin/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.0) + +project(plugin) + +add_library(plugin + src/plugin.c) + +target_include_directories(plugin PUBLIC + include) + +target_link_libraries(plugin PRIVATE + cstring + list + log) + +target_compile_options(plugin PRIVATE -Wall -Wextra) diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..852cfe5 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,12 @@ +# Plugin + +A library for loading plugins and watching plugin updates. + +The plugin engine allows the client to load plugins and call their functions. + +Plugins can also be associated with a state. The engine does not create the +plugin's state because this may require other application-specific state. + +Plugin files are watched for updates. Upon an update, the engine reloads the +plugin into memory and notifies the client. The client should then typically +re-create the plugin's state. diff --git a/plugin/include/plugin.h b/plugin/include/plugin.h new file mode 100644 index 0000000..abca9b5 --- /dev/null +++ b/plugin/include/plugin.h @@ -0,0 +1,66 @@ +/* + * Plugin engine for loading plugins and watching plugin updates. + * + * The plugin engine allows the client to load plugins and call their functions. + * + * Plugins can also be associated with a state. The engine does not create the + * plugin's state because this may require other application-specific state. + * + * Plugin files are watched for updates. Upon an update, the engine reloads the + * plugin into memory and notifies the client. The client should then typically + * re-create the plugin's state. + */ +#pragma once + +#include + +#include + +typedef struct Plugin Plugin; +typedef struct PluginEngine PluginEngine; + +/// Plugin engine creation depluginor. +typedef struct PluginEngineDesc { + const char* plugins_dir; +} PluginEngineDesc; + +/// Create a new plugin engine. +PluginEngine* new_plugin_engine(const PluginEngineDesc*); + +/// Destroy the plugin engine. +void delete_plugin_engine(PluginEngine**); + +/// Update the plugin engine. +/// +/// This looks for any plugins that have been modified and reloads them. +void plugin_engine_update(PluginEngine*); + +/// Load a plugin. +Plugin* load_plugin(PluginEngine*, const char* filename); + +/// Delete the plugin. +/// +/// This unloads the plugin from memory and removes it from the engine. +void delete_plugin(Plugin**); + +/// Set the plugin's state. +/// +/// The plugin's previous state is deleted if non-null. +void set_plugin_state(Plugin*, void* state); + +/// Get the plugin's state. Return null if the plugin has no state. +void* get_plugin_state(Plugin*); + +/// Return true if the plugin has been reloaded. +/// +/// If the plugin has been reloaded, subsequent calls to this function return +/// false until the plugin is reloaded again. +bool plugin_reloaded(Plugin*); + +/// Resolve a function in the plugin. +#define plugin_resolve(plugin, func_sig, func_name) \ + (func_sig)(dlsym(*((void**)(plugin)), func_name)) + +/// Call a function in the plugin. +#define plugin_call(plugin, func_sig, func_name, ...) \ + (*plugin_resolve(plugin, func_sig, func_name))(__VA_ARGS__) 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