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