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