Property System
Property System
Overview
The Waxed Property System enables runtime configuration of plugins through a centralized registry. Plugins expose configurable properties that can be modified at runtime via waxedctl or IPC commands, without requiring a restart.
The system follows a callback-based pattern:
- Plugins register properties during initialization with a setter callback
- External tools (waxedctl) request property changes via IPC
- The core dispatches the change to the plugin’s callback
- The plugin applies the new value immediately
Architecture
Property Key Format
Properties are identified by a hierarchical key with three components:
- plugin: The plugin name (e.g., “desktop”, “loginscreen”)
- namespace: A grouping category (e.g., “yaml”, “transition”)
- key: The specific property name (e.g., “background”, “mode”)
Examples:
desktop:yaml:background- Background image path for desktop plugindesktop:yaml:mode- Render mode for desktop plugintransitioner:initial:plugin- Initial plugin for transitioner
Data Types
Properties support the following types (defined in PropertyType enum):
| Type | Description | Example Values |
|---|---|---|
String | Text string | "/path/to/image.jpg" |
Integer | Whole number | 42 |
Boolean | True/false | "true", "false" |
Float | Floating-point | 3.14 |
All property values are passed as strings. The plugin’s setter callback is responsible for parsing the string value into the appropriate type.
Core Components
PropertySetterFn Callback Type
The callback signature for property setters (defined in core_api.h):
extern "C" {
using PropertySetterFn = auto (*)(const char* value, void* user_data) -> void;
}
Parameters:
value: The new property value as a C stringuser_data: Opaque pointer provided during registration
Return: void (no error reporting via return value)
The callback is invoked synchronously by the core when a property change request is received. The callback should:
- Validate the value
- Parse the string into the appropriate type
- Update the plugin’s internal state
- Apply any side effects (e.g., reload resources, re-render)
PropertyCallback Storage
Stored in PluginManager (plugin_manager.h):
struct PropertyCallback {
std::string plugin_name; // Plugin name
std::string property_namespace;// Property namespace
std::string property_key; // Property key
PropertySetterFn setter; // Callback function pointer
void* user_data; // Opaque user data pointer
};
Callbacks are stored in a map indexed by the full key:
std::unordered_map<std::string, PropertyCallback> property_callbacks_;
register_property() API
Core API function (from core_api.h):
auto (*register_property)(CoreAPI* core,
const char* namespace_,
const char* key,
const char* description,
PropertySetterFn setter,
void* user_data) -> int;
Parameters:
core: Core API handlenamespace_: Property namespace (e.g., “yaml”)key: Property key (e.g., “background”)description: Human-readable descriptionsetter: Callback function when property is setuser_data: User data passed to callback
Return: 0 on success, non-zero on error
Implementation (core_api.cpp):
auto core_register_property(CoreAPI* core, const char* namespace_,
const char* key, const char* description,
PropertySetterFn setter, void* user_data) -> int {
if (!core || !core->internal_handle || !namespace_ || !key || !setter) {
return -1;
}
auto* manager = static_cast<PluginManager*>(core->internal_handle);
// Build the full property key: plugin_name:namespace:key
std::string full_key = std::string(core->own_name) + ":" + namespace_ + ":" + key;
LOGC_INFO("Registering property: {}", full_key);
LOGC_INFO(" Description: {}", description);
// Store the property registration in PluginManager
manager->register_property_callback(core->own_name, namespace_, key, setter, user_data);
LOGC_INFO("Property callback registered");
return 0;
}
invoke_property_callback() Dispatch
PluginManager method (plugin_manager.cpp):
auto PluginManager::invoke_property_callback(
const std::string& plugin_name,
const std::string& property_namespace,
const std::string& property_key,
const std::string& value
) -> Result<void> {
std::shared_lock lock(mutex_);
// Build full key: plugin_name:namespace:key
std::string full_key = plugin_name + ":" + property_namespace + ":" + property_key;
auto it = property_callbacks_.find(full_key);
if (it == property_callbacks_.end()) {
LOGC_ERROR("Property callback not found: {}", full_key);
return std::unexpected(ErrorCode::PropertyNotFound);
}
const auto& callback = it->second;
LOGC_INFO("Invoking property callback: {} = {}", full_key, value);
// Unlock before calling the callback to prevent deadlock
lock.unlock();
// Invoke the callback
callback.setter(value.c_str(), callback.user_data);
return {};
}
Thread Safety Note: The mutex is unlocked before invoking the callback to prevent deadlock if the callback calls back into PluginManager.
How Plugins Register Properties
Registration Pattern
Plugins typically register properties during their waxed_plugin_init() function:
// Example: Background property setter
auto background_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
// Validate
if (!value) return;
// Parse value
std::string path(value);
// Apply change (e.g., load new background)
state->load_background(path);
LOGC_INFO("Background changed to: {}", path);
}
// In init function:
auto waxed_plugin_init(const PluginInitParams* params) -> PluginState* {
auto* state = new PluginState();
state->core_api = params->core_api;
// Register property
if (state->core_api && state->core_api->funcs) {
int result = state->core_api->funcs->register_property(
state->core_api,
"yaml", // namespace
"background", // key
"Background image path", // description
background_property_setter,// callback
state // user_data
);
if (result == 0) {
LOGC_INFO("Registered property 'yaml:background'");
}
}
return state;
}
Real-World Example: Desktop Plugin
From plugins/desktop/plugin.cpp:
// Property setter for background image
auto background_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<VulkanState*>(user_data);
if (state->shutdown_requested_.load(std::memory_order_acquire)) return;
// Use Core API to enqueue async background load with callback
const char* mode_str = get_render_mode_name(state->render_mode);
uint32_t display_width = state->display_config.config.width;
uint32_t display_height = state->display_config.config.height;
int result = state->core_api->funcs->set_background_dma_buf(
state->core_api,
value, // image path
mode_str, // render mode
display_width,
display_height,
background_dma_buf_callback,
state
);
if (result != 0) {
LOGC_ERROR("Failed to enqueue background load: {}", value);
}
}
// Property setter for render mode
auto mode_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<VulkanState*>(user_data);
LOGC_INFO("Property change received: yaml:mode = {}", value);
// Parse the new mode
std::string mode_str(value);
VulkanState::RenderMode new_mode = parse_render_mode(mode_str);
if (new_mode == state->render_mode) {
LOGC_INFO("Mode unchanged: {}", value);
return;
}
state->render_mode = new_mode;
LOGC_INFO("Render mode changed to: {}", value);
// Trigger background reload with new mode
// ...
}
// Registration in init()
int prop_result = state->core_api->funcs->register_property(
state->core_api,
"yaml", // namespace
"background", // key
"Background image path for desktop plugin",
background_property_setter,
state
);
if (prop_result == 0) {
LOGC_INFO("Registered property 'yaml:background'");
}
How waxedctl Sets Properties
IPC Command Flow
The property system integrates with the IPC server to handle property change requests:
IPC Command Handler
From src/core/ipc/server.cpp:
// Property set command handler
auto result = plugin_manager_.invoke_property_callback(
cmd.plugin_name,
cmd.property_namespace,
cmd.property_key,
cmd.property_value
);
if (result) {
response.status = ResponseStatus::Success;
response.message = "Property set: " + cmd.plugin_name + ":" +
cmd.property_namespace + ":" + cmd.property_key;
} else {
response.status = ResponseStatus::Error;
response.message = "Failed to set property: " +
to_string(result.error());
}
waxedctl Usage
# Set a property via waxedctl
waxedctl -p desktop:yaml:background "/path/to/image.jpg"
# Set render mode
waxedctl -p desktop:yaml:mode "cover"
# Set transitioner overlay
waxedctl -p transitioner:yaml:overlay "true"
Property Commands
The IPC server supports the following property commands:
property set <plugin> <namespace> <key> <value>
Set a plugin property value
property get <plugin> <namespace> <key>
Get a plugin property value
property list [plugin] [namespace]
List properties (all plugins or specific plugin/namespace)
Example Property: yaml:background
Full Flow Diagram
Thread Safety
The property system is thread-safe:
- Registration: Protected by
PluginManager::mutex_(shared mutex) - Invocation: Uses shared lock during lookup, unlocks before callback
- Callback execution: Occurs outside the lock to prevent deadlock
Important: Property callbacks may be invoked from any thread (typically the IPC worker thread). Plugins must ensure their internal state updates are thread-safe.
Use Cases and Patterns
1. Configuration Reload
Enable runtime configuration changes without restart:
auto config_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
state->config_path = value;
state->reload_config();
}
2. Resource Swapping
Replace resources at runtime (images, shaders, etc.):
auto background_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
state->enqueue_background_load(value); // Async load
}
3. Debug/Feature Toggles
Enable/disable debug features or modes:
auto debug_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
state->debug_enabled = (strcmp(value, "true") == 0);
}
4. Trigger Actions
Invoke actions via property changes:
auto transition_trigger_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<TransitionerState*>(user_data);
state->target_plugin = value;
state->start_transition();
}
Best Practices
1. Naming Conventions
- Use lowercase for namespaces and keys
- Use descriptive, hierarchical namespaces:
yaml,transition,config - Use clear key names:
background,mode,overlay
2. Error Handling
Property setters cannot return errors. Handle failures gracefully:
auto property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
if (!value) {
LOGC_ERROR("Received null value");
return; // Silent failure
}
if (!validate(value)) {
LOGC_ERROR("Invalid value: {}", value);
return; // Log and ignore
}
// Apply valid value
state->apply(value);
}
3. Async Operations
For expensive operations (loading images, etc.), use async patterns:
auto property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
// Enqueue async load (non-blocking)
state->enqueue_load(value, [](bool success) {
if (success) {
LOGC_INFO("Resource loaded successfully");
} else {
LOGC_ERROR("Resource load failed");
}
});
}
4. Thread Safety
Assume callbacks can be invoked from any thread:
auto property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
// Use mutex or atomics for state updates
std::lock_guard lock(state->mutex);
state->value = value;
state->dirty = true;
}
5. Idempotency
Make setters idempotent when possible:
auto mode_property_setter(const char* value, void* user_data) -> void {
auto* state = reinterpret_cast<PluginState*>(user_data);
RenderMode new_mode = parse_mode(value);
// Check if actually changed
if (new_mode == state->mode) {
LOGC_INFO("Mode unchanged: {}", value);
return; // Skip redundant work
}
state->mode = new_mode;
state->apply_mode();
}
Property Registry (Alternative)
The codebase also contains PropertyRegistry and PropertyDefinition classes (include/waxed/ipc/property_registry.h) which provide a more structured property system with:
- Property type metadata (String, Integer, Boolean, Float)
- Default values
- Runtime-settable flags
- Property descriptions
This registry is designed for future enhancement of the property system but is not currently used by the plugin property callback mechanism.
Summary
The Property System provides:
- Runtime Configuration: Change plugin behavior without restart
- Decoupled Architecture: Plugins register callbacks, core dispatches changes
- Thread Safety: Safe concurrent access to property registry
- Simple API: Single
register_property()function for plugins - IPC Integration: Works seamlessly with waxedctl and IPC commands