Files

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) |