commit 00dd8167ae8c2cd529589b49149ed345ecef6f1a Author: LeRoyce Pearson Date: Tue Aug 8 23:47:01 2023 -0600 Create example of connecting to a wayland server diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee7098f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +zig-cache/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..fd78149 --- /dev/null +++ b/build.zig @@ -0,0 +1,38 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const module = b.addModule("wayland", .{ + .source_file = .{ .path = "src/main.zig" }, + }); + + const lib = b.addStaticLibrary(.{ + .name = "zig-wayland-wire", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + b.installArtifact(lib); + + const main_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_main_tests = b.addRunArtifact(main_tests); + + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_main_tests.step); + + const client_connect_exe = b.addExecutable(.{ + .name = "client_connect", + .root_source_file = .{ .path = "examples/01_client_connect.zig" }, + .target = target, + .optimize = optimize, + }); + client_connect_exe.addModule("wayland", module); + b.installArtifact(client_connect_exe); +} diff --git a/examples/01_client_connect.zig b/examples/01_client_connect.zig new file mode 100644 index 0000000..928cd24 --- /dev/null +++ b/examples/01_client_connect.zig @@ -0,0 +1,124 @@ +const std = @import("std"); +const wayland = @import("wayland"); + +pub fn main() !void { + var general_allocator = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = general_allocator.deinit(); + const gpa = general_allocator.allocator(); + + const display_path = try wayland.getDisplayPath(gpa); + defer gpa.free(display_path); + + const socket = try std.net.connectUnixSocket(display_path); + defer socket.close(); + + // reserve an object id for the registry + const registry_id = 2; + { + var buffer: [5]u32 = undefined; + const message = try wayland.serialize(wayland.core.Display.Request, &buffer, 1, .{ .get_registry = .{ .registry = registry_id } }); + try socket.writeAll(std.mem.sliceAsBytes(message)); + } + + // create a sync callback so we know when the registry is done listing extensions + const registry_done_id = 3; + { + var buffer: [5]u32 = undefined; + const message = try wayland.serialize(wayland.core.Display.Request, &buffer, 1, .{ .sync = .{ .callback = registry_done_id } }); + try socket.writeAll(std.mem.sliceAsBytes(message)); + } + + var shm_id: u32 = 4; + var compositor_id: u32 = 5; + var xdg_wm_base_id: u32 = 6; + var wp_single_pixel_buffer_manager_id: u32 = 7; + + var message_buffer = std.ArrayList(u32).init(gpa); + defer message_buffer.deinit(); + while (true) { + var header: wayland.Header = undefined; + const header_bytes_read = try socket.readAll(std.mem.asBytes(&header)); + if (header_bytes_read < @sizeOf(wayland.Header)) { + break; + } + + try message_buffer.resize((header.size_and_opcode.size - @sizeOf(wayland.Header)) / @sizeOf(u32)); + const bytes_read = try socket.readAll(std.mem.sliceAsBytes(message_buffer.items)); + message_buffer.shrinkRetainingCapacity(bytes_read / @sizeOf(u32)); + + if (header.object_id == registry_id) { + const event = try wayland.deserialize(wayland.core.Registry.Event, header, message_buffer.items); + std.debug.print("{} {s} ", .{ header.object_id, @tagName(event) }); + switch (event) { + .global => |global| { + std.debug.print("{} \"{}\" v{}\n", .{ global.name, std.zig.fmtEscapes(global.interface), global.version }); + + var buffer: [20]u32 = undefined; + if (std.mem.eql(u8, global.interface, "wl_shm")) { + const message = try wayland.serialize( + wayland.core.Registry.Request, + &buffer, + registry_id, + .{ .bind = .{ + .name = global.name, + .interface = global.interface, + .version = global.version, + .new_id = shm_id, + } }, + ); + try socket.writeAll(std.mem.sliceAsBytes(message)); + } else if (std.mem.eql(u8, global.interface, "wl_compositor")) { + const message = try wayland.serialize( + wayland.core.Registry.Request, + &buffer, + registry_id, + .{ .bind = .{ + .name = global.name, + .interface = global.interface, + .version = global.version, + .new_id = compositor_id, + } }, + ); + try socket.writeAll(std.mem.sliceAsBytes(message)); + } else if (std.mem.eql(u8, global.interface, "xdg_wm_base")) { + const message = try wayland.serialize( + wayland.core.Registry.Request, + &buffer, + registry_id, + .{ .bind = .{ + .name = global.name, + .interface = global.interface, + .version = global.version, + .new_id = xdg_wm_base_id, + } }, + ); + try socket.writeAll(std.mem.sliceAsBytes(message)); + } else if (std.mem.eql(u8, global.interface, "wp_single_pixel_buffer_manager_v1")) { + const message = try wayland.serialize( + wayland.core.Registry.Request, + &buffer, + registry_id, + .{ .bind = .{ + .name = global.name, + .interface = global.interface, + .version = global.version, + .new_id = wp_single_pixel_buffer_manager_id, + } }, + ); + try socket.writeAll(std.mem.sliceAsBytes(message)); + } + }, + .global_remove => std.debug.print("{}\n", .{std.zig.fmtEscapes(std.mem.sliceAsBytes(message_buffer.items))}), + } + } else if (header.object_id == registry_done_id) { + std.debug.print("<-", .{}); + for (message_buffer.items) |word| { + std.debug.print(" {}", .{std.fmt.fmtSliceHexLower(std.mem.asBytes(&word))}); + } + std.debug.print(" (sync event id)\n", .{}); + break; + } else { + std.debug.print("{} {x} \"{}\"\n", .{ header.object_id, header.size_and_opcode.opcode, std.zig.fmtEscapes(std.mem.sliceAsBytes(message_buffer.items)) }); + } + } +} diff --git a/src/core.zig b/src/core.zig new file mode 100644 index 0000000..6c48993 --- /dev/null +++ b/src/core.zig @@ -0,0 +1,85 @@ +pub const Display = struct { + pub const Request = union(Request.Tag) { + sync: Sync, + get_registry: GetRegistry, + + pub const Tag = enum(u16) { + sync, + get_registry, + }; + + pub const Sync = struct { + /// new_id + callback: u32, + }; + + pub const GetRegistry = struct { + /// new_id + registry: u32, + }; + }; + + pub const Event = union(Event.Tag) { + @"error": Event.Error, + delete_id: DeleteId, + + pub const Tag = enum(u16) { + @"error", + delete_id, + }; + + pub const Error = struct { + object_id: u32, + code: u32, + message: []const u8, + }; + + pub const DeleteId = struct { + name: u32, + }; + }; + + pub const Error = enum(u32) { + invalid_object, + invalid_method, + no_memory, + implementation, + }; +}; + +pub const Registry = struct { + pub const Event = union(Event.Tag) { + global: Global, + global_remove: GlobalRemove, + + pub const Tag = enum(u16) { + global, + global_remove, + }; + + pub const Global = struct { + name: u32, + interface: [:0]const u8, + version: u32, + }; + + pub const GlobalRemove = struct { + name: u32, + }; + }; + + pub const Request = union(Request.Tag) { + bind: Bind, + + pub const Tag = enum(u16) { + bind, + }; + + pub const Bind = struct { + name: u32, + interface: [:0]const u8, + version: u32, + new_id: u32, + }; + }; +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..099b4f4 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,263 @@ +const std = @import("std"); +const testing = std.testing; +pub const core = @import("./core.zig"); + +pub fn getDisplayPath(gpa: std.mem.Allocator) ![]u8 { + const xdg_runtime_dir_path = try std.process.getEnvVarOwned(gpa, "XDG_RUNTIME_DIR"); + defer gpa.free(xdg_runtime_dir_path); + const display_name = try std.process.getEnvVarOwned(gpa, "WAYLAND_DISPLAY"); + defer gpa.free(display_name); + + return try std.fs.path.join(gpa, &.{ xdg_runtime_dir_path, display_name }); +} + +pub const Header = extern struct { + object_id: u32 align(1), + size_and_opcode: SizeAndOpcode align(1), + + pub const SizeAndOpcode = packed struct(u32) { + opcode: u16, + size: u16, + }; +}; + +test "[]u32 from header" { + try std.testing.expectEqualSlices( + u32, + &[_]u32{ + 1, + (@as(u32, 12) << 16) | (4), + }, + &@as([2]u32, @bitCast(Header{ + .object_id = 1, + .size_and_opcode = .{ + .size = 12, + .opcode = 4, + }, + })), + ); +} + +test "header from []u32" { + try std.testing.expectEqualDeep( + Header{ + .object_id = 1, + .size_and_opcode = .{ + .size = 12, + .opcode = 4, + }, + }, + @as(Header, @bitCast([2]u32{ + 1, + (@as(u32, 12) << 16) | (4), + })), + ); +} + +pub fn deserializeArguments(comptime Signature: type, buffer: []const u32) !Signature { + var result: Signature = undefined; + var pos: usize = 0; + inline for (std.meta.fields(Signature)) |field| { + switch (@typeInfo(field.type)) { + .Int => { + @field(result, field.name) = @bitCast(buffer[pos]); + pos += 1; + }, + .Pointer => |ptr| switch (ptr.size) { + .Slice => { + const len = buffer[pos]; + pos += 1; + const byte_pos = pos * @sizeOf(u32); + @field(result, field.name) = std.mem.sliceAsBytes(buffer)[byte_pos..][0 .. len - 1 :0]; + pos += std.mem.alignForward(usize, len, @sizeOf(u32)) / @sizeOf(u32); + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + } + } + return result; +} + +pub fn deserialize(comptime Union: type, header: Header, buffer: []const u32) !Union { + const op = try std.meta.intToEnum(std.meta.Tag(Union), header.size_and_opcode.opcode); + switch (op) { + inline else => |f| { + const Payload = std.meta.TagPayload(Union, f); + const payload = try deserializeArguments(Payload, buffer); + return @unionInit(Union, @tagName(f), payload); + }, + } +} + +/// Returns the length of the serialized message in `u32` words. +pub fn calculateSerializedWordLen(comptime Signature: type, message: Signature) usize { + var pos: usize = 0; + inline for (std.meta.fields(Signature)) |field| { + switch (@typeInfo(field.type)) { + .Int => pos += 1, + .Pointer => |ptr| switch (ptr.size) { + .Slice => { + // for size of string in bytes + pos += 1; + + const str = @field(message, field.name); + pos += std.mem.alignForward(usize, str.len + 1, @sizeOf(u32)) / @sizeOf(u32); + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + } + } + return pos; +} + +/// Message must live until the iovec array is written. +pub fn serializeArguments(comptime Signature: type, buffer: []u32, message: Signature) ![]u32 { + var pos: usize = 0; + inline for (std.meta.fields(Signature)) |field| { + switch (@typeInfo(field.type)) { + .Int => { + if (pos >= buffer.len) return error.OutOfMemory; + buffer[pos] = @field(message, field.name); + pos += 1; + }, + .Pointer => |ptr| switch (ptr.size) { + .Slice => { + const str = @field(message, field.name); + if (str.len >= std.math.maxInt(u32)) return error.StringTooLong; + + buffer[pos] = @intCast(str.len + 1); + pos += 1; + + const str_len_aligned = std.mem.alignForward(usize, str.len + 1, @sizeOf(u32)); + const padding_len = str_len_aligned - str.len; + if (str_len_aligned / @sizeOf(u32) >= buffer[pos..].len) return error.OutOfMemory; + const buffer_bytes = std.mem.sliceAsBytes(buffer[pos..]); + @memcpy(buffer_bytes[0..str.len], str); + @memset(buffer_bytes[str.len..][0..padding_len], 0); + pos += str_len_aligned / @sizeOf(u32); + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + } + } + return buffer[0..pos]; +} + +pub fn serialize(comptime Union: type, buffer: []u32, object_id: u32, message: Union) ![]u32 { + const header_wordlen = @sizeOf(Header) / @sizeOf(u32); + const header: *Header = @ptrCast(buffer[0..header_wordlen]); + header.object_id = object_id; + + const tag = std.meta.activeTag(message); + header.size_and_opcode.opcode = @intFromEnum(tag); + + const arguments = switch (message) { + inline else => |payload| try serializeArguments(@TypeOf(payload), buffer[header_wordlen..], payload), + }; + + header.size_and_opcode.size = @intCast(@sizeOf(Header) + arguments.len * @sizeOf(u32)); + return buffer[0 .. header.size_and_opcode.size / @sizeOf(u32)]; +} + +test "deserialize Registry.Event.Global" { + const words = [_]u32{ + 1, + 7, + @bitCast(@as([4]u8, "wl_s".*)), + @bitCast(@as([4]u8, "hm\x00\x00".*)), + 3, + }; + const parsed = try deserializeArguments(core.Registry.Event.Global, &words); + try std.testing.expectEqualDeep(core.Registry.Event.Global{ + .name = 1, + .interface = "wl_shm", + .version = 3, + }, parsed); +} + +test "deserialize Registry.Event" { + const header = Header{ + .object_id = 123, + .size_and_opcode = .{ + .size = 28, + .opcode = @intFromEnum(core.Registry.Event.Tag.global), + }, + }; + const words = [_]u32{ + 1, + 7, + @bitCast(@as([4]u8, "wl_s".*)), + @bitCast(@as([4]u8, "hm\x00\x00".*)), + 3, + }; + const parsed = try deserialize(core.Registry.Event, header, &words); + try std.testing.expectEqualDeep( + core.Registry.Event{ + .global = .{ + .name = 1, + .interface = "wl_shm", + .version = 3, + }, + }, + parsed, + ); + + const header2 = Header{ + .object_id = 1, + .size_and_opcode = .{ + .size = 14 * @sizeOf(u32), + .opcode = @intFromEnum(core.Display.Event.Tag.@"error"), + }, + }; + const words2 = [_]u32{ + 1, + 15, + 40, + @bitCast(@as([4]u8, "inva".*)), + @bitCast(@as([4]u8, "lid ".*)), + @bitCast(@as([4]u8, "argu".*)), + @bitCast(@as([4]u8, "ment".*)), + @bitCast(@as([4]u8, "s to".*)), + @bitCast(@as([4]u8, " wl_".*)), + @bitCast(@as([4]u8, "regi".*)), + @bitCast(@as([4]u8, "stry".*)), + @bitCast(@as([4]u8, "@2.b".*)), + @bitCast(@as([4]u8, "ind\x00".*)), + }; + const parsed2 = try deserialize(core.Display.Event, header2, &words2); + try std.testing.expectEqualDeep( + core.Display.Event{ + .@"error" = .{ + .object_id = 1, + .code = 15, + .message = "invalid arguments to wl_registry@2.bind", + }, + }, + parsed2, + ); +} + +test "serialize Registry.Event.Global" { + const message = core.Registry.Event.Global{ + .name = 1, + .interface = "wl_shm", + .version = 3, + }; + var buffer: [5]u32 = undefined; + const serialized = try serializeArguments(core.Registry.Event.Global, &buffer, message); + + try std.testing.expectEqualSlices( + u32, + &[_]u32{ + 1, + 7, + @bitCast(@as([4]u8, "wl_s".*)), + @bitCast(@as([4]u8, "hm\x00\x00".*)), + 3, + }, + serialized, + ); +}