Skip to content

Commit

Permalink
Merge pull request #2573 from CromFr/cabi_plugins
Browse files Browse the repository at this point in the history
C ABI plugin system
  • Loading branch information
Alexays authored Dec 18, 2023
2 parents 7b79281 + 75f9141 commit f5370fc
Show file tree
Hide file tree
Showing 12 changed files with 435 additions and 0 deletions.
1 change: 1 addition & 0 deletions include/AModule.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions include/factory.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
#include "modules/cava.hpp"
#endif
#include "bar.hpp"
#include "modules/cffi.hpp"
#include "modules/custom.hpp"
#include "modules/image.hpp"
#include "modules/temperature.hpp"
Expand Down
60 changes: 60 additions & 0 deletions include/modules/cffi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once

#include <string>

#include "AModule.hpp"
#include "util/command.hpp"
#include "util/json.hpp"
#include "util/sleeper_thread.hpp"

namespace waybar::modules {

namespace ffi {
extern "C" {
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:
CFFI(const std::string&, const std::string&, const Json::Value&);
virtual ~CFFI();

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)(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);
typedef void(UpdateFn)(void* instance);

// FFI hooks
struct {
std::function<InitFn> init = nullptr;
std::function<DenitFn> deinit = nullptr;
std::function<RefreshFn> refresh = [](void*, int) {};
std::function<DoActionFn> doAction = [](void*, const char*) {};
std::function<UpdateFn> update = [](void*) {};
} hooks_;
};

} // namespace waybar::modules
37 changes: 37 additions & 0 deletions man/waybar-cffi.5.scd
Original file line number Diff line number Diff line change
@@ -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/<name>*

*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.
3 changes: 3 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ if is_linux
add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp')
src_files += files(
'src/modules/battery.cpp',
'src/modules/cffi.cpp',
'src/modules/cpu.cpp',
'src/modules/cpu_frequency/common.cpp',
'src/modules/cpu_frequency/linux.cpp',
Expand All @@ -218,6 +219,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/cffi.cpp',
'src/modules/cpu.cpp',
'src/modules/cpu_frequency/bsd.cpp',
'src/modules/cpu_frequency/common.cpp',
Expand Down Expand Up @@ -468,6 +470,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',
Expand Down
1 change: 1 addition & 0 deletions resources/custom_modules/cffi_example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.cache/
38 changes: 38 additions & 0 deletions resources/custom_modules/cffi_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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.

Symbols to implement are documented in the
[waybar_cffi_module.h](waybar_cffi_module.h) file.

# 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"
}
}
```
70 changes: 70 additions & 0 deletions resources/custom_modules/cffi_example/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

#include "waybar_cffi_module.h"

typedef struct {
wbcffi_module* waybar_module;
GtkBox* container;
GtkButton* button;
} ExampleMod;

// 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(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++) {
printf(" %s = %s\n", config_entries[i].key, config_entries[i].value);
}

// Allocate the instance object
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(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_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);
}
13 changes: 13 additions & 0 deletions resources/custom_modules/cffi_example/meson.build
Original file line number Diff line number Diff line change
@@ -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: ''
)
89 changes: 89 additions & 0 deletions resources/custom_modules/cffi_example/waybar_cffi_module.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#pragma once

#include <gtk/gtk.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/// 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
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 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(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
///
/// MANDATORY CFFI function
///
/// @param instance Module instance data (as returned by `wbcffi_init`)
void wbcffi_deinit(void* instance);

/// 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
///
/// @param instance Module instance data (as returned by `wbcffi_init`)
/// @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 action_name Action name
void wbcffi_doaction(void* instance, const char* action_name);

#ifdef __cplusplus
}
#endif
3 changes: 3 additions & 0 deletions src/factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
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, "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());
throw std::runtime_error(err);
Expand Down
Loading

0 comments on commit f5370fc

Please sign in to comment.