- CSharp 91.8%
- Markdown 8.2%
Ai Usage Note
This project was entirely made with ai.
Wayland.Client
A .NET 10 Wayland client library with a Linux Unix socket transport, wire codec, object lifetimes, event dispatch, and an incremental XML Source Generator. The six protocols in xmls/ provide 44 interface bindings: 41 generated proxies and the handwritten wl_display, wl_registry, and wl_callback bootstrap proxies.
See the component plan!!missing!! and the Source Generator guide!!missing!!.
Quick start
For the transparent Vulkan triangle overlay, see Examples/Vk. Run it with dotnet run --project Examples/Vk -c Release in a Linux layer-shell session, or use -- --self-test to verify the Vulkan renderer without Wayland.
using Wayland.Client;
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var connection = WaylandConnection.Connect(cancellationToken: timeout.Token);
var registry = connection.Display.GetRegistry(timeout.Token);
connection.Roundtrip(timeout.Token);
foreach (var global in registry.Globals.Values)
Console.WriteLine($"{global.Name}: {global.InterfaceName} v{global.Version}");
Build and run the included example from this directory:
dotnet build Wayland.slnx -c Release
dotnet run --project Examples/ListGlobals -c Release --no-build
# Optionally select a display:
dotnet run --project Examples/ListGlobals -c Release --no-build -- /run/user/1000/wayland-0
Run the example inside a Linux Wayland session. It lists globals and exits; it does not create a window. The native transport explicitly supports Linux x64 and arm64. Codec and injected-transport tests run on Windows too.
Generated protocol bindings
Wayland.Client includes all XML files under xmls/ as compiler AdditionalFiles. Build normally to generate core Wayland, XDG shell, viewporter, presentation timing, Linux DMA-BUF, and tablet bindings in the Wayland.Client namespace. Generated C# stays under the compiler's intermediate output and is not checked in.
Use typed bindings after discovering the registry:
var compositorGlobal = registry.Globals.Values.First(g => g.InterfaceName == WlCompositor.ProtocolInterfaceName);
var compositor = WlCompositor.Bind(registry, compositorGlobal);
var surface = compositor.CreateSurface();
var shellGlobal = registry.Globals.Values.First(g => g.InterfaceName == XdgWmBase.ProtocolInterfaceName);
var shell = XdgWmBase.Bind(registry, shellGlobal);
shell.Ping += serial => shell.Pong(serial);
var xdgSurface = shell.GetXdgSurface(surface);
var toplevel = xdgSurface.GetToplevel();
toplevel.SetTitle("Wayland in C#");
xdgSurface.Configure += serial => xdgSurface.AckConfigure(serial);
surface.Commit();
This sets up protocol objects. Displaying pixels additionally requires configuring and attaching a buffer, then dispatching events. Bind chooses the lower supported interface version; pass version: to request a specific version. Child objects inherit a version capped at their own maximum, including frozen version-1 callbacks.
Add custom XML files to xmls/, or reference the generator as an analyzer from another project as shown in the generator guide!!missing!!. The generator reports malformed XML, unresolved references, name collisions, and unsupported signatures as compiler diagnostics.
Components
| Component | Responsibility |
|---|---|
UnixSocketTransport | Connect, short I/O, SCM_RIGHTS, received descriptor close-on-exec, cancellation |
IWaylandTransport | Injectable ordered byte/descriptor stream, owned by the connection |
WaylandWriter / WaylandReader | Native-endian values, UTF-8, four-byte padding, array bounds, out-of-band descriptors |
WaylandObjectId / WaylandFixed | Object identity and exact signed 24.8 wire representation |
WaylandConnection | Message assembly, separate descriptor queue, object registration, ID reuse, dispatch, round trips |
WaylandProxy / WaylandEventDescriptor | Base class and metadata contract for your generated bindings |
WlDisplay / WlRegistry / WlCallback | Bootstrap, globals, versioned binding, sync, deletion acknowledgements, server errors |
Wayland.SourceGenerator | XML parsing, cross-protocol references, typed bindings, enums, event metadata, compiler diagnostics |
Connection and lifetime rules
- With no explicit display name, a nonempty
WAYLAND_SOCKETtakes precedence. The transport duplicates that descriptor with close-on-exec; it leaves the original descriptor and environment variable owned by the caller. - Otherwise, use the explicit display name,
WAYLAND_DISPLAY, orwayland-0. Absolute display paths are used directly; relative names require an absoluteXDG_RUNTIME_DIR. WaylandConnectionowns its transport.UnixSocketTransport(Socket)takes ownership of a connected Unix stream socket on successful construction. Disposal closes pending incoming descriptors and invalidates proxies.- Create, use, dispatch, and dispose a connection on the same managed thread. Dispatch is synchronous. A dedicated thread is appropriate for an application event loop. Avoid
awaitacross connection operations unless your scheduler guarantees the same managed thread. Dispatch(token)processes one event;Run(token)continues until cancellation or failure. Handlers execute inline and may send requests. Recursive dispatch and round trips from a handler are rejected.- Requests are sent immediately; there is no separate flush or background receive loop. Serialization callbacks and proxy factories must be side-effect free except for writing the request payload.
- A cancelled receive retains incomplete buffered input so dispatch can resume. A send failure or cancellation after entering the transport fails the connection, since part of the request may already be on the wire. A cancelled round trip leaves its sync callback registered until it arrives or the connection is disposed.
- Protocol errors, EOF, transport failures, and unhandled event-handler exceptions are terminal. The first error is available as
Failure. Compositor errors include object ID, code, and message inWaylandServerException. - Generated destructor requests mark the sender destroyed after successful send. Destructor events mark it before handler invocation.
ReleaseLocal()only abandons the local proxy; it does not send a destructor. Use a generated destroy/release request when the protocol defines one. - Client object IDs are dense and reused only after
wl_display.delete_id. Destroyed proxies retain event metadata to discard in-flight events and close their descriptors. Server-created IDs may replace destroyed server proxies when a later event introduces that ID. - Call
GetRegistryonce and retain it for discovery.Roundtripdispatches other pending events as it waits for sync; it is not a guarantee that asynchronous application work has finished.
File descriptor ownership
Outgoing SafeFileHandle arguments are borrowed until synchronous sending completes. Keep them open through the call; successful sends do not dispose the originals.
Generated events lend incoming handles only for the duration of synchronous dispatch. Do not retain or dispose those handles in event handlers; duplicate a handle if you need to retain it. Generated decoders close handles after all subscribers return, including on decoding/handler failure and when there are no subscribers. Event arrays are copied into owned byte[] values and may be retained.
For handwritten decoders, WaylandReader.ReadFileDescriptor() transfers ownership immediately to the decoder. Dispose handles you take, including if later decoding or a handler throws. Untaken handles are automatically closed after dispatch, when dropping a destroyed object's event, or on connection failure/disposal.
The wire contains no bytes for an fd argument. The runtime buffers message bytes and descriptors separately because ancillary data can accompany any part of the stream. It currently limits individual messages to 65,532 bytes and 253 descriptors, and buffered input to 1 MiB and 4,096 queued descriptors. Exceeding incoming limits fails the connection.
Verification
Both test projects are executable test harnesses; use dotnet run, not dotnet test:
dotnet run --project Wayland.Client.Tests -c Release
dotnet run --project Wayland.SourceGenerator.Tests -c Release
There are 24 portable tests covering native-endian wire bytes, Unicode and malformed input, framing, globals and binding, ID lifetimes, round trips, cancellation, terminal errors, descriptor ordering/ownership, server IDs, reentrancy, and thread confinement. On Linux x64/arm64 the same executable additionally runs three real Unix socket tests for descriptor transfer and FD_CLOEXEC, short I/O with a 1 MiB stream, cancellation, and EOF.
The generator suite adds 23 tests for protocol coverage, diagnostics, generated-code compilation, incremental updates/removal, partial classes, every wire argument type, typed and untyped constructors, generated event ownership, version checks, destructors, XDG shell, and shared-memory requests.
Validation on the development Windows host: Release build passed with zero warnings, including AOT compatibility analyzers, and all 47 portable tests passed. The three native tests were skipped because no Linux environment was configured. A live compositor and native AOT publication have not been tested.
The client library has no runtime package dependencies. The Source Generator targets .NET Standard 2.0 and references Roslyn 4.9.2 as a private build-time dependency; the generator tests also use Roslyn. IsAotCompatible enables the SDK's trim/AOT analyzers, which can require restoring its analyzer package. In an offline environment that lacks that analyzer package, use -p:IsAotCompatible=false to skip those checks; the generator's packages still need to be cached. Native AOT publishing is selected on the consuming executable, for example on Linux:
dotnet publish Examples/ListGlobals -c Release -r linux-x64 -p:PublishAot=true
Scope
The library includes generated bindings for all supplied protocols. Rendering, shared-memory pool allocation, EGL/Vulkan, keyboard interpretation, window policy, and desktop shell abstractions are higher layers. This runtime does not depend on libwayland-client; the native transport calls Linux libc directly.
Protocol references: Wayland wire format and model, upstream core protocol XML, recvmsg, and ancillary data layout.