aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2023-06-16 09:15:34 -0700
committer3gg <3gg@shellblade.net>2023-06-16 09:15:34 -0700
commit2f8ff39a8d95b95288875d92abb74b1428713906 (patch)
tree95c23359a86b539c4b11f42ad8694ac6682d1b41
parentbfabb435e5c5bf313005c4636747fce59eb4ca6f (diff)
Add plugin library.
-rw-r--r--plugin/CMakeLists.txt16
-rw-r--r--plugin/README.md12
-rw-r--r--plugin/include/plugin.h66
-rw-r--r--plugin/src/plugin.c250
4 files changed, 344 insertions, 0 deletions
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 @@
1cmake_minimum_required(VERSION 3.0)
2
3project(plugin)
4
5add_library(plugin
6 src/plugin.c)
7
8target_include_directories(plugin PUBLIC
9 include)
10
11target_link_libraries(plugin PRIVATE
12 cstring
13 list
14 log)
15
16target_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 @@
1# Plugin
2
3A library for loading plugins and watching plugin updates.
4
5The plugin engine allows the client to load plugins and call their functions.
6
7Plugins can also be associated with a state. The engine does not create the
8plugin's state because this may require other application-specific state.
9
10Plugin files are watched for updates. Upon an update, the engine reloads the
11plugin into memory and notifies the client. The client should then typically
12re-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 @@
1/*
2 * Plugin engine for loading plugins and watching plugin updates.
3 *
4 * The plugin engine allows the client to load plugins and call their functions.
5 *
6 * Plugins can also be associated with a state. The engine does not create the
7 * plugin's state because this may require other application-specific state.
8 *
9 * Plugin files are watched for updates. Upon an update, the engine reloads the
10 * plugin into memory and notifies the client. The client should then typically
11 * re-create the plugin's state.
12 */
13#pragma once
14
15#include <stdbool.h>
16
17#include <dlfcn.h>
18
19typedef struct Plugin Plugin;
20typedef struct PluginEngine PluginEngine;
21
22/// Plugin engine creation depluginor.
23typedef struct PluginEngineDesc {
24 const char* plugins_dir;
25} PluginEngineDesc;
26
27/// Create a new plugin engine.
28PluginEngine* new_plugin_engine(const PluginEngineDesc*);
29
30/// Destroy the plugin engine.
31void delete_plugin_engine(PluginEngine**);
32
33/// Update the plugin engine.
34///
35/// This looks for any plugins that have been modified and reloads them.
36void plugin_engine_update(PluginEngine*);
37
38/// Load a plugin.
39Plugin* load_plugin(PluginEngine*, const char* filename);
40
41/// Delete the plugin.
42///
43/// This unloads the plugin from memory and removes it from the engine.
44void delete_plugin(Plugin**);
45
46/// Set the plugin's state.
47///
48/// The plugin's previous state is deleted if non-null.
49void set_plugin_state(Plugin*, void* state);
50
51/// Get the plugin's state. Return null if the plugin has no state.
52void* get_plugin_state(Plugin*);
53
54/// Return true if the plugin has been reloaded.
55///
56/// If the plugin has been reloaded, subsequent calls to this function return
57/// false until the plugin is reloaded again.
58bool plugin_reloaded(Plugin*);
59
60/// Resolve a function in the plugin.
61#define plugin_resolve(plugin, func_sig, func_name) \
62 (func_sig)(dlsym(*((void**)(plugin)), func_name))
63
64/// Call a function in the plugin.
65#define plugin_call(plugin, func_sig, func_name, ...) \
66 (*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 @@
1#include "plugin.h"
2
3#include "cstring.h"
4#include "list.h"
5#include "log/log.h" // TODO: Use the error library instead. Move it to clib.
6
7#include <assert.h>
8#include <stdbool.h>
9#include <stdlib.h>
10#include <string.h>
11
12#include <errno.h>
13#include <linux/limits.h>
14#include <poll.h>
15#include <sys/inotify.h>
16#include <unistd.h>
17
18// Watching for IN_CREATE leads the plugin engine to try to reload a plugin's
19// shared library before the compiler has fully written to it.
20static const int WATCH_MASK = IN_CLOSE_WRITE;
21
22typedef struct Plugin {
23 void* handle; // First member so that Plugin can be cast to handle.
24 void* state; // Plugin's internal state.
25 bool reloaded; // Whether the plugin has been reloaded state needs to be
26 // re-created.
27 PluginEngine* eng; // So that the public API can do stuff with just a Plugin*.
28 mstring filename;
29} Plugin;
30
31DEF_LIST(Plugin);
32
33typedef struct PluginEngine {
34 int inotify_instance;
35 int dir_watch; // inotify watch on the plugins directory.
36 Plugin_list plugins;
37 mstring plugins_dir;
38} PluginEngine;
39
40// -----------------------------------------------------------------------------
41// Plugin.
42// -----------------------------------------------------------------------------
43
44static mstring plugin_lib_name(const Plugin* plugin) {
45 return mstring_concat(
46 mstring_make("lib"), mstring_concat_cstr(plugin->filename, ".so"));
47}
48
49static mstring plugin_lib_path(const Plugin* plugin) {
50 return mstring_concat(plugin->eng->plugins_dir, plugin_lib_name(plugin));
51}
52
53static bool load_library(Plugin* plugin) {
54 assert(plugin);
55 assert(plugin->eng);
56
57 // Handle reloading a previously-loaded library.
58 if (plugin->handle) {
59 dlclose(plugin->handle);
60 plugin->handle = 0;
61 }
62
63 const mstring lib = plugin_lib_path(plugin);
64
65 // If the plugin fails to load, make sure to keep the plugin's old handle to
66 // handle the error gracefully. This handles reload failures, specifically.
67 void* handle = 0;
68 if ((handle = dlopen(mstring_cstr(&lib), RTLD_NOW))) {
69 LOGD("Plugin [%s] loaded successfully", mstring_cstr(&plugin->filename));
70 plugin->handle = handle;
71 return true;
72 } else {
73 LOGE("dlopen() failed: %s", dlerror());
74 }
75
76 return false;
77}
78
79static void destroy_plugin(Plugin* plugin) {
80 if (plugin) {
81 if (plugin->handle) {
82 dlclose(plugin->handle);
83 plugin->handle = 0;
84 }
85 if (plugin->state) {
86 free(plugin->state);
87 plugin->state = 0;
88 }
89 }
90}
91
92Plugin* load_plugin(PluginEngine* eng, const char* filename) {
93 assert(eng);
94 assert(filename);
95
96 Plugin plugin = (Plugin){.eng = eng, .filename = mstring_make(filename)};
97
98 if (!load_library(&plugin)) {
99 return 0;
100 }
101
102 list_push(eng->plugins, plugin);
103 return &eng->plugins.head->val;
104}
105
106void delete_plugin(Plugin** pPlugin) {
107 assert(pPlugin);
108 Plugin* plugin = *pPlugin;
109 if (plugin) {
110 assert(plugin->eng);
111 destroy_plugin(plugin);
112 list_remove_ptr(plugin->eng->plugins, plugin);
113 *pPlugin = 0;
114 }
115}
116
117static void delete_plugin_state(Plugin* plugin) {
118 if (plugin->state) {
119 free(plugin->state);
120 plugin->state = 0;
121 }
122}
123
124void set_plugin_state(Plugin* plugin, void* state) {
125 assert(plugin);
126 delete_plugin_state(plugin);
127 plugin->state = state;
128}
129
130void* get_plugin_state(Plugin* plugin) {
131 assert(plugin);
132 return plugin->state;
133}
134
135bool plugin_reloaded(Plugin* plugin) {
136 assert(plugin);
137 const bool reloaded = plugin->reloaded;
138 plugin->reloaded = false;
139 return reloaded;
140}
141
142// -----------------------------------------------------------------------------
143// Plugin Engine.
144// -----------------------------------------------------------------------------
145
146PluginEngine* new_plugin_engine(const PluginEngineDesc* desc) {
147 PluginEngine* eng = 0;
148
149 if (!(eng = calloc(1, sizeof(PluginEngine)))) {
150 goto cleanup;
151 }
152 eng->plugins = make_list(Plugin);
153 eng->plugins_dir = mstring_concat_cstr(mstring_make(desc->plugins_dir), "/");
154
155 LOGD("Watch plugins directory: %s", mstring_cstr(&eng->plugins_dir));
156
157 if ((eng->inotify_instance = inotify_init()) == -1) {
158 LOGE("Failed to create inotify instance");
159 goto cleanup;
160 }
161 if ((eng->dir_watch = inotify_add_watch(
162 eng->inotify_instance, mstring_cstr(&eng->plugins_dir),
163 WATCH_MASK)) == -1) {
164 LOGE("Failed to watch directory: %s", mstring_cstr(&eng->plugins_dir));
165 goto cleanup;
166 }
167
168 return eng;
169
170cleanup:
171 delete_plugin_engine(&eng);
172 return 0;
173}
174
175void delete_plugin_engine(PluginEngine** pEng) {
176 assert(pEng);
177 PluginEngine* eng = *pEng;
178 if (eng) {
179 list_foreach_mut(eng->plugins, { destroy_plugin(value); });
180 del_list(eng->plugins);
181 if (eng->dir_watch != -1) {
182 inotify_rm_watch(eng->dir_watch, eng->inotify_instance);
183 close(eng->dir_watch);
184 eng->dir_watch = 0;
185 }
186 if (eng->inotify_instance != -1) {
187 close(eng->inotify_instance);
188 }
189 free(eng);
190 *pEng = 0;
191 }
192}
193
194void plugin_engine_update(PluginEngine* eng) {
195 assert(eng);
196
197 struct pollfd pollfds[1] = {
198 {eng->inotify_instance, POLLIN, 0}
199 };
200
201 int ret = 0;
202 while ((ret = poll(pollfds, 1, 0)) != 0) {
203 if (ret > 0) {
204 const struct pollfd* pfd = &pollfds[0];
205 if (pfd->revents & POLLIN) {
206 // inotify instances don't like to be partially read, and the events,
207 // when watching a directory, have a variable-length file name.
208 uint8_t buf[sizeof(struct inotify_event) + NAME_MAX + 1] = {0};
209 ssize_t length = read(eng->inotify_instance, &buf, sizeof(buf));
210 if (length == -1) {
211 LOGE(
212 "read() on inotify instance failed with error [%d]: %s", errno,
213 strerror(errno));
214 break;
215 }
216 const uint8_t* next = buf;
217 const uint8_t* end = buf + sizeof(buf);
218 while (next < end) {
219 const struct inotify_event* event = (const struct inotify_event*)next;
220 if (event->mask & WATCH_MASK) {
221 if (event->wd == eng->dir_watch) {
222 if (event->len > 0) {
223 // Name does not include directory, e.g., libfoo.so
224 const mstring file = mstring_make(event->name);
225 list_foreach_mut(eng->plugins, {
226 Plugin* plugin = value;
227 if (mstring_eq(file, plugin_lib_name(plugin))) {
228 if (load_library(plugin)) {
229 plugin->reloaded = true;
230 }
231 break;
232 }
233 });
234 }
235 }
236 }
237 next += sizeof(struct inotify_event) + event->len;
238 }
239 }
240 if ((pfd->revents & POLLERR) || (pfd->revents & POLLHUP) ||
241 (pfd->revents & POLLNVAL)) {
242 LOGE("inotify instance is in a bad state");
243 break;
244 }
245 } else if (ret == -1) {
246 LOGE("poll() failed with error [%d]: %s", errno, strerror(errno));
247 break;
248 }
249 }
250}