aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2026-01-06 23:08:25 -0700
committerJosh Rahm <joshuarahm@gmail.com>2026-01-06 23:09:52 -0700
commit37e5037b68fb35276f0c426779f61aa278d5b21b (patch)
tree94e0b7a413b2899e7114c7453cd0f234a438ce15
parent09860c75bb129c70768692aaec3bd42d0b2735e3 (diff)
downloadmontis-37e5037b68fb35276f0c426779f61aa278d5b21b.tar.gz
montis-37e5037b68fb35276f0c426779f61aa278d5b21b.tar.bz2
montis-37e5037b68fb35276f0c426779f61aa278d5b21b.zip
[reorg] use X macro system rather than perl generation.
This remove the dependency on Perl and makes things less brittle.
-rw-r--r--README.md2
-rw-r--r--arken/CMakeLists.txt20
-rw-r--r--arken/README.md5
-rw-r--r--arken/include/plugin.h88
-rw-r--r--arken/include/plugin_exports.h117
-rw-r--r--arken/include/plugin_interface.h23
-rw-r--r--arken/src/plugin_load.c39
7 files changed, 188 insertions, 106 deletions
diff --git a/README.md b/README.md
index 954a725..2aaaefb 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ Note: the `run` target currently starts `arken` with `-s foot`, so you’ll want
If you disable the bundled plugin (`-DMONTIS_BUILD_BUNDLED_PLUGIN=OFF`), you can
build and supply your own plugin `.so` at runtime.
-- Plugin ABI: `arken/include/plugin.h` (generated convenience header: `build/plugin_interface.h`)
+- Plugin ABI: `arken/include/plugin.h` (plugin header: `arken/include/plugin_interface.h`)
- Optional bridge helpers: `erebor/include` + `build/erebor/liberebor.a`
Runtime invocation looks like:
diff --git a/arken/CMakeLists.txt b/arken/CMakeLists.txt
index 2bfd1d7..9219879 100644
--- a/arken/CMakeLists.txt
+++ b/arken/CMakeLists.txt
@@ -58,24 +58,6 @@ else()
message(FATAL_ERROR "Failed to execute pkg-config")
endif()
-set(PLUGIN_INTF ${CMAKE_BINARY_DIR}/plugin_interface.h)
-add_custom_command(
- OUTPUT ${PLUGIN_INTF}
- COMMAND perl ${PROJECT_SOURCE_DIR}/tools/genintf.pl <
- ${PROJECT_SOURCE_DIR}/include/plugin.h > ${PLUGIN_INTF}
- DEPENDS ${PROJECT_SOURCE_DIR}/include/plugin.h
- DEPENDS ${PROJECT_SOURCE_DIR}/tools/genintf.pl
-)
-
-set(PLUGIN_LOAD ${CMAKE_BINARY_DIR}/gen_plugin_load.c)
-add_custom_command(
- OUTPUT ${PLUGIN_LOAD}
- COMMAND perl ${PROJECT_SOURCE_DIR}/tools/genbuild.pl <
- ${PROJECT_SOURCE_DIR}/include/plugin.h > ${PLUGIN_LOAD}
- DEPENDS ${PROJECT_SOURCE_DIR}/include/plugin.h
- DEPENDS ${PROJECT_SOURCE_DIR}/tools/genbuild.pl
-)
-
add_custom_command(
OUTPUT xdg-shell-protocol.h
COMMAND ${WAYLAND_SCANNER} server-header
@@ -95,7 +77,7 @@ file (GLOB_RECURSE SOURCES src/*.c)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
-add_executable (arken ${SOURCES} ${PLUGIN_LOAD} ${PLUGIN_INTF} ${WLROOTS_LIB_LINK}
+add_executable (arken ${SOURCES} ${WLROOTS_LIB_LINK}
xdg-shell-protocol.c)
find_package(PkgConfig REQUIRED)
diff --git a/arken/README.md b/arken/README.md
index 5e1ede8..edcddf4 100644
--- a/arken/README.md
+++ b/arken/README.md
@@ -20,10 +20,9 @@ defined in:
- `arken/include/plugin.h`
-At build time, a convenience header is generated from that ABI definition for
-consumers that want a minimal include surface:
+Plugins should include:
-- `build/plugin_interface.h` (generated)
+- `arken/include/plugin_interface.h`
Key files
---------
diff --git a/arken/include/plugin.h b/arken/include/plugin.h
index 3098602..2407212 100644
--- a/arken/include/plugin.h
+++ b/arken/include/plugin.h
@@ -10,20 +10,7 @@
#include <wlr/types/wlr_pointer.h>
#include "plugin_types.h"
-
-/*
- * Marker macro to define what functions should be exported. This generates the
- * interface which the plugin needs to implement.
- */
-#define EXPORT(a) a
-
-#define EXPORT_INCLUDE(a)
-
-// clang-format off
-EXPORT_INCLUDE(<wlr/types/wlr_keyboard.h>)
-EXPORT_INCLUDE(<wlr/types/wlr_input_device.h>)
-EXPORT_INCLUDE(<wlr/types/wlr_pointer.h>)
-// clang-format on
+#include "plugin_exports.h"
#define MAX_QUEUED_ACTIONS 8
@@ -87,72 +74,10 @@ typedef struct PLUGIN {
* will be NULL. */
const char *plugin_name;
- /**
- * Initializes the plugin on the first time, and only the first time, it is
- * loaded. This is used to do things like setup a runtime that cannot be
- * reliably torn down. It is up to the plugin to ensure this won't interfere
- * with hot-reloading.
- */
- EXPORT(void (*plugin_metaload)(int argc, char **argv));
-
- /** Intializes the plugin with the given argc/argv. This is the first thing
- * called on the plugin and is called immediately after the library is loaded.
- */
- EXPORT(void (*plugin_load)(int argc, char **argv));
-
- /* Start the plugin with the marshalled state from the previous plugin.
- *
- * This should return the opaque state from the mashalled_state.
- *
- * This function should not fail if the state cannot be demarshalled, rather a
- * default state should be returned. This is because changing the plugin and
- * hot-reloading can produce incompatibilities between the old state and the
- * new state, and this should not cause a failure.
- */
- EXPORT(opqst_t (*plugin_hot_start)(void *self, uint8_t *mashalled_state,
- uint32_t n));
-
- /*
- * Starts the plugin without a marshalled state. Happens during the first boot
- * when there is not state.
- */
- EXPORT(opqst_t (*plugin_cold_start)(void* self));
-
- /*
- * Marshals the state to a bytestring. The returned pointer should be malloc'd
- * on the heap. The harness takes ownership of the malloc'd pointer.
- *
- * This is usually called in preparation for a teardown followed by a
- * hot-start.
- */
- EXPORT(uint8_t *(*plugin_marshal_state)(opqst_t st, uint32_t *szout));
-
- /*
- * Teardown the plugin in preperation for the library's imminent unloading.
- */
- EXPORT(void (*plugin_teardown)(opqst_t));
-
- /*
- * Handles a keybinding.
- */
- EXPORT(opqst_t (*plugin_handle_keybinding)(
- struct wlr_keyboard *keyboard, struct wlr_keyboard_key_event *event,
- uint32_t modifiers, uint32_t keysym, uint32_t codepoint, int *out_handled,
- opqst_t state));
-
- EXPORT(opqst_t (*plugin_handle_button)(struct wlr_pointer_button_event *event,
- uint32_t modifiers, opqst_t state));
-
- /* Absolute motion only for now; relative motion stays in the runtime. */
- EXPORT(opqst_t (*plugin_handle_motion)(void *event, uint32_t modifiers,
- uint32_t is_absolute, double lx,
- double ly, opqst_t state));
-
- /*
- * Handles a surface being mapped, unmapped or destroyed.
- */
- EXPORT(opqst_t (*plugin_handle_surface)(void *surface, surface_event_t event,
- opqst_t));
+ /* Plugin function table populated by the runtime loader. */
+#define PLUGIN_FN_PTR(ret, name, args) ret (*name) args;
+ MONTIS_PLUGIN_EXPORTS(PLUGIN_FN_PTR)
+#undef PLUGIN_FN_PTR
/* List of requested actions by the plugin. Right now there is a maximum of 8
* allowed at one time. That should be plenty. The actions should be flushed
@@ -161,9 +86,6 @@ typedef struct PLUGIN {
requested_action_t requested_actions[MAX_QUEUED_ACTIONS];
} plugin_t;
-#undef EXPORT
-#undef EXPORT_INCLUDE
-
/* Reloads the plugin. This tears down the existing plugin, marshals the state
* for it and reloads it.
*
diff --git a/arken/include/plugin_exports.h b/arken/include/plugin_exports.h
new file mode 100644
index 0000000..4518d92
--- /dev/null
+++ b/arken/include/plugin_exports.h
@@ -0,0 +1,117 @@
+#pragma once
+
+/*
+ * Single source of truth for the plugin ABI.
+ *
+ * Consumers must define an X-macro like:
+ * #define X(ret, name, args) ...
+ * and then invoke:
+ * MONTIS_PLUGIN_EXPORTS(X)
+ *
+ * Note: this file intentionally does not include headers. Include whatever you
+ * need (e.g. <stdint.h>, plugin_types.h, wlroots headers) before expanding.
+ */
+
+#define MONTIS_PLUGIN_EXPORTS(X) \
+ /* \
+ * `plugin_metaload` \
+ * \
+ * Called at most once per process *per `plugin_name`*. This is the place \
+ * for truly global initialization that should survive hot reloads (e.g. \
+ * one-time language runtime init, registering global hooks, etc.). \
+ * \
+ * Notes: \
+ * - May be a no-op. \
+ * - Must be safe to call before any other plugin entrypoint. \
+ */ \
+ X(void, plugin_metaload, (int argc, char **argv)) \
+ /* \
+ * `plugin_load` \
+ * \
+ * Called every time the shared object is loaded (cold start and each hot \
+ * reload). Use this for per-load initialization that must be re-done after \
+ * `dlopen`. \
+ */ \
+ X(void, plugin_load, (int argc, char **argv)) \
+ /* \
+ * `plugin_hot_start` \
+ * \
+ * Called after a hot reload with the previous plugin's marshalled state. \
+ * Must return the new live opaque state (`opqst_t`) to be used for future \
+ * handler calls. \
+ * \
+ * Contract: \
+ * - Must not fail hard on state incompatibility; fall back to a default. \
+ * - `self` is an opaque pointer owned by the runtime; treat it as \
+ * read-only. \
+ */ \
+ X(opqst_t, plugin_hot_start, \
+ (void *self, uint8_t *marshalled_state, uint32_t n)) \
+ /* \
+ * `plugin_cold_start` \
+ * \
+ * Called on first boot when no previous state exists. Must construct and \
+ * return an initial opaque state. \
+ */ \
+ X(opqst_t, plugin_cold_start, (void *self)) \
+ /* \
+ * `plugin_marshal_state` \
+ * \
+ * Called before unloading the current plugin during hot reload. Must \
+ * serialize the provided opaque state into a newly allocated byte buffer. \
+ * \
+ * Ownership: \
+ * - Return a heap-allocated buffer (e.g. `malloc`). \
+ * - The runtime takes ownership and will `free()` it after \
+ * `plugin_hot_start`. \
+ */ \
+ X(uint8_t *, plugin_marshal_state, (opqst_t st, uint32_t *szout)) \
+ /* \
+ * `plugin_teardown` \
+ * \
+ * Called immediately before the shared object is unloaded. Use this to \
+ * release resources owned by the opaque state (and any per-load resources \
+ * created in `plugin_load`). \
+ */ \
+ X(void, plugin_teardown, (opqst_t st)) \
+ /* \
+ * `plugin_handle_keybinding` \
+ * \
+ * Called for keyboard events. Returns the updated opaque state. Set \
+ * `*out_handled` to non-zero if the event is consumed. \
+ */ \
+ X(opqst_t, plugin_handle_keybinding, \
+ (struct wlr_keyboard * keyboard, struct wlr_keyboard_key_event * event, \
+ uint32_t modifiers, uint32_t keysym, uint32_t codepoint, \
+ int *out_handled, opqst_t state)) \
+ /* \
+ * `plugin_handle_button` \
+ * \
+ * Called for pointer button events (mouse/trackpad clicks). Returns the \
+ * updated opaque state. \
+ */ \
+ X(opqst_t, plugin_handle_button, \
+ (struct wlr_pointer_button_event * event, uint32_t modifiers, \
+ opqst_t state)) \
+ /* \
+ * `plugin_handle_motion` \
+ * \
+ * Called for pointer motion. Returns the updated opaque state. \
+ * \
+ * Parameters: \
+ * - `event` is an opaque pointer to the underlying wlroots event struct \
+ * (type depends on `is_absolute`). \
+ * - `lx`/`ly` are layout coordinates in compositor space. \
+ */ \
+ X(opqst_t, plugin_handle_motion, \
+ (void *event, uint32_t modifiers, uint32_t is_absolute, double lx, \
+ double ly, opqst_t state)) \
+ /* \
+ * `plugin_handle_surface` \
+ * \
+ * Called when a surface is mapped/unmapped/destroyed. `surface` is an \
+ * opaque pointer to the runtime surface representation. Returns the updated \
+ * opaque state. \
+ */ \
+ X(opqst_t, plugin_handle_surface, \
+ (void *surface, surface_event_t event, opqst_t state))
diff --git a/arken/include/plugin_interface.h b/arken/include/plugin_interface.h
new file mode 100644
index 0000000..0583289
--- /dev/null
+++ b/arken/include/plugin_interface.h
@@ -0,0 +1,23 @@
+#ifndef _PLUG_INTF
+#define _PLUG_INTF
+
+#include <stdint.h>
+
+#include "plugin_types.h"
+#include "plugin_exports.h"
+
+#include <wlr/types/wlr_input_device.h>
+#include <wlr/types/wlr_keyboard.h>
+#include <wlr/types/wlr_pointer.h>
+
+/*
+ * Plugin ABI: plugins must export these symbols.
+ *
+ * This header is intended to be included by plugin implementations.
+ */
+
+#define DECLARE_PLUGIN_EXPORT(ret, name, args) ret name args;
+MONTIS_PLUGIN_EXPORTS(DECLARE_PLUGIN_EXPORT)
+#undef DECLARE_PLUGIN_EXPORT
+
+#endif /* _PLUG_INTF */
diff --git a/arken/src/plugin_load.c b/arken/src/plugin_load.c
new file mode 100644
index 0000000..6525969
--- /dev/null
+++ b/arken/src/plugin_load.c
@@ -0,0 +1,39 @@
+#include "plugin.h"
+
+#include <dlfcn.h>
+#include <stdio.h>
+
+#include "plugin_exports.h"
+
+int load_plugin_from_dl_(dlhandle_t dl, plugin_t *plug)
+{
+ void *ptr;
+ int ret = 0;
+
+ const char **name = dlsym(dl, "plugin_name");
+ if (name) {
+ plug->plugin_name = *name;
+ }
+ else {
+ plug->plugin_name = NULL;
+ }
+
+ plug->state = NULL;
+ plug->library_handle = dl;
+
+#define LOAD_SYM(ret_type, sym, args) \
+ do { \
+ ptr = dlsym(dl, #sym); \
+ if (!ptr) { \
+ fprintf(stderr, "Plugin missing %s\n", #sym); \
+ ret |= 1; \
+ } \
+ plug->sym = (ret_type(*) args)ptr; \
+ } while (0);
+
+ MONTIS_PLUGIN_EXPORTS(LOAD_SYM);
+
+#undef LOAD_SYM
+
+ return ret;
+}