From 6ae354f564c02a89813543a0a91aa74c28b36b74 Mon Sep 17 00:00:00 2001 From: "Crom (Thibaut CHARLES)" Date: Sat, 14 Oct 2023 19:14:46 +0200 Subject: [PATCH 1/5] PoC --- cabi_example/main.c | 39 +++++++++++++++++++++++++++++++ cabi_example/meson.build | 21 +++++++++++++++++ include/factory.hpp | 1 + include/modules/cabi.hpp | 28 ++++++++++++++++++++++ meson.build | 2 ++ src/factory.cpp | 3 +++ src/modules/cabi.cpp | 50 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 144 insertions(+) create mode 100644 cabi_example/main.c create mode 100644 cabi_example/meson.build create mode 100644 include/modules/cabi.hpp create mode 100644 src/modules/cabi.cpp diff --git a/cabi_example/main.c b/cabi_example/main.c new file mode 100644 index 00000000..babb3aea --- /dev/null +++ b/cabi_example/main.c @@ -0,0 +1,39 @@ +#include +#include + +typedef struct { + GtkContainer* root; + GtkBox* container; + GtkLabel* label; + GtkButton* button; +} Instance; + +// +const size_t wbcabi_version = 1; + +void onclicked() { printf("You clicked the button\n"); } + +void* wbcabi_init(GtkContainer* root) { + // Allocate the instance object + Instance* inst = malloc(sizeof(Instance)); + inst->root = root; + + // Add a container for displaying the next widgets + inst->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5)); + gtk_container_add(GTK_CONTAINER(inst->root), GTK_WIDGET(inst->container)); + + // Add a label + inst->label = GTK_LABEL(gtk_label_new("This is a button:")); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(inst->label)); + + // Add a button + inst->button = GTK_BUTTON(gtk_button_new_with_label("click me !")); + g_signal_connect(inst->button, "clicked", G_CALLBACK(onclicked), NULL); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(inst->button)); + + // Return instance object + return inst; +} +void wbcabi_deinit(void* instance) { free(instance); } + +char* wbcabi_last_error_str(void* instance) { return NULL; } \ No newline at end of file diff --git a/cabi_example/meson.build b/cabi_example/meson.build new file mode 100644 index 00000000..3f69d2b8 --- /dev/null +++ b/cabi_example/meson.build @@ -0,0 +1,21 @@ +project( + 'waybar_cabi_c_example', 'c', + version: '0.1.0', + license: 'MIT', +) + +compiler = meson.get_compiler('c') + + +shared_library('waybar_cabi_c_example', + [ + 'main.c', + ], + dependencies: [ + dependency('gtk+-3.0', version : ['>=3.22.0']) + ] + # include_directories: include_directories(['../../../include']), + # dependencies: [], + # install: true, + # install_dir: 'plugins', +) diff --git a/include/factory.hpp b/include/factory.hpp index cb25078d..068245e6 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -91,6 +91,7 @@ #include "modules/cava.hpp" #endif #include "bar.hpp" +#include "modules/cabi.hpp" #include "modules/custom.hpp" #include "modules/image.hpp" #include "modules/temperature.hpp" diff --git a/include/modules/cabi.hpp b/include/modules/cabi.hpp new file mode 100644 index 00000000..7eae48b7 --- /dev/null +++ b/include/modules/cabi.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include +#include + +#include "AModule.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CABI : public AModule { + public: + CABI(const std::string&, const std::string&, const Json::Value&); + virtual ~CABI(); + + private: + void* cabi_instance_ = nullptr; + + std::function wbcabi_init_ = nullptr; + std::function wbcabi_deinit_ = [](void*) {}; + std::function wbcabi_last_error_str_ = [](void*) { return nullptr; }; +}; + +} // namespace waybar::modules diff --git a/meson.build b/meson.build index bb9abdeb..99e8b562 100644 --- a/meson.build +++ b/meson.build @@ -190,6 +190,7 @@ if is_linux add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') src_files += files( 'src/modules/battery.cpp', + 'src/modules/cabi.cpp', 'src/modules/cpu.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_frequency/linux.cpp', @@ -202,6 +203,7 @@ elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd add_project_arguments('-DHAVE_CPU_BSD', language: 'cpp') add_project_arguments('-DHAVE_MEMORY_BSD', language: 'cpp') src_files += files( + 'src/modules/cabi.cpp', 'src/modules/cpu.cpp', 'src/modules/cpu_frequency/bsd.cpp', 'src/modules/cpu_frequency/common.cpp', diff --git a/src/factory.cpp b/src/factory.cpp index aaf46036..73815842 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -201,6 +201,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name]); } + if (ref.compare(0, 5, "cabi/") == 0 && ref.size() > 5) { + return new waybar::modules::CABI(ref.substr(5), id, config_[name]); + } } catch (const std::exception& e) { auto err = fmt::format("Disabling module \"{}\", {}", name, e.what()); throw std::runtime_error(err); diff --git a/src/modules/cabi.cpp b/src/modules/cabi.cpp new file mode 100644 index 00000000..7fd0ad5c --- /dev/null +++ b/src/modules/cabi.cpp @@ -0,0 +1,50 @@ +#include "modules/cabi.hpp" + +#include + +namespace waybar::modules { + +CABI::CABI(const std::string& name, const std::string& id, const Json::Value& config) + : AModule(config, name, id, true, true) { + const auto dynlib_path = config_["path"].asString(); + + void* handle = dlopen(dynlib_path.c_str(), RTLD_LAZY); + if (handle == nullptr) { + throw std::runtime_error{dlerror()}; + } + + // Fetch ABI version + auto wbcabi_version = reinterpret_cast(dlsym(handle, "wbcabi_version")); + if (wbcabi_version == nullptr) { + throw std::runtime_error{"Missing wbcabi_version"}; + } + + // Fetch functions + if (*wbcabi_version == 1) { + wbcabi_init_ = reinterpret_cast(dlsym(handle, "wbcabi_init")); + if (wbcabi_init_ == nullptr) { + throw std::runtime_error{"Missing wbcabi_init function"}; + } + if (auto fn = reinterpret_cast(dlsym(handle, "wbcabi_deinit"))) + wbcabi_deinit_ = fn; + if (auto fn = reinterpret_cast(dlsym(handle, "wbcabi_last_error_str"))) + wbcabi_last_error_str_ = fn; + } else { + throw std::runtime_error{"Unknown wbcabi_version " + std::to_string(*wbcabi_version)}; + } + + cabi_instance_ = wbcabi_init_(dynamic_cast(&event_box_)->gobj()); + if (cabi_instance_ == nullptr) { + const auto err_str = wbcabi_last_error_str_(cabi_instance_); + throw std::runtime_error{std::string{"Failed to initialize C ABI plugin: "} + + (err_str != nullptr ? err_str : "unknown error")}; + } +} + +CABI::~CABI() { + if (cabi_instance_ != nullptr) { + wbcabi_deinit_(cabi_instance_); + } +} + +} // namespace waybar::modules From 088ca6b963a8ae1bedf9c0178404f3e3e11e5509 Mon Sep 17 00:00:00 2001 From: "Crom (Thibaut CHARLES)" Date: Mon, 16 Oct 2023 22:13:51 +0200 Subject: [PATCH 2/5] Added cffi/* module for third-party advanced modules --- cabi_example/main.c | 39 ------- cabi_example/meson.build | 21 ---- include/factory.hpp | 2 +- include/modules/cabi.hpp | 28 ----- include/modules/cffi.hpp | 50 ++++++++ meson.build | 4 +- .../custom_modules/cffi_example/.gitignore | 1 + .../custom_modules/cffi_example/README.md | 35 ++++++ resources/custom_modules/cffi_example/main.c | 63 ++++++++++ .../custom_modules/cffi_example/meson.build | 13 +++ .../cffi_example/waybar_cffi_module.h | 59 ++++++++++ src/factory.cpp | 4 +- src/modules/cabi.cpp | 50 -------- src/modules/cffi.cpp | 108 ++++++++++++++++++ 14 files changed, 334 insertions(+), 143 deletions(-) delete mode 100644 cabi_example/main.c delete mode 100644 cabi_example/meson.build delete mode 100644 include/modules/cabi.hpp create mode 100644 include/modules/cffi.hpp create mode 100644 resources/custom_modules/cffi_example/.gitignore create mode 100644 resources/custom_modules/cffi_example/README.md create mode 100644 resources/custom_modules/cffi_example/main.c create mode 100644 resources/custom_modules/cffi_example/meson.build create mode 100644 resources/custom_modules/cffi_example/waybar_cffi_module.h delete mode 100644 src/modules/cabi.cpp create mode 100644 src/modules/cffi.cpp diff --git a/cabi_example/main.c b/cabi_example/main.c deleted file mode 100644 index babb3aea..00000000 --- a/cabi_example/main.c +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include - -typedef struct { - GtkContainer* root; - GtkBox* container; - GtkLabel* label; - GtkButton* button; -} Instance; - -// -const size_t wbcabi_version = 1; - -void onclicked() { printf("You clicked the button\n"); } - -void* wbcabi_init(GtkContainer* root) { - // Allocate the instance object - Instance* inst = malloc(sizeof(Instance)); - inst->root = root; - - // Add a container for displaying the next widgets - inst->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5)); - gtk_container_add(GTK_CONTAINER(inst->root), GTK_WIDGET(inst->container)); - - // Add a label - inst->label = GTK_LABEL(gtk_label_new("This is a button:")); - gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(inst->label)); - - // Add a button - inst->button = GTK_BUTTON(gtk_button_new_with_label("click me !")); - g_signal_connect(inst->button, "clicked", G_CALLBACK(onclicked), NULL); - gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(inst->button)); - - // Return instance object - return inst; -} -void wbcabi_deinit(void* instance) { free(instance); } - -char* wbcabi_last_error_str(void* instance) { return NULL; } \ No newline at end of file diff --git a/cabi_example/meson.build b/cabi_example/meson.build deleted file mode 100644 index 3f69d2b8..00000000 --- a/cabi_example/meson.build +++ /dev/null @@ -1,21 +0,0 @@ -project( - 'waybar_cabi_c_example', 'c', - version: '0.1.0', - license: 'MIT', -) - -compiler = meson.get_compiler('c') - - -shared_library('waybar_cabi_c_example', - [ - 'main.c', - ], - dependencies: [ - dependency('gtk+-3.0', version : ['>=3.22.0']) - ] - # include_directories: include_directories(['../../../include']), - # dependencies: [], - # install: true, - # install_dir: 'plugins', -) diff --git a/include/factory.hpp b/include/factory.hpp index 068245e6..bb00410e 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -91,7 +91,7 @@ #include "modules/cava.hpp" #endif #include "bar.hpp" -#include "modules/cabi.hpp" +#include "modules/cffi.hpp" #include "modules/custom.hpp" #include "modules/image.hpp" #include "modules/temperature.hpp" diff --git a/include/modules/cabi.hpp b/include/modules/cabi.hpp deleted file mode 100644 index 7eae48b7..00000000 --- a/include/modules/cabi.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include "AModule.hpp" -#include "util/command.hpp" -#include "util/json.hpp" -#include "util/sleeper_thread.hpp" - -namespace waybar::modules { - -class CABI : public AModule { - public: - CABI(const std::string&, const std::string&, const Json::Value&); - virtual ~CABI(); - - private: - void* cabi_instance_ = nullptr; - - std::function wbcabi_init_ = nullptr; - std::function wbcabi_deinit_ = [](void*) {}; - std::function wbcabi_last_error_str_ = [](void*) { return nullptr; }; -}; - -} // namespace waybar::modules diff --git a/include/modules/cffi.hpp b/include/modules/cffi.hpp new file mode 100644 index 00000000..43545a4f --- /dev/null +++ b/include/modules/cffi.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include "AModule.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +extern "C" { +// C ABI representation of a config key/value pair +struct wbcffi_config_entry { + const char* key; + const char* value; +}; +} + +class CFFI : public AModule { + public: + CFFI(const std::string&, const std::string&, const Json::Value&); + virtual ~CFFI(); + + // virtual auto update() -> void override; + // virtual operator Gtk::Widget&() override; + + virtual auto refresh(int signal) -> void override; + virtual auto doAction(const std::string& name) -> void override; + + private: + /// + void* cffi_instance_ = nullptr; + + typedef void*(InitFn)(GtkContainer*, const struct wbcffi_config_entry* config_entries, + size_t config_entries_len); + typedef void(DenitFn)(void* instance); + typedef void(RefreshFn)(void* instance, int signal); + typedef void(DoActionFn)(void* instance, const char* name); + + // FFI hooks + struct { + std::function init = nullptr; + std::function deinit = nullptr; + std::function refresh = [](void*, int) {}; + std::function doAction = [](void*, const char*) {}; + } hooks_; +}; + +} // namespace waybar::modules diff --git a/meson.build b/meson.build index 99e8b562..7c0f9965 100644 --- a/meson.build +++ b/meson.build @@ -190,7 +190,7 @@ if is_linux add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') src_files += files( 'src/modules/battery.cpp', - 'src/modules/cabi.cpp', + 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_frequency/linux.cpp', @@ -203,7 +203,7 @@ elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd add_project_arguments('-DHAVE_CPU_BSD', language: 'cpp') add_project_arguments('-DHAVE_MEMORY_BSD', language: 'cpp') src_files += files( - 'src/modules/cabi.cpp', + 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', 'src/modules/cpu_frequency/bsd.cpp', 'src/modules/cpu_frequency/common.cpp', diff --git a/resources/custom_modules/cffi_example/.gitignore b/resources/custom_modules/cffi_example/.gitignore new file mode 100644 index 00000000..988107fe --- /dev/null +++ b/resources/custom_modules/cffi_example/.gitignore @@ -0,0 +1 @@ +.cache/ \ No newline at end of file diff --git a/resources/custom_modules/cffi_example/README.md b/resources/custom_modules/cffi_example/README.md new file mode 100644 index 00000000..71e6ce83 --- /dev/null +++ b/resources/custom_modules/cffi_example/README.md @@ -0,0 +1,35 @@ +# C FFI module + +A C FFI module is a dynamic library that exposes standard C functions and +constants, that Waybar can load and execute to create custom advanced widgets. + +Most language can implement the required functions and constants (C, C++, Rust, +Go, Python, ...), meaning you can develop custom modules using your language of +choice, as long as there's GTK bindings. + +# Usage + +## Building this module + +```bash +meson setup build +meson compile -C build +``` + +## Load the module + +Edit your waybar config: +```json +{ + // ... + "modules-center": [ + // ... + "cffi/c_example" + ], + // ... + "cffi/c_example": { + // Path to the compiled dynamic library file + "module_path": "resources/custom_modules/cffi_example/build/wb_cffi_example.so" + } +} +``` diff --git a/resources/custom_modules/cffi_example/main.c b/resources/custom_modules/cffi_example/main.c new file mode 100644 index 00000000..24644afe --- /dev/null +++ b/resources/custom_modules/cffi_example/main.c @@ -0,0 +1,63 @@ + +#include "waybar_cffi_module.h" + +typedef struct { + GtkContainer* root; + GtkBox* container; + GtkButton* button; +} Instance; + +// This static variable is shared between all instances of this module +static int instance_count = 0; + +void onclicked(GtkButton* button) { + char text[256]; + snprintf(text, 256, "Dice throw result: %d", rand() % 6 + 1); + gtk_button_set_label(button, text); +} + +// You must +const size_t wbcffi_version = 1; + +void* wbcffi_init(GtkContainer* root, const struct wbcffi_config_entry* config_entries, + size_t config_entries_len) { + printf("cffi_example: init config:\n"); + for (size_t i = 0; i < config_entries_len; i++) { + printf(" %s = %s\n", config_entries[i].key, config_entries[i].value); + } + + // Allocate the instance object + Instance* inst = malloc(sizeof(Instance)); + inst->root = root; + + // Add a container for displaying the next widgets + inst->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5)); + gtk_container_add(GTK_CONTAINER(inst->root), GTK_WIDGET(inst->container)); + + // Add a label + GtkLabel* label = GTK_LABEL(gtk_label_new("[Example C FFI Module:")); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(label)); + + // Add a button + inst->button = GTK_BUTTON(gtk_button_new_with_label("click me !")); + g_signal_connect(inst->button, "clicked", G_CALLBACK(onclicked), NULL); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(inst->button)); + + // Add a label + label = GTK_LABEL(gtk_label_new("]")); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(label)); + + // Return instance object + printf("cffi_example inst=%p: init success ! (%d total instances)\n", inst, ++instance_count); + return inst; +} +void wbcffi_deinit(void* instance) { + printf("cffi_example inst=%p: free memory\n", instance); + free(instance); +} +void wbcffi_refresh(void* instance, int signal) { + printf("cffi_example inst=%p: Received refresh signal %d\n", instance, signal); +} +void wbcffi_doaction(void* instance, const char* name) { + printf("cffi_example inst=%p: doAction(%s)\n", instance, name); +} \ No newline at end of file diff --git a/resources/custom_modules/cffi_example/meson.build b/resources/custom_modules/cffi_example/meson.build new file mode 100644 index 00000000..dcde1048 --- /dev/null +++ b/resources/custom_modules/cffi_example/meson.build @@ -0,0 +1,13 @@ +project( + 'waybar_cffi_example', 'c', + version: '0.1.0', + license: 'MIT', +) + +shared_library('wb_cffi_example', + ['main.c'], + dependencies: [ + dependency('gtk+-3.0', version : ['>=3.22.0']) + ], + name_prefix: '' +) diff --git a/resources/custom_modules/cffi_example/waybar_cffi_module.h b/resources/custom_modules/cffi_example/waybar_cffi_module.h new file mode 100644 index 00000000..af2fe581 --- /dev/null +++ b/resources/custom_modules/cffi_example/waybar_cffi_module.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Waybar ABI version. 1 is the latest version +extern const size_t wbcffi_version; + +/// Config key-value pair +struct wbcffi_config_entry { + /// Entry key + const char* key; + /// Entry value as string. JSON object and arrays are serialized. + const char* value; +}; + +/// Module init/new function, called on module instantiation +/// +/// MANDATORY CFFI function +/// +/// @param root_widget Root GTK widget instantiated by Waybar +/// @param config_entries Flat representation of the module JSON config. The data only available +/// during wbcffi_init call. +/// @param config_entries_len Number of entries in `config_entries` +/// +/// @return A untyped pointer to module data, NULL if the module failed to load. +void* wbcffi_init(GtkContainer* root_widget, const struct wbcffi_config_entry* config_entries, + size_t config_entries_len); + +/// Module deinit/delete function, called when Waybar is closed or when the module is removed +/// +/// MANDATORY CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +void wbcffi_deinit(void* instance); + +/// When Waybar receives a POSIX signal, it forwards the signal to each module, calling this +/// function +/// +/// Optional CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +/// @param signal Signal ID +void wbcffi_refresh(void* instance, int signal); + +/// +/// Optional CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +/// @param name Action name +void wbcffi_doaction(void* instance, const char* name); + +#ifdef __cplusplus +} +#endif diff --git a/src/factory.cpp b/src/factory.cpp index 73815842..4df27cd4 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -201,8 +201,8 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name]); } - if (ref.compare(0, 5, "cabi/") == 0 && ref.size() > 5) { - return new waybar::modules::CABI(ref.substr(5), id, config_[name]); + if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { + return new waybar::modules::CFFI(ref.substr(5), id, config_[name]); } } catch (const std::exception& e) { auto err = fmt::format("Disabling module \"{}\", {}", name, e.what()); diff --git a/src/modules/cabi.cpp b/src/modules/cabi.cpp deleted file mode 100644 index 7fd0ad5c..00000000 --- a/src/modules/cabi.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "modules/cabi.hpp" - -#include - -namespace waybar::modules { - -CABI::CABI(const std::string& name, const std::string& id, const Json::Value& config) - : AModule(config, name, id, true, true) { - const auto dynlib_path = config_["path"].asString(); - - void* handle = dlopen(dynlib_path.c_str(), RTLD_LAZY); - if (handle == nullptr) { - throw std::runtime_error{dlerror()}; - } - - // Fetch ABI version - auto wbcabi_version = reinterpret_cast(dlsym(handle, "wbcabi_version")); - if (wbcabi_version == nullptr) { - throw std::runtime_error{"Missing wbcabi_version"}; - } - - // Fetch functions - if (*wbcabi_version == 1) { - wbcabi_init_ = reinterpret_cast(dlsym(handle, "wbcabi_init")); - if (wbcabi_init_ == nullptr) { - throw std::runtime_error{"Missing wbcabi_init function"}; - } - if (auto fn = reinterpret_cast(dlsym(handle, "wbcabi_deinit"))) - wbcabi_deinit_ = fn; - if (auto fn = reinterpret_cast(dlsym(handle, "wbcabi_last_error_str"))) - wbcabi_last_error_str_ = fn; - } else { - throw std::runtime_error{"Unknown wbcabi_version " + std::to_string(*wbcabi_version)}; - } - - cabi_instance_ = wbcabi_init_(dynamic_cast(&event_box_)->gobj()); - if (cabi_instance_ == nullptr) { - const auto err_str = wbcabi_last_error_str_(cabi_instance_); - throw std::runtime_error{std::string{"Failed to initialize C ABI plugin: "} + - (err_str != nullptr ? err_str : "unknown error")}; - } -} - -CABI::~CABI() { - if (cabi_instance_ != nullptr) { - wbcabi_deinit_(cabi_instance_); - } -} - -} // namespace waybar::modules diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp new file mode 100644 index 00000000..d0cd765f --- /dev/null +++ b/src/modules/cffi.cpp @@ -0,0 +1,108 @@ +#include "modules/cffi.hpp" + +#include +#include + +#include +#include +#include + +namespace waybar::modules { + +CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& config) + : AModule(config, name, id, true, true) { + const auto dynlib_path = config_["module_path"].asString(); + if (dynlib_path.empty()) { + throw std::runtime_error{"Missing or empty 'module_path' in module config"}; + } + + void* handle = dlopen(dynlib_path.c_str(), RTLD_LAZY); + if (handle == nullptr) { + throw std::runtime_error{std::string{"Failed to load CFFI module: "} + dlerror()}; + } + + // Fetch ABI version + auto wbcffi_version = reinterpret_cast(dlsym(handle, "wbcffi_version")); + if (wbcffi_version == nullptr) { + throw std::runtime_error{std::string{"Missing wbcffi_version function: "} + dlerror()}; + } + + // Fetch functions + if (*wbcffi_version == 1) { + // Mandatory functions + hooks_.init = reinterpret_cast(dlsym(handle, "wbcffi_init")); + if (!hooks_.init) { + throw std::runtime_error{std::string{"Missing wbcffi_init function: "} + dlerror()}; + } + hooks_.deinit = reinterpret_cast(dlsym(handle, "wbcffi_deinit")); + if (!hooks_.init) { + throw std::runtime_error{std::string{"Missing wbcffi_deinit function: "} + dlerror()}; + } + // Optional functions + if (auto fn = reinterpret_cast(dlsym(handle, "wbcffi_refresh"))) { + hooks_.refresh = fn; + } + if (auto fn = reinterpret_cast(dlsym(handle, "wbcffi_doaction"))) { + hooks_.doAction = fn; + } + } else { + throw std::runtime_error{"Unknown wbcffi_version " + std::to_string(*wbcffi_version)}; + } + + // Prepare init() arguments + // Convert JSON values to string + std::vector config_entries_stringstor; + const auto& keys = config.getMemberNames(); + for (size_t i = 0; i < keys.size(); i++) { + const auto& value = config[keys[i]]; + if (value.isConvertibleTo(Json::ValueType::stringValue)) { + config_entries_stringstor.push_back(config[keys[i]].asString()); + } else { + config_entries_stringstor.push_back(config[keys[i]].toStyledString()); + } + } + + // Prepare config_entries array + std::vector config_entries; + for (size_t i = 0; i < keys.size(); i++) { + config_entries.push_back( + wbcffi_config_entry{keys[i].c_str(), config_entries_stringstor[i].c_str()}); + } + + // Call init + cffi_instance_ = hooks_.init(dynamic_cast(&event_box_)->gobj(), + config_entries.data(), config_entries.size()); + + // Handle init failures + if (cffi_instance_ == nullptr) { + throw std::runtime_error{"Failed to initialize C ABI module"}; + } +} + +CFFI::~CFFI() { + if (cffi_instance_ != nullptr) { + hooks_.deinit(cffi_instance_); + } +} + +auto CFFI::refresh(int signal) -> void { + assert(cffi_instance_ != nullptr); + hooks_.refresh(cffi_instance_, signal); +} + +auto CFFI::doAction(const std::string& name) -> void { + assert(cffi_instance_ != nullptr); + if (!name.empty()) { + // TODO: Make a decision + // Calling AModule::doAction and hooks_.doAction will execute the action twice if it is + // configured in AModule::eventActionMap_ and implemented in the CFFI module. + // + // Should we block AModule::doAction() if the action is implemented in hooks_.doAction() ? + // (doAction could return true if the action has been processed) + // + hooks_.doAction(cffi_instance_, name.c_str()); + AModule::doAction(name); + } +} + +} // namespace waybar::modules From d86059016e6e6aaa29036faedbd0151413300902 Mon Sep 17 00:00:00 2001 From: "Crom (Thibaut CHARLES)" Date: Tue, 17 Oct 2023 18:11:48 +0200 Subject: [PATCH 3/5] Send update dispatcher though FFI --- include/AModule.hpp | 1 + include/modules/cffi.hpp | 9 ++++--- .../custom_modules/cffi_example/README.md | 3 +++ resources/custom_modules/cffi_example/main.c | 10 ++++++-- .../cffi_example/waybar_cffi_module.h | 23 +++++++++++++---- src/modules/cffi.cpp | 25 +++++++++++-------- 6 files changed, 50 insertions(+), 21 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index 479755b7..0960b116 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -17,6 +17,7 @@ class AModule : public IModule { operator Gtk::Widget &() override; auto doAction(const std::string &name) -> void override; + /// Emitting on this dispatcher triggers a update() call Glib::Dispatcher dp; protected: diff --git a/include/modules/cffi.hpp b/include/modules/cffi.hpp index 43545a4f..ac3b3de7 100644 --- a/include/modules/cffi.hpp +++ b/include/modules/cffi.hpp @@ -22,21 +22,21 @@ class CFFI : public AModule { CFFI(const std::string&, const std::string&, const Json::Value&); virtual ~CFFI(); - // virtual auto update() -> void override; - // virtual operator Gtk::Widget&() override; - virtual auto refresh(int signal) -> void override; virtual auto doAction(const std::string& name) -> void override; + virtual auto update() -> void override; private: /// void* cffi_instance_ = nullptr; - typedef void*(InitFn)(GtkContainer*, const struct wbcffi_config_entry* config_entries, + typedef void*(InitFn)(GtkContainer* root_widget, const void (*trigger_update)(void*), + void* trigger_update_arg, const struct wbcffi_config_entry* config_entries, size_t config_entries_len); typedef void(DenitFn)(void* instance); typedef void(RefreshFn)(void* instance, int signal); typedef void(DoActionFn)(void* instance, const char* name); + typedef void(UpdateFn)(void* instance); // FFI hooks struct { @@ -44,6 +44,7 @@ class CFFI : public AModule { std::function deinit = nullptr; std::function refresh = [](void*, int) {}; std::function doAction = [](void*, const char*) {}; + std::function update = [](void*) {}; } hooks_; }; diff --git a/resources/custom_modules/cffi_example/README.md b/resources/custom_modules/cffi_example/README.md index 71e6ce83..88396c19 100644 --- a/resources/custom_modules/cffi_example/README.md +++ b/resources/custom_modules/cffi_example/README.md @@ -7,6 +7,9 @@ Most language can implement the required functions and constants (C, C++, Rust, Go, Python, ...), meaning you can develop custom modules using your language of choice, as long as there's GTK bindings. +Symbols to implement are documented in the +[waybar_cffi_module.h](waybar_cffi_module.h) file. + # Usage ## Building this module diff --git a/resources/custom_modules/cffi_example/main.c b/resources/custom_modules/cffi_example/main.c index 24644afe..c959c9a8 100644 --- a/resources/custom_modules/cffi_example/main.c +++ b/resources/custom_modules/cffi_example/main.c @@ -19,7 +19,8 @@ void onclicked(GtkButton* button) { // You must const size_t wbcffi_version = 1; -void* wbcffi_init(GtkContainer* root, const struct wbcffi_config_entry* config_entries, +void* wbcffi_init(GtkContainer* root_widget, void (*trigger_update)(void*), + void* trigger_update_arg, const struct wbcffi_config_entry* config_entries, size_t config_entries_len) { printf("cffi_example: init config:\n"); for (size_t i = 0; i < config_entries_len; i++) { @@ -28,7 +29,7 @@ void* wbcffi_init(GtkContainer* root, const struct wbcffi_config_entry* config_e // Allocate the instance object Instance* inst = malloc(sizeof(Instance)); - inst->root = root; + inst->root = root_widget; // Add a container for displaying the next widgets inst->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5)); @@ -51,13 +52,18 @@ void* wbcffi_init(GtkContainer* root, const struct wbcffi_config_entry* config_e printf("cffi_example inst=%p: init success ! (%d total instances)\n", inst, ++instance_count); return inst; } + void wbcffi_deinit(void* instance) { printf("cffi_example inst=%p: free memory\n", instance); free(instance); } + +void wbcffi_update(void* instance) { printf("cffi_example inst=%p: Update request\n", instance); } + void wbcffi_refresh(void* instance, int signal) { printf("cffi_example inst=%p: Received refresh signal %d\n", instance, signal); } + void wbcffi_doaction(void* instance, const char* name) { printf("cffi_example inst=%p: doAction(%s)\n", instance, name); } \ No newline at end of file diff --git a/resources/custom_modules/cffi_example/waybar_cffi_module.h b/resources/custom_modules/cffi_example/waybar_cffi_module.h index af2fe581..2696b670 100644 --- a/resources/custom_modules/cffi_example/waybar_cffi_module.h +++ b/resources/custom_modules/cffi_example/waybar_cffi_module.h @@ -23,12 +23,16 @@ struct wbcffi_config_entry { /// MANDATORY CFFI function /// /// @param root_widget Root GTK widget instantiated by Waybar +/// @param trigger_update Call this function with trigger_update_arg as argument to trigger +/// wbcffi_update() on the next GTK main event loop iteration +/// @param trigger_update_arg Argument for trigger_update call /// @param config_entries Flat representation of the module JSON config. The data only available /// during wbcffi_init call. /// @param config_entries_len Number of entries in `config_entries` /// /// @return A untyped pointer to module data, NULL if the module failed to load. -void* wbcffi_init(GtkContainer* root_widget, const struct wbcffi_config_entry* config_entries, +void* wbcffi_init(GtkContainer* root_widget, const void (*trigger_update)(void*), + void* trigger_update_arg, const struct wbcffi_config_entry* config_entries, size_t config_entries_len); /// Module deinit/delete function, called when Waybar is closed or when the module is removed @@ -38,8 +42,15 @@ void* wbcffi_init(GtkContainer* root_widget, const struct wbcffi_config_entry* c /// @param instance Module instance data (as returned by `wbcffi_init`) void wbcffi_deinit(void* instance); -/// When Waybar receives a POSIX signal, it forwards the signal to each module, calling this -/// function +/// Called from the GTK main event loop, to update the UI +/// +/// Optional CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +/// @param action_name Action name +void wbcffi_update(void* instance); + +/// Called when Waybar receives a POSIX signal and forwards it to each module /// /// Optional CFFI function /// @@ -47,12 +58,14 @@ void wbcffi_deinit(void* instance); /// @param signal Signal ID void wbcffi_refresh(void* instance, int signal); +/// Called on module action (see +/// https://github.com/Alexays/Waybar/wiki/Configuration#module-actions-config) /// /// Optional CFFI function /// /// @param instance Module instance data (as returned by `wbcffi_init`) -/// @param name Action name -void wbcffi_doaction(void* instance, const char* name); +/// @param action_name Action name +void wbcffi_doaction(void* instance, const char* action_name); #ifdef __cplusplus } diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp index d0cd765f..e7a28edd 100644 --- a/src/modules/cffi.cpp +++ b/src/modules/cffi.cpp @@ -39,6 +39,9 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co throw std::runtime_error{std::string{"Missing wbcffi_deinit function: "} + dlerror()}; } // Optional functions + if (auto fn = reinterpret_cast(dlsym(handle, "wbcffi_update"))) { + hooks_.update = fn; + } if (auto fn = reinterpret_cast(dlsym(handle, "wbcffi_refresh"))) { hooks_.refresh = fn; } @@ -70,8 +73,10 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co } // Call init - cffi_instance_ = hooks_.init(dynamic_cast(&event_box_)->gobj(), - config_entries.data(), config_entries.size()); + cffi_instance_ = hooks_.init( + dynamic_cast(&event_box_)->gobj(), + [](void* dp) { ((Glib::Dispatcher*)dp)->emit(); }, &dp, config_entries.data(), + config_entries.size()); // Handle init failures if (cffi_instance_ == nullptr) { @@ -85,6 +90,14 @@ CFFI::~CFFI() { } } +auto CFFI::update() -> void { + assert(cffi_instance_ != nullptr); + hooks_.update(cffi_instance_); + + // Execute the on-update command set in config + AModule::update(); +} + auto CFFI::refresh(int signal) -> void { assert(cffi_instance_ != nullptr); hooks_.refresh(cffi_instance_, signal); @@ -93,15 +106,7 @@ auto CFFI::refresh(int signal) -> void { auto CFFI::doAction(const std::string& name) -> void { assert(cffi_instance_ != nullptr); if (!name.empty()) { - // TODO: Make a decision - // Calling AModule::doAction and hooks_.doAction will execute the action twice if it is - // configured in AModule::eventActionMap_ and implemented in the CFFI module. - // - // Should we block AModule::doAction() if the action is implemented in hooks_.doAction() ? - // (doAction could return true if the action has been processed) - // hooks_.doAction(cffi_instance_, name.c_str()); - AModule::doAction(name); } } From 02c64f3f1eb382827cda1280be8b1beed57aa761 Mon Sep 17 00:00:00 2001 From: "Crom (Thibaut CHARLES)" Date: Thu, 19 Oct 2023 22:10:32 +0200 Subject: [PATCH 4/5] Moved all waybar info into a single struct --- include/modules/cffi.hpp | 17 +++++++--- resources/custom_modules/cffi_example/main.c | 15 +++++---- .../cffi_example/waybar_cffi_module.h | 33 ++++++++++++++----- src/modules/cffi.cpp | 20 +++++++---- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/include/modules/cffi.hpp b/include/modules/cffi.hpp index ac3b3de7..85f12989 100644 --- a/include/modules/cffi.hpp +++ b/include/modules/cffi.hpp @@ -9,13 +9,23 @@ namespace waybar::modules { +namespace ffi { extern "C" { -// C ABI representation of a config key/value pair +typedef struct wbcffi_module wbcffi_module; + +typedef struct { + wbcffi_module* obj; + const char* waybar_version; + GtkContainer* (*get_root_widget)(wbcffi_module*); + void (*queue_update)(wbcffi_module*); +} wbcffi_init_info; + struct wbcffi_config_entry { const char* key; const char* value; }; } +} // namespace ffi class CFFI : public AModule { public: @@ -30,9 +40,8 @@ class CFFI : public AModule { /// void* cffi_instance_ = nullptr; - typedef void*(InitFn)(GtkContainer* root_widget, const void (*trigger_update)(void*), - void* trigger_update_arg, const struct wbcffi_config_entry* config_entries, - size_t config_entries_len); + typedef void*(InitFn)(const ffi::wbcffi_init_info* init_info, + const ffi::wbcffi_config_entry* config_entries, size_t config_entries_len); typedef void(DenitFn)(void* instance); typedef void(RefreshFn)(void* instance, int signal); typedef void(DoActionFn)(void* instance, const char* name); diff --git a/resources/custom_modules/cffi_example/main.c b/resources/custom_modules/cffi_example/main.c index c959c9a8..ba2c8cf4 100644 --- a/resources/custom_modules/cffi_example/main.c +++ b/resources/custom_modules/cffi_example/main.c @@ -2,10 +2,10 @@ #include "waybar_cffi_module.h" typedef struct { - GtkContainer* root; + wbcffi_module* waybar_module; GtkBox* container; GtkButton* button; -} Instance; +} ExampleMod; // This static variable is shared between all instances of this module static int instance_count = 0; @@ -19,8 +19,7 @@ void onclicked(GtkButton* button) { // You must const size_t wbcffi_version = 1; -void* wbcffi_init(GtkContainer* root_widget, void (*trigger_update)(void*), - void* trigger_update_arg, const struct wbcffi_config_entry* config_entries, +void* wbcffi_init(const wbcffi_init_info* init_info, const wbcffi_config_entry* config_entries, size_t config_entries_len) { printf("cffi_example: init config:\n"); for (size_t i = 0; i < config_entries_len; i++) { @@ -28,12 +27,14 @@ void* wbcffi_init(GtkContainer* root_widget, void (*trigger_update)(void*), } // Allocate the instance object - Instance* inst = malloc(sizeof(Instance)); - inst->root = root_widget; + ExampleMod* inst = malloc(sizeof(ExampleMod)); + inst->waybar_module = init_info->obj; + + GtkContainer* root = init_info->get_root_widget(init_info->obj); // Add a container for displaying the next widgets inst->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5)); - gtk_container_add(GTK_CONTAINER(inst->root), GTK_WIDGET(inst->container)); + gtk_container_add(GTK_CONTAINER(root), GTK_WIDGET(inst->container)); // Add a label GtkLabel* label = GTK_LABEL(gtk_label_new("[Example C FFI Module:")); diff --git a/resources/custom_modules/cffi_example/waybar_cffi_module.h b/resources/custom_modules/cffi_example/waybar_cffi_module.h index 2696b670..a7886bea 100644 --- a/resources/custom_modules/cffi_example/waybar_cffi_module.h +++ b/resources/custom_modules/cffi_example/waybar_cffi_module.h @@ -10,29 +10,46 @@ extern "C" { /// Waybar ABI version. 1 is the latest version extern const size_t wbcffi_version; +/// Private Waybar CFFI module +typedef struct wbcffi_module wbcffi_module; + +/// Waybar module information +typedef struct { + /// Waybar CFFI object pointer + wbcffi_module* obj; + + /// Waybar version string + const char* waybar_version; + + /// Returns the waybar widget allocated for this module + /// @param obj Waybar CFFI object pointer + GtkContainer* (*get_root_widget)(wbcffi_module* obj); + + /// Queues a request for calling wbcffi_update() on the next GTK main event + /// loop iteration + /// @param obj Waybar CFFI object pointer + void (*queue_update)(wbcffi_module*); +} wbcffi_init_info; + /// Config key-value pair -struct wbcffi_config_entry { +typedef struct { /// Entry key const char* key; /// Entry value as string. JSON object and arrays are serialized. const char* value; -}; +} wbcffi_config_entry; /// Module init/new function, called on module instantiation /// /// MANDATORY CFFI function /// -/// @param root_widget Root GTK widget instantiated by Waybar -/// @param trigger_update Call this function with trigger_update_arg as argument to trigger -/// wbcffi_update() on the next GTK main event loop iteration -/// @param trigger_update_arg Argument for trigger_update call +/// @param init_info Waybar module information /// @param config_entries Flat representation of the module JSON config. The data only available /// during wbcffi_init call. /// @param config_entries_len Number of entries in `config_entries` /// /// @return A untyped pointer to module data, NULL if the module failed to load. -void* wbcffi_init(GtkContainer* root_widget, const void (*trigger_update)(void*), - void* trigger_update_arg, const struct wbcffi_config_entry* config_entries, +void* wbcffi_init(const wbcffi_init_info* init_info, const wbcffi_config_entry* config_entries, size_t config_entries_len); /// Module deinit/delete function, called when Waybar is closed or when the module is removed diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp index e7a28edd..e560659b 100644 --- a/src/modules/cffi.cpp +++ b/src/modules/cffi.cpp @@ -66,17 +66,23 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co } // Prepare config_entries array - std::vector config_entries; + std::vector config_entries; for (size_t i = 0; i < keys.size(); i++) { - config_entries.push_back( - wbcffi_config_entry{keys[i].c_str(), config_entries_stringstor[i].c_str()}); + config_entries.push_back({keys[i].c_str(), config_entries_stringstor[i].c_str()}); } + ffi::wbcffi_init_info init_info = { + .obj = (ffi::wbcffi_module*)this, + .waybar_version = VERSION, + .get_root_widget = + [](ffi::wbcffi_module* obj) { + return dynamic_cast(&((CFFI*)obj)->event_box_)->gobj(); + }, + .queue_update = [](ffi::wbcffi_module* obj) { ((CFFI*)obj)->dp.emit(); }, + }; + // Call init - cffi_instance_ = hooks_.init( - dynamic_cast(&event_box_)->gobj(), - [](void* dp) { ((Glib::Dispatcher*)dp)->emit(); }, &dp, config_entries.data(), - config_entries.size()); + cffi_instance_ = hooks_.init(&init_info, config_entries.data(), config_entries.size()); // Handle init failures if (cffi_instance_ == nullptr) { From 75f9141cac9af2f33bac7311ba72793750cc141a Mon Sep 17 00:00:00 2001 From: "Crom (Thibaut CHARLES)" Date: Tue, 12 Dec 2023 16:16:21 +0100 Subject: [PATCH 5/5] Added cffi man page --- man/waybar-cffi.5.scd | 37 +++++++++++++++++++++++++++++++++++++ meson.build | 1 + 2 files changed, 38 insertions(+) create mode 100644 man/waybar-cffi.5.scd diff --git a/man/waybar-cffi.5.scd b/man/waybar-cffi.5.scd new file mode 100644 index 00000000..926511d8 --- /dev/null +++ b/man/waybar-cffi.5.scd @@ -0,0 +1,37 @@ +waybar-cffi(5) +# NAME + +waybar - cffi module + +# DESCRIPTION + +The *cffi* module gives full control of a GTK widget to a third-party dynamic library, to create more complex modules using different programming languages. + +# CONFIGURATION + +Addressed by *cffi/* + +*module_path*: ++ + typeof: string ++ + The path to the dynamic library to load to control the widget. + +Some additional configuration may be required depending on the cffi dynamic library being used. + + +# EXAMPLES + +## C example: + +An example module written in C can be found at https://github.com/Alexays/Waybar/resources/custom_modules/cffi_example/ + +Waybar config to enable the module: +``` +"cffi/c_example": { + "module_path": ".config/waybar/cffi/wb_cffi_example.so" +} +``` + + +# STYLE + +The classes and IDs are managed by the cffi dynamic library. diff --git a/meson.build b/meson.build index 7c0f9965..a7d9e5b4 100644 --- a/meson.build +++ b/meson.build @@ -442,6 +442,7 @@ if scdoc.found() 'waybar-backlight-slider.5.scd', 'waybar-battery.5.scd', 'waybar-cava.5.scd', + 'waybar-cffi.5.scd', 'waybar-clock.5.scd', 'waybar-cpu.5.scd', 'waybar-custom.5.scd',