20 KiB
qwlroots Wrapping Patterns Best Practices Guide
Overview
qwlroots is a Qt-style wrapper library for wlroots C interfaces, providing multiple wrapping patterns through a macro system to accommodate different types of wlroots structures. This document summarizes the main wrapping patterns used in the project and provides guidance for adding new class wrappers.
Core Design Philosophy
- Qt-style Interface: Follows Qt specification interface style
- Signal-Slot Mechanism: Wraps wl_signal as Qt signals
- Memory Management: Automatic object lifecycle and ownership handling
- Type Safety: Uses C++ templates and concepts to ensure type safety
- Zero-cost Abstraction: Provides zero-overhead wrapping in Release mode
Wrapping Pattern Classification
1. QW_CLASS_OBJECT Pattern - Complete QObject-based Wrapping
Use Cases:
- wlroots structures contain signals (events field)
- Need complete object lifecycle management
- Require Qt signal-slot mechanism
Features:
- Inherits from
qw_object<Handle, Derive>, indirectly inherits QObject - Automatically connects wlroots destroy signals
- Supports signal binding and forwarding
- Provides complete memory management and ownership control
Usage Example:
// Header file declaration
class QW_CLASS_OBJECT(compositor)
{
QW_OBJECT // Required macro, provides basic functionality
Q_OBJECT // Qt meta-object system
// Signal binding - converts wlroots signals to Qt signals
QW_SIGNAL(new_surface, wlr_surface*)
public:
// Static function binding - create function automatically returns qw_compositor*
QW_FUNC_STATIC(compositor, create, qw_compositor *, wl_display *display, uint32_t version, wlr_renderer *renderer)
};
class QW_CLASS_OBJECT(surface)
{
QW_OBJECT
Q_OBJECT
// Multiple signal bindings
QW_SIGNAL(client_commit)
QW_SIGNAL(commit)
QW_SIGNAL(new_subsurface, wlr_subsurface*)
QW_SIGNAL(map)
QW_SIGNAL(unmap)
public:
// Static functions - from/get functions
QW_FUNC_STATIC(surface, from_resource, qw_surface *, wl_resource *resource)
// Member function binding
QW_FUNC_MEMBER(surface, for_each_surface, void, wlr_surface_iterator_func_t iterator, void *user_data)
QW_FUNC_MEMBER(surface, has_buffer, bool)
QW_FUNC_MEMBER(surface, send_frame_done, void, const timespec *when)
QW_FUNC_MEMBER(surface, map, void)
QW_FUNC_MEMBER(surface, unmap, void)
};
Key Macro Descriptions:
QW_OBJECT: Provides basic object functionality, must be in protected sectionQW_SIGNAL(name, ...): Binds wlroots signals to Qt signalsQW_FUNC_STATIC: Binds static functions, create/from/get functions automatically handle return typesQW_FUNC_MEMBER: Binds member functions
2. QW_CLASS_REINTERPRET_CAST Pattern - Lightweight Type Conversion
Use Cases:
- wlroots structures don't contain signals
- Only need function wrapping, no object lifecycle management
- Pursue minimal memory overhead
Features:
- Zero-overhead wrapping based on
reinterpret_cast - No additional memory allocation
- No signal mechanism support
- Mainly used for utility classes and simple structures
Usage Example:
class QW_CLASS_REINTERPRET_CAST(client_buffer)
{
public:
QW_FUNC_STATIC(client_buffer, get, qw_client_buffer *, wlr_buffer *buffer)
};
3. QW_CLASS_BOX Pattern - Value Type Wrapping
Use Cases:
- Simple data structures
- Need interoperability with Qt types
- Value semantic objects
Features:
- Contains actual data members
- Supports conversion with Qt types
- Value semantics, supports copy and assignment
Usage Example:
class qw_box
{
public:
explicit qw_box() = default;
explicit qw_box(const wlr_box &box);
explicit qw_box(const QRect &box);
QRect toQRect();
operator wlr_box*();
private:
wlr_box m_handle;
};
4. QW_CLASS_INTERFACE Pattern - Interface Implementation Wrapping
Use Cases:
- Need to implement wlroots interfaces
- Provide callback function implementations
- Custom behavior extensions
Features:
- Template-based interface binding
- Automatic function pointer binding
- Supports virtual function overriding
Usage Example:
class QW_CLASS_INTERFACE(output)
{
QW_INTERFACE_INIT(output)
// Interface function implementations
QW_INTERFACE(commit, bool, wlr_output_state *state)
QW_INTERFACE(set_cursor, bool, wlr_buffer *buffer, int hotspot_x, int hotspot_y)
protected:
// Implement specific interface functions
bool commit(wlr_output_state *state);
bool set_cursor(wlr_buffer *buffer, int hotspot_x, int hotspot_y);
};
Directory Structure and File Naming Patterns
qwlroots' directory structure and file naming completely follow wlroots' organization, ensuring project structure consistency and maintainability.
Directory Structure Correspondence
qwlroots' src/ directory structure completely corresponds to wlroots' include/wlr/ directory structure:
wlroots/include/wlr/ qwlroots/src/
├── types/ ←→ ├── types/
├── util/ ←→ ├── util/
├── render/ ←→ ├── render/
├── interfaces/ ←→ ├── interfaces/
├── backend/ ←→ ├── (backend-related files directly in src/)
└── xwayland/ ←→ └── (xwayland-related files in types/)
File Naming Patterns
-
Header File Naming:
- wlroots:
wlr_compositor.h→ qwlroots:qwcompositor.h - wlroots:
wlr_output.h→ qwlroots:qwoutput.h - wlroots:
wlr_buffer.h→ qwlroots:qwbuffer.h
- wlroots:
-
Class Name Naming:
- wlroots:
struct wlr_compositor→ qwlroots:class qw_compositor - wlroots:
struct wlr_surface→ qwlroots:class qw_surface
- wlroots:
-
Directory Classification Patterns:
types/: Main Wayland protocol types and core objectsutil/: Utility classes and helper functionsrender/: Rendering-related typesinterfaces/: Interface implementation-related types
Method for Finding Correspondences
When creating wrappers for new wlroots types:
- Locate Source File: Find the corresponding header file in
wlroots/include/wlr/ - Determine Directory: Determine the location in qwlroots based on wlroots' directory structure
- Name Conversion: Follow the
wlr_xxx.h→qwxxx.hpattern for naming
Example:
# File in wlroots
wlroots/include/wlr/types/wlr_xdg_shell.h
# Corresponding qwlroots file should be
qwlroots/src/types/qwxdgshell.h
Complete Workflow for Adding New Wrappers
When users request "add xxx class" or "add xxxx function", follow this workflow:
Step 0: Update wlroots Submodule
Before starting any wrapping work, ensure the wlroots submodule is up to date:
# Enter wlroots submodule directory
cd wlroots
# Fetch latest tags and commits
git fetch --tags
# View latest version tags
git tag --sort=-version:refname | head -5
# Checkout to latest stable version (e.g., 0.19.0)
git checkout 0.19.0
# Return to project root
cd ..
# Commit submodule update
git add wlroots
git commit -m "submodule: update wlroots to latest version"
Step 1: Locate Target Type in wlroots Submodule
Use the following methods to locate the type to be wrapped in the wlroots submodule:
# Find specific header files
find wlroots/include/wlr -name "*target_name*" -type f
# Search for struct definitions
grep -r "struct wlr_target_name" wlroots/include/wlr/
# View specific file content
cat wlroots/include/wlr/types/wlr_target_name.h
Step 2: Determine Version Information
Find out which version introduced this type:
# View file commit history in wlroots directory
cd wlroots
git log --oneline --follow include/wlr/types/wlr_target_name.h
# View detailed information of specific commit
git show commit_hash
# Find earliest tag containing this commit
git tag --contains commit_hash --sort=version:refname | head -1
This information will be used to add version compatibility macros in qwlroots.
Step 3: Analyze wlroots Structure
-
Check for events field:
- Has events field → Use
QW_CLASS_OBJECT - No events field → Use
QW_CLASS_REINTERPRET_CAST
- Has events field → Use
-
Check if interface implementation is needed:
- Need callback implementation → Use
QW_CLASS_INTERFACE
- Need callback implementation → Use
-
Check if it's a simple data structure:
- Simple data structure → Consider custom wrapping or
QW_CLASS_BOX
- Simple data structure → Consider custom wrapping or
Step 2: Determine File Location and Naming
Determine the corresponding qwlroots file location based on the wlroots source file location:
# 1. Find the source file in wlroots
find /path/to/wlroots/include/wlr -name "wlr_example.h"
# Example: wlroots/include/wlr/types/wlr_example.h
# 2. Determine corresponding location in qwlroots
# src/types/qwexample.h
Step 3: Create Header File
// File: src/types/qwexample.h
#pragma once
#include <qwobject.h>
extern "C" {
#include <wlr/types/wlr_example.h>
}
QW_BEGIN_NAMESPACE
class QW_CLASS_OBJECT(example)
{
QW_OBJECT
Q_OBJECT
// Bind all related signals
QW_SIGNAL(signal_name, param_type)
public:
// Bind static functions
QW_FUNC_STATIC(example, create, qw_example *, param_type param)
QW_FUNC_STATIC(example, from_resource, qw_example *, wl_resource *resource)
// Bind member functions
QW_FUNC_MEMBER(example, method_name, return_type, param_type param)
// If there's a destroy method, destructor should be protected
protected:
QW_FUNC_MEMBER(example, destroy, void)
};
QW_END_NAMESPACE
Step 4: Handle Version Compatibility
// Handle API differences between different wlroots versions
#if WLR_VERSION_MINOR < 18
QW_SIGNAL(old_signal, param_type)
#else
QW_SIGNAL(new_signal, param_type)
#endif
#if WLR_VERSION_MINOR < 19
QW_FUNC_MEMBER(example, old_method, void, param_type)
#else
QW_FUNC_MEMBER(example, new_method, void, param_type)
#endif
Step 5: Add to Build System
Add the new header file to src/CMakeLists.txt:
set(HEADERS
# ... other header files
types/qwexample.h
)
Best Practices and Considerations
1. Naming Conventions
- Class names:
qw_+ wlroots type name (removewlr_prefix) - Signal names:
notify_+ original signal name - Function names: Keep consistent with wlroots function names (remove type prefix)
2. Signal Handling
// Correct signal binding
QW_SIGNAL(destroy) // Automatically handled, no manual binding needed
QW_SIGNAL(custom_event, param_type) // Custom signals
// Signal connection example
connect(surface, &qw_surface::notify_commit, this, [](){
// Handle commit event
});
3. Memory Management
// Create object (owns ownership)
auto *compositor = qw_compositor::create(display, version, renderer);
// Get object from existing handle (doesn't own ownership)
auto *surface = qw_surface::from(wlr_surface);
// Check if object exists
auto *existing = qw_surface::get(wlr_surface); // May return nullptr
4. Function Binding Principles
4.1 Determining Function Ownership and Placement
When wrapping wlroots functions, it's crucial to determine whether a function should be a static function or a member function, and which class it belongs to. Follow this systematic approach:
Function Name Analysis Pattern:
wlroots functions follow the pattern: wlr_<type>_<function_name>
Step 1: Parse the Function Name
- Split the function name at underscores after
wlr_ - Identify the type part and function name part
- The type part should correspond to an existing wlroots type
Example Analysis:
// Function: wlr_surface_get_image_description_v1_data
// Parsing: wlr_ + surface + _ + get_image_description_v1_data
// Type: wlr_surface
// Function name: get_image_description_v1_data
// Why not wlr_surface_get?
// Because wlr_surface_get is not a valid wlroots type
// get_image_description_v1_data is a more reasonable function name
Step 2: Verify Type Existence
- Check if
wlr_<type>exists as a structure in wlroots headers - If the type exists, this confirms the parsing is correct
Step 3: Determine Static vs Member Function
-
Member Function: First parameter type matches the parsed type
// wlr_surface_get_image_description_v1_data(struct wlr_surface *surface) // First parameter is wlr_surface* → Member function of qw_surface QW_FUNC_MEMBER(surface, get_image_description_v1_data, const wlr_image_description_v1_data *) -
Static Function: First parameter type doesn't match the parsed type
// wlr_compositor_create(wl_display *display, ...) // First parameter is wl_display*, not wlr_compositor* → Static function QW_FUNC_STATIC(compositor, create, qw_compositor *, wl_display *display, ...)
Step 4: Handle Cross-File Functions Functions may be defined in different header files than their target type. This is common in wlroots:
// wlr_surface_get_image_description_v1_data is defined in wlr_color_management_v1.h
// but operates on wlr_surface, so it belongs to qw_surface class
// In qwcompositor.h:
#if WLR_VERSION_MINOR >= 19
#include <wlr/types/wlr_color_management_v1.h>
#endif
class QW_CLASS_OBJECT(surface) {
// ...
#if WLR_VERSION_MINOR >= 19
QW_FUNC_MEMBER(surface, get_image_description_v1_data, const wlr_image_description_v1_data *)
#endif
};
Common Patterns:
// Create functions - always static, return the type
QW_FUNC_STATIC(compositor, create, qw_compositor *, ...)
// From/Get functions - usually static, return the type
QW_FUNC_STATIC(surface, from_resource, qw_surface *, wl_resource *resource)
// Instance methods - member functions, first param matches type
QW_FUNC_MEMBER(surface, has_buffer, bool)
QW_FUNC_MEMBER(surface, send_frame_done, void, const timespec *when)
// Utility functions - check first parameter to determine placement
// If first param is wlr_surface* → member of qw_surface
// If first param is something else → static function or belongs to other class
4.2 Function Binding Syntax
// Static function binding
QW_FUNC_STATIC(type, create, qw_type *, ...) // Create function, same type returns qw_*
QW_FUNC_STATIC(type, from, qw_type *, ...) // Conversion function, same type returns qw_*
QW_FUNC_STATIC(type, get, qw_type *, ...) // Get function, same type returns qw_*
QW_FUNC_STATIC(type, get_other, wlr_other *, ...) // Get other types, keep wlr_*
QW_FUNC_STATIC(type, utility, return_type, ...) // Utility function, by actual return type
// Member function binding
QW_FUNC_MEMBER(type, method, return_type, ...) // Instance method, return type by actual situation
5. Error Handling
// Check object validity
if (!surface->is_valid()) {
return;
}
// Safe handle access
if (auto *handle = surface->handle()) {
// Use handle
}
6. Version Compatibility Handling
- Use conditional compilation to handle API changes
- Prioritize supporting the latest version API
- Provide compatibility macros for older versions
7. Testing
Create tests for new wrapper classes:
// tests/test_qwexample.cpp
#include "qwexample.h"
#include <QtTest>
class TestQwExample : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testCreate();
void testSignals();
void testMethods();
};
Common Issues and Solutions
1. Compilation Error: wlroots Function Not Found
Problem: Compilation fails when using QW_FUNC_STATIC or QW_FUNC_MEMBER.
Solution:
- Check wlroots version compatibility
- Confirm function name spelling is correct
- Use conditional compilation to handle version differences
2. Signal Connection Failure
Problem: Qt signals don't trigger properly.
Solution:
- Ensure
QW_OBJECTandQ_OBJECTmacros are used - Check signal parameter types are correct
- Verify wlroots structure actually has corresponding signals
3. Memory Leaks or Crashes
Problem: Issues occur during object destruction.
Solution:
- Handle object ownership correctly
- For classes with destroy methods, make destructor protected
- Use smart pointers to manage lifecycle
Common Wrapping Errors Based on PR #322
Based on actual code review experience, here are common mistakes developers make when adding new wrappers:
4. Incorrect Interface Type Wrapping
Problem: Incorrectly attempting to wrap interface-related init functions.
Solution:
- init interfaces are for interface use, not wrapped in regular classes
- Should wrap corresponding interface types, such as
qw_allocator_interface - Interface-related wrapping usually needs separate handling
5. Static Function Return Type Errors
Problem: Static function return type uses wlr_* type instead of qw_* type when the return type matches the current wrapped class type.
❌ Incorrect Example:
// In qw_color_transform class
QW_FUNC_STATIC(color_transform, create, wlr_color_transform *, ...) // Error: should return qw_color_transform*
// In qw_abc class
QW_FUNC_STATIC(abc, create, wlr_abc *, ...) // Error: should return qw_abc*
QW_FUNC_STATIC(abc, get_xxx_data, wlr_xxx *, ...) // Correct: other types remain unchanged
✅ Correct Format:
// In qw_color_transform class
QW_FUNC_STATIC(color_transform, create, qw_color_transform *, ...) // Correct: same type returns qw_*
// In qw_abc class
QW_FUNC_STATIC(abc, create, qw_abc *, ...) // Correct: same type returns qw_*
QW_FUNC_STATIC(abc, get_xxx_data, wlr_xxx *, ...) // Correct: other types keep wlr_*
Solution:
- Only when the static function's return type matches the currently wrapped class type, change
wlr_*toqw_* - If returning other types (like
wlr_xxx *), keep the originalwlr_*type - This ensures wrapping consistency: same type returns wrapped type, other types keep original type
6. Redundant Destroy Signal Declaration
Problem: Manually declaring destroy signal in QW_CLASS_OBJECT class.
❌ Incorrect Example:
class QW_CLASS_OBJECT(example)
{
QW_OBJECT
Q_OBJECT
QW_SIGNAL(destroy) // Redundant: base class handles automatically
};
✅ Correct Format:
class QW_CLASS_OBJECT(example)
{
QW_OBJECT
Q_OBJECT
// destroy signal is handled automatically by base class, no explicit declaration needed
// Only declare specific business signals
QW_SIGNAL(custom_event, param_type)
};
Solution:
- destroy signal is specially handled in
QW_CLASS_OBJECT, no manual declaration needed - Only declare specific business signals
7. Type Mixing Wrapping Errors
Problem: Mixing functions of different types in one class.
❌ Incorrect Example:
class QW_CLASS_REINTERPRET_CAST(linux_drm_syncobj_manager_v1)
{
public:
QW_FUNC_STATIC(linux_drm_syncobj_manager_v1, create, ...)
// Error: wrapping syncobj_v1 type functions in manager class
QW_FUNC_STATIC(linux_drm_syncobj_v1, get_surface_state, ...)
};
✅ Correct Design:
// Separate different types
class QW_CLASS_REINTERPRET_CAST(linux_drm_syncobj_manager_v1)
{
public:
QW_FUNC_STATIC(linux_drm_syncobj_manager_v1, create, ...)
};
class QW_CLASS_REINTERPRET_CAST(linux_drm_syncobj_v1)
{
public:
QW_FUNC_STATIC(linux_drm_syncobj_v1, get_surface_state, ...)
};
Solution:
- Should not wrap
linux_drm_syncobj_v1type functions inqw_linux_drm_syncobj_manager_v1 - Each class should only wrap functions belonging to its own type
- Functions of different types should be separated into corresponding classes
Summary
qwlroots' wrapping pattern design is sophisticated, providing flexible and efficient C++ wrapping through template and macro systems. When adding new class wrappers, you should:
- Carefully analyze wlroots structure characteristics
- Choose appropriate wrapping patterns
- Follow project naming and coding conventions
- Handle version compatibility issues
- Write corresponding test code
By following these best practices, you can ensure that newly added wrapper classes remain consistent with the overall project architecture and provide a good development experience.