292 lines
13 KiB
Markdown
292 lines
13 KiB
Markdown
# 02 — Wayland Protocol Layer (MIT-licensed)
|
|
|
|
This chapter covers how the Wayland protocol layer works in the MIT
|
|
approach: protocol XML → generators → runtime, for both the browser side
|
|
and the Python host side.
|
|
|
|
## Layer 1 — Protocol XML (REUSED AS-IS)
|
|
|
|
Same 4 XML files as Greenfield, all upstream MIT or Greenfield's own MIT
|
|
extension:
|
|
|
|
- `protocol/wayland.xml` — upstream Wayland core (MIT)
|
|
- `protocol/xdg-shell.xml` — stable xdg-shell (MIT)
|
|
- `protocol/wayland-drm.xml` — legacy wl_drm (MIT, host-side only)
|
|
- `protocol/web-bitmapbuf-unstable-v1.xml` — Greenfield's custom extension
|
|
for browser JS apps (MIT)
|
|
|
|
**No changes needed.** These files are the source of truth.
|
|
|
|
## Layer 2 — Protocol Generators (REUSED AS-IS + pywayland scanner)
|
|
|
|
### Browser side — REUSED from Greenfield (MIT)
|
|
|
|
- `@gfld/client-generator` (MIT) — generates TS `*Proxy` classes for browser
|
|
apps that want to be Wayland clients of OUR compositor.
|
|
- `@gfld/compositor-generator` (MIT) — generates TS `*Resource` + `*Requests`
|
|
classes for our custom browser compositor.
|
|
- `@gfld/compositor-proxy-generator` — **NOT NEEDED** (this generates
|
|
interceptor files for the Node N-API addon; we don't have one).
|
|
|
|
### Python side — pywayland's built-in scanner
|
|
|
|
pywayland ships its own scanner that parses the same `wayland.xml` +
|
|
`wayland-protocols/*.xml` and emits Python bindings
|
|
(`pywayland/protocol/*.py`). It uses the upstream `wayland-scanner`
|
|
C tool under the hood.
|
|
|
|
**No new generator code to write.** The scanner produces:
|
|
- `pywayland/protocol/wayland.py` — wl_display, wl_registry, wl_compositor,
|
|
wl_surface, wl_buffer, wl_seat, wl_keyboard, wl_pointer, wl_touch,
|
|
wl_output, wl_shm, wl_data_device_manager, wl_subcompositor
|
|
- `pywayland/protocol/xdg_shell.py` — xdg_wm_base, xdg_surface,
|
|
xdg_toplevel, xdg_popup, xdg_positioner
|
|
- 60+ additional files for stable and unstable Wayland protocols
|
|
|
|
For Greenfield's custom `web-bitmapbuf-unstable-v1.xml`: **not needed on
|
|
the Python side** (native apps don't use it; it's browser-only). If you
|
|
need to expose it to native apps later, pywayland supports custom
|
|
protocol XMLs via `pywayland.scanner`.
|
|
|
|
## Layer 3 — Wayland Runtime
|
|
|
|
### Browser side — REUSED from Greenfield (MIT, verified clean)
|
|
|
|
`@gfld/common` (`libs/common/src/Connection.ts`) — the shared wire format
|
|
codec. Standard upstream Wayland wire format, byte-for-byte. 8-byte
|
|
header (`u32 object_id | u16 opcode | u16 size`), per-arg bytes.
|
|
|
|
`@gfld/compositor-protocol` (`libs/compositor-protocol/src/`) — the
|
|
server-side runtime. Provides `Display`, `Client`, `Resource`, `Global`,
|
|
`Registry`, plus generated `*Resource` classes for wayland, xdg_shell,
|
|
web_bitmapbuf.
|
|
|
|
**CRITICAL VERIFICATION** (from Agent M's audit): `@gfld/compositor-protocol`
|
|
imports ZERO code from `@gfld/compositor` (AGPL). It only depends on
|
|
`@gfld/common` (MIT). So it's safe to reuse in a proprietary codebase.
|
|
|
|
### Python side — pywayland (Apache-2.0)
|
|
|
|
pywayland is the Python bindings to libwayland-server. License:
|
|
**Apache-2.0** (not MIT as the task brief assumed, but Apache-2.0 is
|
|
permissive — allows proprietary use, modification, distribution).
|
|
|
|
#### API surface
|
|
|
|
**`Display`** — wraps `struct wl_display*`:
|
|
- `Display()` — create
|
|
- `.add_socket(name=None)` → returns `"wayland-N"` (becomes WAYLAND_DISPLAY)
|
|
- `.get_event_loop()` → `EventLoop`
|
|
- `.run()` — block (we don't call this — we use asyncio integration instead)
|
|
- `.terminate()` — break the run loop
|
|
- `.init_shm()` — register wl_shm global
|
|
- `.add_shm_format(fmt)` — add SHM format (YUV420, NV12, etc.)
|
|
- `.flush_clients()` — send pending events
|
|
- `.get_serial()` → next serial number
|
|
- `.next_serial()` — allocate a serial
|
|
- `.destroy()`
|
|
- `.add_destroy_listener(listener)`
|
|
|
|
**`EventLoop`** — wraps `struct wl_event_loop*`:
|
|
- `.add_fd(fd, cb, mask, data)` — register an FD for polling
|
|
- `.add_signal(signum, cb, data)`
|
|
- `.add_timer(cb, data)` → `Timer`
|
|
- `.add_idle(cb, data)`
|
|
- `.dispatch(timeout_ms)` — process pending events (we call this from asyncio)
|
|
- `.dispatch_idle()`
|
|
- `.add_destroy_listener(listener)`
|
|
- **MISSING: `.get_fd()`** — needed for asyncio integration, see §asyncio below
|
|
|
|
**`Client`** — wraps `struct wl_client*`:
|
|
- `Client(display, fd)` — create from inbound socketpair
|
|
- `Client(ptr=existing_ptr)` — wrap existing
|
|
- `.destroy()`
|
|
- `.flush()`
|
|
- `.get_credentials()` → `(pid, uid, gid)` — used to identify which app spawned this client
|
|
- `.add_destroy_listener(listener)`
|
|
- `.get_object(id)` → `Resource`
|
|
- `Client.from_resource(res)` — static method
|
|
- **MISSING: `wl_display_add_client_created_listener`** — for new-client notifications. Available in upstream libwayland, exposed via 5-line cffi shim.
|
|
|
|
**`Resource`** (base class for generated `Wl*` types):
|
|
- `Resource(client, version, id=0)`
|
|
- `.destroy()`
|
|
- `.get_id()` → int
|
|
- `.add_destroy_listener(listener)`
|
|
- `._post_event(opcode, *args)` — emit event to client
|
|
- `._post_error(code, msg)` — send protocol error
|
|
- `.dispatcher[opcode_or_name] = func` — per-resource dispatch table (KEY mechanism)
|
|
- `.dispatcher.destructor = func`
|
|
|
|
**`Global`** (base class for generated `Wl*.global_class`):
|
|
- `WlFoo.global_class(display)` — instantiate
|
|
- `.bind_func = handler` — callback when a client binds
|
|
- `.destroy()`
|
|
|
|
**`Listener`** + **`Signal`** — standard observer pattern:
|
|
- `Listener(callable)`
|
|
- `Signal.add(listener)`
|
|
- `Signal.emit(data)`
|
|
- `Listener.remove()`
|
|
|
|
#### asyncio integration (verified working)
|
|
|
|
pywayland bundles vanilla upstream libwayland 1.26.0 (verified by `objdump`
|
|
of the bundled `.so` — only standard `wl_*` exports, NO Greenfield patch
|
|
additions). To integrate with asyncio:
|
|
|
|
```python
|
|
import asyncio, cffi, os, pywayland
|
|
from pywayland.server import Display
|
|
|
|
# 5-line cffi shim for wl_event_loop_get_fd (pywayland doesn't expose it)
|
|
extras_ffi = cffi.FFI()
|
|
extras_ffi.cdef("""
|
|
int wl_event_loop_get_fd(void *loop);
|
|
void *wl_display_get_event_loop(void *display);
|
|
int wl_event_loop_dispatch(void *loop, int timeout);
|
|
void wl_display_flush_clients(void *display);
|
|
""")
|
|
_pywl_libs = os.path.join(
|
|
os.path.dirname(os.path.dirname(pywayland.__file__)),
|
|
'pywayland.libs'
|
|
)
|
|
_lib = extras_ffi.dlopen([
|
|
os.path.join(_pywl_libs, f) for f in os.listdir(_pywl_libs)
|
|
if 'libwayland-server' in f
|
|
][0])
|
|
|
|
display = Display()
|
|
display.add_socket() # returns "wayland-N"
|
|
display.init_shm()
|
|
|
|
_ptr_int = int(pywayland.ffi.cast('uintptr_t', display._ptr))
|
|
_display_voidp = extras_ffi.cast('void *', _ptr_int)
|
|
_loop_voidp = _lib.wl_display_get_event_loop(_display_voidp)
|
|
_wl_fd = _lib.wl_event_loop_get_fd(_loop_voidp)
|
|
|
|
async def main():
|
|
loop = asyncio.get_running_loop()
|
|
def on_ready():
|
|
_lib.wl_event_loop_dispatch(_loop_voidp, 0)
|
|
display.flush_clients()
|
|
loop.add_reader(_wl_fd, on_ready)
|
|
await asyncio.Future() # run forever
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
This pattern is used in production by **qtile's Wayland backend**
|
|
(`libqtile/backend/wayland/core.py`). The qtile code is GPL-2.0 so
|
|
**don't copy** — but the architectural pattern is well-documented and
|
|
reusable.
|
|
|
|
#### Does pywayland need a patched libwayland-server?
|
|
|
|
**NO** — this is one of pywayland's biggest advantages over Greenfield.
|
|
|
|
- pywayland bundles and links against **vanilla upstream libwayland 1.26.0**
|
|
(no patches needed for basic use).
|
|
- Greenfield patches upstream libwayland-server with 9+ additions (in
|
|
`native/wayland/src/wayland-server/changes.diff`, MIT-licensed) to add
|
|
wire-message interception hooks: `wl_client_set_wire_message_cb`,
|
|
`wl_client_set_registry_created_cb`, `wl_client_set_sync_done_cb`, etc.
|
|
|
|
**Should we apply Greenfield's `changes.diff` to our libwayland-server?**
|
|
|
|
- **Optional, recommended** for the fast-sync optimization (skip browser
|
|
round-trip when no prior browser-bound request occurred).
|
|
- The patches are MIT-licensed, so applying them to a vendored
|
|
libwayland-server is fine.
|
|
- Without the patches: per-resource dispatchers work fine but with slightly
|
|
more overhead (every request goes through the full Python dispatch path).
|
|
- With the patches: wire-message interception hook lets us route requests
|
|
to `{native: true}` (handle in proxy) or `{browser: true}` (forward to
|
|
browser compositor) before they hit the per-resource dispatcher.
|
|
|
|
#### Comparison to Greenfield's `wayland-server.ts` + `wayland-server-addon.node`
|
|
|
|
| Greenfield function | pywayland equivalent | Status |
|
|
|---|---|---|
|
|
| `createDisplay`, `destroyDisplay`, `addSocketAuto` | `Display()`, `Display.destroy()`, `Display.add_socket()` | ✅ |
|
|
| `flush`, `getCredentials`, `initShm` | `Display.flush_clients()`, `Client.get_credentials()`, `Display.init_shm()` | ✅ |
|
|
| `getFd`, `dispatchRequests` (libuv integration) | NOT exposed — 5-line cffi shim | ✅ (shimmable) |
|
|
| `setWireMessageCallback`, `setWireMessageEndCallback` | NO EQUIVALENT in upstream — Greenfield patch | Use per-resource dispatchers + apply changes.diff if you want the hook |
|
|
| `setRegistryCreatedCallback`, `setSyncDoneCallback`, `setBufferCreatedCallback` | NO EQUIVALENT in upstream — Greenfield patch | Reimplement by intercepting corresponding requests via per-resource dispatchers |
|
|
| `setClientDestroyedCallback` | `Client.add_destroy_listener(Listener(cb))` | ✅ (different style) |
|
|
| `destroyClient`, `sendEvents`, `createWlResource` | `Client.destroy()`, `_post_event()`, `WlFoo.resource_class(client, version, id)` | ✅ |
|
|
| `initDrm`, `setupXWayland` | NO EQUIVALENT — use vendored Greenfield C files (MIT) | ✅ (vendored) |
|
|
| `createMemoryMappedFile`, `makePipe` | `pywayland.utils.AnonymousFile(size)`, `os.pipe()` | ✅ |
|
|
|
|
## Wire format (REUSED, same as Greenfield)
|
|
|
|
Standard upstream Wayland, byte-for-byte. See
|
|
`../greenfield-analysis/02-wayland-protocol.md` for the full byte layout.
|
|
|
|
## Object lifecycle (REUSED semantics, different API)
|
|
|
|
pywayland uses standard libwayland object lifecycle:
|
|
- Client-allocated IDs (1-indexed, recycled via `wl_display.delete_id`)
|
|
- Server-allocated IDs (from `0xff000000`)
|
|
- `Resource.add_destroy_listener(Listener(cb))` for cleanup
|
|
- `Client.add_destroy_listener(Listener(cb))` for client disconnect
|
|
|
|
## File descriptor handling (REUSED approach)
|
|
|
|
Same ProxyFD JSON descriptor as Greenfield:
|
|
```
|
|
ProxyFD = { handle: number, type: 'pipe-read'|'pipe-write'|'shm'|'unknown', host: string }
|
|
```
|
|
|
|
In Python:
|
|
- `os.pipe()` for pipes
|
|
- `os.memfd_create()` for shared memory (replaces Greenfield's `createMemoryMappedFile` native addon)
|
|
- Subprocess + WebSocket endpoints for the FD-over-WebSocket workaround (same 9 endpoints)
|
|
|
|
## Why this works
|
|
|
|
The protocol layer is fundamentally just:
|
|
1. XML describing the protocol surface (upstream MIT)
|
|
2. Code generators that produce typed bindings (MIT CLIs from Greenfield + pywayland's scanner)
|
|
3. A runtime that marshalls/unmarshals wire bytes (MIT @gfld/common + Apache-2.0 pywayland)
|
|
|
|
None of these layers require AGPL code. The AGPL parts of Greenfield
|
|
(`@gfld/compositor`, `@gfld/compositor-proxy`) are the **consumers** of
|
|
these protocol layers — they implement the actual Wayland semantics
|
|
(Surface state, Seat, Keyboard, etc.). We replace those consumers with
|
|
custom clean-room implementations.
|
|
|
|
## File reference
|
|
|
|
### Reused from Greenfield (MIT)
|
|
|
|
| Path | Role |
|
|
|---|---|
|
|
| `protocol/wayland.xml` | Upstream Wayland core protocol |
|
|
| `protocol/xdg-shell.xml` | Stable xdg-shell protocol |
|
|
| `protocol/wayland-drm.xml` | Legacy wl_drm for DMA-BUF |
|
|
| `protocol/web-bitmapbuf-unstable-v1.xml` | Greenfield's custom extension for browser JS apps |
|
|
| `libs/common/src/Connection.ts` | Wire format codec (REUSED) |
|
|
| `libs/common/src/index.ts` | Public exports |
|
|
| `libs/compositor-protocol/src/westfield-runtime-server.ts` | Server-side runtime (REUSED) |
|
|
| `libs/compositor-protocol/src/protocol/` | Generated server bindings (REUSED) |
|
|
| `libs/client-protocol/src/westfield-runtime-client.ts` | Client-side runtime (REUSED) |
|
|
| `libs/client-protocol/src/protocol/` | Generated client bindings (REUSED) |
|
|
| `libs/client-generator/{ProtocolParser,ProtocolArguments,index}.mjs` | Browser client bindings generator (REUSED) |
|
|
| `libs/compositor-generator/{ProtocolParser,ProtocolArguments,index}.mjs` | Browser server bindings generator (REUSED) |
|
|
| `greenfield/packages/compositor-proxy/native/wayland/src/wayland-server/changes.diff` | Patches to libwayland-server (MIT, optional but recommended) |
|
|
|
|
### Python equivalents (Apache-2.0 / MIT)
|
|
|
|
| Component | Role |
|
|
|---|---|
|
|
| pywayland | Python bindings to libwayland-server |
|
|
| `pywayland.server.Display` | wl_display wrapper |
|
|
| `pywayland.server.EventLoop` | wl_event_loop wrapper |
|
|
| `pywayland.server.Client` | wl_client wrapper |
|
|
| `pywayland.server.Resource` | wl_resource base class |
|
|
| `pywayland.server.Global` | wl_global base class |
|
|
| `pywayland.server.Listener`, `Signal` | Observer pattern |
|
|
| `pywayland.protocol.*` | Auto-generated protocol bindings (60+ interfaces) |
|
|
| `pywayland.utils.AnonymousFile` | memfd_create wrapper (replaces Greenfield's createMemoryMappedFile) |
|