summaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2023-02-27 12:32:38 +0000
committerGitHub <noreply@github.com>2023-02-27 12:32:38 +0000
commit8b81f41e52b55835aaaa7e4962af23a00188cbdc (patch)
treebca65923843ea931c0e0222f29d4e2901cfa8637 /src/plugins
parent74a10f26a469de54968e584525f6507928f615f0 (diff)
Plugin System (#1590)
--------- Co-authored-by: Mihai Fufezan <fufexan@protonmail.com>
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/HookSystem.cpp151
-rw-r--r--src/plugins/HookSystem.hpp48
-rw-r--r--src/plugins/PluginAPI.cpp193
-rw-r--r--src/plugins/PluginAPI.hpp217
-rw-r--r--src/plugins/PluginSystem.cpp139
-rw-r--r--src/plugins/PluginSystem.hpp44
6 files changed, 792 insertions, 0 deletions
diff --git a/src/plugins/HookSystem.cpp b/src/plugins/HookSystem.cpp
new file mode 100644
index 00000000..a8ddd90e
--- /dev/null
+++ b/src/plugins/HookSystem.cpp
@@ -0,0 +1,151 @@
+#include "HookSystem.hpp"
+
+#define register
+#include <udis86.h>
+#undef register
+#include <sys/mman.h>
+#include <unistd.h>
+#include <cstring>
+
+CFunctionHook::CFunctionHook(HANDLE owner, void* source, void* destination) {
+ m_pSource = source;
+ m_pDestination = destination;
+ m_pOwner = owner;
+}
+
+CFunctionHook::~CFunctionHook() {
+ if (m_bActive) {
+ unhook();
+ }
+}
+
+size_t getInstructionLenAt(void* start) {
+ ud_t udis;
+
+ ud_init(&udis);
+ ud_set_mode(&udis, 64);
+ ud_set_syntax(&udis, UD_SYN_INTEL);
+
+ size_t curOffset = 1;
+ size_t insSize = 0;
+ while (true) {
+ ud_set_input_buffer(&udis, (uint8_t*)start, curOffset);
+ insSize = ud_disassemble(&udis);
+ if (insSize != curOffset)
+ break;
+ curOffset++;
+ }
+
+ return insSize;
+}
+
+size_t probeMinimumJumpSize(void* start, size_t min) {
+
+ size_t size = 0;
+
+ while (size <= min) {
+ // find info about this instruction
+ size_t insLen = getInstructionLenAt(start + size);
+ size += insLen;
+ }
+
+ return size;
+}
+
+bool CFunctionHook::hook() {
+
+ // check for unsupported platforms
+#if !defined(__x86_64__)
+ return false;
+#endif
+
+ // movabs $0,%rax | jmpq *%rax
+ static constexpr uint8_t ABSOLUTE_JMP_ADDRESS[] = {0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0};
+ // pushq %rax
+ static constexpr uint8_t PUSH_RAX[] = {0x50};
+ // popq %rax
+ static constexpr uint8_t POP_RAX[] = {0x58};
+ // nop
+ static constexpr uint8_t NOP = 0x90;
+
+ // get minimum size to overwrite
+ const auto HOOKSIZE = probeMinimumJumpSize(m_pSource, sizeof(ABSOLUTE_JMP_ADDRESS) + sizeof(PUSH_RAX) + sizeof(POP_RAX));
+
+ // alloc trampoline
+ m_pTrampolineAddr = mmap(NULL, sizeof(ABSOLUTE_JMP_ADDRESS) + HOOKSIZE + sizeof(PUSH_RAX), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ // populate trampoline
+ memcpy(m_pTrampolineAddr, m_pSource, HOOKSIZE); // first, original func bytes
+ memcpy(m_pTrampolineAddr + HOOKSIZE, PUSH_RAX, sizeof(PUSH_RAX)); // then, pushq %rax
+ memcpy(m_pTrampolineAddr + HOOKSIZE + sizeof(PUSH_RAX), ABSOLUTE_JMP_ADDRESS, sizeof(ABSOLUTE_JMP_ADDRESS)); // then, jump to source
+
+ // fixup trampoline addr
+ *(uint64_t*)(m_pTrampolineAddr + HOOKSIZE + 2 + sizeof(PUSH_RAX)) = (uint64_t)(m_pSource + sizeof(ABSOLUTE_JMP_ADDRESS));
+
+ // make jump to hk
+ mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_WRITE | PROT_EXEC);
+ memcpy(m_pSource, ABSOLUTE_JMP_ADDRESS, sizeof(ABSOLUTE_JMP_ADDRESS));
+
+ // make popq %rax and NOP all remaining
+ memcpy(m_pSource + sizeof(ABSOLUTE_JMP_ADDRESS), POP_RAX, sizeof(POP_RAX));
+ size_t currentOp = sizeof(ABSOLUTE_JMP_ADDRESS) + sizeof(POP_RAX);
+ memset(m_pSource + currentOp, NOP, HOOKSIZE - currentOp);
+
+ // fixup jump addr
+ *(uint64_t*)(m_pSource + 2) = (uint64_t)(m_pDestination);
+
+ // revert mprot
+ mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_EXEC);
+
+ // set original addr to trampo addr
+ m_pOriginal = m_pTrampolineAddr;
+
+ m_bActive = true;
+ m_iHookLen = HOOKSIZE;
+ m_iTrampoLen = HOOKSIZE + sizeof(ABSOLUTE_JMP_ADDRESS);
+
+ return true;
+}
+
+bool CFunctionHook::unhook() {
+ // check for unsupported platforms
+#if !defined(__x86_64__)
+ return false;
+#endif
+
+ if (!m_bActive)
+ return false;
+
+ // allow write to src
+ mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_WRITE | PROT_EXEC);
+
+ // write back original bytes
+ memcpy(m_pSource, m_pTrampolineAddr, m_iHookLen);
+
+ // revert mprot
+ mprotect(m_pSource - ((uint64_t)m_pSource) % sysconf(_SC_PAGE_SIZE), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_EXEC);
+
+ // unmap
+ munmap(m_pTrampolineAddr, m_iTrampoLen);
+
+ // reset vars
+ m_bActive = false;
+ m_iHookLen = 0;
+ m_iTrampoLen = 0;
+ m_pTrampolineAddr = nullptr;
+
+ return true;
+}
+
+CFunctionHook* CHookSystem::initHook(HANDLE owner, void* source, void* destination) {
+ return m_vHooks.emplace_back(std::make_unique<CFunctionHook>(owner, source, destination)).get();
+}
+
+bool CHookSystem::removeHook(CFunctionHook* hook) {
+ std::erase_if(m_vHooks, [&](const auto& other) { return other.get() == hook; });
+ return true; // todo: make false if not found
+}
+
+void CHookSystem::removeAllHooksFrom(HANDLE handle) {
+ std::erase_if(m_vHooks, [&](const auto& other) { return other->m_pOwner == handle; });
+} \ No newline at end of file
diff --git a/src/plugins/HookSystem.hpp b/src/plugins/HookSystem.hpp
new file mode 100644
index 00000000..56227449
--- /dev/null
+++ b/src/plugins/HookSystem.hpp
@@ -0,0 +1,48 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <memory>
+
+#define HANDLE void*
+
+class CFunctionHook {
+ public:
+ CFunctionHook(HANDLE owner, void* source, void* destination);
+ ~CFunctionHook();
+
+ bool hook();
+ bool unhook();
+
+ CFunctionHook(const CFunctionHook&) = delete;
+ CFunctionHook(CFunctionHook&&) = delete;
+ CFunctionHook& operator=(const CFunctionHook&) = delete;
+ CFunctionHook& operator=(CFunctionHook&&) = delete;
+
+ void* m_pOriginal = nullptr;
+
+ private:
+ void* m_pSource = nullptr;
+ void* m_pFunctionAddr = nullptr;
+ void* m_pTrampolineAddr = nullptr;
+ void* m_pDestination = nullptr;
+ size_t m_iHookLen = 0;
+ size_t m_iTrampoLen = 0;
+ HANDLE m_pOwner = nullptr;
+ bool m_bActive = false;
+
+ friend class CHookSystem;
+};
+
+class CHookSystem {
+ public:
+ CFunctionHook* initHook(HANDLE handle, void* source, void* destination);
+ bool removeHook(CFunctionHook* hook);
+
+ void removeAllHooksFrom(HANDLE handle);
+
+ private:
+ std::vector<std::unique_ptr<CFunctionHook>> m_vHooks;
+};
+
+inline std::unique_ptr<CHookSystem> g_pFunctionHookSystem; \ No newline at end of file
diff --git a/src/plugins/PluginAPI.cpp b/src/plugins/PluginAPI.cpp
new file mode 100644
index 00000000..dc8c9707
--- /dev/null
+++ b/src/plugins/PluginAPI.cpp
@@ -0,0 +1,193 @@
+#include "PluginAPI.hpp"
+#include "../Compositor.hpp"
+#include "../debug/HyprCtl.hpp"
+#include <dlfcn.h>
+
+APICALL bool HyprlandAPI::registerCallbackStatic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN* fn) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ g_pHookSystem->hookStatic(event, fn, handle);
+ PLUGIN->registeredCallbacks.emplace_back(std::make_pair<>(event, fn));
+
+ return true;
+}
+
+APICALL HOOK_CALLBACK_FN* HyprlandAPI::registerCallbackDynamic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN fn) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return nullptr;
+
+ auto* const PFN = g_pHookSystem->hookDynamic(event, fn, handle);
+ PLUGIN->registeredCallbacks.emplace_back(std::make_pair<>(event, PFN));
+ return PFN;
+}
+
+APICALL bool HyprlandAPI::unregisterCallback(HANDLE handle, HOOK_CALLBACK_FN* fn) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ g_pHookSystem->unhook(fn);
+ std::erase_if(PLUGIN->registeredCallbacks, [&](const auto& other) { return other.second == fn; });
+
+ return true;
+}
+
+APICALL std::string HyprlandAPI::invokeHyprctlCommand(const std::string& call, const std::string& args, const std::string& format) {
+ std::string COMMAND = format + "/" + call + " " + args;
+ return HyprCtl::makeDynamicCall(COMMAND);
+}
+
+APICALL bool HyprlandAPI::addLayout(HANDLE handle, const std::string& name, IHyprLayout* layout) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ PLUGIN->registeredLayouts.push_back(layout);
+
+ return g_pLayoutManager->addLayout(name, layout);
+}
+
+APICALL bool HyprlandAPI::removeLayout(HANDLE handle, IHyprLayout* layout) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ std::erase(PLUGIN->registeredLayouts, layout);
+
+ return g_pLayoutManager->removeLayout(layout);
+}
+
+APICALL bool HyprlandAPI::reloadConfig() {
+ g_pConfigManager->m_bForceReload = true;
+ return true;
+}
+
+APICALL bool HyprlandAPI::addNotification(HANDLE handle, const std::string& text, const CColor& color, const float timeMs) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ g_pHyprNotificationOverlay->addNotification(text, color, timeMs);
+
+ return true;
+}
+
+APICALL CFunctionHook* HyprlandAPI::createFunctionHook(HANDLE handle, const void* source, const void* destination) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return nullptr;
+
+ return g_pFunctionHookSystem->initHook(handle, (void*)source, (void*)destination);
+}
+
+APICALL bool HyprlandAPI::removeFunctionHook(HANDLE handle, CFunctionHook* hook) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ return g_pFunctionHookSystem->removeHook(hook);
+}
+
+APICALL bool HyprlandAPI::addWindowDecoration(HANDLE handle, CWindow* pWindow, IHyprWindowDecoration* pDecoration) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ if (!g_pCompositor->windowValidMapped(pWindow))
+ return false;
+
+ PLUGIN->registeredDecorations.push_back(pDecoration);
+
+ pWindow->m_dWindowDecorations.emplace_back(pDecoration);
+
+ return true;
+}
+
+APICALL bool HyprlandAPI::removeWindowDecoration(HANDLE handle, IHyprWindowDecoration* pDecoration) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ for (auto& w : g_pCompositor->m_vWindows) {
+ for (auto& d : w->m_dWindowDecorations) {
+ if (d.get() == pDecoration) {
+ std::erase(w->m_dWindowDecorations, d);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+APICALL bool HyprlandAPI::addConfigValue(HANDLE handle, const std::string& name, const SConfigValue& value) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!g_pPluginSystem->m_bAllowConfigVars)
+ return false;
+
+ if (!PLUGIN)
+ return false;
+
+ if (name.find("plugin:") != 0)
+ return false;
+
+ g_pConfigManager->addPluginConfigVar(handle, name, value);
+ return true;
+}
+
+APICALL SConfigValue* HyprlandAPI::getConfigValue(HANDLE handle, const std::string& name) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return nullptr;
+
+ return g_pConfigManager->getConfigValuePtrSafe(name);
+}
+
+APICALL void* HyprlandAPI::getFunctionAddressFromSignature(HANDLE handle, const std::string& sig) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return nullptr;
+
+ return dlsym(nullptr, sig.c_str());
+}
+
+APICALL bool HyprlandAPI::addDispatcher(HANDLE handle, const std::string& name, std::function<void(std::string)> handler) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ PLUGIN->registeredDispatchers.push_back(name);
+
+ g_pKeybindManager->m_mDispatchers[name] = handler;
+
+ return true;
+}
+
+APICALL bool HyprlandAPI::removeDispatcher(HANDLE handle, const std::string& name) {
+ auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);
+
+ if (!PLUGIN)
+ return false;
+
+ std::erase_if(g_pKeybindManager->m_mDispatchers, [&](const auto& other) { return other.first == name; });
+ std::erase_if(PLUGIN->registeredDispatchers, [&](const auto& other) { return other == name; });
+
+ return true;
+} \ No newline at end of file
diff --git a/src/plugins/PluginAPI.hpp b/src/plugins/PluginAPI.hpp
new file mode 100644
index 00000000..756a5631
--- /dev/null
+++ b/src/plugins/PluginAPI.hpp
@@ -0,0 +1,217 @@
+#pragma once
+
+/*
+
+Hyprland Plugin API.
+
+Most documentation will be made with comments in this code, but more info can be also found on the wiki.
+
+!WARNING!
+The Hyprland API passes C++ objects over, so no ABI compatibility is guaranteed.
+Make sure to compile your plugins with the same compiler as Hyprland, and ideally,
+on the same machine.
+
+See examples/examplePlugin for an example plugin
+
+*/
+
+#define HYPRLAND_API_VERSION "0.1"
+
+#include "../helpers/Color.hpp"
+#include "HookSystem.hpp"
+
+#include <any>
+#include <functional>
+#include <string>
+
+typedef std::function<void(void*, std::any)> HOOK_CALLBACK_FN;
+typedef struct {
+ std::string name;
+ std::string description;
+ std::string author;
+ std::string version;
+} PLUGIN_DESCRIPTION_INFO;
+
+#define APICALL extern "C"
+#define EXPORT __attribute__((visibility("default")))
+#define REQUIRED
+#define OPTIONAL
+#define HANDLE void*
+
+class IHyprLayout;
+class CWindow;
+class IHyprWindowDecoration;
+struct SConfigValue;
+
+/*
+ These methods are for the plugin to implement
+ Methods marked with REQUIRED are required.
+*/
+
+/*
+ called pre-plugin init.
+ In case of a version mismatch, will eject the .so.
+
+ This function should not be modified, see the example plugin.
+*/
+typedef REQUIRED std::string (*PPLUGIN_API_VERSION_FUNC)();
+#define PLUGIN_API_VERSION pluginAPIVersion
+#define PLUGIN_API_VERSION_FUNC_STR "pluginAPIVersion"
+
+/*
+ called on plugin init. Passes a handle as the parameter, which the plugin should keep for identification later.
+ The plugin should return a PLUGIN_DESCRIPTION_INFO struct with information about itself.
+
+ Keep in mind this is executed synchronously, and as such any blocking calls to hyprland might hang. (e.g. system("hyprctl ..."))
+*/
+typedef REQUIRED PLUGIN_DESCRIPTION_INFO (*PPLUGIN_INIT_FUNC)(HANDLE);
+#define PLUGIN_INIT pluginInit
+#define PLUGIN_INIT_FUNC_STR "pluginInit"
+
+/*
+ called on plugin unload, if that was a user action. If the plugin is being unloaded by an error,
+ this will not be called.
+
+ Hooks are unloaded after exit.
+*/
+typedef OPTIONAL void (*PPLUGIN_EXIT_FUNC)(void);
+#define PLUGIN_EXIT pluginExit
+#define PLUGIN_EXIT_FUNC_STR "pluginExit"
+
+/*
+ End plugin methods
+*/
+
+namespace HyprlandAPI {
+
+ /*
+ Add a config value.
+ All config values MUST be in the plugin: namespace
+ This method may only be called in "pluginInit"
+
+ After you have registered ALL of your config values, you may call `getConfigValue`
+
+ returns: true on success, false on fail
+ */
+ APICALL bool addConfigValue(HANDLE handle, const std::string& name, const SConfigValue& value);
+
+ /*
+ Get a config value.
+
+ returns: a pointer to the config value struct, which is guaranteed to be valid for the life of this plugin, unless another `addConfigValue` is called afterwards.
+ nullptr on error.
+ */
+ APICALL SConfigValue* getConfigValue(HANDLE handle, const std::string& name);
+
+ /*
+ Register a static (pointer) callback to a selected event.
+ Pointer must be kept valid until unregisterCallback() is called.
+
+ returns: true on success, false on fail
+ */
+ APICALL bool registerCallbackStatic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN* fn);
+
+ /*
+ Register a dynamic (function) callback to a selected event.
+ Pointer will be free'd by Hyprland on unregisterCallback().
+
+ returns: a pointer to the newly allocated function. nullptr on fail.
+ */
+ APICALL HOOK_CALLBACK_FN* registerCallbackDynamic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN fn);
+
+ /*
+ Unregisters a callback. If the callback was dynamic, frees the memory.
+
+ returns: true on success, false on fail
+ */
+ APICALL bool unregisterCallback(HANDLE handle, HOOK_CALLBACK_FN* fn);
+
+ /*
+ Calls a hyprctl command.
+
+ returns: the output (as in hyprctl)
+ */
+ APICALL std::string invokeHyprctlCommand(const std::string& call, const std::string& args, const std::string& format = "");
+
+ /*
+ Adds a layout to Hyprland.
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool addLayout(HANDLE handle, const std::string& name, IHyprLayout* layout);
+
+ /*
+ Removes an added layout from Hyprland.
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool removeLayout(HANDLE handle, IHyprLayout* layout);
+
+ /*
+ Queues a config reload. Does not take effect immediately.
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool reloadConfig();
+
+ /*
+ Adds a notification.
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool addNotification(HANDLE handle, const std::string& text, const CColor& color, const float timeMs);
+
+ /*
+ Creates a trampoline function hook to an internal hl func.
+
+ returns: CFunctionHook*
+
+ !WARNING! Hooks are *not* guaranteed any API stability. Internal methods may be removed, added, or renamed. Consider preferring the API whenever possible.
+ */
+ APICALL CFunctionHook* createFunctionHook(HANDLE handle, const void* source, const void* destination);
+
+ /*
+ Removes a trampoline function hook. Will unhook if still hooked.
+
+ returns: true on success. False otherwise.
+
+ !WARNING! Hooks are *not* guaranteed any API stability. Internal methods may be removed, added, or renamed. Consider preferring the API whenever possible.
+ */
+ APICALL bool removeFunctionHook(HANDLE handle, CFunctionHook* hook);
+
+ /*
+ Gets a function address from a signature.
+ This is useful for hooking private functions.
+
+ returns: function address, or nullptr on fail.
+ */
+ APICALL void* getFunctionAddressFromSignature(HANDLE handle, const std::string& sig);
+
+ /*
+ Adds a window decoration to a window
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool addWindowDecoration(HANDLE handle, CWindow* pWindow, IHyprWindowDecoration* pDecoration);
+
+ /*
+ Removes a window decoration
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool removeWindowDecoration(HANDLE handle, IHyprWindowDecoration* pDecoration);
+
+ /*
+ Adds a keybind dispatcher.
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool addDispatcher(HANDLE handle, const std::string& name, std::function<void(std::string)> handler);
+
+ /*
+ Removes a keybind dispatcher.
+
+ returns: true on success. False otherwise.
+ */
+ APICALL bool removeDispatcher(HANDLE handle, const std::string& name);
+}; \ No newline at end of file
diff --git a/src/plugins/PluginSystem.cpp b/src/plugins/PluginSystem.cpp
new file mode 100644
index 00000000..5b15782d
--- /dev/null
+++ b/src/plugins/PluginSystem.cpp
@@ -0,0 +1,139 @@
+#include "PluginSystem.hpp"
+
+#include <dlfcn.h>
+#include "../Compositor.hpp"
+
+CPluginSystem::CPluginSystem() {
+ g_pFunctionHookSystem = std::make_unique<CHookSystem>();
+}
+
+CPlugin* CPluginSystem::loadPlugin(const std::string& path) {
+
+ if (getPluginByPath(path)) {
+ Debug::log(ERR, " [PluginSystem] Cannot load a plugin twice!");
+ return nullptr;
+ }
+
+ auto* const PLUGIN = m_vLoadedPlugins.emplace_back(std::make_unique<CPlugin>()).get();
+
+ PLUGIN->path = path;
+
+ HANDLE MODULE = dlopen(path.c_str(), RTLD_LAZY);
+
+ if (!MODULE) {
+ Debug::log(ERR, " [PluginSystem] Plugin %s could not be loaded: %s", path.c_str(), dlerror());
+ m_vLoadedPlugins.pop_back();
+ return nullptr;
+ }
+
+ PLUGIN->m_pHandle = MODULE;
+
+ PPLUGIN_API_VERSION_FUNC apiVerFunc = (PPLUGIN_API_VERSION_FUNC)dlsym(MODULE, PLUGIN_API_VERSION_FUNC_STR);
+ PPLUGIN_INIT_FUNC initFunc = (PPLUGIN_INIT_FUNC)dlsym(MODULE, PLUGIN_INIT_FUNC_STR);
+
+ if (!apiVerFunc || !initFunc) {
+ Debug::log(ERR, " [PluginSystem] Plugin %s could not be loaded. (No apiver/init func)", path.c_str());
+ dlclose(MODULE);
+ m_vLoadedPlugins.pop_back();
+ return nullptr;
+ }
+
+ const std::string PLUGINAPIVER = apiVerFunc();
+
+ if (PLUGINAPIVER != HYPRLAND_API_VERSION) {
+ Debug::log(ERR, " [PluginSystem] Plugin %s could not be loaded. (API version mismatch)", path.c_str());
+ dlclose(MODULE);
+ m_vLoadedPlugins.pop_back();
+ return nullptr;
+ }
+
+ PLUGIN_DESCRIPTION_INFO PLUGINDATA;
+
+ try {
+ if (!setjmp(m_jbPluginFaultJumpBuf)) {
+ m_bAllowConfigVars = true;
+ PLUGINDATA = initFunc(MODULE);
+ } else {
+ // this module crashed.
+ throw std::exception();
+ }
+ } catch (std::exception& e) {
+ m_bAllowConfigVars = false;
+ Debug::log(ERR, " [PluginSystem] Plugin %s (Handle %lx) crashed in init. Unloading.", path.c_str(), MODULE);
+ unloadPlugin(PLUGIN, true); // Plugin could've already hooked/done something
+ return nullptr;
+ }
+
+ m_bAllowConfigVars = false;
+
+ PLUGIN->author = PLUGINDATA.author;
+ PLUGIN->description = PLUGINDATA.description;
+ PLUGIN->version = PLUGINDATA.version;
+ PLUGIN->name = PLUGINDATA.name;
+
+ Debug::log(LOG, " [PluginSystem] Plugin %s loaded. Handle: %lx, path: \"%s\", author: \"%s\", description: \"%s\", version: \"%s\"", PLUGINDATA.name.c_str(), MODULE,
+ path.c_str(), PLUGINDATA.author.c_str(), PLUGINDATA.description.c_str(), PLUGINDATA.version.c_str());
+
+ return PLUGIN;
+}
+
+void CPluginSystem::unloadPlugin(const CPlugin* plugin, bool eject) {
+ if (!plugin)
+ return;
+
+ if (!eject) {
+ PPLUGIN_EXIT_FUNC exitFunc = (PPLUGIN_EXIT_FUNC)dlsym(plugin->m_pHandle, PLUGIN_EXIT_FUNC_STR);
+ if (exitFunc)
+ exitFunc();
+ }
+
+ for (auto& [k, v] : plugin->registeredCallbacks)
+ g_pHookSystem->unhook(v);
+
+ const auto ls = plugin->registeredLayouts;
+ for (auto& l : ls)
+ g_pLayoutManager->removeLayout(l);
+
+ g_pFunctionHookSystem->removeAllHooksFrom(plugin->m_pHandle);
+
+ const auto rd = plugin->registeredDecorations;
+ for (auto& d : rd)
+ HyprlandAPI::removeWindowDecoration(plugin->m_pHandle, d);
+
+ const auto rdi = plugin->registeredDispatchers;
+ for (auto& d : rdi)
+ HyprlandAPI::removeDispatcher(plugin->m_pHandle, d);
+
+ g_pConfigManager->removePluginConfig(plugin->m_pHandle);
+
+ dlclose(plugin->m_pHandle);
+
+ Debug::log(LOG, " [PluginSystem] Plugin %s unloaded.", plugin->name.c_str());
+
+ std::erase_if(m_vLoadedPlugins, [&](const auto& other) { return other->m_pHandle == plugin->m_pHandle; });
+}
+
+CPlugin* CPluginSystem::getPluginByPath(const std::string& path) {
+ for (auto& p : m_vLoadedPlugins) {
+ if (p->path == path)
+ return p.get();
+ }
+
+ return nullptr;
+}
+
+CPlugin* CPluginSystem::getPluginByHandle(HANDLE handle) {
+ for (auto& p : m_vLoadedPlugins) {
+ if (p->m_pHandle == handle)
+ return p.get();
+ }
+
+ return nullptr;
+}
+
+std::vector<CPlugin*> CPluginSystem::getAllPlugins() {
+ std::vector<CPlugin*> results(m_vLoadedPlugins.size());
+ for (size_t i = 0; i < m_vLoadedPlugins.size(); ++i)
+ results[i] = m_vLoadedPlugins[i].get();
+ return results;
+} \ No newline at end of file
diff --git a/src/plugins/PluginSystem.hpp b/src/plugins/PluginSystem.hpp
new file mode 100644
index 00000000..041ee47e
--- /dev/null
+++ b/src/plugins/PluginSystem.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "../defines.hpp"
+#include "PluginAPI.hpp"
+#include <csetjmp>
+
+class IHyprWindowDecoration;
+
+class CPlugin {
+ public:
+ std::string name = "";
+ std::string description = "";
+ std::string author = "";
+ std::string version = "";
+
+ std::string path = "";
+
+ HANDLE m_pHandle = nullptr;
+
+ std::vector<IHyprLayout*> registeredLayouts;
+ std::vector<IHyprWindowDecoration*> registeredDecorations;
+ std::vector<std::pair<std::string, HOOK_CALLBACK_FN*>> registeredCallbacks;
+ std::vector<std::string> registeredDispatchers;
+};
+
+class CPluginSystem {
+ public:
+ CPluginSystem();
+
+ CPlugin* loadPlugin(const std::string& path);
+ void unloadPlugin(const CPlugin* plugin, bool eject = false);
+ CPlugin* getPluginByPath(const std::string& path);
+ CPlugin* getPluginByHandle(HANDLE handle);
+ std::vector<CPlugin*> getAllPlugins();
+
+ bool m_bAllowConfigVars = false;
+
+ private:
+ std::vector<std::unique_ptr<CPlugin>> m_vLoadedPlugins;
+
+ jmp_buf m_jbPluginFaultJumpBuf;
+};
+
+inline std::unique_ptr<CPluginSystem> g_pPluginSystem; \ No newline at end of file