| name | treeland-private-wayland-protocol |
| description | Use this skill for treeland private Wayland server-side protocol integration. Trigger whenever the task involves treeland-owned xml, `treeland_*` interfaces, `src/modules/*`, QtWayland scanner output, `QtWaylandServer::*`, `local_qtwayland_server_protocol_treeland(...)`, `globalRemove()`, `destroy_resource(...)`, or wiring a private compositor protocol into `Helper::init`. Do not use it for upstream wlroots/waylib protocol wrappers. |
Treeland Private Protocol Integration
Scope
Use this skill only for treeland private protocols, not for upstream wlroots/waylib protocols.
Use this skill when any of these are true:
- the protocol xml is maintained by treeland
- the interface name is typically
treeland_*
- the code lives under
src/modules/*
- CMake uses
local_qtwayland_server_protocol_treeland(...)
- the generated type is
QtWaylandServer::*
If the protocol is a standard/ext/unstable/wlr upstream protocol, stop and use the upstream protocol skill instead.
Goal
Complete these four things correctly:
- generate the server protocol code
- implement the manager and resource classes
- integrate it through
WServerInterface
- wire it into the startup path from
Helper::init
Read First
- The target module
CMakeLists.txt
src/modules/tools/CMakeLists.txt
- Generated files in the build directory
src/seat/helper.cpp
Look at generated files first:
qwayland-server-<basename>.h/.cpp
wayland-<basename>-server-protocol.h/.c
Do not guess generated class names, Resource callback signatures, init(...) overloads, or interfaceName().
Note: the treeland_*.xml protocol files are provided by an external CMake package TreelandProtocols (via find_package(TreelandProtocols REQUIRED)), not from the treeland source tree. They are accessed through ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-*.xml. Do not search for XML files under src/modules/.
From generated qwayland-server-*.h/.cpp, confirm at least these facts directly:
- which helper APIs are generated, such as
resourceMap(), resource(), isGlobalRemoved(), and interfaceVersion()
- what the default behavior of
destroy(...), destroy_resource(...), and bind_resource(...) is
- that
handle_destroy(...) only forwards the destroy request to your override of destroy(Resource *resource); actual destruction still usually requires wl_resource_destroy(resource->handle)
- that
destroy_func removes the resource from m_resource_map before calling your destroy_resource(...)
- that after
destroy_resource(...) returns, generated code deletes the generated Resource wrapper object; this does not mean your own QObject/private object is deleted automatically
- that
globalRemove() does not call wl_global_destroy() immediately; it first calls wl_global_remove(...) and then performs deferred destruction
These details affect implementation boundaries:
destroy_resource(...) is responsible for cleaning up business objects and business state, not for deleting the generated Resource wrapper
- after
globalRemove(), the more accurate meaning is "global removal scheduled", not "destroyed synchronously"
- if the manager interface in xml has no destructor request, generated code will not provide
handle_destroy(...) or virtual void destroy(Resource *resource) for that manager; do not assume you can override manager destroy in that case
Build Path
Prefer the existing helper:
local_qtwayland_server_protocol_treeland(...)
Typical outputs:
wayland-<basename>-server-protocol.c
qwayland-server-<basename>.h/.cpp
At minimum, verify:
- the target source list includes
wayland-<basename>-server-protocol.c
- include paths contain
${CMAKE_CURRENT_BINARY_DIR}
BASENAME, generated filenames, and C++ type names are consistent
Recommended Code Structure
- public class:
QObject + WServerInterface
- private manager: inherits
QtWaylandServer::treeland_xxx_manager_v1
- private resource: inherits
QtWaylandServer::treeland_xxx_v1
Keep QtWaylandServer::* inheritance in .cpp private implementation classes so those types do not leak into public headers.
Layering Model
The dominant design in current treeland private protocols separates business API from protocol implementation details.
Public class
Public headers should expose business-facing APIs, not QtWaylandServer::* types.
The public class usually:
- inherits
QObject and WServerInterface
- exposes methods, signals, and small query APIs to business code
- implements
create/destroy/global/interfaceName
- owns the private manager via
std::unique_ptr
This public class is what Helper::init and other treeland business code attach, connect, and call.
Private manager class
Defined in .cpp, directly inherits QtWaylandServer::treeland_xxx_manager_v1.
It usually owns:
- protocol global lifetime
- manager request dispatch
- initial state sync in
bind_resource(...)
- creation of child resources or context objects
Private resource / context class
Defined in .cpp, inherits QtWaylandServer::treeland_xxx_v1.
It usually exists for one concrete protocol object and is responsible for:
- handling requests for that resource
- cleanup in
destroy_resource(...)
- translating protocol events into public class signals, callbacks, or business actions
When a public child wrapper is needed
Not every child resource needs its own public header.
If the child object is only an internal detail of the manager with no business API needed by other treeland code, keep it as a private .cpp resource class.
If the child object must be referenced by treeland business code for a longer lifetime, needs a parent, emits signals, provides extra methods, or is owned by other classes, expose it as a public QObject wrapper (like the handle/context classes in foreign-toplevel), with a private protocol implementation class under it.
Design Preference
Use this rule:
- create a public class only when business code actually needs a business-facing API
- keep pure protocol details inside
.cpp private classes
Do not create a public header for every protocol child object just because the protocol contains one. Promote it to a public wrapper only when business code actually needs to know about it.
Manager Interface Wiring
The public class usually implements:
create(WServer *server)
destroy(WServer *server)
global() const
interfaceName() const
Recommended pattern:
void Manager::create(WServer *server)
{
d->init(*server->handle(), InterfaceVersion);
}
void Manager::destroy(WServer *server)
{
Q_UNUSED(server);
d->globalRemove();
}
wl_global *globalHandle() const { return m_global; }
wl_global *Manager::global() const
{
return d->globalHandle();
}
QByteArrayView Manager::interfaceName() const
{
return QtWaylandServer::treeland_foo_manager_v1::interfaceName();
}
Rules:
- define
static constexpr int InterfaceVersion = N; in the public manager class and use it in create()
- call
globalRemove() directly in destroy()
- do not wrap it with redundant
if (m_global) checks
- use class-scoped constants for child interface/resource versions as well when they are not tied to
resource->version()
Common Manager Variants
Do not think of the manager as only "the thing that creates the global".
In current code, the manager also often:
- sends initial state in
bind_resource(Resource *resource)
- manually creates child resources with
wl_resource_create(...) inside request handlers
- uses
resource->version() for child resource version instead of inventing a new hard-coded version
New private protocols should default to having a manager destroy request and implement:
void destroy(Resource *resource) override
{
wl_resource_destroy(resource->handle);
}
A few existing managers missing a destroy request are historical design issues. Treat them as compatibility exceptions, not as templates for new protocols.
Resource Destruction
This is the most error-prone part.
Explicit client destroy
In destroy(Resource *resource), do only:
wl_resource_destroy(resource->handle);
Do not put real business cleanup here.
Final cleanup
Prefer destroy_resource(Resource *resource) for the actual cleanup path. This is where you:
- update liveness state
- emit disconnect/close-related signals
- clean up callbacks and connections
delete this or deleteLater()
Typical pattern:
void destroy_resource(Resource *) override
{
m_alive = false;
if (onDisconnected)
onDisconnected();
delete this;
}
If the callback can run inside Qt signal dispatch or another re-entrant path, prefer deleteLater().
Do not assume destroy_resource(...) always deletes the object itself. In current code, these patterns also exist:
- a manager resource only clears maps, watch lists, or owner maps associated with
resource->handle
- QObject lifetime is owned by a parent, an outer wrapper object, or an explicit container
In other words, the core responsibility of destroy_resource(...) is "finish this resource lifetime cleanly", not necessarily "delete every related object immediately".
When to delete the object
If the QObject/private object was created exclusively for this single Wayland resource and has no other owner, destroy_resource(...) should end that object lifetime.
Typical signs:
- the constructor directly takes
wl_resource * or Resource *
- one object corresponds to exactly one client resource
- the object's purpose is only to wrap this protocol object
- no parent, container, or manager continues to own it
Typical implementation:
void destroy_resource(Resource *) override
{
delete this;
}
If there is re-entrancy risk, use:
void destroy_resource(Resource *) override
{
deleteLater();
}
When not to delete the object
If the QObject is not exclusively owned by one resource and only uses the resource to track client-specific state, destroy_resource(...) should only clean up that resource-specific state.
Typical signs:
- the object itself is the manager and follows global or outer interface lifetime
- the object has a clear QObject parent and that parent owns destruction
- the object is still held by static lists, management containers, or business code
- one object manages multiple resources rather than a strict one-to-one mapping
In that case, typically do only:
- remove
resource->handle from maps/lists/sets
- clear owner, watch, or callback bookkeeping
- update connection state or availability flags
Do not delete a manager or shared object just because one client resource is destroyed if that object still serves other state.
Decision rule
Ask one question first:
"Was this C++ object created only for this resource, and should it die with this resource?"
- if yes,
destroy_resource(...) should delete the object
- if no,
destroy_resource(...) should only clean up the state tied to that resource
Manager per-client resources
If the manager only has a lightweight per-client proxy resource, destroy(Resource*) calling wl_resource_destroy(...) is usually enough. Do not pile business state cleanup into it.
Child resource creation
Many current private protocols use the pattern "manager manually creates a child resource, then hands it to a private wrapper object", for example:
auto *child = wl_resource_create(resource->client(),
&treeland_foo_v1_interface,
resource->version(),
id);
if (!child) {
wl_client_post_no_memory(resource->client());
return;
}
new FooContext(child);
In this pattern, verify:
- child resource version follows
resource->version()
- failures use
wl_client_post_no_memory(...)
- child destruction removes the object from any static list, map, or owner structure when needed
Wiring Through Helper::init
Eventually this must connect back into src/seat/helper.cpp.
Check:
- whether it is initialized through
m_server->attach<...>()
- whether it is stored as a
Helper member when needed
- whether business
connect(...) calls are made immediately after attach
- whether it is wired into
ShellHandler, RootSurfaceContainer, QmlEngine, or other existing objects
Do not interpret "the registration point is in Helper::init" too rigidly.
The repository also has cases where a higher-level object encapsulates protocol initialization, for example:
m_shellHandler->init(m_server, m_seat);
That still counts as a protocol registration entry point. The real criteria are:
- whether startup still flows in from
Helper::init
- whether a higher-level object cleanly encapsulates initialization for a protocol family
- whether the registration entry point and lifetime ownership are still easy to locate
If a protocol is neither registered directly from Helper::init nor initialized through a higher-level object such as ShellHandler, it is usually still not fully integrated.
Samples To Avoid
capture uses ws_generate_local (raw wayland-scanner) instead of the preferred local_qtwayland_server_protocol_treeland(...) path, and does not use QtWaylandServer::* at all. Do not use it as a template.
foreign-toplevel has been fully migrated to the QtWaylandServer::* plus local_qtwayland_server_protocol_treeland(...) path. It is a valid reference for protocols that need public child wrappers (ForeignToplevelHandleV1, DockPreviewContextV1), but is too large and complex to be a starting point for simple new protocols.
For new private protocols, prefer appidresolver, prelaunch-splash, or screensaver as templates.
Reference Starting Points
src/modules/app-id-resolver/appidresolver.cpp
src/modules/prelaunch-splash/prelaunchsplash.cpp
src/modules/screensaver/screensaverinterfacev1.cpp
src/modules/tools/CMakeLists.txt
src/seat/helper.cpp
Output Requirements
When using this skill for a real task, make these explicit first:
- where the xml and generated files are
- how manager
create/destroy/global/interfaceName should be implemented
- how
destroy(...) and destroy_resource(...) split responsibilities
- where the protocol enters the startup path from
Helper::init