const std = @import("std"); const wayland = @import("../root.zig"); const wl = wayland.wl; const ancillary_data = @import("../ancillary-data.zig"); const log = std.log.scoped(.display); const Self = @This(); pub const Error = enum(u32) { invalid_object = 0, invalid_method = 1, no_memory = 2, implementation = 3, }; pub const Request = union(enum) { sync: struct { wayland.Object.Ref }, get_registry: struct { wayland.Object.Ref }, }; pub const Events = wayland.EventSet(Self, .{ err, delete_id, }); stream: std.net.Stream, registry: wl.Registry, handle: wayland.Object.Ref, pub fn init( self: *Self, allocator: std.mem.Allocator, handlers: []const wl.Registry.GlobalHandler, ) !void { try self.registry.init(allocator, self, handlers); } pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { self.close(); self.registry.deinit(allocator); } pub fn from_fd(fd: std.posix.fd_t) Self { var self: Self = undefined; self.stream = std.net.Stream{ .handle = fd }; return self; } pub fn open(path: []const u8) !Self { var self: Self = undefined; self.stream = try std.net.connectUnixSocket(path); return self; } pub fn close(self: *Self) void { self.stream.close(); } pub fn open_default(allocator: std.mem.Allocator) !Self { const path = try default_path(allocator); defer allocator.free(path); return open(path); } pub fn default_path(allocator: std.mem.Allocator) ![]const u8 { const env = try std.process.getEnvMap(allocator); const xdg_runtime_dir = env.get("XDG_RUNTIME_DIR") orelse return error.MissingRuntimeDir; const wayland_display = env.get("WAYLAND_DISPLAY") orelse "wayland-0"; return std.fmt.allocPrint(allocator, "{s}/{s}", .{ xdg_runtime_dir, wayland_display }); } fn padding_len(T: type, length: usize) usize { return @mod(@as(usize, @bitCast(-@as(isize, @intCast(length * @sizeOf(T))))), 4); } pub fn request(self: *Self, object: anytype, args: @TypeOf(object.*).Request) !void { comptime std.debug.assert(@FieldType(@TypeOf(object.*), "handle") == wayland.Object.Ref); comptime std.debug.assert(@typeInfo(@TypeOf(object.*).Request) == .@"union"); const tag_type = @typeInfo(@TypeOf(args)).@"union".tag_type.?; const opcode: u32 = @intFromEnum(args); inline for (@typeInfo(tag_type).@"enum".fields) |f| { if (f.value == opcode) { const data = if (@FieldType(@TypeOf(args), f.name) == void) .{} else @field(args, f.name); const id: u32 = @truncate(object.handle); var size: u32 = 8; comptime var count_fds = 0; inline for (data) |value| { switch (@TypeOf(value)) { u32, i32 => |t| size += @sizeOf(t), wayland.Object.Ref, ?wayland.Object.Ref => size += @sizeOf(u32), []const u8, []u8 => { const padding_size: usize = padding_len(u8, value.len + 1); size += @as(u32, @intCast(@sizeOf(u32) + (value.len + 1) + padding_size)); }, wayland.types.Fd => count_fds += 1, else => |t| switch (@typeInfo(t)) { .pointer => |ptr| if (ptr.size == .slice) { const padding_size: usize = padding_len(ptr.child, value.len); size += @intCast(@sizeOf(u32) + (value.len * @sizeOf(ptr.child)) + padding_size); } else @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), .@"enum" => size += @sizeOf(u32), else => @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), }, } } { var fds: [count_fds]std.posix.fd_t = undefined; comptime var current_fd_index = 0; inline for (data) |value| { switch (@TypeOf(value)) { wayland.types.Fd => { fds[current_fd_index] = value.fd; current_fd_index += 1; }, else => {}, } } try ancillary_data.send_fds(self.stream.handle, &fds, std.mem.asBytes(&id)); } _ = try self.stream.writeAll(&std.mem.toBytes((size << 16) | opcode)); inline for (data) |value| { switch (@TypeOf(value)) { u32, i32 => _ = try self.stream.writeAll(std.mem.asBytes(&value)), wayland.Object.Ref => try self.stream.writeAll(std.mem.asBytes(&(@as(u32, @truncate(value))))), ?wayland.Object.Ref => try self.stream.writeAll(std.mem.asBytes(&(@as(u32, @truncate(if (value) |v| v else 0))))), []const u8, []u8 => { const padding_size: usize = padding_len(u8, value.len + 1); _ = try self.stream.writeAll(std.mem.asBytes(&@as(u32, @truncate(value.len + 1)))); _ = try self.stream.writeAll(value); for (0..padding_size + 1) |_| { _ = try self.stream.writeAll(std.mem.asBytes(&@as(u8, 0))); } }, wayland.types.Fd => { }, else => |t| switch (@typeInfo(t)) { .pointer => |ptr| if (ptr.size == .slice) { const padding_size: usize = padding_len(ptr.child, value.len); _ = try self.stream.writeAll(std.mem.asBytes(&@as(u32, @truncate(value.len)))); _ = try self.stream.writeAll(std.mem.sliceAsBytes(value)); for (0..padding_size) |_| { _ = try self.stream.writeAll(std.mem.asBytes(&@as(u8, 0))); } } else @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), .@"enum" => _ = try self.stream.writeAll(std.mem.asBytes(&@as(u32, @intFromEnum(value)))), else => @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})), }, } } return; } } return error.InvalidOpcode; } fn read_event(self: *Self, ctx: *const wayland.Context) !void { const endian = @import("builtin").target.cpu.arch.endian(); const reader = self.stream.reader(); const object_id = try reader.readInt(u32, endian); const message_info = try reader.readInt(u32, endian); const opcode: u16 = @truncate(message_info & 0xffff); const size: u16 = @truncate(((message_info >> 16) & 0xffff)); const body_size = size - 2 * @sizeOf(u32); const body = try ctx.allocator.alloc(u8, body_size); defer ctx.allocator.free(body); _ = try reader.readAll(body); if (object_id >= self.registry.objects.items.len) { return error.UnregisteredObject; } if (self.registry.get(object_id)) |object| { try object.on_event(ctx, opcode, body); } else { return error.UnregisteredObject; } } pub fn roundtrip(self: *Self, allocator: std.mem.Allocator) !void { var context: wayland.Context = .{ .display = self, .allocator = allocator, }; var done = false; var callback: wl.Callback = undefined; try callback.init(&context, &done); defer callback.deinit(&context); errdefer self.read_event(&context) catch |e| { log.err("cleanup error: {}", .{e}); }; while (!done) { try self.read_event(&context); } } fn err( self: *Self, ctx: *const wayland.Context, object: *wayland.Object, code: u32, message: []const u8, ) void { _ = self; _ = ctx; log.err("{} ({}): {s}", .{ object, code, message }); } fn delete_id( self: *Self, ctx: *const wayland.Context, id: u32, ) void { _ = ctx; self.registry.release_object(id); } test "request" { const Sample = struct { pub const Request = union(enum) { req1: struct { u32 }, req2: struct { wayland.Object.Ref }, req3: struct { []const u8 }, req4: struct { []const u16 }, req5: struct { wayland.types.Fd }, req6: void, }; pub const Events = wayland.EventSet(@This(), .{}); handle: wayland.Object.Ref, pub fn init() @This() { return .{ .handle = 42 }; } }; var tmpdir = std.testing.tmpDir(.{}); defer tmpdir.cleanup(); var pathbuf: [std.fs.max_path_bytes]u8 = undefined; var dirbuf: [std.fs.max_path_bytes]u8 = undefined; const path = try std.fmt.bufPrint(&pathbuf, "{s}/testing", .{try tmpdir.dir.realpath(".", &dirbuf)}); const address = try std.net.Address.initUnix(path); var server = try address.listen(.{}); { var sample: Sample = .init(); var displ: Self = try .open(path); defer displ.deinit(std.testing.allocator); try displ.init(std.testing.allocator, &.{}); try displ.request(&sample, .{ .req1 = .{432} }); try displ.request(&sample, .{ .req2 = .{sample.handle} }); try displ.request(&sample, .{ .req3 = .{"abcd"} }); try displ.request(&sample, .{ .req4 = .{&[_]u16{ 1, 32, 4, 5, 5 }} }); try displ.request(&sample, .{ .req5 = .{ .{ .fd = displ.stream.handle } } }); try displ.request(&sample, .{ .req6 = {} }); } const conn = try server.accept(); const file = conn.stream; var test_buffer: [64]u8 = undefined; var size = try file.readAll(test_buffer[0..12]); try std.testing.expectEqualSlices( u8, &(std.mem.toBytes(@as(u32, 1)) ++ std.mem.toBytes(@as(u32, (12 << 16) | 1)) ++ std.mem.toBytes(@as(u32, 2))), test_buffer[0..size], ); size = try file.readAll(test_buffer[0..12]); try std.testing.expectEqualSlices( u8, &(std.mem.toBytes(@as(u32, 42)) ++ std.mem.toBytes(@as(u32, (12 << 16) | 0)) ++ std.mem.toBytes(@as(u32, 432))), test_buffer[0..size], ); size = try file.readAll(test_buffer[0..12]); try std.testing.expectEqualSlices( u8, &(std.mem.toBytes(@as(u32, 42)) ++ std.mem.toBytes(@as(u32, (12 << 16) | 1)) ++ std.mem.toBytes(@as(u32, 42))), test_buffer[0..size], ); size = try file.readAll(test_buffer[0..20]); try std.testing.expectEqualSlices( u8, &(std.mem.toBytes(@as(u32, 42)) ++ std.mem.toBytes(@as(u32, (20 << 16) | 2)) ++ std.mem.toBytes(@as(u32, 5))) ++ "abcd\x00\x00\x00\x00", test_buffer[0..size], ); size = try file.readAll(test_buffer[0..24]); try std.testing.expectEqualSlices( u8, &(std.mem.toBytes(@as(u32, 42)) ++ std.mem.toBytes(@as(u32, (24 << 16) | 3)) ++ std.mem.toBytes(@as(u32, 5))) ++ std.mem.toBytes([_]u16{ 1, 32, 4, 5, 5, 0 }), test_buffer[0..size], ); size = try file.readAll(test_buffer[0..8]); try std.testing.expectEqualSlices( u8, &(std.mem.toBytes(@as(u32, 42)) ++ std.mem.toBytes(@as(u32, (8 << 16) | 4))), test_buffer[0..size], ); size = try file.readAll(test_buffer[0..8]); try std.testing.expectEqualSlices( u8, &(std.mem.toBytes(@as(u32, 42)) ++ std.mem.toBytes(@as(u32, (8 << 16) | 5))), test_buffer[0..size], ); size = try file.readAll(test_buffer[0..]); try std.testing.expectEqual(0, size); }