scanner: require explicit target globals/versions

Now only wl_display, wl_registry, and wl_callback are generated by
default (these interfaces are locked to version 1 for eternity).

All other interfaces must be explicitly requested in the program's
build.zig using ScanProtocolsStep.generate(global_name, version).

This ensures forwards compatibility of programs written using
zig-wayland with newer protocol xml.
This commit is contained in:
Isaac Freund
2022-05-09 21:15:54 +02:00
parent 003984f3bf
commit c9b8c0c424
3 changed files with 280 additions and 59 deletions
+13 -1
View File
@@ -19,7 +19,19 @@ pub fn build(b: *Builder) void {
const scanner = ScanProtocolsStep.create(b);
scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
scanner.addProtocolPath("protocol/foobar.xml");
scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-v1.xml");
scanner.addProtocolPath("protocol/private_foobar.xml");
// Pass the maximum version implemented by your wayland server or client.
// Requests, events, enums, etc. from newer versions will not be generated,
// ensuring forwards compatibility with newer protocol xml.
// This will also generate code for interfaces created using the provided
// global interface, in this example wl_keyboard, wl_pointer, xdg_surface,
// xdg_toplevel, etc. would be generated.
scanner.generate("wl_seat", 4);
scanner.generate("xdg_wm_base", 3);
scanner.generate("ext_session_lock_manager_v1", 1);
scanner.generate("private_foobar_manager", 1);
const exe = b.addExecutable("foo", "foo.zig");
exe.setTarget(target);
+20 -3
View File
@@ -13,6 +13,11 @@ pub fn build(b: *zbs.Builder) void {
.path = .{ .generated = &scanner.result },
};
scanner.generate("wl_compositor", 1);
scanner.generate("wl_shm", 1);
scanner.generate("wl_seat", 2);
scanner.generate("wl_output", 1);
inline for ([_][]const u8{ "globals", "list", "listener", "seats" }) |example| {
const exe = b.addExecutable(example, "example/" ++ example ++ ".zig");
exe.setTarget(target);
@@ -64,6 +69,7 @@ pub const ScanProtocolsStep = struct {
/// Slice of absolute paths of protocol xml files to be scanned
protocol_paths: std.ArrayList([]const u8),
targets: std.ArrayList(scanner.Target),
pub fn create(builder: *zbs.Builder) *ScanProtocolsStep {
const ally = builder.allocator;
@@ -73,16 +79,18 @@ pub const ScanProtocolsStep = struct {
.step = zbs.Step.init(.custom, "Scan Protocols", ally, make),
.result = .{ .step = &self.step, .path = null },
.protocol_paths = std.ArrayList([]const u8).init(ally),
.targets = std.ArrayList(scanner.Target).init(ally),
};
self.targets.append(.{ .name = "wl_display", .version = 1 }) catch unreachable;
return self;
}
/// Generate bindings from the protocol xml at the given absolute or relative path
/// Scan the protocol xml at the given absolute or relative path
pub fn addProtocolPath(self: *ScanProtocolsStep, path: []const u8) void {
self.protocol_paths.append(path) catch unreachable;
}
/// Generate bindings from protocol xml provided by the wayland-protocols
/// Scan the protocol xml provided by the wayland-protocols
/// package given the relative path (e.g. "stable/xdg-shell/xdg-shell.xml")
pub fn addSystemProtocol(self: *ScanProtocolsStep, relative_path: []const u8) void {
const protocol_dir = mem.trim(u8, self.builder.exec(
@@ -94,6 +102,15 @@ pub const ScanProtocolsStep = struct {
) catch unreachable);
}
/// Generate code for the given global interface at the given version,
/// as well as all interfaces that can be created using it at that version.
/// If the version found in the protocol xml is less than the requested version,
/// an error will be printed and code generation will fail.
/// Code is always generated for wl_display, wl_registry, and wl_callback.
pub fn generate(self: *ScanProtocolsStep, global_interface: []const u8, version: u32) void {
self.targets.append(.{ .name = global_interface, .version = version }) catch unreachable;
}
fn make(step: *zbs.Step) !void {
const self = @fieldParentPtr(ScanProtocolsStep, "step", step);
const ally = self.builder.allocator;
@@ -109,7 +126,7 @@ pub const ScanProtocolsStep = struct {
defer root_dir.close();
var out_dir = try root_dir.makeOpenPath(out_path, .{});
defer out_dir.close();
try scanner.scan(root_dir, out_dir, wayland_xml, self.protocol_paths.items);
try scanner.scan(root_dir, out_dir, wayland_xml, self.protocol_paths.items, self.targets.items);
// Once https://github.com/ziglang/zig/issues/131 is implemented
// we can stop generating/linking C code.
+247 -55
View File
@@ -1,4 +1,5 @@
const std = @import("std");
const assert = std.debug.assert;
const fs = std.fs;
const mem = std.mem;
const fmtId = std.zig.fmtId;
@@ -8,7 +9,24 @@ const xml = @import("xml.zig");
const gpa = allocator_instance.allocator();
var allocator_instance = std.heap.GeneralPurposeAllocator(.{}){};
pub fn scan(root_dir: fs.Dir, out_dir: fs.Dir, wayland_xml: []const u8, protocols: []const []const u8) !void {
pub const Target = struct {
/// Name of the target global interface
name: []const u8,
/// Interface version for which to generate code.
/// If the version found in the protocol xml is less than this version,
/// an error will be printed and code generation will fail.
/// This version applies to interfaces that may be created through the
/// global interface as well.
version: u32,
};
pub fn scan(
root_dir: fs.Dir,
out_dir: fs.Dir,
wayland_xml: []const u8,
protocols: []const []const u8,
targets: []const Target,
) !void {
const wayland_file = try out_dir.createFile("wayland.zig", .{});
try wayland_file.writeAll(
\\pub const client = @import("client.zig");
@@ -18,9 +36,10 @@ pub fn scan(root_dir: fs.Dir, out_dir: fs.Dir, wayland_xml: []const u8, protocol
var scanner = Scanner{};
try scanner.scanProtocol(root_dir, out_dir, wayland_xml);
for (protocols) |xml_path|
try scanner.scanProtocol(root_dir, out_dir, xml_path);
try scanner.scanProtocol(root_dir, out_dir, wayland_xml, targets);
for (protocols) |xml_path| {
try scanner.scanProtocol(root_dir, out_dir, xml_path, targets);
}
{
const client_core_file = try out_dir.createFile("wayland_client_core.zig", .{});
@@ -90,7 +109,13 @@ const Scanner = struct {
server: Map = Map.init(gpa),
common: Map = Map.init(gpa),
fn scanProtocol(scanner: *Scanner, root_dir: fs.Dir, out_dir: fs.Dir, xml_path: []const u8) !void {
fn scanProtocol(
scanner: *Scanner,
root_dir: fs.Dir,
out_dir: fs.Dir,
xml_path: []const u8,
targets: []const Target,
) !void {
const xml_file = try root_dir.openFile(xml_path, .{});
defer xml_file.close();
@@ -103,11 +128,12 @@ const Scanner = struct {
const protocol_name = try gpa.dupe(u8, protocol.name);
const protocol_namespace = try gpa.dupe(u8, protocol.namespace);
// TODO Use buffered I/O
{
const client_filename = try mem.concat(gpa, u8, &[_][]const u8{ protocol_name, "_client.zig" });
const client_file = try out_dir.createFile(client_filename, .{});
defer client_file.close();
try protocol.emitClient(client_file.writer());
try protocol.emitClient(targets, client_file.writer());
try (try scanner.client.getOrPutValue(protocol_namespace, .{})).value_ptr.append(gpa, client_filename);
}
@@ -115,7 +141,7 @@ const Scanner = struct {
const server_filename = try mem.concat(gpa, u8, &[_][]const u8{ protocol_name, "_server.zig" });
const server_file = try out_dir.createFile(server_filename, .{});
defer server_file.close();
try protocol.emitServer(server_file.writer());
try protocol.emitServer(targets, server_file.writer());
try (try scanner.server.getOrPutValue(protocol_namespace, .{})).value_ptr.append(gpa, server_filename);
}
@@ -123,7 +149,7 @@ const Scanner = struct {
const common_filename = try mem.concat(gpa, u8, &[_][]const u8{ protocol_name, "_common.zig" });
const common_file = try out_dir.createFile(common_filename, .{});
defer common_file.close();
try protocol.emitCommon(common_file.writer());
try protocol.emitCommon(targets, common_file.writer());
try (try scanner.common.getOrPutValue(protocol_namespace, .{})).value_ptr.append(gpa, common_filename);
}
}
@@ -131,11 +157,16 @@ const Scanner = struct {
/// All data in this struct is immutable after creation in parse().
const Protocol = struct {
const Global = struct {
interface: Interface,
children: []const Interface,
};
name: []const u8,
namespace: []const u8,
copyright: ?[]const u8,
toplevel_description: ?[]const u8,
interfaces: []const Interface,
globals: []const Global,
fn parseXML(arena: mem.Allocator, xml_bytes: []const u8) !Protocol {
var parser = xml.Parser.init(xml_bytes);
@@ -150,7 +181,7 @@ const Protocol = struct {
var name: ?[]const u8 = null;
var copyright: ?[]const u8 = null;
var toplevel_description: ?[]const u8 = null;
var interfaces = std.ArrayList(Interface).init(gpa);
var interfaces = std.StringArrayHashMap(Interface).init(gpa);
defer interfaces.deinit();
while (parser.next()) |ev| switch (ev) {
@@ -179,7 +210,10 @@ const Protocol = struct {
return error.UnexpectedEndOfFile;
}
} else if (mem.eql(u8, tag, "interface")) {
try interfaces.append(try Interface.parse(arena, parser));
const interface = try Interface.parse(arena, parser);
const gop = try interfaces.getOrPut(interface.name);
if (gop.found_existing) return error.DuplicateInterfaceName;
gop.value_ptr.* = interface;
}
},
.attribute => |attr| if (mem.eql(u8, attr.name, "name")) {
@@ -187,17 +221,25 @@ const Protocol = struct {
name = try attr.dupeValue(arena);
},
.close_tag => |tag| if (mem.eql(u8, tag, "protocol")) {
if (interfaces.items.len == 0) return error.NoInterfaces;
if (interfaces.count() == 0) return error.NoInterfaces;
const globals = try find_globals(arena, interfaces);
if (globals.len == 0) return error.NoGlobals;
const namespace = prefix(interfaces.values()[0].name) orelse return error.NoNamespace;
for (interfaces.values()) |interface| {
const other = prefix(interface.name) orelse return error.NoNamespace;
if (!mem.eql(u8, namespace, other)) return error.InconsistentNamespaces;
}
return Protocol{
.name = name orelse return error.MissingName,
// TODO: support mixing namespaces in a protocol
.namespace = prefix(interfaces.items[0].name) orelse return error.NoNamespace,
.interfaces = try arena.dupe(Interface, interfaces.items),
.namespace = namespace,
// Missing copyright or toplevel description is bad style, but not illegal.
.copyright = copyright,
.toplevel_description = toplevel_description,
.globals = globals,
};
},
else => {},
@@ -205,6 +247,88 @@ const Protocol = struct {
return error.UnexpectedEndOfFile;
}
fn find_globals(arena: mem.Allocator, interfaces: std.StringArrayHashMap(Interface)) ![]const Global {
var non_globals = std.StringHashMap(void).init(gpa);
defer non_globals.deinit();
for (interfaces.values()) |interface| {
for (interface.requests) |message| {
if (message.kind == .constructor) {
if (message.kind.constructor) |child_interface_name| {
try non_globals.put(child_interface_name, {});
}
}
}
for (interface.events) |message| {
if (message.kind == .constructor) {
if (message.kind.constructor) |child_interface_name| {
try non_globals.put(child_interface_name, {});
}
}
}
}
var globals = std.ArrayList(Global).init(gpa);
defer globals.deinit();
for (interfaces.values()) |interface| {
if (!non_globals.contains(interface.name)) {
var children = std.ArrayList(Interface).init(gpa);
defer children.deinit();
try find_children(interface, interfaces, &children);
try globals.append(.{
.interface = interface,
.children = try arena.dupe(Interface, children.items),
});
}
}
return arena.dupe(Global, globals.items);
}
fn find_children(
parent: Interface,
interfaces: std.StringArrayHashMap(Interface),
children: *std.ArrayList(Interface),
) error{ OutOfMemory, UnknownInterface }!void {
for (parent.requests) |message| {
if (message.kind == .constructor) {
if (message.kind.constructor) |child_name| {
// wl_callback breaks the standard object hierarchy and
// can be generated through multiple different globals.
if (mem.eql(u8, child_name, "wl_callback") and
!mem.eql(u8, parent.name, "wl_display"))
{
continue;
}
const child = interfaces.get(child_name) orelse return error.UnknownInterface;
try children.append(child);
try find_children(child, interfaces, children);
}
}
}
for (parent.events) |message| {
if (message.kind == .constructor) {
if (message.kind.constructor) |child_name| {
// wl_callback breaks the standard object hierarchy and
// can be generated through multiple different globals.
if (mem.eql(u8, child_name, "wl_callback") and
!mem.eql(u8, parent.name, "wl_display"))
{
continue;
}
const child = interfaces.get(child_name) orelse return error.UnknownInterface;
try children.append(child);
try find_children(child, interfaces, children);
}
}
}
}
fn emitCopyrightAndToplevelDescription(protocol: Protocol, writer: anytype) !void {
try writer.writeAll("// Generated by zig-wayland\n\n");
if (protocol.copyright) |copyright| {
@@ -223,35 +347,74 @@ const Protocol = struct {
}
}
fn emitClient(protocol: Protocol, writer: anytype) !void {
fn emitClient(protocol: Protocol, targets: []const Target, writer: anytype) !void {
try protocol.emitCopyrightAndToplevelDescription(writer);
try writer.writeAll(
\\const os = @import("std").os;
\\const client = @import("wayland.zig").client;
\\const common = @import("common.zig");
);
for (protocol.interfaces) |interface|
try interface.emit(.client, protocol.namespace, writer);
for (targets) |target| {
for (protocol.globals) |global| {
if (mem.eql(u8, target.name, global.interface.name)) {
if (global.interface.version < target.version) {
std.log.err("requested {s} version {d} but only version {d} is available", .{
target.name,
target.version,
global.interface.version,
});
return error.VersionMismatch;
}
try global.interface.emit(.client, target.version, protocol.namespace, writer);
for (global.children) |child| {
try child.emit(.client, target.version, protocol.namespace, writer);
}
}
}
}
}
fn emitServer(protocol: Protocol, writer: anytype) !void {
fn emitServer(protocol: Protocol, targets: []const Target, writer: anytype) !void {
try protocol.emitCopyrightAndToplevelDescription(writer);
try writer.writeAll(
\\const os = @import("std").os;
\\const server = @import("wayland.zig").server;
\\const common = @import("common.zig");
);
for (protocol.interfaces) |interface|
try interface.emit(.server, protocol.namespace, writer);
for (targets) |target| {
for (protocol.globals) |global| {
if (mem.eql(u8, target.name, global.interface.name)) {
// We check this in emitClient() which is called first.
assert(global.interface.version >= target.version);
try global.interface.emit(.server, target.version, protocol.namespace, writer);
for (global.children) |child| {
try child.emit(.server, target.version, protocol.namespace, writer);
}
}
}
}
}
fn emitCommon(protocol: Protocol, writer: anytype) !void {
fn emitCommon(protocol: Protocol, targets: []const Target, writer: anytype) !void {
try protocol.emitCopyrightAndToplevelDescription(writer);
try writer.writeAll(
\\const common = @import("common.zig");
);
for (protocol.interfaces) |interface|
try interface.emitCommon(writer);
for (targets) |target| {
for (protocol.globals) |global| {
if (mem.eql(u8, target.name, global.interface.name)) {
// We check this in emitClient() which is called first.
assert(global.interface.version >= target.version);
try global.interface.emitCommon(target.version, writer);
for (global.children) |child| {
try child.emitCommon(target.version, writer);
}
}
}
}
}
};
@@ -306,7 +469,7 @@ const Interface = struct {
return error.UnexpectedEndOfFile;
}
fn emit(interface: Interface, side: Side, namespace: []const u8, writer: anytype) !void {
fn emit(interface: Interface, side: Side, target_version: u32, namespace: []const u8, writer: anytype) !void {
try writer.print(
\\pub const {[type]} = opaque {{
\\ pub const getInterface = common.{[namespace]}.{[interface]}.getInterface;
@@ -317,11 +480,13 @@ const Interface = struct {
});
for (interface.enums) |e| {
try writer.print("pub const {[type]} = common.{[namespace]}.{[interface]}.{[type]};\n", .{
.@"type" = titleCase(e.name),
.namespace = fmtId(namespace),
.interface = fmtId(trimPrefix(interface.name)),
});
if (e.since <= target_version) {
try writer.print("pub const {[type]} = common.{[namespace]}.{[interface]}.{[type]};\n", .{
.@"type" = titleCase(e.name),
.namespace = fmtId(namespace),
.interface = fmtId(trimPrefix(interface.name)),
});
}
}
if (side == .client) {
@@ -334,9 +499,18 @@ const Interface = struct {
.interface = fmtId(trimPrefix(interface.name)),
.@"type" = titleCaseTrim(interface.name),
});
if (interface.events.len > 0) {
const has_event = for (interface.events) |event| {
if (event.since <= target_version) break true;
} else false;
if (has_event) {
try writer.writeAll("pub const Event = union(enum) {");
for (interface.events) |event| try event.emitField(.client, writer);
for (interface.events) |event| {
if (event.since <= target_version) {
try event.emitField(.client, writer);
}
}
try writer.writeAll("};\n");
try writer.print(
\\pub inline fn setListener(
@@ -357,8 +531,10 @@ const Interface = struct {
var has_destroy = false;
for (interface.requests) |request, opcode| {
if (mem.eql(u8, request.name, "destroy")) has_destroy = true;
try request.emitFn(side, writer, interface, opcode);
if (request.since <= target_version) {
if (mem.eql(u8, request.name, "destroy")) has_destroy = true;
try request.emitFn(side, writer, interface, opcode);
}
}
if (mem.eql(u8, interface.name, "wl_display")) {
@@ -420,9 +596,17 @@ const Interface = struct {
});
}
if (interface.requests.len > 0) {
const has_request = for (interface.requests) |request| {
if (request.since <= target_version) break true;
} else false;
if (has_request) {
try writer.writeAll("pub const Request = union(enum) {");
for (interface.requests) |request| try request.emitField(.server, writer);
for (interface.requests) |request| {
if (request.since <= target_version) {
try request.emitField(.server, writer);
}
}
try writer.writeAll("};\n");
@setEvalBranchQuota(2500);
try writer.print(
@@ -488,7 +672,7 @@ const Interface = struct {
try writer.writeAll("};\n");
}
fn emitCommon(interface: Interface, writer: anytype) !void {
fn emitCommon(interface: Interface, target_version: u32, writer: anytype) !void {
try writer.print("pub const {}", .{fmtId(trimPrefix(interface.name))});
// TODO: stop linking libwayland generated interface structs when
@@ -502,7 +686,11 @@ const Interface = struct {
\\ }}
, .{ .interface = interface.name });
for (interface.enums) |e| try e.emit(writer);
for (interface.enums) |e| {
if (e.since <= target_version) {
try e.emit(target_version, writer);
}
}
try writer.writeAll("};");
}
};
@@ -873,26 +1061,28 @@ const Enum = struct {
return error.UnexpectedEndOfFile;
}
fn emit(e: Enum, writer: anytype) !void {
fn emit(e: Enum, target_version: u32, writer: anytype) !void {
try writer.print("pub const {}", .{titleCase(e.name)});
if (e.bitfield) {
var entries_emitted: u8 = 0;
try writer.writeAll(" = packed struct {");
for (e.entries) |entry| {
const value = entry.intValue();
if (value != 0 and std.math.isPowerOfTwo(value)) {
try writer.print("{s}", .{entry.name});
if (entries_emitted == 0) {
// Align the first field to ensure the entire packed
// struct matches the alignment of a u32. This allows
// using the packed struct as the field of an extern
// struct where a u32 is expected.
try writer.writeAll(": bool align(@alignOf(u32)) = false,");
} else {
try writer.writeAll(": bool = false,");
if (entry.since <= target_version) {
const value = entry.intValue();
if (value != 0 and std.math.isPowerOfTwo(value)) {
try writer.print("{s}", .{entry.name});
if (entries_emitted == 0) {
// Align the first field to ensure the entire packed
// struct matches the alignment of a u32. This allows
// using the packed struct as the field of an extern
// struct where a u32 is expected.
try writer.writeAll(": bool align(@alignOf(u32)) = false,");
} else {
try writer.writeAll(": bool = false,");
}
entries_emitted += 1;
}
entries_emitted += 1;
}
}
// Pad to 32 bits. Use only bools to avoid zig stage1 packed
@@ -908,7 +1098,9 @@ const Enum = struct {
try writer.writeAll(" = enum(c_int) {");
for (e.entries) |entry| {
try writer.print("{s}= {s},", .{ fmtId(entry.name), entry.value });
if (entry.since <= target_version) {
try writer.print("{s}= {s},", .{ fmtId(entry.name), entry.value });
}
}
// Always generate non-exhaustive enums to ensure forward compatability.
// Entries have been added to wl_shm.format without bumping the version.
@@ -1027,10 +1219,10 @@ test "parsing" {
const protocol = try Protocol.parseXML(arena.allocator(), @embedFile("../protocol/wayland.xml"));
try testing.expectEqualSlices(u8, "wayland", protocol.name);
try testing.expectEqual(@as(usize, 22), protocol.interfaces.len);
try testing.expectEqual(@as(usize, 8), protocol.globals.len);
{
const wl_display = protocol.interfaces[0];
const wl_display = protocol.globals[0].interface;
try testing.expectEqualSlices(u8, "wl_display", wl_display.name);
try testing.expectEqual(@as(u32, 1), wl_display.version);
try testing.expectEqual(@as(usize, 2), wl_display.requests.len);
@@ -1115,7 +1307,7 @@ test "parsing" {
}
{
const wl_data_offer = protocol.interfaces[7];
const wl_data_offer = protocol.globals[3].children[2];
try testing.expectEqualSlices(u8, "wl_data_offer", wl_data_offer.name);
try testing.expectEqual(@as(u32, 3), wl_data_offer.version);
try testing.expectEqual(@as(usize, 5), wl_data_offer.requests.len);