Something went wrong. Try again.
A Wayland client for Zig 0.16 with no libwayland: protocol scanner, sans-I/O wire protocol, and socket client.
Something went wrong. Try again.
9.1 kB · 222 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The client over a real socket, with the test playing the compositor at//! the other end of a socketpair.//!//! Nothing here needs a second thread: a socket buffers in both directions,//! and object ids are allocated deterministically, so the test can write the//! compositor's replies before the client asks for them, and read the//! client's requests after it has sent them.
const std = @import("std");const testing = std.testing;const linux = std.os.linux;const protocol = @import("protocol");const Connection = @import("client").Connection;const wl = @import("wayland-protocols").wl;
const Fd = protocol.Fd;const Pair = @import("fake.zig").Pair;
const Globals = struct { names: std.ArrayList([]u8) = .empty, removed: u32 = 0,
fn deinit(g: *Globals) void { for (g.names.items) |n| testing.allocator.free(n); g.names.deinit(testing.allocator); }
fn onEvent(g: *Globals, _: *Connection, _: wl.Registry, event: wl.Registry.Event) !void { switch (event) { .global => |global| try g.names.append(testing.allocator, try testing.allocator.dupe(u8, global.interface)), .global_remove => |r| g.removed = r.name, } }};
test "a round trip delivers the globals" { var p: Pair = try .init(); defer p.deinit();
const display: wl.Display = .{ .id = .display }; const registry = try display.getRegistry(&p.conn.session); var globals: Globals = .{}; defer globals.deinit(); try p.conn.setListener(registry, &globals, Globals.onEvent);
// The registry is 2, so the round trip's callback will be 3. try p.send(registry.id, &wl.Registry.interface, 0, &.{ .{ .uint = 1 }, .{ .string = "wl_compositor" }, .{ .uint = 6 } }, &.{}); try p.send(registry.id, &wl.Registry.interface, 0, &.{ .{ .uint = 2 }, .{ .string = "wl_shm" }, .{ .uint = 2 } }, &.{}); try p.send(registry.id, &wl.Registry.interface, 1, &.{.{ .uint = 1 }}, &.{}); try p.send(@enumFromInt(3), &wl.Callback.interface, 0, &.{.{ .uint = 0 }}, &.{}); try p.send(.display, &wl.Display.interface, 1, &.{.{ .uint = 3 }}, &.{}); try p.conn.roundtrip();
try testing.expectEqual(@as(usize, 2), globals.names.items.len); try testing.expectEqualStrings("wl_compositor", globals.names.items[0]); try testing.expectEqualStrings("wl_shm", globals.names.items[1]); try testing.expectEqual(@as(u32, 1), globals.removed);
// What the compositor received: get_registry, then sync. var bytes: [24]u8 = undefined; var fds: std.ArrayList(Fd) = .empty; defer fds.deinit(testing.allocator); try p.receive(&bytes, &fds); const words: [6]u32 = @bitCast(bytes); try testing.expectEqualSlices(u32, &.{ 1, 12 << 16 | 1, 2, 1, 12 << 16 | 0, 3 }, &words);}
const Keymap = struct { contents: [16]u8 = undefined, len: usize = 0, size: u32 = 0,
fn onEvent(k: *Keymap, _: *Connection, _: wl.Keyboard, event: wl.Keyboard.Event) void { switch (event) { .keymap => |keymap| { defer _ = linux.close(keymap.fd); k.size = keymap.size; k.len = linux.read(keymap.fd, &k.contents, k.contents.len); }, else => {}, } }};
test "a file descriptor from the compositor reaches the listener" { var p: Pair = try .init(); defer p.deinit();
const registry = try (wl.Display{ .id = .display }).getRegistry(&p.conn.session); const seat = try registry.bind(&p.conn.session, 1, wl.Seat, 7); const keyboard = try seat.getKeyboard(&p.conn.session); var keymap: Keymap = .{}; try p.conn.setListener(keyboard, &keymap, Keymap.onEvent); try p.conn.flush();
var pipe: [2]i32 = undefined; try testing.expectEqual(@as(usize, 0), linux.pipe2(&pipe, .{ .CLOEXEC = true })); try testing.expectEqual(@as(usize, 6), linux.write(pipe[1], "keymap", 6)); _ = linux.close(pipe[1]); try p.send(keyboard.id, &wl.Keyboard.interface, 0, &.{ .{ .uint = @intFromEnum(wl.Keyboard.KeymapFormat.xkb_v1) }, .{ .fd = 0 }, .{ .uint = 6 } }, &.{pipe[0]}); // The client has its own copy now. _ = linux.close(pipe[0]);
while (keymap.size == 0) _ = try p.conn.dispatch(); try testing.expectEqual(@as(u32, 6), keymap.size); try testing.expectEqualStrings("keymap", keymap.contents[0..keymap.len]);}
test "a file descriptor from the client reaches the compositor" { var p: Pair = try .init(); defer p.deinit();
const registry = try (wl.Display{ .id = .display }).getRegistry(&p.conn.session); const shm = try registry.bind(&p.conn.session, 1, wl.Shm, 1); try p.conn.flush(); try p.drain();
var pipe: [2]i32 = undefined; try testing.expectEqual(@as(usize, 0), linux.pipe2(&pipe, .{ .CLOEXEC = true })); defer _ = linux.close(pipe[0]); _ = try shm.createPool(&p.conn.session, pipe[1], 4096); try p.conn.flush(); // Sent, so the client's copy may go. _ = linux.close(pipe[1]);
var bytes: [16]u8 = undefined; var fds: std.ArrayList(Fd) = .empty; defer fds.deinit(testing.allocator); try p.receive(&bytes, &fds); try testing.expectEqual(@as(usize, 1), fds.items.len); defer _ = linux.close(fds.items[0]);
// It is the same pipe: what goes in one end comes out of the other. try testing.expectEqual(@as(usize, 2), linux.write(fds.items[0], "ok", 2)); var out: [2]u8 = undefined; try testing.expectEqual(@as(usize, 2), linux.read(pipe[0], &out, 2)); try testing.expectEqualStrings("ok", &out);}
test "more descriptors than one sendmsg carries are all delivered" { var p: Pair = try .init(); defer p.deinit(); const registry = try (wl.Display{ .id = .display }).getRegistry(&p.conn.session); const shm = try registry.bind(&p.conn.session, 1, wl.Shm, 1); try p.conn.flush(); try p.drain();
var pipe: [2]i32 = undefined; try testing.expectEqual(@as(usize, 0), linux.pipe2(&pipe, .{ .CLOEXEC = true })); defer _ = linux.close(pipe[0]); defer _ = linux.close(pipe[1]); const count = protocol.wire.max_fds_out + 12; for (0..count) |_| _ = try shm.createPool(&p.conn.session, pipe[1], 4096); try p.conn.flush();
var bytes: [16 * count]u8 = undefined; var fds: std.ArrayList(Fd) = .empty; defer fds.deinit(testing.allocator); try p.receive(&bytes, &fds); defer for (fds.items) |fd| { _ = linux.close(fd); }; try testing.expectEqual(@as(usize, count), fds.items.len);}
test "a protocol error comes out of dispatch" { var p: Pair = try .init(); defer p.deinit(); const registry = try (wl.Display{ .id = .display }).getRegistry(&p.conn.session); const compositor = try registry.bind(&p.conn.session, 1, wl.Compositor, 6); try p.send(.display, &wl.Display.interface, 0, &.{ .{ .object = compositor.id }, .{ .uint = 0 }, .{ .string = "no" } }, &.{});
try testing.expectError(error.ProtocolError, p.conn.dispatch()); const e = p.conn.session.protocol_error.?; try testing.expectEqual(compositor.id, e.object); try testing.expectEqualStrings("wl_compositor", e.interface.?.name); try testing.expectEqualStrings("no", e.message);}
test "the compositor going away is an error, not a hang" { var p: Pair = try .init(); defer p.deinit(); _ = linux.close(p.server); p.server = -1; try testing.expectError(error.ConnectionClosed, p.conn.dispatch());}
test "connecting finds the socket the environment names" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); const runtime_dir = try std.fmt.allocPrint(testing.allocator, ".zig-cache/tmp/{s}", .{tmp.sub_path}); defer testing.allocator.free(runtime_dir); const socket_path = try std.fs.path.join(testing.allocator, &.{ runtime_dir, "wayland-test" }); defer testing.allocator.free(socket_path);
const address = try std.Io.net.UnixAddress.init(socket_path); var server = try address.listen(testing.io, .{}); defer server.deinit(testing.io);
var environ: std.process.Environ.Map = .init(testing.allocator); defer environ.deinit(); try environ.put("XDG_RUNTIME_DIR", runtime_dir); try environ.put("WAYLAND_DISPLAY", "wayland-test");
var conn: Connection = try .connect(testing.allocator, testing.io, &environ); defer conn.deinit(); const accepted = try server.accept(testing.io); defer accepted.close(testing.io);
try environ.put("WAYLAND_DISPLAY", "wayland-missing"); try testing.expectError(error.FileNotFound, Connection.connect(testing.allocator, testing.io, &environ));
var bare: std.process.Environ.Map = .init(testing.allocator); defer bare.deinit(); try testing.expectError(error.NoRuntimeDir, Connection.connect(testing.allocator, testing.io, &bare));
try bare.put("WAYLAND_SOCKET", "not a number"); try testing.expectError(error.InvalidWaylandSocket, Connection.connect(testing.allocator, testing.io, &bare));}