├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── examples ├── c.zig ├── test.zig └── xr.xml └── generator ├── id_render.zig ├── index.zig ├── main.zig ├── openxr ├── build_integration.zig ├── c_parse.zig ├── generator.zig ├── parse.zig ├── registry.zig └── render.zig └── xml.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Robin Voetter, Sol Bekic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openxr-zig 2 | 3 | A OpenXR binding generator for Zig. 4 | The generator is almost a verbatim copy of Snektron's [vulkan-zig][vulkan-zig], and all the hard work was done there. 5 | 6 | ## Overview 7 | 8 | openxr-zig attempts to provide a better experience to programming OpenXR applications in Zig, by providing features such as integration of openxr errors with Zig's error system, function pointer loading, renaming fields to standard Zig style, better bitfield handling, turning out parameters into return values and more. 9 | 10 | openxr-zig is automatically tested daily against the latest xr.xml and zig, and supports xr.xml from version 1.x.163. 11 | 12 | ### Zig versions 13 | 14 | openxr-zig aims to be always compatible with the ever-changing Zig master branch (however, development may lag a few days behind). Sometimes, the Zig master branch breaks a bunch of functionality however, which may make the latest version openxr-zig incompatible with older releases of Zig. Versions compatible with older versions of zig are marked with the tag `zig-`. 15 | 16 | ## Differences from Spec 17 | * `XR_SESSION_STATE_LOSS_PENDING` results are treated as errorcodes, contrary to the spec. This way, the API wrapper returns them as part of the error union, so that the actual return type can be allocated more usefully. 18 | 19 | ## Features 20 | ### CLI-interface 21 | A CLI-interface is provided to generate xr.zig from the [OpenXR XML registry](https://github.com/KhronosGroup/OpenXR-Docs/blob/master/xml), which is built by default when invoking `zig build` in the project root. To generate xr.zig, simply invoke the program as follows: 22 | ``` 23 | $ zig-cache/bin/openxr-zig-generator path/to/xr.xml output/path/to/xr.zig 24 | ``` 25 | This reads the xml file, parses its contents, renders the OpenXR bindings, and formats file, before writing the result to the output path. While the intended usage of openxr-zig is through direct generation from build.zig (see below), the CLI-interface can be used for one-off generation and vendoring the result. 26 | 27 | ### Generation from build.zig 28 | OpenXR bindings can be generated from the OpenXR XML registry at compile time with build.zig, by using the provided OpenXR generation step: 29 | ```zig 30 | const xrgen = @import("openxr-zig/generator/index.zig"); 31 | 32 | pub fn build(b: *Builder) void { 33 | ... 34 | const exe = b.addExecutable("my-executable", "src/main.zig"); 35 | 36 | // Create a step that generates xr.zig (stored in zig-cache) from the provided openxr registry. 37 | const gen = xrgen.XrGenerateStep.init(b, "path/to/xr.xml", "xr.zig"); 38 | exe.step.dependOn(&gen.step); 39 | 40 | // Add the generated file as package to the final executable 41 | exe.addPackagePath("openxr", gen.full_out_path); 42 | } 43 | ``` 44 | This reads xr.xml, parses its contents, and renders the OpenXR bindings to "xr.zig", which is then formatted and placed in `zig-cache`. The resulting file can then be added to an executable by using `addPackagePath`. 45 | 46 | ### Function & field renaming 47 | Functions and fields are renamed to be more or less in line with [Zig's standard library style](https://ziglang.org/documentation/master/#Style-Guide): 48 | * The xr prefix is removed everywhere 49 | * Structs like `XrInstanceCreateInfo` are renamed to `InstanceCreateInfo`. 50 | * Handles like `XrSwapchainKHR` are renamed to `SwapchainKHR` (note that the tag is retained in caps). 51 | * Functions like `xrCreateInstance` are generated as `createInstance` as wrapper and as `PfnCreateInstance` as function pointer. 52 | * API constants like `XR_WHOLE_SIZE` retain screaming snake case, and are generates as `WHOLE_SIZE`. 53 | * The type name is stripped from enumeration fields and bitflags, and they are generated in (lower) snake case. For example, `XR_ACTION_TYPE_BOOLEAN_INPUT` is generated as just `boolean_input`. Note that author tags are also generated to lower case: `XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR` is translated to `application_main_khr`. 54 | * Container fields and function parameter names are generated in (lower) snake case in a similar manner: `viewConfigurationType` becomes `view_configuration_type`. 55 | * Any name which is either an illegal Zig name or a reserved identifier is rendered using `@"name"` syntax. For example, `XR_ENVIRONMENT_BLEND_MODE_OPAQUE` is translated to `@"opaque"`. 56 | 57 | ### Function pointers & Wrappers 58 | openxr-zig provides no integration for statically linking libopenxr, and these symbols are not generated at all. Instead, openxr functions are to be loaded dynamically. For each OpenXR function, a function pointer type is generated using the exact parameters and return types as defined by the OpenXR specification: 59 | 60 | ```zig 61 | pub const PfnCreateInstance = fn ( 62 | create_info: *const InstanceCreateInfo, 63 | instance: *Instance, 64 | ) callconv(openxr_call_conv) Result; 65 | ``` 66 | 67 | fn getProcAddr(instance: xr.Instance, name: [*:0]const u8) xr.PfnVoidFunction { 68 | var out: xr.PfnVoidFunction = undefined; 69 | _ = c.xrGetInstanceProcAddr(instance, name, &out); 70 | return out; 71 | } 72 | 73 | For each function, a wrapper is generated into one of three structs: 74 | * BaseWrapper. This contains wrappers for functions which are loaded by `xrGetInstanceProcAddr` without an instance, such as `xrCreateInstance`, `xrEnumerateApiLayerProperties`, etc. 75 | * InstanceWrapper. This contains wrappers for functions which are otherwise loaded by `xrGetInstanceProcAddr`. 76 | 77 | Each wrapper struct is to be used as a mixin on a struct containing **just** function pointers as members: 78 | ```zig 79 | const xr = @import("openxr"); 80 | const BaseDispatch = struct { 81 | xrCreateInstance: xr.PfnCreateInstance, 82 | usingnamespace xr.BaseWrapper(@This()); 83 | }; 84 | ``` 85 | The wrapper struct then provides wrapper functions for each function pointer in the dispatch struct: 86 | ```zig 87 | pub const BaseWrapper(comptime Self: type) type { 88 | return struct { 89 | pub fn createInstance( 90 | self: Self, 91 | create_info: InstanceCreateInfo, 92 | ) error{ 93 | OutOfMemory, 94 | LimitReached, 95 | InstanceLost, 96 | RuntimeFailure, 97 | InitializationFailed, 98 | ApiVersionUnsupported, 99 | ApiLayerNotPresent, 100 | ExtensionNotPresent, 101 | ValidationFailure, 102 | NameInvalid, 103 | Unknown, 104 | }!Instance { 105 | var instance: Instance = undefined; 106 | const result = self.xrCreateInstance( 107 | &create_info, 108 | &instance, 109 | ); 110 | switch (result) { 111 | .success => {}, 112 | .error_out_of_memory => return error.OutOfMemory, 113 | .error_limit_reached => return error.LimitReached, 114 | .error_instance_lost => return error.InstanceLost, 115 | .error_runtime_failure => return error.RuntimeFailure, 116 | .error_initialization_failed => return error.InitializationFailed, 117 | .error_api_version_unsupported => return error.ApiVersionUnsupported, 118 | .error_api_layer_not_present => return error.ApiLayerNotPresent, 119 | .error_extension_not_present => return error.ExtensionNotPresent, 120 | .error_validation_failure => return error.ValidationFailure, 121 | .error_name_invalid => return error.NameInvalid, 122 | else => return error.Unknown, 123 | } 124 | return instance; 125 | } 126 | 127 | ... 128 | } 129 | } 130 | ``` 131 | Wrappers are generated according to the following rules: 132 | * The return type is determined from the original return type and the parameters. 133 | * Any non-const, non-optional single-item pointer is interpreted as an out parameter. 134 | * If a command returns a non-error `XrResult` other than `XR_SUCCESS` it is also returned. 135 | * If there are multiple return values selected, an additional struct is generated. The original call's return value is called `return_value`, `XrResult` is named `result`, and the out parameters are called the same. They are generated in this order. 136 | * Any const non-optional single-item pointer is interpreted as an in-parameter. For these, one level of indirection is removed so that create info structure pointers can now be passed as values, enabling the ability to use struct literals for these parameters. 137 | * Error codes are translated into Zig errors. 138 | * As of yet, there is no specific handling of enumeration style commands or other commands which accept slices. 139 | 140 | Furthermore, each wrapper contains a function to load each function pointer member when passed `PfnGetInstanceProcAddr`, which attempts to load each member as function pointer and casts it to the appropriate type. These functions are loaded literally, and any wrongly named member or member with a wrong function pointer type will result in problems. 141 | * For `BaseWrapper`, this function has signature `fn load(loader: PfnGetInstanceProcAddr) !Self`. 142 | * For `InstanceWrapper`, this function has signature `fn load(instance: Instance, loader: PfnGetInstanceProcAddr) !Self`. 143 | 144 | #### `openxr-loader` 145 | 146 | By linking against `openxr_loader` and including `openxr.h`, `xrGetInstanceProcAddr` can be obtained and wrapped like so: 147 | 148 | ```zig 149 | fn getProcAddr(instance: xr.Instance, name: [*:0]const u8) !xr.PfnVoidFunction { 150 | var out: xr.PfnVoidFunction = undefined; 151 | const result = c.xrGetInstanceProcAddr(instance, name, &out); 152 | return switch (result) { 153 | .success => out, 154 | .error_handle_invalid => error.HandleInvalid, 155 | .error_instance_lost => error.InstanceLost, 156 | .error_runtime_failure => error.RuntimeFailure, 157 | .error_out_of_memory => error.OutOfMemory, 158 | .error_function_unsupported => error.FunctionUnsupported, 159 | .error_validation_failure => error.ValidationFailure, 160 | else => error.Unknown, 161 | } 162 | } 163 | 164 | const BaseDispatch = struct { 165 | xrCreateInstance: xr.PfnCreateInstance, 166 | usingnamespace xr.BaseWrapper(@This()); 167 | }; 168 | 169 | ... 170 | 171 | const xrb = try BaseDispatch.load(getProcAddr); 172 | ``` 173 | 174 | ### Bitflags 175 | Packed structs of bools are used for bit flags in openxr-zig, instead of both a `FlagBits` and `Flags64` variant. Places where either of these variants are used are both replaced by this packed struct instead. This means that even in places where just one flag would normally be accepted, the packed struct is accepted. The programmer is responsible for only enabling a single bit. 176 | 177 | Each bit is defaulted to `false`, and the first `bool` is aligned to guarantee the overal alignment 178 | of each Flags type to guarantee ABI compatibility when passing bitfields through structs: 179 | ```zig 180 | pub const ViewStateFlags = packed struct { 181 | orientation_valid_bit: bool align(@alignOf(Flags64)) = false, 182 | position_valid_bit: bool = false, 183 | orientation_tracked_bit: bool = false, 184 | position_tracked_bit: bool = false, 185 | _reserved_bit_4: bool = false, 186 | _reserved_bit_5: bool = false, 187 | ... 188 | pub usingnamespace FlagsMixin(ViewStateFlags); 189 | }; 190 | ``` 191 | Note that on function call ABI boundaries, this alignment trick is not sufficient. Instead, the flags 192 | are reinterpreted as an integer which is passed instead. Each flags type is augmented by a mixin which provides `IntType`, an integer which represents the flags on function ABI boundaries. This mixin also provides some common set operation on bitflags: 193 | ```zig 194 | pub fn FlagsMixin(comptime FlagsType: type) type { 195 | return struct { 196 | pub const IntType = Flags64; 197 | 198 | // Return the integer representation of these flags 199 | pub fn toInt(self: FlagsType) IntType {...} 200 | 201 | // Turn an integer representation back into a flags type 202 | pub fn fromInt(flags: IntType) FlagsType { ... } 203 | 204 | // Return the set-union of `lhs` and `rhs. 205 | pub fn merge(lhs: FlagsType, rhs: FlagsType) FlagsType { ... } 206 | 207 | // Return the set-intersection of `lhs` and `rhs`. 208 | pub fn intersect(lhs: FlagsType, rhs: FlagsType) FlagsType { ... } 209 | 210 | // Return the set-complement of `lhs` and `rhs`. Note: this also inverses reserved bits. 211 | pub fn complement(self: FlagsType) FlagsType { ... } 212 | 213 | // Return the set-subtraction of `lhs` and `rhs`: All fields set in `rhs` are cleared in `lhs`. 214 | pub fn subtract(lhs: FlagsType, rhs: FlagsType) FlagsType { ... } 215 | 216 | // Returns whether all bits set in `rhs` are also set in `lhs`. 217 | pub fn contains(lhs: FlagsType, rhs: FlagsType) bool { ... } 218 | }; 219 | } 220 | ``` 221 | 222 | ### Handles 223 | Handles are generated to a non-exhaustive enum, backed by a `u64` for non-dispatchable handles and `usize` for dispatchable ones: 224 | ```zig 225 | const Instance = extern enum(usize) { null_handle = 0, _ }; 226 | ``` 227 | This means that handles are type-safe even when compiling for a 32-bit target. 228 | 229 | ### Structs 230 | Defaults are generated for certain fields of structs: 231 | * `type` is defaulted to the appropriate value. 232 | * `next` is defaulted to `null`. 233 | * for math primitives (`Vector*`, `Color*`, `Quaternionf`, `Offset*`, `Extent*`, `Posef`, `Rect*`), all fields are zero-initialized by default. 234 | * No other fields have default values. 235 | 236 | All structs contain an `empty()` function that returns an instance with only `type` and `next` set, which can be used whenever OpenXR requires an uninitialized (but typed) structure to output into. 237 | 238 | ```zig 239 | pub const InstanceCreateInfo = extern struct { 240 | type: StructureType = .instance_create_info, 241 | next: ?*const c_void = null, 242 | create_flags: InstanceCreateFlags, 243 | application_info: ApplicationInfo, 244 | enabled_api_layer_count: u32, 245 | enabled_api_layer_names: [*]const [*:0]const u8, 246 | enabled_extension_count: u32, 247 | enabled_extension_names: [*]const [*:0]const u8, 248 | pub fn empty() @This() { 249 | var value: @This() = undefined; 250 | value.type = .instance_create_info; 251 | value.next = null; 252 | return value; 253 | } 254 | }; 255 | ``` 256 | 257 | ### Pointer types 258 | Pointer types in both commands (wrapped and function pointers) and struct fields are augmented with the following information, where available in the registry: 259 | * Pointer optional-ness. 260 | * Pointer const-ness. 261 | * Pointer size: Either single-item, null-terminated or many-items. 262 | 263 | Some of these are detected wrong, most notably `next`, which has been overriden to be optional in all cases. 264 | 265 | ## Limitations 266 | * Currently, the self-hosted version of Zig's cache-hash API is not yet ready for usage, which means that the bindings are regenerated every time an executable is built. 267 | 268 | * openxr-zig has as of yet no functionality for selecting feature levels and extensions when generating bindings. This is because when an extension is promoted to OpenXR core, its fields and commands are renamed to lose the extensions author tag. This leads to inconsistencies when only items from up to a certain feature level is included, as these promoted items then need to re-gain a tag. 269 | 270 | [vulkan-zig]: https://github.com/Snektron/vulkan-zig 271 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const xrgen = @import("generator/index.zig"); 3 | 4 | const XrGenerateStep = xrgen.XrGenerateStep; 5 | 6 | pub fn build(b: *std.Build) void { 7 | const target = b.standardTargetOptions(.{}); 8 | const optimize = b.standardOptimizeOption(.{}); 9 | const xr_xml_path: ?[]const u8 = b.option([]const u8, "registry", "Override the path to the OpenXR registry"); 10 | const test_step = b.step("test", "Run all the tests"); 11 | 12 | // using the package manager, this artifact can be obtained by the user 13 | // through `b.dependency(, .{}).artifact("openxr-zig-generator")`. 14 | // with that, the user need only `.addArg("path/to/xr.xml")`, and then obtain 15 | // a file source to the generated code with `.addOutputArg("xr.zig")` 16 | const generator_exe = b.addExecutable(.{ 17 | .name = "openxr-zig-generator", 18 | .root_source_file = b.path("generator/main.zig"), 19 | .target = target, 20 | .optimize = optimize, 21 | }); 22 | b.installArtifact(generator_exe); 23 | 24 | // or they can skip all that, and just make sure to pass `.registry = "path/to/xr.xml"` to `b.dependency`, 25 | // and then obtain the module directly via `.module("openxr-zig")`. 26 | if (xr_xml_path) |path| { 27 | const generate_cmd = b.addRunArtifact(generator_exe); 28 | 29 | if (!std.fs.path.isAbsolute(path)) @panic("Make sure to assign an absolute path to the `registry` option (see: std.Build.pathFromRoot).\n"); 30 | generate_cmd.addArg(path); 31 | 32 | const xr_zig = generate_cmd.addOutputFileArg("xr.zig"); 33 | const xr_zig_module = b.addModule("openxr-zig", .{ 34 | .root_source_file = xr_zig, 35 | }); 36 | 37 | // Also install xr.zig, if passed. 38 | 39 | const xr_zig_install_step = b.addInstallFile(xr_zig, "src/xr.zig"); 40 | b.getInstallStep().dependOn(&xr_zig_install_step.step); 41 | 42 | // example 43 | 44 | const example_exe = b.addExecutable(.{ 45 | .name = "example", 46 | .root_source_file = b.path("examples/test.zig"), 47 | .target = target, 48 | .optimize = optimize, 49 | }); 50 | b.installArtifact(example_exe); 51 | example_exe.linkLibC(); 52 | example_exe.linkSystemLibrary("openxr_loader"); 53 | 54 | // const example_registry = b.option([]const u8, "example-registry", "Override the path to the OpenXR registry used for the examples") orelse "examples/xr.xml"; 55 | // const gen = XrGenerateStep.create(b, example_registry); 56 | example_exe.root_module.addImport("openxr", xr_zig_module); 57 | 58 | const example_run_cmd = b.addRunArtifact(example_exe); 59 | example_run_cmd.step.dependOn(b.getInstallStep()); 60 | 61 | const example_run_step = b.step("run-example", "Run the example"); 62 | example_run_step.dependOn(&example_run_cmd.step); 63 | } 64 | 65 | // remainder of the script is for examples/testing 66 | 67 | const test_target = b.addTest(.{ 68 | .root_source_file = b.path("generator/index.zig"), 69 | }); 70 | test_step.dependOn(&b.addRunArtifact(test_target).step); 71 | } 72 | -------------------------------------------------------------------------------- /examples/c.zig: -------------------------------------------------------------------------------- 1 | const xr = @import("openxr"); 2 | 3 | pub extern fn xrGetInstanceProcAddr(instance: xr.Instance, procname: [*:0]const u8, function: *xr.PfnVoidFunction) xr.Result; 4 | -------------------------------------------------------------------------------- /examples/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const xr = @import("openxr"); 3 | const c = @import("c.zig"); 4 | const Allocator = std.mem.Allocator; 5 | 6 | const BaseDispatch = xr.BaseWrapper(.{ 7 | .createInstance = true, 8 | }); 9 | 10 | const InstanceDispatch = xr.InstanceWrapper(.{ 11 | .destroyInstance = true, 12 | .getSystem = true, 13 | .getSystemProperties = true, 14 | .createSession = true, 15 | .pollEvent = true, 16 | }); 17 | 18 | fn getProcAddr(instance: xr.Instance, name: [*:0]const u8) !xr.PfnVoidFunction { 19 | var out: xr.PfnVoidFunction = undefined; 20 | const result = c.xrGetInstanceProcAddr(instance, name, &out); 21 | return switch (result) { 22 | .success => out, 23 | .error_handle_invalid => error.HandleInvalid, 24 | .error_instance_lost => error.InstanceLost, 25 | .error_runtime_failure => error.RuntimeFailure, 26 | .error_out_of_memory => error.OutOfMemory, 27 | .error_function_unsupported => error.FunctionUnsupported, 28 | .error_validation_failure => error.ValidationFailure, 29 | else => error.Unknown, 30 | }; 31 | } 32 | 33 | pub fn main() !void { 34 | var name: [128]u8 = undefined; 35 | std.mem.copyForwards(u8, name[0..], "openxr-zig-test" ++ [_]u8{0}); 36 | 37 | const xrb = try BaseDispatch.load(c.xrGetInstanceProcAddr); 38 | 39 | const inst = try xrb.createInstance(&.{ 40 | .application_info = .{ 41 | .application_name = name, 42 | .application_version = 0, 43 | .engine_name = name, 44 | .engine_version = 0, 45 | .api_version = xr.makeVersion(1, 0, 0), 46 | }, 47 | }); 48 | 49 | const xri = try InstanceDispatch.load(inst, c.xrGetInstanceProcAddr); 50 | defer xri.destroyInstance(inst) catch unreachable; 51 | 52 | const system = try xri.getSystem(inst, &.{ .form_factor = .head_mounted_display }); 53 | 54 | var system_properties = xr.SystemProperties.empty(); 55 | try xri.getSystemProperties(inst, system, &system_properties); 56 | 57 | std.debug.print( 58 | \\system {}: 59 | \\ vendor Id: {} 60 | \\ systemName: {s} 61 | \\ gfx 62 | \\ max swapchain image resolution: {}x{} 63 | \\ max layer count: {} 64 | \\ tracking 65 | \\ orientation tracking: {} 66 | \\ positional tracking: {} 67 | , .{ 68 | system, 69 | system_properties.vendor_id, 70 | system_properties.system_name, 71 | system_properties.graphics_properties.max_swapchain_image_width, 72 | system_properties.graphics_properties.max_swapchain_image_height, 73 | system_properties.graphics_properties.max_layer_count, 74 | system_properties.tracking_properties.orientation_tracking, 75 | system_properties.tracking_properties.position_tracking, 76 | }); 77 | 78 | _ = try xri.createSession(inst, &.{ 79 | .system_id = system, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /generator/id_render.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const Allocator = mem.Allocator; 4 | 5 | pub fn isZigPrimitiveType(name: []const u8) bool { 6 | if (name.len > 1 and (name[0] == 'u' or name[0] == 'i')) { 7 | for (name[1..]) |c| { 8 | switch (c) { 9 | '0'...'9' => {}, 10 | else => break, 11 | } 12 | } else return true; 13 | } 14 | 15 | const primitives = [_][]const u8{ 16 | "void", 17 | "comptime_float", 18 | "comptime_int", 19 | "bool", 20 | "isize", 21 | "usize", 22 | "f16", 23 | "f32", 24 | "f64", 25 | "f128", 26 | "noreturn", 27 | "type", 28 | "anyerror", 29 | "c_short", 30 | "c_ushort", 31 | "c_int", 32 | "c_uint", 33 | "c_long", 34 | "c_ulong", 35 | "c_longlong", 36 | "c_ulonglong", 37 | "c_longdouble", 38 | // Removed in stage 2 in https://github.com/ziglang/zig/commit/05cf44933d753f7a5a53ab289ea60fd43761de57, 39 | // but these are still invalid identifiers in stage 1. 40 | "undefined", 41 | "true", 42 | "false", 43 | "null", 44 | }; 45 | 46 | for (primitives) |reserved| { 47 | if (mem.eql(u8, reserved, name)) { 48 | return true; 49 | } 50 | } 51 | 52 | return false; 53 | } 54 | 55 | pub fn writeIdentifier(writer: anytype, id: []const u8) !void { 56 | // https://github.com/ziglang/zig/issues/2897 57 | if (isZigPrimitiveType(id)) { 58 | try writer.print("@\"{}\"", .{std.zig.fmtEscapes(id)}); 59 | } else { 60 | try writer.print("{}", .{std.zig.fmtId(id)}); 61 | } 62 | } 63 | 64 | pub const CaseStyle = enum { 65 | snake, 66 | screaming_snake, 67 | title, 68 | camel, 69 | }; 70 | 71 | pub const SegmentIterator = struct { 72 | text: []const u8, 73 | offset: usize, 74 | 75 | pub fn init(text: []const u8) SegmentIterator { 76 | return .{ 77 | .text = text, 78 | .offset = 0, 79 | }; 80 | } 81 | 82 | fn nextBoundary(self: SegmentIterator) usize { 83 | var i = self.offset + 1; 84 | 85 | while (true) { 86 | if (i == self.text.len or self.text[i] == '_') { 87 | return i; 88 | } 89 | 90 | const prev_lower = std.ascii.isLower(self.text[i - 1]); 91 | const next_lower = std.ascii.isLower(self.text[i]); 92 | 93 | if (prev_lower and !next_lower) { 94 | return i; 95 | } else if (i != self.offset + 1 and !prev_lower and next_lower) { 96 | return i - 1; 97 | } 98 | 99 | i += 1; 100 | } 101 | } 102 | 103 | pub fn next(self: *SegmentIterator) ?[]const u8 { 104 | while (self.offset < self.text.len and self.text[self.offset] == '_') { 105 | self.offset += 1; 106 | } 107 | 108 | if (self.offset == self.text.len) { 109 | return null; 110 | } 111 | 112 | const end = self.nextBoundary(); 113 | const word = self.text[self.offset..end]; 114 | self.offset = end; 115 | return word; 116 | } 117 | 118 | pub fn rest(self: SegmentIterator) []const u8 { 119 | if (self.offset >= self.text.len) { 120 | return &[_]u8{}; 121 | } else { 122 | return self.text[self.offset..]; 123 | } 124 | } 125 | }; 126 | 127 | pub const IdRenderer = struct { 128 | tags: []const []const u8, 129 | text_cache: std.ArrayList(u8), 130 | 131 | pub fn init(allocator: Allocator, tags: []const []const u8) IdRenderer { 132 | return .{ 133 | .tags = tags, 134 | .text_cache = std.ArrayList(u8).init(allocator), 135 | }; 136 | } 137 | 138 | pub fn deinit(self: IdRenderer) void { 139 | self.text_cache.deinit(); 140 | } 141 | 142 | fn renderSnake(self: *IdRenderer, screaming: bool, id: []const u8, tag: ?[]const u8) !void { 143 | var it = SegmentIterator.init(id); 144 | var first = true; 145 | 146 | while (it.next()) |segment| { 147 | if (first) { 148 | first = false; 149 | } else { 150 | try self.text_cache.append('_'); 151 | } 152 | 153 | for (segment) |c| { 154 | try self.text_cache.append(if (screaming) std.ascii.toUpper(c) else std.ascii.toLower(c)); 155 | } 156 | } 157 | 158 | if (tag) |name| { 159 | try self.text_cache.append('_'); 160 | 161 | for (name) |c| { 162 | try self.text_cache.append(if (screaming) std.ascii.toUpper(c) else std.ascii.toLower(c)); 163 | } 164 | } 165 | } 166 | 167 | fn renderCamel(self: *IdRenderer, title: bool, id: []const u8, tag: ?[]const u8) !void { 168 | var it = SegmentIterator.init(id); 169 | var lower_first = !title; 170 | 171 | while (it.next()) |segment| { 172 | var i: usize = 0; 173 | while (i < segment.len and std.ascii.isDigit(segment[i])) { 174 | try self.text_cache.append(segment[i]); 175 | i += 1; 176 | } 177 | 178 | if (i == segment.len) { 179 | continue; 180 | } 181 | 182 | if (i == 0 and lower_first) { 183 | try self.text_cache.append(std.ascii.toLower(segment[i])); 184 | } else { 185 | try self.text_cache.append(std.ascii.toUpper(segment[i])); 186 | } 187 | lower_first = false; 188 | 189 | for (segment[i + 1 ..]) |c| { 190 | try self.text_cache.append(std.ascii.toLower(c)); 191 | } 192 | } 193 | 194 | if (tag) |name| { 195 | try self.text_cache.appendSlice(name); 196 | } 197 | } 198 | 199 | pub fn renderFmt(self: *IdRenderer, out: anytype, comptime fmt: []const u8, args: anytype) !void { 200 | self.text_cache.items.len = 0; 201 | try std.fmt.format(self.text_cache.writer(), fmt, args); 202 | try writeIdentifier(out, self.text_cache.items); 203 | } 204 | 205 | pub fn renderWithCase(self: *IdRenderer, out: anytype, case_style: CaseStyle, id: []const u8) !void { 206 | const tag = self.getAuthorTag(id); 207 | // The trailing underscore doesn't need to be removed here as its removed by the SegmentIterator. 208 | const adjusted_id = if (tag) |name| id[0 .. id.len - name.len] else id; 209 | 210 | self.text_cache.items.len = 0; 211 | 212 | switch (case_style) { 213 | .snake => try self.renderSnake(false, adjusted_id, tag), 214 | .screaming_snake => try self.renderSnake(true, adjusted_id, tag), 215 | .title => try self.renderCamel(true, adjusted_id, tag), 216 | .camel => try self.renderCamel(false, adjusted_id, tag), 217 | } 218 | 219 | try writeIdentifier(out, self.text_cache.items); 220 | } 221 | 222 | pub fn getAuthorTag(self: IdRenderer, id: []const u8) ?[]const u8 { 223 | for (self.tags) |tag| { 224 | if (mem.endsWith(u8, id, tag)) { 225 | return tag; 226 | } 227 | } 228 | 229 | // HACK for EXTX? 230 | if (mem.endsWith(u8, id, "EXTX")) { 231 | return "EXTX"; 232 | } 233 | 234 | return null; 235 | } 236 | 237 | pub fn stripAuthorTag(self: IdRenderer, id: []const u8) []const u8 { 238 | if (self.getAuthorTag(id)) |tag| { 239 | return mem.trimRight(u8, id[0 .. id.len - tag.len], "_"); 240 | } 241 | 242 | return id; 243 | } 244 | }; 245 | -------------------------------------------------------------------------------- /generator/index.zig: -------------------------------------------------------------------------------- 1 | pub const generateXr = @import("openxr/generator.zig").generate; 2 | pub const XrGenerateStep = @import("openxr/build_integration.zig").GenerateStep; 3 | 4 | test "main" { 5 | _ = @import("xml.zig"); 6 | _ = @import("openxr/c_parse.zig"); 7 | } 8 | -------------------------------------------------------------------------------- /generator/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const generator = @import("openxr/generator.zig"); 3 | 4 | const usage = "Usage: {s} [-h|--help] \n"; 5 | 6 | pub fn main() !void { 7 | const stderr = std.io.getStdErr(); 8 | 9 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 10 | defer arena.deinit(); 11 | const allocator = arena.allocator(); 12 | 13 | var args = try std.process.argsWithAllocator(allocator); 14 | const prog_name = args.next() orelse return error.ExecutableNameMissing; 15 | 16 | var maybe_xml_path: ?[]const u8 = null; 17 | var maybe_out_path: ?[]const u8 = null; 18 | 19 | while (args.next()) |arg| { 20 | if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { 21 | @setEvalBranchQuota(2000); 22 | try stderr.writer().print( 23 | \\Utility to generate a Zig binding from the OpenXR XML API registry. 24 | \\ 25 | \\The most recent OpenXR XML API registry can be obtained from 26 | \\https://github.com/KhronosGroup/OpenXR-Docs/blob/master/xml/xr.xml, 27 | \\and the most recent LunarG OpenXR SDK version can be found at 28 | \\$OPENXR_SDK/x86_64/share/openxr/registry/xr.xml. 29 | \\ 30 | \\ 31 | ++ usage, 32 | .{prog_name}, 33 | ); 34 | return; 35 | } else if (maybe_xml_path == null) { 36 | maybe_xml_path = arg; 37 | } else if (maybe_out_path == null) { 38 | maybe_out_path = arg; 39 | } else { 40 | try stderr.writer().print("Error: Superficial argument '{s}'\n", .{arg}); 41 | return; 42 | } 43 | } 44 | 45 | const xml_path = maybe_xml_path orelse { 46 | try stderr.writer().print("Error: Missing required argument \n" ++ usage, .{prog_name}); 47 | return; 48 | }; 49 | 50 | const out_path = maybe_out_path orelse { 51 | try stderr.writer().print("Error: Missing required argument \n" ++ usage, .{prog_name}); 52 | return; 53 | }; 54 | 55 | const cwd = std.fs.cwd(); 56 | const xml_src = cwd.readFileAlloc(allocator, xml_path, std.math.maxInt(usize)) catch |err| { 57 | try stderr.writer().print("Error: Failed to open input file '{s}' ({s})\n", .{ xml_path, @errorName(err) }); 58 | return; 59 | }; 60 | 61 | var out_buffer = std.ArrayList(u8).init(allocator); 62 | try generator.generate(allocator, xml_src, out_buffer.writer()); 63 | try out_buffer.append(0); 64 | 65 | const src = out_buffer.items[0 .. out_buffer.items.len - 1 :0]; 66 | const tree = try std.zig.Ast.parse(allocator, src, .zig); 67 | const formatted = try tree.render(allocator); 68 | defer allocator.free(formatted); 69 | 70 | if (std.fs.path.dirname(out_path)) |dir| { 71 | cwd.makePath(dir) catch |err| { 72 | try stderr.writer().print("Error: Failed to create output directory '{s}' ({s})\n", .{ dir, @errorName(err) }); 73 | return; 74 | }; 75 | } 76 | 77 | cwd.writeFile(.{ 78 | .sub_path = out_path, 79 | .data = formatted, 80 | }) catch |err| { 81 | try stderr.writer().print("Error: Failed to write to output file '{s}' ({s})\n", .{ out_path, @errorName(err) }); 82 | return; 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /generator/openxr/build_integration.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const generator = @import("generator.zig"); 3 | const Build = std.Build; 4 | 5 | /// build.zig integration for OpenXR binding generation. This step can be used to generate 6 | /// OpenXR bindings at compiletime from xr.xml, by providing the path to xr.xml and the output 7 | /// path relative to zig-cache. The final package can then be obtained by `package()`, the result 8 | /// of which can be added to the project using `std.Build.addModule`. 9 | pub const GenerateStep = struct { 10 | step: Build.Step, 11 | generated_file: Build.GeneratedFile, 12 | /// The path to xr.xml 13 | spec_path: []const u8, 14 | 15 | /// Initialize an OpenXR generation step, for `builder`. `spec_path` is the path to 16 | /// xr.xml, relative to the project root. The generated bindings will be placed at 17 | /// `out_path`, which is relative to the zig-cache directory. 18 | pub fn create(builder: *Build, spec_path: []const u8) *GenerateStep { 19 | const self = builder.allocator.create(GenerateStep) catch unreachable; 20 | self.* = .{ 21 | .step = Build.Step.init(.{ 22 | .id = .custom, 23 | .name = "openxr-generate", 24 | .owner = builder, 25 | .makeFn = make, 26 | }), 27 | .generated_file = .{ 28 | .step = &self.step, 29 | }, 30 | .spec_path = spec_path, 31 | }; 32 | return self; 33 | } 34 | 35 | /// Initialize an OpenXR generation step for `builder`, by extracting xr.xml from the LunarG installation 36 | /// root. Typically, the location of the LunarG SDK root can be retrieved by querying for the OPENXR_SDK 37 | /// environment variable, set by activating the environment setup script located in the SDK root. 38 | /// `builder` and `out_path` are used in the same manner as `init`. 39 | pub fn createFromSdk(builder: *Build, sdk_path: []const u8, output_name: []const u8) *GenerateStep { 40 | const spec_path = std.fs.path.join( 41 | builder.allocator, 42 | &[_][]const u8{ sdk_path, "share/openxr/registry/xr.xml" }, 43 | ) catch unreachable; 44 | 45 | return create(builder, spec_path, output_name); 46 | } 47 | 48 | /// Returns the module with the generated bindings, with name `module_name`. 49 | pub fn getModule(self: *GenerateStep) *Build.Module { 50 | return self.step.owner.createModule(.{ 51 | .source_file = self.getSource(), 52 | }); 53 | } 54 | 55 | /// Returns the file source for the generated bindings. 56 | pub fn getSource(self: *GenerateStep) Build.FileSource { 57 | return .{ .generated = &self.generated_file }; 58 | } 59 | 60 | /// Internal build function. This reads `xr.xml`, and passes it to `generate`, which then generates 61 | /// the final bindings. The resulting generated bindings are not formatted, which is why an ArrayList 62 | /// writer is passed instead of a file writer. This is then formatted into standard formatting 63 | /// by parsing it and rendering with `std.zig.parse` and `std.zig.render` respectively. 64 | fn make(step: *Build.Step, progress: *std.Progress.Node) !void { 65 | _ = progress; 66 | const b = step.owner; 67 | const self: GenerateStep = @fieldParentPtr("step", step); 68 | const cwd = std.fs.cwd(); 69 | 70 | var man = b.cache.obtain(); 71 | defer man.deinit(); 72 | 73 | const spec = try cwd.readFileAlloc(b.allocator, self.spec_path, std.math.maxInt(usize)); 74 | // TODO: Look into whether this is the right way to be doing 75 | // this - maybe the file-level caching API has some benefits I 76 | // don't understand. 77 | man.hash.addBytes(spec); 78 | 79 | const already_exists = try step.cacheHit(&man); 80 | const digest = man.final(); 81 | const output_file_path = try b.cache_root.join(b.allocator, &.{ "o", &digest, "xr.zig" }); 82 | if (already_exists) { 83 | self.generated_file.path = output_file_path; 84 | return; 85 | } 86 | 87 | var out_buffer = std.ArrayList(u8).init(b.allocator); 88 | try generator.generate(b.allocator, spec, out_buffer.writer()); // catch |err| switch (err) { 89 | // error.InvalidXml => { 90 | // std.log.err("invalid openxr registry - invalid xml", .{}); 91 | // std.log.err("please check that the correct xr.xml file is passed", .{}); 92 | // return err; 93 | // }, 94 | // error.InvalidRegistry => { 95 | // std.log.err("invalid openxr registry - registry is valid xml but contents are invalid", .{}); 96 | // std.log.err("please check that the correct xr.xml file is passed", .{}); 97 | // return err; 98 | // }, 99 | // error.UnhandledBitfieldStruct => { 100 | // std.log.err("unhandled struct with bit fields detected in xr.xml", .{}); 101 | // std.log.err("this is a bug in openxr-zig", .{}); 102 | // std.log.err("please make a bug report at https://github.com/s-ol/openxr-zig/issues/", .{}); 103 | // return err; 104 | // }, 105 | // error.OutOfMemory => return error.OutOfMemory, 106 | //}; 107 | try out_buffer.append(0); 108 | 109 | const src = out_buffer.items[0 .. out_buffer.items.len - 1 :0]; 110 | const tree = try std.zig.Ast.parse(b.allocator, src, .zig); 111 | std.debug.assert(tree.errors.len == 0); // If this triggers, openxr-zig produced invalid code. 112 | 113 | const formatted = try tree.render(b.allocator); 114 | 115 | const output_dir_path = std.fs.path.dirname(output_file_path).?; 116 | cwd.makePath(output_dir_path) catch |err| { 117 | std.debug.print("unable to make path {s}: {s}\n", .{ output_dir_path, @errorName(err) }); 118 | return err; 119 | }; 120 | 121 | try cwd.writeFile(output_file_path, formatted); 122 | self.generated_file.path = output_file_path; 123 | try step.writeManifest(&man); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /generator/openxr/c_parse.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const registry = @import("registry.zig"); 3 | const xml = @import("../xml.zig"); 4 | const mem = std.mem; 5 | const Allocator = mem.Allocator; 6 | const testing = std.testing; 7 | const ArraySize = registry.Array.ArraySize; 8 | const TypeInfo = registry.TypeInfo; 9 | 10 | pub const Token = struct { 11 | kind: Kind, 12 | text: []const u8, 13 | 14 | const Kind = enum { 15 | id, // Any id thats not a keyword 16 | name, // OpenXR ... 17 | type_name, // OpenXR ... 18 | enum_name, // OpenXR ... 19 | int, 20 | star, 21 | comma, 22 | semicolon, 23 | colon, 24 | minus, 25 | tilde, 26 | dot, 27 | hash, 28 | lparen, 29 | rparen, 30 | lbracket, 31 | rbracket, 32 | kw_typedef, 33 | kw_const, 34 | kw_xrapi_ptr, 35 | kw_struct, 36 | }; 37 | }; 38 | 39 | pub const CTokenizer = struct { 40 | source: []const u8, 41 | offset: usize = 0, 42 | in_comment: bool = false, 43 | 44 | fn peek(self: CTokenizer) ?u8 { 45 | return if (self.offset < self.source.len) self.source[self.offset] else null; 46 | } 47 | 48 | fn consumeNoEof(self: *CTokenizer) u8 { 49 | const c = self.peek().?; 50 | self.offset += 1; 51 | return c; 52 | } 53 | 54 | fn consume(self: *CTokenizer) !u8 { 55 | return if (self.offset < self.source.len) 56 | return self.consumeNoEof() 57 | else 58 | return null; 59 | } 60 | 61 | fn keyword(self: *CTokenizer) Token { 62 | const start = self.offset; 63 | _ = self.consumeNoEof(); 64 | 65 | while (true) { 66 | const c = self.peek() orelse break; 67 | switch (c) { 68 | 'A'...'Z', 'a'...'z', '_', '0'...'9' => _ = self.consumeNoEof(), 69 | else => break, 70 | } 71 | } 72 | 73 | const token_text = self.source[start..self.offset]; 74 | 75 | const kind = if (mem.eql(u8, token_text, "typedef")) 76 | Token.Kind.kw_typedef 77 | else if (mem.eql(u8, token_text, "const")) 78 | Token.Kind.kw_const 79 | else if (mem.eql(u8, token_text, "XRAPI_PTR")) 80 | Token.Kind.kw_xrapi_ptr 81 | else if (mem.eql(u8, token_text, "struct")) 82 | Token.Kind.kw_struct 83 | else 84 | Token.Kind.id; 85 | 86 | return .{ .kind = kind, .text = token_text }; 87 | } 88 | 89 | fn int(self: *CTokenizer) Token { 90 | const start = self.offset; 91 | _ = self.consumeNoEof(); 92 | 93 | while (true) { 94 | const c = self.peek() orelse break; 95 | switch (c) { 96 | '0'...'9' => _ = self.consumeNoEof(), 97 | else => break, 98 | } 99 | } 100 | 101 | return .{ 102 | .kind = .int, 103 | .text = self.source[start..self.offset], 104 | }; 105 | } 106 | 107 | fn skipws(self: *CTokenizer) void { 108 | while (true) { 109 | switch (self.peek() orelse break) { 110 | ' ', '\t', '\n', '\r' => _ = self.consumeNoEof(), 111 | else => break, 112 | } 113 | } 114 | } 115 | 116 | pub fn next(self: *CTokenizer) !?Token { 117 | self.skipws(); 118 | if (mem.startsWith(u8, self.source[self.offset..], "//") or self.in_comment) { 119 | const end = mem.indexOfScalarPos(u8, self.source, self.offset, '\n') orelse { 120 | self.offset = self.source.len; 121 | self.in_comment = true; 122 | return null; 123 | }; 124 | self.in_comment = false; 125 | self.offset = end + 1; 126 | } 127 | self.skipws(); 128 | 129 | const c = self.peek() orelse return null; 130 | var kind: Token.Kind = undefined; 131 | switch (c) { 132 | 'A'...'Z', 'a'...'z', '_' => return self.keyword(), 133 | '0'...'9' => return self.int(), 134 | '*' => kind = .star, 135 | ',' => kind = .comma, 136 | ';' => kind = .semicolon, 137 | ':' => kind = .colon, 138 | '-' => kind = .minus, 139 | '~' => kind = .tilde, 140 | '.' => kind = .dot, 141 | '#' => kind = .hash, 142 | '[' => kind = .lbracket, 143 | ']' => kind = .rbracket, 144 | '(' => kind = .lparen, 145 | ')' => kind = .rparen, 146 | else => return error.UnexpectedCharacter, 147 | } 148 | 149 | const start = self.offset; 150 | _ = self.consumeNoEof(); 151 | return Token{ .kind = kind, .text = self.source[start..self.offset] }; 152 | } 153 | }; 154 | 155 | pub const XmlCTokenizer = struct { 156 | it: xml.Element.ChildIterator, 157 | ctok: ?CTokenizer = null, 158 | current: ?Token = null, 159 | 160 | pub fn init(elem: *xml.Element) XmlCTokenizer { 161 | return .{ 162 | .it = elem.iterator(), 163 | }; 164 | } 165 | 166 | fn elemToToken(elem: *xml.Element) !?Token { 167 | if (elem.children.len != 1 or elem.children[0] != .char_data) { 168 | return error.InvalidXml; 169 | } 170 | 171 | const text = elem.children[0].char_data; 172 | if (mem.eql(u8, elem.tag, "type")) { 173 | return Token{ .kind = .type_name, .text = text }; 174 | } else if (mem.eql(u8, elem.tag, "enum")) { 175 | return Token{ .kind = .enum_name, .text = text }; 176 | } else if (mem.eql(u8, elem.tag, "name")) { 177 | return Token{ .kind = .name, .text = text }; 178 | } else if (mem.eql(u8, elem.tag, "comment")) { 179 | return null; 180 | } else { 181 | return error.InvalidTag; 182 | } 183 | } 184 | 185 | fn next(self: *XmlCTokenizer) !?Token { 186 | if (self.current) |current| { 187 | const token = current; 188 | self.current = null; 189 | return token; 190 | } 191 | 192 | var in_comment: bool = false; 193 | 194 | while (true) { 195 | if (self.ctok) |*ctok| { 196 | if (try ctok.next()) |tok| { 197 | return tok; 198 | } 199 | in_comment = ctok.in_comment; 200 | } 201 | 202 | self.ctok = null; 203 | 204 | if (self.it.next()) |child| { 205 | switch (child.*) { 206 | .char_data => |cdata| self.ctok = CTokenizer{ .source = cdata, .in_comment = in_comment }, 207 | .comment => {}, // xml comment 208 | .element => |elem| if (!in_comment) if (try elemToToken(elem)) |tok| return tok, 209 | } 210 | } else { 211 | return null; 212 | } 213 | } 214 | } 215 | 216 | fn nextNoEof(self: *XmlCTokenizer) !Token { 217 | return (try self.next()) orelse return error.UnexpectedEof; 218 | } 219 | 220 | fn peek(self: *XmlCTokenizer) !?Token { 221 | if (self.current) |current| { 222 | return current; 223 | } 224 | 225 | self.current = try self.next(); 226 | return self.current; 227 | } 228 | 229 | fn peekNoEof(self: *XmlCTokenizer) !Token { 230 | return (try self.peek()) orelse return error.UnexpectedEof; 231 | } 232 | 233 | fn expect(self: *XmlCTokenizer, kind: Token.Kind) !Token { 234 | const tok = (try self.next()) orelse return error.UnexpectedEof; 235 | if (tok.kind != kind) { 236 | return error.UnexpectedToken; 237 | } 238 | 239 | return tok; 240 | } 241 | }; 242 | 243 | // TYPEDEF = kw_typedef DECLARATION ';' 244 | pub fn parseTypedef(allocator: Allocator, xctok: *XmlCTokenizer, ptrs_optional: bool) !registry.Declaration { 245 | const first_tok = (try xctok.next()) orelse return error.UnexpectedEof; 246 | 247 | _ = switch (first_tok.kind) { 248 | .kw_typedef => { 249 | const decl = try parseDeclaration(allocator, xctok, ptrs_optional); 250 | _ = try xctok.expect(.semicolon); 251 | if (try xctok.peek()) |_| { 252 | return error.InvalidSyntax; 253 | } 254 | 255 | return registry.Declaration{ 256 | .name = decl.name orelse return error.MissingTypeIdentifier, 257 | .decl_type = .{ .typedef = decl.decl_type }, 258 | }; 259 | }, 260 | .type_name => { 261 | if (!mem.eql(u8, first_tok.text, "XR_DEFINE_ATOM")) { 262 | return error.InvalidSyntax; 263 | } 264 | 265 | _ = try xctok.expect(.lparen); 266 | const name = try xctok.expect(.name); 267 | _ = try xctok.expect(.rparen); 268 | 269 | return registry.Declaration{ 270 | .name = name.text, 271 | .decl_type = .{ .typedef = TypeInfo{ .name = "uint64_t" } }, 272 | }; 273 | }, 274 | else => { 275 | std.debug.print("unexpected first token in typedef: {}\n", .{first_tok.kind}); 276 | return error.InvalidSyntax; 277 | }, 278 | }; 279 | } 280 | 281 | // MEMBER = DECLARATION (':' int)? 282 | pub fn parseMember(allocator: Allocator, xctok: *XmlCTokenizer, ptrs_optional: bool) !registry.Container.Field { 283 | const decl = try parseDeclaration(allocator, xctok, ptrs_optional); 284 | var field = registry.Container.Field{ 285 | .name = decl.name orelse return error.MissingTypeIdentifier, 286 | .field_type = decl.decl_type, 287 | .bits = null, 288 | .is_buffer_len = false, 289 | .is_optional = false, 290 | }; 291 | 292 | if (try xctok.peek()) |tok| { 293 | if (tok.kind != .colon) { 294 | return error.InvalidSyntax; 295 | } 296 | 297 | _ = try xctok.nextNoEof(); 298 | const bits = try xctok.expect(.int); 299 | field.bits = try std.fmt.parseInt(usize, bits.text, 10); 300 | 301 | // Assume for now that there won't be any invalid C types like `char char* x : 4`. 302 | 303 | if (try xctok.peek()) |_| { 304 | return error.InvalidSyntax; 305 | } 306 | } 307 | 308 | return field; 309 | } 310 | 311 | pub fn parseParamOrProto(allocator: Allocator, xctok: *XmlCTokenizer, ptrs_optional: bool) !registry.Declaration { 312 | var decl = try parseDeclaration(allocator, xctok, ptrs_optional); 313 | if (try xctok.peek()) |_| { 314 | return error.InvalidSyntax; 315 | } 316 | 317 | // Decay pointers 318 | switch (decl.decl_type) { 319 | .array => { 320 | const child = try allocator.create(TypeInfo); 321 | child.* = decl.decl_type; 322 | 323 | decl.decl_type = .{ 324 | .pointer = .{ 325 | .is_const = decl.is_const, 326 | .is_optional = false, 327 | .size = .one, 328 | .child = child, 329 | }, 330 | }; 331 | }, 332 | else => {}, 333 | } 334 | 335 | return registry.Declaration{ 336 | .name = decl.name orelse return error.MissingTypeIdentifier, 337 | .decl_type = .{ .typedef = decl.decl_type }, 338 | }; 339 | } 340 | 341 | pub const Declaration = struct { 342 | name: ?[]const u8, // Parameter names may be optional, especially in case of func(void) 343 | decl_type: TypeInfo, 344 | is_const: bool, 345 | }; 346 | 347 | pub const ParseError = error{ 348 | OutOfMemory, 349 | InvalidSyntax, 350 | InvalidTag, 351 | InvalidXml, 352 | Overflow, 353 | UnexpectedEof, 354 | UnexpectedCharacter, 355 | UnexpectedToken, 356 | MissingTypeIdentifier, 357 | }; 358 | 359 | // DECLARATION = kw_const? type_name DECLARATOR 360 | // DECLARATOR = POINTERS (id | name)? ('[' ARRAY_DECLARATOR ']')* 361 | // | POINTERS '(' FNPTRSUFFIX 362 | fn parseDeclaration(allocator: Allocator, xctok: *XmlCTokenizer, ptrs_optional: bool) ParseError!Declaration { 363 | // Parse declaration constness 364 | var tok = try xctok.nextNoEof(); 365 | const inner_is_const = tok.kind == .kw_const; 366 | if (inner_is_const) { 367 | tok = try xctok.nextNoEof(); 368 | } 369 | 370 | if (tok.kind == .kw_struct) { 371 | tok = try xctok.nextNoEof(); 372 | } 373 | // Parse type name 374 | if (tok.kind != .type_name and tok.kind != .id) return error.InvalidSyntax; 375 | const type_name = tok.text; 376 | 377 | var type_info = TypeInfo{ .name = type_name }; 378 | 379 | // Parse pointers 380 | type_info = try parsePointers(allocator, xctok, inner_is_const, type_info, ptrs_optional); 381 | 382 | // Parse name / fn ptr 383 | 384 | if (try parseFnPtrSuffix(allocator, xctok, type_info, ptrs_optional)) |decl| { 385 | return Declaration{ 386 | .name = decl.name, 387 | .decl_type = decl.decl_type, 388 | .is_const = inner_is_const, 389 | }; 390 | } 391 | 392 | const name = blk: { 393 | const name_tok = (try xctok.peek()) orelse break :blk null; 394 | if (name_tok.kind == .id or name_tok.kind == .name) { 395 | _ = try xctok.nextNoEof(); 396 | break :blk name_tok.text; 397 | } else { 398 | break :blk null; 399 | } 400 | }; 401 | 402 | var inner_type = &type_info; 403 | while (try parseArrayDeclarator(xctok)) |array_size| { 404 | // Move the current inner type to a new node on the heap 405 | const child = try allocator.create(TypeInfo); 406 | child.* = inner_type.*; 407 | 408 | // Re-assign the previous inner type for the array type info node 409 | inner_type.* = .{ 410 | .array = .{ 411 | .size = array_size, 412 | .child = child, 413 | }, 414 | }; 415 | 416 | // update the inner_type pointer so it points to the proper 417 | // inner type again 418 | inner_type = child; 419 | } 420 | 421 | return Declaration{ 422 | .name = name, 423 | .decl_type = type_info, 424 | .is_const = inner_is_const, 425 | }; 426 | } 427 | 428 | // FNPTRSUFFIX = kw_xrapi_ptr '*' name' ')' '(' ('void' | (DECLARATION (',' DECLARATION)*)?) ')' 429 | fn parseFnPtrSuffix(allocator: Allocator, xctok: *XmlCTokenizer, return_type: TypeInfo, ptrs_optional: bool) !?Declaration { 430 | const lparen = try xctok.peek(); 431 | if (lparen == null or lparen.?.kind != .lparen) { 432 | return null; 433 | } 434 | _ = try xctok.nextNoEof(); 435 | _ = try xctok.expect(.kw_xrapi_ptr); 436 | _ = try xctok.expect(.star); 437 | const name = try xctok.expect(.name); 438 | _ = try xctok.expect(.rparen); 439 | _ = try xctok.expect(.lparen); 440 | 441 | const return_type_heap = try allocator.create(TypeInfo); 442 | return_type_heap.* = return_type; 443 | 444 | var command_ptr = Declaration{ 445 | .name = name.text, 446 | .decl_type = .{ 447 | .command_ptr = .{ 448 | .params = &[_]registry.Command.Param{}, 449 | .return_type = return_type_heap, 450 | .success_codes = &[_][]const u8{}, 451 | .error_codes = &[_][]const u8{}, 452 | }, 453 | }, 454 | .is_const = false, 455 | }; 456 | 457 | const first_param = try parseDeclaration(allocator, xctok, ptrs_optional); 458 | if (first_param.name == null) { 459 | if (first_param.decl_type != .name or !mem.eql(u8, first_param.decl_type.name, "void")) { 460 | return error.InvalidSyntax; 461 | } 462 | 463 | _ = try xctok.expect(.rparen); 464 | return command_ptr; 465 | } 466 | 467 | // There is no good way to estimate the number of parameters beforehand. 468 | // Fortunately, there are usually a relatively low number of parameters to a function pointer, 469 | // so an ArrayList backed by an arena allocator is good enough. 470 | var params = std.ArrayList(registry.Command.Param).init(allocator); 471 | try params.append(.{ 472 | .name = first_param.name.?, 473 | .param_type = first_param.decl_type, 474 | .is_buffer_len = false, 475 | }); 476 | 477 | while (true) { 478 | switch ((try xctok.peekNoEof()).kind) { 479 | .rparen => break, 480 | .comma => _ = try xctok.nextNoEof(), 481 | else => return error.InvalidSyntax, 482 | } 483 | 484 | const decl = try parseDeclaration(allocator, xctok, ptrs_optional); 485 | try params.append(.{ 486 | .name = decl.name orelse return error.MissingTypeIdentifier, 487 | .param_type = decl.decl_type, 488 | .is_buffer_len = false, 489 | }); 490 | } 491 | 492 | _ = try xctok.nextNoEof(); 493 | command_ptr.decl_type.command_ptr.params = try params.toOwnedSlice(); 494 | return command_ptr; 495 | } 496 | 497 | // POINTERS = (kw_const? '*')* 498 | fn parsePointers(allocator: Allocator, xctok: *XmlCTokenizer, inner_const: bool, inner: TypeInfo, ptrs_optional: bool) !TypeInfo { 499 | var type_info = inner; 500 | var first_const = inner_const; 501 | 502 | while (true) { 503 | var tok = (try xctok.peek()) orelse return type_info; 504 | var is_const = first_const; 505 | first_const = false; 506 | 507 | if (tok.kind == .kw_const) { 508 | is_const = true; 509 | _ = try xctok.nextNoEof(); 510 | tok = (try xctok.peek()) orelse return type_info; 511 | } 512 | 513 | if (tok.kind != .star) { 514 | // if `is_const` is true at this point, there was a trailing const, 515 | // and the declaration itself is const. 516 | return type_info; 517 | } 518 | 519 | _ = try xctok.nextNoEof(); 520 | 521 | const child = try allocator.create(TypeInfo); 522 | child.* = type_info; 523 | 524 | type_info = .{ 525 | .pointer = .{ 526 | .is_const = is_const or first_const, 527 | .is_optional = ptrs_optional, // set elsewhere 528 | .size = .one, // set elsewhere 529 | .child = child, 530 | }, 531 | }; 532 | } 533 | } 534 | 535 | // ARRAY_DECLARATOR = '[' (int | enum_name) ']' 536 | fn parseArrayDeclarator(xctok: *XmlCTokenizer) !?ArraySize { 537 | const lbracket = try xctok.peek(); 538 | if (lbracket == null or lbracket.?.kind != .lbracket) { 539 | return null; 540 | } 541 | 542 | _ = try xctok.nextNoEof(); 543 | 544 | const size_tok = try xctok.nextNoEof(); 545 | const size: ArraySize = switch (size_tok.kind) { 546 | .int => .{ 547 | .int = std.fmt.parseInt(usize, size_tok.text, 10) catch |err| switch (err) { 548 | error.Overflow => return error.Overflow, 549 | error.InvalidCharacter => unreachable, 550 | }, 551 | }, 552 | .enum_name => .{ .alias = size_tok.text }, 553 | .id => .{ .alias = size_tok.text }, 554 | else => return error.InvalidSyntax, 555 | }; 556 | 557 | _ = try xctok.expect(.rbracket); 558 | return size; 559 | } 560 | 561 | pub fn parseVersion(xctok: *XmlCTokenizer) ![3][]const u8 { 562 | _ = try xctok.expect(.hash); 563 | const define = try xctok.expect(.id); 564 | if (!mem.eql(u8, define.text, "define")) { 565 | return error.InvalidVersion; 566 | } 567 | 568 | _ = try xctok.expect(.name); 569 | const xr_make_version = try xctok.expect(.type_name); 570 | if (!mem.eql(u8, xr_make_version.text, "XR_MAKE_VERSION")) { 571 | return error.NotVersion; 572 | } 573 | 574 | _ = try xctok.expect(.lparen); 575 | var version: [3][]const u8 = undefined; 576 | for (&version, 0..) |*part, i| { 577 | if (i != 0) { 578 | _ = try xctok.expect(.comma); 579 | } 580 | 581 | const tok = try xctok.nextNoEof(); 582 | switch (tok.kind) { 583 | .id, .int => part.* = tok.text, 584 | else => return error.UnexpectedToken, 585 | } 586 | } 587 | _ = try xctok.expect(.rparen); 588 | return version; 589 | } 590 | 591 | fn testTokenizer(tokenizer: anytype, expected_tokens: []const Token) !void { 592 | for (expected_tokens) |expected| { 593 | const tok = (tokenizer.next() catch unreachable).?; 594 | try testing.expectEqual(expected.kind, tok.kind); 595 | try testing.expectEqualSlices(u8, expected.text, tok.text); 596 | } 597 | 598 | if (tokenizer.next() catch unreachable) |_| unreachable; 599 | } 600 | 601 | test "CTokenizer" { 602 | var ctok = CTokenizer{ .source = "typedef ([const)]** XRAPI_PTR 123,;aaaa" }; 603 | 604 | try testTokenizer(&ctok, &[_]Token{ 605 | .{ .kind = .kw_typedef, .text = "typedef" }, 606 | .{ .kind = .lparen, .text = "(" }, 607 | .{ .kind = .lbracket, .text = "[" }, 608 | .{ .kind = .kw_const, .text = "const" }, 609 | .{ .kind = .rparen, .text = ")" }, 610 | .{ .kind = .rbracket, .text = "]" }, 611 | .{ .kind = .star, .text = "*" }, 612 | .{ .kind = .star, .text = "*" }, 613 | .{ .kind = .kw_xrapi_ptr, .text = "XRAPI_PTR" }, 614 | .{ .kind = .int, .text = "123" }, 615 | .{ .kind = .comma, .text = "," }, 616 | .{ .kind = .semicolon, .text = ";" }, 617 | .{ .kind = .id, .text = "aaaa" }, 618 | }); 619 | } 620 | 621 | test "XmlCTokenizer" { 622 | const document = try xml.parse(testing.allocator, 623 | \\// comment commented name commented type trailing 624 | \\ typedef void (XRAPI_PTR *PFN_xrVoidFunction)(void); 625 | \\ 626 | ); 627 | defer document.deinit(); 628 | 629 | var xctok = XmlCTokenizer.init(document.root); 630 | 631 | try testTokenizer(&xctok, &[_]Token{ 632 | .{ .kind = .kw_typedef, .text = "typedef" }, 633 | .{ .kind = .id, .text = "void" }, 634 | .{ .kind = .lparen, .text = "(" }, 635 | .{ .kind = .kw_xrapi_ptr, .text = "XRAPI_PTR" }, 636 | .{ .kind = .star, .text = "*" }, 637 | .{ .kind = .name, .text = "PFN_xrVoidFunction" }, 638 | .{ .kind = .rparen, .text = ")" }, 639 | .{ .kind = .lparen, .text = "(" }, 640 | .{ .kind = .id, .text = "void" }, 641 | .{ .kind = .rparen, .text = ")" }, 642 | .{ .kind = .semicolon, .text = ";" }, 643 | }); 644 | } 645 | 646 | test "parseTypedef" { 647 | const document = try xml.parse(testing.allocator, 648 | \\ // comment commented name trailing 649 | \\ typedef const struct Python* pythons[4]; 650 | \\ // more comments 651 | \\ 652 | \\ 653 | ); 654 | defer document.deinit(); 655 | 656 | var arena = std.heap.ArenaAllocator.init(testing.allocator); 657 | defer arena.deinit(); 658 | 659 | var xctok = XmlCTokenizer.init(document.root); 660 | const decl = try parseTypedef(&arena.allocator, &xctok, false); 661 | 662 | try testing.expectEqualSlices(u8, "pythons", decl.name); 663 | const array = decl.decl_type.typedef.array; 664 | try testing.expectEqual(ArraySize{ .int = 4 }, array.size); 665 | const ptr = array.child.pointer; 666 | try testing.expectEqual(true, ptr.is_const); 667 | try testing.expectEqualSlices(u8, "Python", ptr.child.name); 668 | } 669 | -------------------------------------------------------------------------------- /generator/openxr/generator.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const reg = @import("registry.zig"); 3 | const xml = @import("../xml.zig"); 4 | const renderRegistry = @import("render.zig").render; 5 | const parseXml = @import("parse.zig").parseXml; 6 | const IdRenderer = @import("../id_render.zig").IdRenderer; 7 | const mem = std.mem; 8 | const Allocator = mem.Allocator; 9 | const FeatureLevel = reg.FeatureLevel; 10 | 11 | const EnumFieldMerger = struct { 12 | const EnumExtensionMap = std.StringArrayHashMapUnmanaged(std.ArrayListUnmanaged(reg.Enum.Field)); 13 | const FieldSet = std.StringArrayHashMapUnmanaged(void); 14 | 15 | arena: Allocator, 16 | registry: *reg.Registry, 17 | enum_extensions: EnumExtensionMap, 18 | field_set: FieldSet, 19 | 20 | fn init(arena: Allocator, registry: *reg.Registry) EnumFieldMerger { 21 | return .{ 22 | .arena = arena, 23 | .registry = registry, 24 | .enum_extensions = .{}, 25 | .field_set = .{}, 26 | }; 27 | } 28 | 29 | fn putEnumExtension(self: *EnumFieldMerger, enum_name: []const u8, field: reg.Enum.Field) !void { 30 | const res = try self.enum_extensions.getOrPut(self.arena, enum_name); 31 | if (!res.found_existing) { 32 | res.value_ptr.* = std.ArrayListUnmanaged(reg.Enum.Field){}; 33 | } 34 | 35 | try res.value_ptr.append(self.arena, field); 36 | } 37 | 38 | fn addRequires(self: *EnumFieldMerger, reqs: []const reg.Require) !void { 39 | for (reqs) |req| { 40 | for (req.extends) |enum_ext| { 41 | try self.putEnumExtension(enum_ext.extends, enum_ext.field); 42 | } 43 | } 44 | } 45 | 46 | fn mergeEnumFields(self: *EnumFieldMerger, name: []const u8, base_enum: *reg.Enum) !void { 47 | // If there are no extensions for this enum, assume its valid. 48 | const extensions = self.enum_extensions.get(name) orelse return; 49 | 50 | self.field_set.clearRetainingCapacity(); 51 | 52 | const n_fields_upper_bound = base_enum.fields.len + extensions.items.len; 53 | const new_fields = try self.arena.alloc(reg.Enum.Field, n_fields_upper_bound); 54 | var i: usize = 0; 55 | 56 | for (base_enum.fields) |field| { 57 | const res = try self.field_set.getOrPut(self.arena, field.name); 58 | if (!res.found_existing) { 59 | new_fields[i] = field; 60 | i += 1; 61 | } 62 | } 63 | 64 | // Assume that if a field name clobbers, the value is the same 65 | for (extensions.items) |field| { 66 | const res = try self.field_set.getOrPut(self.arena, field.name); 67 | if (!res.found_existing) { 68 | new_fields[i] = field; 69 | i += 1; 70 | } 71 | } 72 | 73 | // Existing base_enum.fields was allocated by `self.arena`, so 74 | // it gets cleaned up whenever that is deinited. 75 | base_enum.fields = new_fields[0..i]; 76 | } 77 | 78 | fn merge(self: *EnumFieldMerger) !void { 79 | for (self.registry.features) |feature| { 80 | try self.addRequires(feature.requires); 81 | } 82 | 83 | for (self.registry.extensions) |ext| { 84 | try self.addRequires(ext.requires); 85 | } 86 | 87 | // Merge all the enum fields. 88 | // Assume that all keys of enum_extensions appear in `self.registry.decls` 89 | for (self.registry.decls) |*decl| { 90 | if (decl.decl_type == .enumeration) { 91 | try self.mergeEnumFields(decl.name, &decl.decl_type.enumeration); 92 | } 93 | } 94 | } 95 | }; 96 | 97 | pub const Generator = struct { 98 | arena: std.heap.ArenaAllocator, 99 | registry: reg.Registry, 100 | id_renderer: IdRenderer, 101 | 102 | fn init(allocator: Allocator, spec: *xml.Element) !Generator { 103 | const result = try parseXml(allocator, spec); 104 | 105 | const tags = try allocator.alloc([]const u8, result.registry.tags.len); 106 | for (tags, result.registry.tags) |*tag, registry_tag| tag.* = registry_tag.name; 107 | 108 | return Generator{ 109 | .arena = result.arena, 110 | .registry = result.registry, 111 | .id_renderer = IdRenderer.init(allocator, tags), 112 | }; 113 | } 114 | 115 | fn deinit(self: Generator) void { 116 | self.arena.deinit(); 117 | } 118 | 119 | fn removePromotedExtensions(self: *Generator) void { 120 | var write_index: usize = 0; 121 | for (self.registry.extensions) |ext| { 122 | if (ext.promoted_to == .none) { 123 | self.registry.extensions[write_index] = ext; 124 | write_index += 1; 125 | } 126 | } 127 | self.registry.extensions.len = write_index; 128 | } 129 | 130 | fn stripFlagBits(self: Generator, name: []const u8) []const u8 { 131 | const tagless = self.id_renderer.stripAuthorTag(name); 132 | return tagless[0 .. tagless.len - "FlagBits".len]; 133 | } 134 | 135 | fn stripFlags(self: Generator, name: []const u8) []const u8 { 136 | const tagless = self.id_renderer.stripAuthorTag(name); 137 | return tagless[0 .. tagless.len - "Flags64".len]; 138 | } 139 | 140 | // Solve `registry.declarations` according to `registry.extensions` and `registry.features`. 141 | fn mergeEnumFields(self: *Generator) !void { 142 | var merger = EnumFieldMerger.init(self.arena.allocator(), &self.registry); 143 | try merger.merge(); 144 | } 145 | 146 | fn render(self: *Generator, writer: anytype) !void { 147 | try renderRegistry(writer, self.arena.allocator(), &self.registry, &self.id_renderer); 148 | } 149 | }; 150 | 151 | /// Main function for generating the OpenXR bindings. xr.xml is to be provided via `spec_xml`, 152 | /// and the resulting binding is written to `writer`. `allocator` will be used to allocate temporary 153 | /// internal datastructures - mostly via an ArenaAllocator, but sometimes a hashmap uses this allocator 154 | /// directly. 155 | pub fn generate(allocator: Allocator, spec_xml: []const u8, writer: anytype) !void { 156 | const spec = xml.parse(allocator, spec_xml) catch |err| switch (err) { 157 | error.InvalidDocument, 158 | error.UnexpectedEof, 159 | error.UnexpectedCharacter, 160 | error.IllegalCharacter, 161 | error.InvalidEntity, 162 | error.InvalidName, 163 | error.InvalidStandaloneValue, 164 | error.NonMatchingClosingTag, 165 | error.UnclosedComment, 166 | error.UnclosedValue, 167 | => return error.InvalidXml, 168 | error.OutOfMemory => return error.OutOfMemory, 169 | }; 170 | defer spec.deinit(); 171 | 172 | var gen = Generator.init(allocator, spec.root) catch |err| switch (err) { 173 | error.InvalidXml, 174 | error.InvalidCharacter, 175 | error.Overflow, 176 | error.InvalidFeatureLevel, 177 | error.InvalidSyntax, 178 | error.InvalidTag, 179 | error.MissingTypeIdentifier, 180 | error.UnexpectedCharacter, 181 | error.UnexpectedEof, 182 | error.UnexpectedToken, 183 | error.InvalidRegistry, 184 | => return error.InvalidRegistry, 185 | error.OutOfMemory => return error.OutOfMemory, 186 | }; 187 | defer gen.deinit(); 188 | 189 | gen.removePromotedExtensions(); 190 | try gen.mergeEnumFields(); 191 | gen.render(writer) catch |err| switch (err) { 192 | error.InvalidApiConstant, 193 | error.InvalidConstantExpr, 194 | error.InvalidRegistry, 195 | error.UnexpectedCharacter, 196 | => return error.InvalidRegistry, 197 | else => |others| return others, 198 | }; 199 | } 200 | -------------------------------------------------------------------------------- /generator/openxr/parse.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const registry = @import("registry.zig"); 3 | const xml = @import("../xml.zig"); 4 | const cparse = @import("c_parse.zig"); 5 | const mem = std.mem; 6 | const Allocator = mem.Allocator; 7 | const ArenaAllocator = std.heap.ArenaAllocator; 8 | 9 | const api_constants_name = "API Constants"; 10 | 11 | pub const ParseResult = struct { 12 | arena: ArenaAllocator, 13 | registry: registry.Registry, 14 | 15 | pub fn deinit(self: ParseResult) void { 16 | self.arena.deinit(); 17 | } 18 | }; 19 | 20 | pub fn parseXml(backing_allocator: Allocator, root: *xml.Element) !ParseResult { 21 | var arena = ArenaAllocator.init(backing_allocator); 22 | errdefer arena.deinit(); 23 | 24 | const allocator = arena.allocator(); 25 | 26 | const reg = registry.Registry{ 27 | .decls = try parseDeclarations(allocator, root), 28 | .api_constants = try parseApiConstants(allocator, root), 29 | .tags = try parseTags(allocator, root), 30 | .features = try parseFeatures(allocator, root), 31 | .extensions = try parseExtensions(allocator, root), 32 | }; 33 | 34 | return ParseResult{ 35 | .arena = arena, 36 | .registry = reg, 37 | }; 38 | } 39 | 40 | fn parseDeclarations(allocator: Allocator, root: *xml.Element) ![]registry.Declaration { 41 | const types_elem = root.findChildByTag("types") orelse return error.InvalidRegistry; 42 | const commands_elem = root.findChildByTag("commands") orelse return error.InvalidRegistry; 43 | 44 | const decl_upper_bound = types_elem.children.len + commands_elem.children.len; 45 | const decls = try allocator.alloc(registry.Declaration, decl_upper_bound); 46 | 47 | var count: usize = 0; 48 | count += try parseTypes(allocator, decls, types_elem); 49 | count += try parseEnums(allocator, decls[count..], root); 50 | count += try parseCommands(allocator, decls[count..], commands_elem); 51 | return decls[0..count]; 52 | } 53 | 54 | fn parseTypes(allocator: Allocator, out: []registry.Declaration, types_elem: *xml.Element) !usize { 55 | var i: usize = 0; 56 | var it = types_elem.findChildrenByTag("type"); 57 | while (it.next()) |ty| { 58 | out[i] = blk: { 59 | const category = ty.getAttribute("category") orelse { 60 | break :blk try parseForeigntype(ty); 61 | }; 62 | 63 | if (mem.eql(u8, category, "bitmask")) { 64 | break :blk try parseBitmaskType(ty); 65 | } else if (mem.eql(u8, category, "handle")) { 66 | break :blk try parseHandleType(ty); 67 | } else if (mem.eql(u8, category, "basetype")) { 68 | break :blk try parseBaseType(allocator, ty); 69 | } else if (mem.eql(u8, category, "struct")) { 70 | break :blk try parseContainer(allocator, ty, false); 71 | } else if (mem.eql(u8, category, "union")) { 72 | break :blk try parseContainer(allocator, ty, true); 73 | } else if (mem.eql(u8, category, "funcpointer")) { 74 | break :blk try parseFuncPointer(allocator, ty); 75 | } else if (mem.eql(u8, category, "enum")) { 76 | break :blk (try parseEnumAlias(ty)) orelse continue; 77 | } 78 | 79 | continue; 80 | }; 81 | 82 | i += 1; 83 | } 84 | 85 | return i; 86 | } 87 | 88 | fn parseForeigntype(ty: *xml.Element) !registry.Declaration { 89 | const name = ty.getAttribute("name") orelse return error.InvalidRegistry; 90 | const depends = ty.getAttribute("requires") orelse if (mem.eql(u8, name, "int")) 91 | "openxr_platform_defines" // for some reason, int doesn't depend on xr_platform (but the other c types do) 92 | else 93 | return error.InvalidRegistry; 94 | 95 | return registry.Declaration{ 96 | .name = name, 97 | .decl_type = .{ .foreign = .{ .depends = depends } }, 98 | }; 99 | } 100 | 101 | fn parseBitmaskType(ty: *xml.Element) !registry.Declaration { 102 | if (ty.getAttribute("name")) |name| { 103 | const alias = ty.getAttribute("alias") orelse return error.InvalidRegistry; 104 | return registry.Declaration{ 105 | .name = name, 106 | .decl_type = .{ .alias = .{ .name = alias, .target = .other_type } }, 107 | }; 108 | } else { 109 | return registry.Declaration{ 110 | .name = ty.getCharData("name") orelse return error.InvalidRegistry, 111 | .decl_type = .{ .bitmask = .{ .bits_enum = ty.getAttribute("bitvalues") } }, 112 | }; 113 | } 114 | } 115 | 116 | fn parseHandleType(ty: *xml.Element) !registry.Declaration { 117 | // Parent is not handled in case of an alias 118 | if (ty.getAttribute("name")) |name| { 119 | const alias = ty.getAttribute("alias") orelse return error.InvalidRegistry; 120 | return registry.Declaration{ 121 | .name = name, 122 | .decl_type = .{ .alias = .{ .name = alias, .target = .other_type } }, 123 | }; 124 | } else { 125 | const name = ty.getCharData("name") orelse return error.InvalidRegistry; 126 | const handle_type = ty.getCharData("type") orelse return error.InvalidRegistry; 127 | const dispatchable = mem.eql(u8, handle_type, "XR_DEFINE_HANDLE"); 128 | if (!dispatchable and !mem.eql(u8, handle_type, "XR_DEFINE_NON_DISPATCHABLE_HANDLE")) { 129 | return error.InvalidRegistry; 130 | } 131 | 132 | return registry.Declaration{ 133 | .name = name, 134 | .decl_type = .{ 135 | .handle = .{ 136 | .parent = ty.getAttribute("parent"), 137 | .is_dispatchable = dispatchable, 138 | }, 139 | }, 140 | }; 141 | } 142 | } 143 | 144 | fn parseBaseType(allocator: Allocator, ty: *xml.Element) !registry.Declaration { 145 | const name = ty.getCharData("name") orelse return error.InvalidRegistry; 146 | if (ty.getCharData("type")) |_| { 147 | var tok = cparse.XmlCTokenizer.init(ty); 148 | return try cparse.parseTypedef(allocator, &tok, false); 149 | } else { 150 | // Either ANativeWindow, AHardwareBuffer or CAMetalLayer. The latter has a lot of 151 | // macros, which is why this part is not built into the xml/c parser. 152 | return registry.Declaration{ 153 | .name = name, 154 | .decl_type = .{ .foreign = .{ .depends = &.{} } }, 155 | }; 156 | } 157 | } 158 | 159 | fn parseContainer(allocator: Allocator, ty: *xml.Element, is_union: bool) !registry.Declaration { 160 | const name = ty.getAttribute("name") orelse return error.InvalidRegistry; 161 | 162 | if (ty.getAttribute("alias")) |alias| { 163 | return registry.Declaration{ 164 | .name = name, 165 | .decl_type = .{ .alias = .{ .name = alias, .target = .other_type } }, 166 | }; 167 | } 168 | 169 | var members = try allocator.alloc(registry.Container.Field, ty.children.len); 170 | 171 | var i: usize = 0; 172 | var it = ty.findChildrenByTag("member"); 173 | var maybe_stype: ?[]const u8 = null; 174 | while (it.next()) |member| { 175 | var xctok = cparse.XmlCTokenizer.init(member); 176 | members[i] = try cparse.parseMember(allocator, &xctok, false); 177 | if (mem.eql(u8, members[i].name, "type")) { 178 | if (member.getAttribute("values")) |stype| { 179 | maybe_stype = stype; 180 | } 181 | } 182 | 183 | if (member.getAttribute("optional")) |optionals| { 184 | var optional_it = mem.splitScalar(u8, optionals, ','); 185 | if (optional_it.next()) |first_optional| { 186 | members[i].is_optional = mem.eql(u8, first_optional, "true"); 187 | } else { 188 | // Optional is empty, probably incorrect. 189 | return error.InvalidRegistry; 190 | } 191 | } 192 | i += 1; 193 | } 194 | 195 | members = members[0..i]; 196 | 197 | var maybe_extends: ?[][]const u8 = null; 198 | if (ty.getAttribute("structextends")) |extends| { 199 | const n_structs = std.mem.count(u8, extends, ",") + 1; 200 | maybe_extends = try allocator.alloc([]const u8, n_structs); 201 | var struct_extends = std.mem.splitScalar(u8, extends, ','); 202 | var j: usize = 0; 203 | while (struct_extends.next()) |struct_extend| { 204 | maybe_extends.?[j] = struct_extend; 205 | j += 1; 206 | } 207 | } 208 | 209 | it = ty.findChildrenByTag("member"); 210 | for (members) |*member| { 211 | const member_elem = it.next().?; 212 | try parsePointerMeta(.{ .container = members }, &member.field_type, member_elem); 213 | 214 | // next isn't always properly marked as optional, so just manually override it, 215 | if (mem.eql(u8, member.name, "next")) { 216 | member.field_type.pointer.is_optional = true; 217 | } 218 | } 219 | 220 | return registry.Declaration{ 221 | .name = name, 222 | .decl_type = .{ 223 | .container = .{ 224 | .stype = maybe_stype, 225 | .fields = members, 226 | .is_union = is_union, 227 | .extends = maybe_extends, 228 | }, 229 | }, 230 | }; 231 | } 232 | 233 | fn parseFuncPointer(allocator: Allocator, ty: *xml.Element) !registry.Declaration { 234 | var xctok = cparse.XmlCTokenizer.init(ty); 235 | return try cparse.parseTypedef(allocator, &xctok, true); 236 | } 237 | 238 | // For some reason, the DeclarationType cannot be passed to lenToPointer, as 239 | // that causes the Zig compiler to generate invalid code for the function. Using a 240 | // dedicated enum fixes the issue... 241 | const Fields = union(enum) { 242 | command: []registry.Command.Param, 243 | container: []registry.Container.Field, 244 | }; 245 | 246 | // returns .{ size, nullable } 247 | fn lenToPointer(fields: Fields, len: []const u8) std.meta.Tuple(&.{ registry.Pointer.PointerSize, bool }) { 248 | switch (fields) { 249 | .command => |params| { 250 | for (params) |*param| { 251 | if (mem.eql(u8, param.name, len)) { 252 | param.is_buffer_len = true; 253 | return .{ .{ .other_field = param.name }, false }; 254 | } 255 | } 256 | }, 257 | .container => |members| { 258 | for (members) |*member| { 259 | if (mem.eql(u8, member.name, len)) { 260 | member.is_buffer_len = true; 261 | return .{ .{ .other_field = member.name }, member.is_optional }; 262 | } 263 | } 264 | }, 265 | } 266 | 267 | if (mem.eql(u8, len, "null-terminated")) { 268 | return .{ .zero_terminated, false }; 269 | } else { 270 | return .{ .many, false }; 271 | } 272 | } 273 | 274 | fn parsePointerMeta(fields: Fields, type_info: *registry.TypeInfo, elem: *xml.Element) !void { 275 | if (elem.getAttribute("len")) |lens| { 276 | var it = mem.splitScalar(u8, lens, ','); 277 | var current_type_info = type_info; 278 | while (current_type_info.* == .pointer) { 279 | // TODO: Check altlen 280 | const size = if (it.next()) |len_str| blk: { 281 | const size_optional = lenToPointer(fields, len_str); 282 | current_type_info.pointer.is_optional = size_optional[1]; 283 | break :blk size_optional[0]; 284 | } else .many; 285 | current_type_info.pointer.size = size; 286 | current_type_info = current_type_info.pointer.child; 287 | } 288 | 289 | if (it.next()) |_| { 290 | // There are more elements in the `len` attribute than there are pointers 291 | // Something probably went wrong 292 | std.log.err("len: {s}", .{lens}); 293 | return error.InvalidRegistry; 294 | } 295 | } 296 | 297 | if (elem.getAttribute("optional")) |optionals| { 298 | var it = mem.splitScalar(u8, optionals, ','); 299 | var current_type_info = type_info; 300 | while (current_type_info.* == .pointer) { 301 | if (it.next()) |current_optional| { 302 | current_type_info.pointer.is_optional = mem.eql(u8, current_optional, "true"); 303 | } else { 304 | current_type_info.pointer.is_optional = true; 305 | // There is no information for this pointer, probably incorrect. 306 | // return error.InvalidRegistry; 307 | } 308 | 309 | current_type_info = current_type_info.pointer.child; 310 | } 311 | } else if (mem.eql(u8, elem.getCharData("name") orelse "", "next")) { 312 | type_info.pointer.is_optional = true; 313 | } 314 | } 315 | 316 | fn parseEnumAlias(elem: *xml.Element) !?registry.Declaration { 317 | if (elem.getAttribute("alias")) |alias| { 318 | const name = elem.getAttribute("name") orelse return error.InvalidRegistry; 319 | return registry.Declaration{ 320 | .name = name, 321 | .decl_type = .{ .alias = .{ .name = alias, .target = .other_type } }, 322 | }; 323 | } 324 | 325 | return null; 326 | } 327 | 328 | fn parseEnums(allocator: Allocator, out: []registry.Declaration, root: *xml.Element) !usize { 329 | var i: usize = 0; 330 | var it = root.findChildrenByTag("enums"); 331 | while (it.next()) |enums| { 332 | const name = enums.getAttribute("name") orelse return error.InvalidRegistry; 333 | if (mem.eql(u8, name, api_constants_name)) { 334 | continue; 335 | } 336 | 337 | out[i] = .{ 338 | .name = name, 339 | .decl_type = .{ .enumeration = try parseEnumFields(allocator, enums) }, 340 | }; 341 | i += 1; 342 | } 343 | 344 | return i; 345 | } 346 | 347 | fn parseEnumFields(allocator: Allocator, elem: *xml.Element) !registry.Enum { 348 | // TODO: `type` was added recently, fall back to checking endswith FlagBits for older versions? 349 | const enum_type = elem.getAttribute("type") orelse return error.InvalidRegistry; 350 | const is_bitmask = mem.eql(u8, enum_type, "bitmask"); 351 | if (!is_bitmask and !mem.eql(u8, enum_type, "enum")) { 352 | return error.InvalidRegistry; 353 | } 354 | 355 | const fields = try allocator.alloc(registry.Enum.Field, elem.children.len); 356 | 357 | var i: usize = 0; 358 | var it = elem.findChildrenByTag("enum"); 359 | while (it.next()) |field| { 360 | fields[i] = try parseEnumField(field); 361 | i += 1; 362 | } 363 | 364 | return registry.Enum{ 365 | .fields = fields[0..i], 366 | .is_bitmask = is_bitmask, 367 | }; 368 | } 369 | 370 | fn parseEnumField(field: *xml.Element) !registry.Enum.Field { 371 | const is_compat_alias = if (field.getAttribute("comment")) |comment| 372 | mem.eql(u8, comment, "Backwards-compatible alias containing a typo") or 373 | mem.eql(u8, comment, "Deprecated name for backwards compatibility") 374 | else 375 | false; 376 | 377 | const name = field.getAttribute("name") orelse return error.InvalidRegistry; 378 | const value: registry.Enum.Value = blk: { 379 | // An enum variant's value could be defined by any of the following attributes: 380 | // - value: Straight up value of the enum variant, in either base 10 or 16 (prefixed with 0x). 381 | // - bitpos: Used for bitmasks, and can also be set in extensions. 382 | // - alias: The field is an alias of another variant within the same enum. 383 | // - offset: Used with features and extensions, where a non-bitpos value is added to an enum. 384 | // The value is given by `1e9 + (extr_nr - 1) * 1e3 + offset`, where `ext_nr` is either 385 | // given by the `extnumber` field (in the case of a feature), or given in the parent 386 | // tag. In the latter case its passed via the `ext_nr` parameter. 387 | if (field.getAttribute("value")) |value| { 388 | if (mem.startsWith(u8, value, "0x")) { 389 | break :blk .{ .bit_vector = try std.fmt.parseInt(i32, value[2..], 16) }; 390 | } else { 391 | break :blk .{ .int = try std.fmt.parseInt(i32, value, 10) }; 392 | } 393 | } else if (field.getAttribute("bitpos")) |bitpos| { 394 | break :blk .{ .bitpos = try std.fmt.parseInt(u5, bitpos, 10) }; 395 | } else if (field.getAttribute("alias")) |alias| { 396 | break :blk .{ .alias = .{ .name = alias, .is_compat_alias = is_compat_alias } }; 397 | } else { 398 | return error.InvalidRegistry; 399 | } 400 | }; 401 | 402 | return registry.Enum.Field{ 403 | .name = name, 404 | .value = value, 405 | }; 406 | } 407 | 408 | fn parseCommands(allocator: Allocator, out: []registry.Declaration, commands_elem: *xml.Element) !usize { 409 | var i: usize = 0; 410 | var it = commands_elem.findChildrenByTag("command"); 411 | while (it.next()) |elem| { 412 | out[i] = try parseCommand(allocator, elem); 413 | i += 1; 414 | } 415 | 416 | return i; 417 | } 418 | 419 | fn splitCommaAlloc(allocator: Allocator, text: []const u8) ![][]const u8 { 420 | var n_codes: usize = 1; 421 | for (text) |c| { 422 | if (c == ',') n_codes += 1; 423 | } 424 | 425 | const codes = try allocator.alloc([]const u8, n_codes); 426 | var it = mem.splitScalar(u8, text, ','); 427 | for (codes) |*code| { 428 | code.* = it.next().?; 429 | } 430 | 431 | return codes; 432 | } 433 | 434 | fn parseCommand(allocator: Allocator, elem: *xml.Element) !registry.Declaration { 435 | if (elem.getAttribute("alias")) |alias| { 436 | const name = elem.getAttribute("name") orelse return error.InvalidRegistry; 437 | return registry.Declaration{ 438 | .name = name, 439 | .decl_type = .{ .alias = .{ .name = alias, .target = .other_command } }, 440 | }; 441 | } 442 | 443 | const proto = elem.findChildByTag("proto") orelse return error.InvalidRegistry; 444 | var proto_xctok = cparse.XmlCTokenizer.init(proto); 445 | const command_decl = try cparse.parseParamOrProto(allocator, &proto_xctok, false); 446 | 447 | var params = try allocator.alloc(registry.Command.Param, elem.children.len); 448 | 449 | var i: usize = 0; 450 | var it = elem.findChildrenByTag("param"); 451 | while (it.next()) |param| { 452 | var xctok = cparse.XmlCTokenizer.init(param); 453 | const decl = try cparse.parseParamOrProto(allocator, &xctok, false); 454 | params[i] = .{ 455 | .name = decl.name, 456 | .param_type = decl.decl_type.typedef, 457 | .is_buffer_len = false, 458 | }; 459 | i += 1; 460 | } 461 | 462 | const return_type = try allocator.create(registry.TypeInfo); 463 | return_type.* = command_decl.decl_type.typedef; 464 | 465 | var success_codes: [][]const u8 = if (elem.getAttribute("successcodes")) |codes| 466 | try splitCommaAlloc(allocator, codes) 467 | else 468 | &[_][]const u8{}; 469 | 470 | var error_codes: [][]const u8 = if (elem.getAttribute("errorcodes")) |codes| 471 | try splitCommaAlloc(allocator, codes) 472 | else 473 | &[_][]const u8{}; 474 | 475 | for (success_codes, 0..) |code, session_i| { 476 | if (mem.eql(u8, code, "XR_SESSION_LOSS_PENDING")) { 477 | var move_i = session_i + 1; 478 | while (move_i < success_codes.len) : (move_i += 1) { 479 | success_codes[move_i - 1] = success_codes[move_i]; 480 | } 481 | success_codes = success_codes[0..success_codes.len]; 482 | success_codes.len = success_codes.len - 1; 483 | 484 | const old_error_codes = error_codes; 485 | error_codes = try allocator.alloc([]const u8, error_codes.len + 1); 486 | mem.copyForwards([]const u8, error_codes, old_error_codes); 487 | error_codes[error_codes.len - 1] = code; 488 | allocator.free(old_error_codes); 489 | break; 490 | } 491 | } 492 | 493 | params = params[0..i]; 494 | 495 | it = elem.findChildrenByTag("param"); 496 | for (params) |*param| { 497 | const param_elem = it.next().?; 498 | try parsePointerMeta(.{ .command = params }, ¶m.param_type, param_elem); 499 | } 500 | 501 | return registry.Declaration{ 502 | .name = command_decl.name, 503 | .decl_type = .{ 504 | .command = .{ 505 | .params = params, 506 | .return_type = return_type, 507 | .success_codes = success_codes, 508 | .error_codes = error_codes, 509 | }, 510 | }, 511 | }; 512 | } 513 | 514 | fn parseApiConstants(allocator: Allocator, root: *xml.Element) ![]registry.ApiConstant { 515 | const enums = blk: { 516 | var it = root.findChildrenByTag("enums"); 517 | while (it.next()) |child| { 518 | const name = child.getAttribute("name") orelse continue; 519 | if (mem.eql(u8, name, api_constants_name)) { 520 | break :blk child; 521 | } 522 | } 523 | 524 | return error.InvalidRegistry; 525 | }; 526 | 527 | const types = root.findChildByTag("types") orelse return error.InvalidRegistry; 528 | const n_defines = blk: { 529 | var n_defines: usize = 0; 530 | var it = types.findChildrenByTag("type"); 531 | while (it.next()) |ty| { 532 | if (ty.getAttribute("category")) |category| { 533 | if (mem.eql(u8, category, "define")) { 534 | n_defines += 1; 535 | } 536 | } 537 | } 538 | break :blk n_defines; 539 | }; 540 | 541 | const extensions = root.findChildByTag("extensions") orelse return error.InvalidRegistry; 542 | const n_extension_defines = blk: { 543 | var n_extension_defines: usize = 0; 544 | var it = extensions.findChildrenByTag("extension"); 545 | while (it.next()) |ext| { 546 | const require = ext.findChildByTag("require") orelse return error.InvalidRegistry; 547 | var defines = require.findChildrenByTag("enum"); 548 | while (defines.next()) |e| { 549 | if (e.getAttribute("offset") != null and e.getAttribute("extends") != null) continue; 550 | 551 | const name = e.getAttribute("name") orelse continue; 552 | if (mem.endsWith(u8, name, "SPEC_VERSION")) continue; 553 | if (mem.endsWith(u8, name, "EXTENSION_NAME")) continue; 554 | 555 | n_extension_defines += 1; 556 | } 557 | } 558 | break :blk n_extension_defines; 559 | }; 560 | 561 | const constants = try allocator.alloc(registry.ApiConstant, enums.children.len + n_defines + n_extension_defines); 562 | 563 | var i: usize = 0; 564 | var it = enums.findChildrenByTag("enum"); 565 | while (it.next()) |constant| { 566 | const expr = if (constant.getAttribute("value")) |expr| 567 | expr 568 | else if (constant.getAttribute("alias")) |alias| 569 | alias 570 | else 571 | return error.InvalidRegistry; 572 | 573 | constants[i] = .{ 574 | .name = constant.getAttribute("name") orelse return error.InvalidRegistry, 575 | .value = .{ .expr = expr }, 576 | }; 577 | 578 | i += 1; 579 | } 580 | 581 | i += try parseDefines(types, constants[i..]); 582 | i += try parseExtensionDefines(extensions, constants[i..]); 583 | return constants[0..i]; 584 | } 585 | 586 | fn parseDefines(types: *xml.Element, out: []registry.ApiConstant) !usize { 587 | var i: usize = 0; 588 | var it = types.findChildrenByTag("type"); 589 | while (it.next()) |ty| { 590 | const category = ty.getAttribute("category") orelse continue; 591 | if (!mem.eql(u8, category, "define")) { 592 | continue; 593 | } 594 | 595 | const name = ty.getCharData("name") orelse continue; 596 | if (mem.eql(u8, name, "XR_CURRENT_API_VERSION")) { 597 | var xctok = cparse.XmlCTokenizer.init(ty); 598 | out[i] = .{ 599 | .name = name, 600 | .value = .{ .version = cparse.parseVersion(&xctok) catch continue }, 601 | }; 602 | } else { 603 | const expr = mem.trim(u8, ty.children[2].char_data, " "); 604 | 605 | // TODO this doesn't work with all #defines yet (need to handle hex, U/L suffix, etc.) 606 | _ = std.fmt.parseInt(i32, expr, 10) catch continue; 607 | 608 | out[i] = .{ 609 | .name = name, 610 | .value = .{ .expr = expr }, 611 | }; 612 | } 613 | i += 1; 614 | } 615 | 616 | return i; 617 | } 618 | 619 | fn parseExtensionDefines(extensions: *xml.Element, out: []registry.ApiConstant) !usize { 620 | var i: usize = 0; 621 | var it = extensions.findChildrenByTag("extension"); 622 | 623 | while (it.next()) |ext| { 624 | const require = ext.findChildByTag("require") orelse return error.InvalidRegistry; 625 | var defines = require.findChildrenByTag("enum"); 626 | while (defines.next()) |e| { 627 | if (e.getAttribute("offset") != null and e.getAttribute("extends") != null) continue; 628 | 629 | const name = e.getAttribute("name") orelse continue; 630 | if (mem.endsWith(u8, name, "SPEC_VERSION")) continue; 631 | if (mem.endsWith(u8, name, "EXTENSION_NAME")) continue; 632 | 633 | out[i] = .{ 634 | .name = name, 635 | .value = .{ .expr = e.getAttribute("value") orelse continue }, 636 | }; 637 | 638 | i += 1; 639 | } 640 | } 641 | 642 | return i; 643 | } 644 | 645 | fn parseTags(allocator: Allocator, root: *xml.Element) ![]registry.Tag { 646 | var tags_elem = root.findChildByTag("tags") orelse return error.InvalidRegistry; 647 | const tags = try allocator.alloc(registry.Tag, tags_elem.children.len); 648 | 649 | var i: usize = 0; 650 | var it = tags_elem.findChildrenByTag("tag"); 651 | while (it.next()) |tag| { 652 | tags[i] = .{ 653 | .name = tag.getAttribute("name") orelse return error.InvalidRegistry, 654 | .author = tag.getAttribute("author") orelse return error.InvalidRegistry, 655 | }; 656 | 657 | i += 1; 658 | } 659 | 660 | return tags[0..i]; 661 | } 662 | 663 | fn parseFeatures(allocator: Allocator, root: *xml.Element) ![]registry.Feature { 664 | var it = root.findChildrenByTag("feature"); 665 | var count: usize = 0; 666 | while (it.next()) |_| count += 1; 667 | 668 | const features = try allocator.alloc(registry.Feature, count); 669 | var i: usize = 0; 670 | it = root.findChildrenByTag("feature"); 671 | while (it.next()) |feature| { 672 | features[i] = try parseFeature(allocator, feature); 673 | i += 1; 674 | } 675 | 676 | return features; 677 | } 678 | 679 | fn parseFeature(allocator: Allocator, feature: *xml.Element) !registry.Feature { 680 | const name = feature.getAttribute("name") orelse return error.InvalidRegistry; 681 | const feature_level = blk: { 682 | const number = feature.getAttribute("number") orelse return error.InvalidRegistry; 683 | break :blk try splitFeatureLevel(number, "."); 684 | }; 685 | 686 | var requires = try allocator.alloc(registry.Require, feature.children.len); 687 | var i: usize = 0; 688 | var it = feature.findChildrenByTag("require"); 689 | while (it.next()) |require| { 690 | requires[i] = try parseRequire(allocator, require, null); 691 | i += 1; 692 | } 693 | 694 | return registry.Feature{ 695 | .name = name, 696 | .level = feature_level, 697 | .requires = requires[0..i], 698 | }; 699 | } 700 | 701 | fn parseEnumExtension(elem: *xml.Element, parent_extnumber: ?u31) !?registry.Require.EnumExtension { 702 | // check for either _SPEC_VERSION or _EXTENSION_NAME 703 | const extends = elem.getAttribute("extends") orelse return null; 704 | 705 | if (elem.getAttribute("offset")) |offset_str| { 706 | const offset = try std.fmt.parseInt(u31, offset_str, 10); 707 | const name = elem.getAttribute("name") orelse return error.InvalidRegistry; 708 | const extnumber = if (elem.getAttribute("extnumber")) |num| 709 | try std.fmt.parseInt(u31, num, 10) 710 | else 711 | null; 712 | 713 | const actual_extnumber = extnumber orelse parent_extnumber orelse return error.InvalidRegistry; 714 | const value = blk: { 715 | const abs_value = enumExtOffsetToValue(actual_extnumber, offset); 716 | if (elem.getAttribute("dir")) |dir| { 717 | if (mem.eql(u8, dir, "-")) { 718 | break :blk -@as(i32, abs_value); 719 | } else { 720 | return error.InvalidRegistry; 721 | } 722 | } 723 | 724 | break :blk @as(i32, abs_value); 725 | }; 726 | 727 | return registry.Require.EnumExtension{ 728 | .extends = extends, 729 | .extnumber = actual_extnumber, 730 | .field = .{ .name = name, .value = .{ .int = value } }, 731 | }; 732 | } 733 | 734 | return registry.Require.EnumExtension{ 735 | .extends = extends, 736 | .extnumber = parent_extnumber, 737 | .field = try parseEnumField(elem), 738 | }; 739 | } 740 | 741 | fn enumExtOffsetToValue(extnumber: u31, offset: u31) u31 { 742 | const extension_value_base = 1000000000; 743 | const extension_block = 1000; 744 | return extension_value_base + (extnumber - 1) * extension_block + offset; 745 | } 746 | 747 | fn parseRequire(allocator: Allocator, require: *xml.Element, extnumber: ?u31) !registry.Require { 748 | var n_extends: usize = 0; 749 | var n_types: usize = 0; 750 | var n_commands: usize = 0; 751 | 752 | var it = require.elements(); 753 | while (it.next()) |elem| { 754 | if (mem.eql(u8, elem.tag, "enum")) { 755 | n_extends += 1; 756 | } else if (mem.eql(u8, elem.tag, "type")) { 757 | n_types += 1; 758 | } else if (mem.eql(u8, elem.tag, "command")) { 759 | n_commands += 1; 760 | } 761 | } 762 | 763 | const extends = try allocator.alloc(registry.Require.EnumExtension, n_extends); 764 | const types = try allocator.alloc([]const u8, n_types); 765 | const commands = try allocator.alloc([]const u8, n_commands); 766 | 767 | var i_extends: usize = 0; 768 | var i_types: usize = 0; 769 | var i_commands: usize = 0; 770 | 771 | it = require.elements(); 772 | while (it.next()) |elem| { 773 | if (mem.eql(u8, elem.tag, "enum")) { 774 | if (try parseEnumExtension(elem, extnumber)) |ext| { 775 | extends[i_extends] = ext; 776 | i_extends += 1; 777 | } 778 | } else if (mem.eql(u8, elem.tag, "type")) { 779 | types[i_types] = elem.getAttribute("name") orelse return error.InvalidRegistry; 780 | i_types += 1; 781 | } else if (mem.eql(u8, elem.tag, "command")) { 782 | commands[i_commands] = elem.getAttribute("name") orelse return error.InvalidRegistry; 783 | i_commands += 1; 784 | } 785 | } 786 | 787 | const required_feature_level = blk: { 788 | const feature_level = require.getAttribute("feature") orelse break :blk null; 789 | if (!mem.startsWith(u8, feature_level, "XR_VERSION_")) { 790 | return error.InvalidRegistry; 791 | } 792 | 793 | break :blk try splitFeatureLevel(feature_level["XR_VERSION_".len..], "_"); 794 | }; 795 | 796 | return registry.Require{ 797 | .extends = extends[0..i_extends], 798 | .types = types[0..i_types], 799 | .commands = commands[0..i_commands], 800 | .required_feature_level = required_feature_level, 801 | .required_extension = require.getAttribute("extension"), 802 | }; 803 | } 804 | 805 | fn parseExtensions(allocator: Allocator, root: *xml.Element) ![]registry.Extension { 806 | const extensions_elem = root.findChildByTag("extensions") orelse return error.InvalidRegistry; 807 | 808 | const extensions = try allocator.alloc(registry.Extension, extensions_elem.children.len); 809 | var i: usize = 0; 810 | var it = extensions_elem.findChildrenByTag("extension"); 811 | while (it.next()) |extension| { 812 | // Some extensions (in particular 94) are disabled, so just skip them 813 | if (extension.getAttribute("supported")) |supported| { 814 | if (mem.eql(u8, supported, "disabled")) { 815 | continue; 816 | } 817 | } 818 | 819 | extensions[i] = try parseExtension(allocator, extension); 820 | i += 1; 821 | } 822 | 823 | return extensions[0..i]; 824 | } 825 | 826 | fn findExtVersion(extension: *xml.Element) !u32 { 827 | var req_it = extension.findChildrenByTag("require"); 828 | while (req_it.next()) |req| { 829 | var enum_it = req.findChildrenByTag("enum"); 830 | while (enum_it.next()) |e| { 831 | const name = e.getAttribute("name") orelse continue; 832 | const value = e.getAttribute("value") orelse continue; 833 | if (mem.endsWith(u8, name, "_SPEC_VERSION")) { 834 | return try std.fmt.parseInt(u32, value, 10); 835 | } 836 | } 837 | } 838 | 839 | return error.InvalidRegistry; 840 | } 841 | 842 | fn parseExtension(allocator: Allocator, extension: *xml.Element) !registry.Extension { 843 | const name = extension.getAttribute("name") orelse return error.InvalidRegistry; 844 | const platform = extension.getAttribute("platform"); 845 | const version = try findExtVersion(extension); 846 | 847 | // For some reason there are two ways for an extension to state its required 848 | // feature level: both seperately in each tag, or using 849 | // the requiresCore attribute. 850 | const requires_core = if (extension.getAttribute("requiresCore")) |feature_level| 851 | try splitFeatureLevel(feature_level, ".") 852 | else 853 | null; 854 | 855 | const promoted_to: registry.Extension.Promotion = blk: { 856 | const promotedto = extension.getAttribute("promotedto") orelse break :blk .none; 857 | if (mem.startsWith(u8, promotedto, "XR_VERSION_")) { 858 | const feature_level = try splitFeatureLevel(promotedto["XR_VERSION_".len..], "_"); 859 | 860 | break :blk .{ .feature = feature_level }; 861 | } 862 | 863 | break :blk .{ .extension = promotedto }; 864 | }; 865 | 866 | const number = blk: { 867 | const number_str = extension.getAttribute("number") orelse return error.InvalidRegistry; 868 | break :blk try std.fmt.parseInt(u31, number_str, 10); 869 | }; 870 | 871 | const ext_type: ?registry.Extension.ExtensionType = blk: { 872 | const ext_type_str = extension.getAttribute("type") orelse break :blk null; 873 | if (mem.eql(u8, ext_type_str, "instance")) { 874 | break :blk .instance; 875 | } else if (mem.eql(u8, ext_type_str, "device")) { 876 | break :blk .device; 877 | } else { 878 | return error.InvalidRegistry; 879 | } 880 | }; 881 | 882 | const depends = blk: { 883 | const requires_str = extension.getAttribute("requires") orelse break :blk &[_][]const u8{}; 884 | break :blk try splitCommaAlloc(allocator, requires_str); 885 | }; 886 | 887 | var requires = try allocator.alloc(registry.Require, extension.children.len); 888 | var i: usize = 0; 889 | var it = extension.findChildrenByTag("require"); 890 | while (it.next()) |require| { 891 | requires[i] = try parseRequire(allocator, require, number); 892 | i += 1; 893 | } 894 | 895 | return registry.Extension{ 896 | .name = name, 897 | .number = number, 898 | .version = version, 899 | .extension_type = ext_type, 900 | .depends = depends, 901 | .promoted_to = promoted_to, 902 | .platform = platform, 903 | .required_feature_level = requires_core, 904 | .requires = requires[0..i], 905 | }; 906 | } 907 | 908 | fn splitFeatureLevel(ver: []const u8, split: []const u8) !registry.FeatureLevel { 909 | var it = mem.splitSequence(u8, ver, split); 910 | 911 | const major = it.next() orelse return error.InvalidFeatureLevel; 912 | const minor = it.next() orelse return error.InvalidFeatureLevel; 913 | if (it.next() != null) { 914 | return error.InvalidFeatureLevel; 915 | } 916 | 917 | return registry.FeatureLevel{ 918 | .major = try std.fmt.parseInt(u32, major, 10), 919 | .minor = try std.fmt.parseInt(u32, minor, 10), 920 | }; 921 | } 922 | -------------------------------------------------------------------------------- /generator/openxr/registry.zig: -------------------------------------------------------------------------------- 1 | pub const Registry = struct { 2 | decls: []Declaration, 3 | api_constants: []ApiConstant, 4 | tags: []Tag, 5 | features: []Feature, 6 | extensions: []Extension, 7 | }; 8 | 9 | pub const Declaration = struct { 10 | name: []const u8, 11 | decl_type: DeclarationType, 12 | }; 13 | 14 | pub const DeclarationType = union(enum) { 15 | container: Container, 16 | enumeration: Enum, 17 | bitmask: Bitmask, 18 | handle: Handle, 19 | command: Command, 20 | alias: Alias, 21 | foreign: Foreign, 22 | typedef: TypeInfo, 23 | external, 24 | }; 25 | 26 | pub const Alias = struct { 27 | pub const Target = enum { 28 | other_command, 29 | other_type, 30 | }; 31 | 32 | name: []const u8, 33 | target: Target, 34 | }; 35 | 36 | pub const ApiConstant = struct { 37 | pub const Value = union(enum) { 38 | expr: []const u8, 39 | version: [3][]const u8, 40 | }; 41 | 42 | name: []const u8, 43 | value: Value, 44 | }; 45 | 46 | pub const Tag = struct { 47 | name: []const u8, 48 | author: []const u8, 49 | }; 50 | 51 | pub const TypeInfo = union(enum) { 52 | name: []const u8, 53 | command_ptr: Command, 54 | pointer: Pointer, 55 | array: Array, 56 | }; 57 | 58 | pub const Container = struct { 59 | pub const Field = struct { 60 | name: []const u8, 61 | field_type: TypeInfo, 62 | bits: ?usize, 63 | is_buffer_len: bool, 64 | is_optional: bool, 65 | }; 66 | 67 | stype: ?[]const u8, 68 | extends: ?[]const []const u8, 69 | fields: []Field, 70 | is_union: bool, 71 | }; 72 | 73 | pub const Enum = struct { 74 | pub const Value = union(enum) { 75 | bitpos: u5, // 1 << bitpos 76 | bit_vector: i32, // Combined flags & some vendor IDs 77 | int: i32, 78 | alias: struct { 79 | name: []const u8, 80 | is_compat_alias: bool, 81 | }, 82 | }; 83 | 84 | pub const Field = struct { 85 | name: []const u8, 86 | value: Value, 87 | }; 88 | 89 | fields: []Field, 90 | is_bitmask: bool, 91 | }; 92 | 93 | pub const Bitmask = struct { 94 | bits_enum: ?[]const u8, 95 | }; 96 | 97 | pub const Handle = struct { 98 | parent: ?[]const u8, // XrInstance has no parent 99 | is_dispatchable: bool, 100 | }; 101 | 102 | pub const Command = struct { 103 | pub const Param = struct { 104 | name: []const u8, 105 | param_type: TypeInfo, 106 | is_buffer_len: bool, 107 | }; 108 | 109 | params: []Param, 110 | return_type: *TypeInfo, 111 | success_codes: []const []const u8, 112 | error_codes: []const []const u8, 113 | }; 114 | 115 | pub const Pointer = struct { 116 | pub const PointerSize = union(enum) { 117 | one, 118 | many, // The length is given by some complex expression, possibly involving another field 119 | other_field: []const u8, // The length is given by some other field or parameter 120 | zero_terminated, 121 | }; 122 | 123 | is_const: bool, 124 | is_optional: bool, 125 | size: PointerSize, 126 | child: *TypeInfo, 127 | }; 128 | 129 | pub const Array = struct { 130 | pub const ArraySize = union(enum) { 131 | int: usize, 132 | alias: []const u8, // Field size is given by an api constant 133 | }; 134 | 135 | size: ArraySize, 136 | child: *TypeInfo, 137 | }; 138 | 139 | pub const Foreign = struct { 140 | depends: []const u8, // Either a header or openxr_platform_defines 141 | }; 142 | 143 | pub const Feature = struct { 144 | name: []const u8, 145 | level: FeatureLevel, // from 'number' 146 | requires: []Require, 147 | }; 148 | 149 | pub const Extension = struct { 150 | pub const ExtensionType = enum { 151 | instance, 152 | device, 153 | }; 154 | 155 | pub const Promotion = union(enum) { 156 | none, 157 | feature: FeatureLevel, 158 | extension: []const u8, 159 | }; 160 | 161 | name: []const u8, 162 | number: u31, 163 | version: u32, 164 | extension_type: ?ExtensionType, 165 | depends: []const []const u8, // Other extensions 166 | promoted_to: Promotion, 167 | platform: ?[]const u8, 168 | required_feature_level: ?FeatureLevel, 169 | requires: []Require, 170 | }; 171 | 172 | pub const Require = struct { 173 | pub const EnumExtension = struct { 174 | extends: []const u8, 175 | extnumber: ?u31, 176 | field: Enum.Field, 177 | }; 178 | 179 | extends: []EnumExtension, 180 | types: []const []const u8, 181 | commands: []const []const u8, 182 | required_feature_level: ?FeatureLevel, 183 | required_extension: ?[]const u8, 184 | }; 185 | 186 | pub const FeatureLevel = struct { 187 | major: u32, 188 | minor: u32, 189 | }; 190 | -------------------------------------------------------------------------------- /generator/openxr/render.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const reg = @import("registry.zig"); 3 | const id_render = @import("../id_render.zig"); 4 | const cparse = @import("c_parse.zig"); 5 | const mem = std.mem; 6 | const Allocator = mem.Allocator; 7 | const CaseStyle = id_render.CaseStyle; 8 | const IdRenderer = id_render.IdRenderer; 9 | 10 | const preamble = 11 | \\// This file is generated from the Khronos OpenXR XML API registry by openxr-zig 12 | \\ 13 | \\const std = @import("std"); 14 | \\const builtin = @import("builtin"); 15 | \\const root = @import("root"); 16 | \\ 17 | \\pub const openxr_call_conv: std.builtin.CallingConvention = if (builtin.os.tag == .windows and builtin.cpu.arch == .i386) 18 | \\ .Stdcall 19 | \\ else if (builtin.abi == .android and (builtin.cpu.arch.isARM() or builtin.cpu.arch.isThumb()) and builtin.Target.arm.featureSetHas(builtin.cpu.features, .has_v7) and builtin.cpu.arch.ptrBitWidth() == 32) 20 | \\ // On Android 32-bit ARM targets, OpenXR functions use the "hardfloat" 21 | \\ // calling convention, i.e. float parameters are passed in registers. This 22 | \\ // is true even if the rest of the application passes floats on the stack, 23 | \\ // as it does by default when compiling for the armeabi-v7a NDK ABI. 24 | \\ .AAPCSVFP 25 | \\ else 26 | \\ .C; 27 | \\pub fn FlagsMixin(comptime FlagsType: type) type { 28 | \\ return struct { 29 | \\ pub const IntType = Flags64; 30 | \\ pub fn toInt(self: FlagsType) IntType { 31 | \\ return @bitCast(self); 32 | \\ } 33 | \\ pub fn fromInt(flags: IntType) FlagsType { 34 | \\ return @bitCast(flags); 35 | \\ } 36 | \\ pub fn merge(lhs: FlagsType, rhs: FlagsType) FlagsType { 37 | \\ return fromInt(toInt(lhs) | toInt(rhs)); 38 | \\ } 39 | \\ pub fn intersect(lhs: FlagsType, rhs: FlagsType) FlagsType { 40 | \\ return fromInt(toInt(lhs) & toInt(rhs)); 41 | \\ } 42 | \\ pub fn complement(self: FlagsType) FlagsType { 43 | \\ return fromInt(~toInt(self)); 44 | \\ } 45 | \\ pub fn subtract(lhs: FlagsType, rhs: FlagsType) FlagsType { 46 | \\ return fromInt(toInt(lhs) & toInt(rhs.complement())); 47 | \\ } 48 | \\ pub fn contains(lhs: FlagsType, rhs: FlagsType) bool { 49 | \\ return toInt(intersect(lhs, rhs)) == toInt(rhs); 50 | \\ } 51 | \\ }; 52 | \\} 53 | \\pub fn makeVersion(major: u16, minor: u16, patch: u32) u64 { 54 | \\ return (@as(u64, major) << 48) | (@as(u64, minor) << 32) | patch; 55 | \\} 56 | \\pub fn versionMajor(version: u64) u16 { 57 | \\ return @truncate(version >> 48); 58 | \\} 59 | \\pub fn versionMinor(version: u16) u16 { 60 | \\ return @truncate(version >> 32); 61 | \\} 62 | \\pub fn versionPatch(version: u64) u32 { 63 | \\ return @truncate(version); 64 | \\} 65 | \\ 66 | ; 67 | 68 | const builtin_types = std.StaticStringMap([]const u8).initComptime(.{ 69 | .{ "void", @typeName(void) }, 70 | .{ "char", @typeName(u8) }, 71 | .{ "float", @typeName(f32) }, 72 | .{ "double", @typeName(f64) }, 73 | .{ "uint8_t", @typeName(u8) }, 74 | .{ "uint16_t", @typeName(u16) }, 75 | .{ "uint32_t", @typeName(u32) }, 76 | .{ "uint64_t", @typeName(u64) }, 77 | .{ "int16_t", @typeName(i16) }, 78 | .{ "int32_t", @typeName(i32) }, 79 | .{ "int64_t", @typeName(i64) }, 80 | .{ "size_t", @typeName(usize) }, 81 | .{ "wchar_t", @typeName(u16) }, 82 | .{ "int", @typeName(c_int) }, 83 | }); 84 | 85 | const foreign_types = std.StaticStringMap([]const u8).initComptime(.{ 86 | .{ "Display", "opaque {}" }, 87 | .{ "VisualID", @typeName(c_uint) }, 88 | .{ "Window", @typeName(c_ulong) }, 89 | .{ "xcb_glx_fbconfig_t", "opaque {}" }, 90 | .{ "xcb_glx_drawable_t", "opaque {}" }, 91 | .{ "xcb_glx_context_t", "opaque {}" }, 92 | .{ "xcb_connection_t", "opaque {}" }, 93 | .{ "xcb_visualid_t", @typeName(u32) }, 94 | .{ "xcb_window_t", @typeName(u32) }, 95 | .{ "VkAllocationCallbacks", "@import(\"vulkan\").AllocationCallbacks" }, 96 | .{ "VkDevice", "@import(\"vulkan\").Device" }, 97 | .{ "VkDeviceCreateInfo", "@import(\"vulkan\").DeviceCreateInfo" }, 98 | .{ "VkFormat", "@import(\"vulkan\").Format" }, 99 | .{ "VkImage", "@import(\"vulkan\").Image" }, 100 | .{ "VkInstance", "@import(\"vulkan\").Instance" }, 101 | .{ "VkInstanceCreateInfo", "@import(\"vulkan\").InstanceCreateInfo" }, 102 | .{ "VkPhysicalDevice", "@import(\"vulkan\").PhysicalDevice" }, 103 | .{ "VkResult", "@import(\"vulkan\").Result" }, 104 | .{ "PFN_vkGetInstanceProcAddr", "@import(\"vulkan\").PfnGetInstanceProcAddr" }, 105 | }); 106 | 107 | const initialized_types = std.StaticStringMap([]const u8).initComptime(.{ 108 | .{ "XrVector2f", "0" }, 109 | .{ "XrVector3f", "0" }, 110 | .{ "XrVector4f", "0" }, 111 | .{ "XrColor4f", "0" }, 112 | .{ "XrQuaternionf", "0" }, 113 | .{ "XrPosef", ".{}" }, 114 | .{ "XrOffset2Df", "0" }, 115 | .{ "XrExtent2Df", "0" }, 116 | .{ "XrRect2Df", ".{}" }, 117 | .{ "XrOffset2Di", "0" }, 118 | .{ "XrExtent2Di", "0" }, 119 | .{ "XrRect2Di", ".{}" }, 120 | }); 121 | 122 | fn eqlIgnoreCase(lhs: []const u8, rhs: []const u8) bool { 123 | if (lhs.len != rhs.len) { 124 | return false; 125 | } 126 | 127 | for (lhs, rhs) |l, r| { 128 | if (std.ascii.toLower(l) != std.ascii.toLower(r)) { 129 | return false; 130 | } 131 | } 132 | 133 | return true; 134 | } 135 | 136 | pub fn trimXrNamespace(id: []const u8) []const u8 { 137 | const prefixes = [_][]const u8{ "XR_", "xr", "Xr", "PFN_xr" }; 138 | for (prefixes) |prefix| { 139 | if (mem.startsWith(u8, id, prefix)) { 140 | return id[prefix.len..]; 141 | } 142 | } 143 | 144 | return id; 145 | } 146 | 147 | fn Renderer(comptime WriterType: type) type { 148 | return struct { 149 | const Self = @This(); 150 | const WriteError = WriterType.Error; 151 | const RenderTypeInfoError = WriteError || error{OutOfMemory}; 152 | 153 | const BitflagName = struct { 154 | /// Name without FlagBits, so XrSurfaceTransformFlagBitsKHR 155 | /// becomes XrSurfaceTransform 156 | base_name: []const u8, 157 | 158 | /// Optional tag of the flag 159 | tag: ?[]const u8, 160 | }; 161 | 162 | const ParamType = enum { 163 | in_pointer, 164 | out_pointer, 165 | in_out_pointer, 166 | bitflags, 167 | mut_buffer_len, 168 | buffer_len, 169 | other, 170 | }; 171 | 172 | const ReturnValue = struct { 173 | name: []const u8, 174 | return_value_type: reg.TypeInfo, 175 | origin: enum { 176 | parameter, 177 | inner_return_value, 178 | }, 179 | }; 180 | 181 | const CommandDispatchType = enum { 182 | base, 183 | instance, 184 | }; 185 | 186 | writer: WriterType, 187 | allocator: Allocator, 188 | registry: *const reg.Registry, 189 | id_renderer: *IdRenderer, 190 | declarations_by_name: std.StringHashMap(*const reg.DeclarationType), 191 | structure_types: std.StringHashMap(void), 192 | 193 | fn init(writer: WriterType, allocator: Allocator, registry: *const reg.Registry, id_renderer: *IdRenderer) !Self { 194 | var declarations_by_name = std.StringHashMap(*const reg.DeclarationType).init(allocator); 195 | errdefer declarations_by_name.deinit(); 196 | 197 | for (registry.decls) |*decl| { 198 | const result = try declarations_by_name.getOrPut(decl.name); 199 | if (result.found_existing) { 200 | return error.InvalidRegistry; 201 | } 202 | 203 | result.value_ptr.* = &decl.decl_type; 204 | } 205 | 206 | const xr_structure_type_decl = declarations_by_name.get("XrStructureType") orelse return error.InvalidRegistry; 207 | const xr_structure_type = switch (xr_structure_type_decl.*) { 208 | .enumeration => |e| e, 209 | else => return error.InvalidRegistry, 210 | }; 211 | var structure_types = std.StringHashMap(void).init(allocator); 212 | errdefer structure_types.deinit(); 213 | 214 | for (xr_structure_type.fields) |field| { 215 | try structure_types.put(field.name, {}); 216 | } 217 | 218 | return Self{ 219 | .writer = writer, 220 | .allocator = allocator, 221 | .registry = registry, 222 | .id_renderer = id_renderer, 223 | .declarations_by_name = declarations_by_name, 224 | .structure_types = structure_types, 225 | }; 226 | } 227 | 228 | fn deinit(self: *Self) void { 229 | self.declarations_by_name.deinit(); 230 | } 231 | 232 | fn writeIdentifier(self: Self, id: []const u8) !void { 233 | try id_render.writeIdentifier(self.writer, id); 234 | } 235 | 236 | fn writeIdentifierWithCase(self: *Self, case: CaseStyle, id: []const u8) !void { 237 | try self.id_renderer.renderWithCase(self.writer, case, id); 238 | } 239 | 240 | fn writeIdentifierFmt(self: *Self, comptime fmt: []const u8, args: anytype) !void { 241 | try self.id_renderer.renderFmt(self.writer, fmt, args); 242 | } 243 | 244 | fn extractEnumFieldName(self: Self, enum_name: []const u8, field_name: []const u8) ![]const u8 { 245 | const adjusted_enum_name = if (mem.eql(u8, enum_name, "XrStructureType")) 246 | "XrType" 247 | else 248 | self.id_renderer.stripAuthorTag(enum_name); 249 | var enum_it = id_render.SegmentIterator.init(adjusted_enum_name); 250 | var field_it = id_render.SegmentIterator.init(field_name); 251 | 252 | while (true) { 253 | const rest = field_it.rest(); 254 | const enum_segment = enum_it.next() orelse return rest; 255 | const field_segment = field_it.next() orelse return error.InvalidRegistry; 256 | 257 | if (!eqlIgnoreCase(enum_segment, field_segment)) { 258 | return rest; 259 | } 260 | } 261 | } 262 | 263 | fn extractBitflagName(self: Self, name: []const u8) ?BitflagName { 264 | const tag = self.id_renderer.getAuthorTag(name); 265 | const base_name = if (tag) |tag_name| name[0 .. name.len - tag_name.len] else name; 266 | 267 | if (!mem.endsWith(u8, base_name, "FlagBits")) { 268 | return null; 269 | } 270 | 271 | return BitflagName{ 272 | .base_name = base_name[0 .. base_name.len - "FlagBits".len], 273 | .tag = tag, 274 | }; 275 | } 276 | 277 | fn isFlags(self: Self, name: []const u8) bool { 278 | const tag = self.id_renderer.getAuthorTag(name); 279 | const base_name = if (tag) |tag_name| name[0 .. name.len - tag_name.len] else name; 280 | 281 | return mem.endsWith(u8, base_name, "Flags64"); 282 | } 283 | 284 | fn resolveDeclaration(self: Self, name: []const u8) ?reg.DeclarationType { 285 | const decl = self.declarations_by_name.get(name) orelse return null; 286 | return self.resolveAlias(decl.*) catch return null; 287 | } 288 | 289 | fn resolveAlias(self: Self, start_decl: reg.DeclarationType) !reg.DeclarationType { 290 | var decl = start_decl; 291 | while (true) { 292 | const name = switch (decl) { 293 | .alias => |alias| alias.name, 294 | else => return decl, 295 | }; 296 | 297 | const decl_ptr = self.declarations_by_name.get(name) orelse return error.InvalidRegistry; 298 | decl = decl_ptr.*; 299 | } 300 | } 301 | 302 | fn isInOutPointer(self: Self, ptr: reg.Pointer) !bool { 303 | if (ptr.child.* != .name) { 304 | return false; 305 | } 306 | 307 | const decl = self.resolveDeclaration(ptr.child.name) orelse return error.InvalidRegistry; 308 | if (decl != .container) { 309 | return false; 310 | } 311 | 312 | const container = decl.container; 313 | if (container.is_union) { 314 | return false; 315 | } 316 | 317 | for (container.fields) |field| { 318 | if (mem.eql(u8, field.name, "next")) { 319 | return true; 320 | } 321 | } 322 | 323 | return false; 324 | } 325 | 326 | fn classifyParam(self: Self, param: reg.Command.Param) !ParamType { 327 | switch (param.param_type) { 328 | .pointer => |ptr| { 329 | if (param.is_buffer_len) { 330 | if (ptr.is_const or ptr.is_optional) { 331 | return error.InvalidRegistry; 332 | } 333 | 334 | return .mut_buffer_len; 335 | } 336 | 337 | if (ptr.child.* == .name) { 338 | const child_name = ptr.child.name; 339 | if (mem.eql(u8, child_name, "void")) { 340 | return .other; 341 | } else if (builtin_types.get(child_name) == null and trimXrNamespace(child_name).ptr == child_name.ptr) { 342 | return .other; // External type 343 | } 344 | } 345 | 346 | if (ptr.size == .one and !ptr.is_optional) { 347 | // Sometimes, a mutable pointer to a struct is taken, even though 348 | // OpenXR expects this struct to be initialized. This is particularly the case 349 | // for getting structs which include next chains. 350 | if (ptr.is_const) { 351 | return .in_pointer; 352 | } else if (try self.isInOutPointer(ptr)) { 353 | return .in_out_pointer; 354 | } else { 355 | return .out_pointer; 356 | } 357 | } 358 | }, 359 | .name => |name| { 360 | if (self.extractBitflagName(name) != null or self.isFlags(name)) { 361 | return .bitflags; 362 | } 363 | }, 364 | else => {}, 365 | } 366 | 367 | if (param.is_buffer_len) { 368 | return .buffer_len; 369 | } 370 | 371 | return .other; 372 | } 373 | 374 | fn classifyCommandDispatch(name: []const u8, command: reg.Command) CommandDispatchType { 375 | const override_functions = std.StaticStringMap(CommandDispatchType).initComptime(.{ 376 | .{ "xrGetInstanceProcAddr", .base }, 377 | .{ "xrCreateInstance", .base }, 378 | .{ "xrEnumerateApiLayerProperties", .base }, 379 | .{ "xrEnumerateInstanceExtensionProperties", .base }, 380 | }); 381 | 382 | if (override_functions.get(name)) |dispatch_type| { 383 | return dispatch_type; 384 | } 385 | 386 | return switch (command.params[0].param_type) { 387 | .name => .instance, 388 | else => return .base, 389 | }; 390 | } 391 | 392 | fn render(self: *Self) !void { 393 | try self.writer.writeAll(preamble); 394 | 395 | for (self.registry.api_constants) |api_constant| { 396 | try self.renderApiConstant(api_constant); 397 | } 398 | 399 | for (self.registry.decls) |decl| { 400 | try self.renderDecl(decl); 401 | } 402 | 403 | try self.renderCommandPtrs(); 404 | try self.renderExtensionInfo(); 405 | try self.renderWrappers(); 406 | } 407 | 408 | fn renderApiConstant(self: *Self, api_constant: reg.ApiConstant) !void { 409 | try self.writer.writeAll("pub const "); 410 | try self.renderName(api_constant.name); 411 | try self.writer.writeAll(" = "); 412 | 413 | switch (api_constant.value) { 414 | .expr => |expr| try self.renderApiConstantExpr(expr), 415 | .version => |version| { 416 | try self.writer.writeAll("makeVersion("); 417 | for (version, 0..) |part, i| { 418 | if (i != 0) { 419 | try self.writer.writeAll(", "); 420 | } 421 | try self.renderApiConstantExpr(part); 422 | } 423 | try self.writer.writeAll(")"); 424 | }, 425 | } 426 | 427 | try self.writer.writeAll(";\n"); 428 | } 429 | 430 | fn renderApiConstantExpr(self: *Self, expr: []const u8) !void { 431 | const adjusted_expr = if (expr.len > 2 and expr[0] == '(' and expr[expr.len - 1] == ')') 432 | expr[1 .. expr.len - 1] 433 | else 434 | expr; 435 | 436 | var tokenizer = cparse.CTokenizer{ .source = adjusted_expr }; 437 | var peeked: ?cparse.Token = null; 438 | while (true) { 439 | const tok = peeked orelse (try tokenizer.next()) orelse break; 440 | peeked = null; 441 | 442 | switch (tok.kind) { 443 | .lparen, .rparen, .tilde, .minus => { 444 | try self.writer.writeAll(tok.text); 445 | continue; 446 | }, 447 | .id => { 448 | try self.renderName(tok.text); 449 | continue; 450 | }, 451 | .int => {}, 452 | else => return error.InvalidApiConstant, 453 | } 454 | 455 | const suffix = (try tokenizer.next()) orelse { 456 | try self.writer.writeAll(tok.text); 457 | break; 458 | }; 459 | 460 | switch (suffix.kind) { 461 | .id => { 462 | if (mem.eql(u8, suffix.text, "ULL")) { 463 | try self.writer.print("@as(u64, {s})", .{tok.text}); 464 | } else if (mem.eql(u8, suffix.text, "U")) { 465 | try self.writer.print("@as(u32, {s})", .{tok.text}); 466 | } else { 467 | return error.InvalidApiConstant; 468 | } 469 | }, 470 | .dot => { 471 | const decimal = (try tokenizer.next()) orelse return error.InvalidConstantExpr; 472 | try self.writer.print("@as(f32, {s}.{s})", .{ tok.text, decimal.text }); 473 | 474 | const f = (try tokenizer.next()) orelse return error.InvalidConstantExpr; 475 | if (f.kind != .id or !mem.eql(u8, f.text, "f")) { 476 | return error.InvalidApiConstant; 477 | } 478 | }, 479 | else => { 480 | try self.writer.writeAll(tok.text); 481 | peeked = suffix; 482 | }, 483 | } 484 | } 485 | } 486 | 487 | fn renderTypeInfo(self: *Self, type_info: reg.TypeInfo) RenderTypeInfoError!void { 488 | switch (type_info) { 489 | .name => |name| try self.renderName(name), 490 | .command_ptr => |command_ptr| try self.renderCommandPtr(command_ptr, true), 491 | .pointer => |pointer| try self.renderPointer(pointer), 492 | .array => |array| try self.renderArray(array), 493 | } 494 | } 495 | 496 | fn renderName(self: *Self, name: []const u8) !void { 497 | if (builtin_types.get(name)) |zig_name| { 498 | try self.writer.writeAll(zig_name); 499 | return; 500 | } else if (self.extractBitflagName(name)) |bitflag_name| { 501 | try self.writeIdentifierFmt("{s}Flags{s}", .{ 502 | trimXrNamespace(bitflag_name.base_name), 503 | @as([]const u8, if (bitflag_name.tag) |tag| tag else ""), 504 | }); 505 | return; 506 | } else if (mem.startsWith(u8, name, "xr")) { 507 | // Function type, always render with the exact same text for linking purposes. 508 | try self.writeIdentifier(name); 509 | return; 510 | } else if (mem.startsWith(u8, name, "Xr")) { 511 | // Type, strip namespace and write, as they are alreay in title case. 512 | try self.writeIdentifier(name[2..]); 513 | return; 514 | } else if (mem.startsWith(u8, name, "PFN_xr")) { 515 | // Function pointer type, strip off the PFN_xr part and replace it with Pfn. Note that 516 | // this function is only called to render the typedeffed function pointers like xrVoidFunction 517 | try self.writeIdentifierFmt("Pfn{s}", .{name[6..]}); 518 | return; 519 | } else if (mem.startsWith(u8, name, "XR_")) { 520 | // Constants 521 | try self.writeIdentifier(name[3..]); 522 | return; 523 | } 524 | 525 | try self.writeIdentifier(name); 526 | } 527 | 528 | fn renderCommandPtr(self: *Self, command_ptr: reg.Command, optional: bool) !void { 529 | if (optional) { 530 | try self.writer.writeByte('?'); 531 | } 532 | try self.writer.writeAll("*const fn("); 533 | for (command_ptr.params) |param| { 534 | try self.writeIdentifierWithCase(.snake, param.name); 535 | try self.writer.writeAll(": "); 536 | 537 | blk: { 538 | if (param.param_type == .name) { 539 | if (self.extractBitflagName(param.param_type.name)) |bitflag_name| { 540 | try self.writeIdentifierFmt("{s}Flags{s}", .{ 541 | trimXrNamespace(bitflag_name.base_name), 542 | @as([]const u8, if (bitflag_name.tag) |tag| tag else ""), 543 | }); 544 | break :blk; 545 | } else if (self.isFlags(param.param_type.name)) { 546 | try self.renderTypeInfo(param.param_type); 547 | break :blk; 548 | } 549 | } 550 | 551 | try self.renderTypeInfo(param.param_type); 552 | } 553 | 554 | try self.writer.writeAll(", "); 555 | } 556 | try self.writer.writeAll(") callconv(openxr_call_conv)"); 557 | try self.renderTypeInfo(command_ptr.return_type.*); 558 | } 559 | 560 | fn renderPointer(self: *Self, pointer: reg.Pointer) !void { 561 | const child_is_void = pointer.child.* == .name and mem.eql(u8, pointer.child.name, "void"); 562 | 563 | if (pointer.is_optional) { 564 | try self.writer.writeByte('?'); 565 | } 566 | 567 | const size = if (child_is_void) .one else pointer.size; 568 | switch (size) { 569 | .one => try self.writer.writeByte('*'), 570 | .many, .other_field => try self.writer.writeAll("[*]"), 571 | .zero_terminated => try self.writer.writeAll("[*:0]"), 572 | } 573 | 574 | if (pointer.is_const) { 575 | try self.writer.writeAll("const "); 576 | } 577 | 578 | if (child_is_void) { 579 | try self.writer.writeAll("anyopaque"); 580 | } else { 581 | try self.renderTypeInfo(pointer.child.*); 582 | } 583 | } 584 | 585 | fn renderArray(self: *Self, array: reg.Array) !void { 586 | try self.writer.writeByte('['); 587 | switch (array.size) { 588 | .int => |size| try self.writer.print("{}", .{size}), 589 | .alias => |alias| try self.renderName(alias), 590 | } 591 | try self.writer.writeByte(']'); 592 | try self.renderTypeInfo(array.child.*); 593 | } 594 | 595 | fn renderDecl(self: *Self, decl: reg.Declaration) !void { 596 | switch (decl.decl_type) { 597 | .container => |container| try self.renderContainer(decl.name, container), 598 | .enumeration => |enumeration| try self.renderEnumeration(decl.name, enumeration), 599 | .handle => |handle| try self.renderHandle(decl.name, handle), 600 | .alias => |alias| try self.renderAlias(decl.name, alias), 601 | .foreign => |foreign| try self.renderForeign(decl.name, foreign), 602 | .typedef => |type_info| try self.renderTypedef(decl.name, type_info), 603 | .external => try self.renderExternal(decl.name), 604 | .command, .bitmask => {}, 605 | } 606 | } 607 | 608 | fn renderContainer(self: *Self, name: []const u8, container: reg.Container) !void { 609 | try self.writer.writeAll("pub const "); 610 | try self.renderName(name); 611 | try self.writer.writeAll(" = "); 612 | 613 | for (container.fields) |field| { 614 | if (field.bits != null) { 615 | return error.UnhandledBitfieldStruct; 616 | } 617 | } else { 618 | try self.writer.writeAll("extern "); 619 | } 620 | 621 | if (container.is_union) { 622 | try self.writer.writeAll("union {"); 623 | } else { 624 | try self.writer.writeAll("struct {"); 625 | } 626 | 627 | for (container.fields) |field| { 628 | try self.writeIdentifierWithCase(.snake, field.name); 629 | try self.writer.writeAll(": "); 630 | if (field.bits) |bits| { 631 | try self.writer.print(" u{},", .{bits}); 632 | if (field.field_type != .name or builtin_types.get(field.field_type.name) == null) { 633 | try self.writer.writeAll("// "); 634 | try self.renderTypeInfo(field.field_type); 635 | try self.writer.writeByte('\n'); 636 | } 637 | } else { 638 | try self.renderTypeInfo(field.field_type); 639 | if (!container.is_union) { 640 | try self.renderContainerDefaultField(container, name, field); 641 | } 642 | try self.writer.writeAll(", "); 643 | } 644 | } 645 | 646 | if (!container.is_union) { 647 | const have_next_or_type = for (container.fields) |field| { 648 | if (mem.eql(u8, field.name, "next") or mem.eql(u8, field.name, "type")) { 649 | break true; 650 | } 651 | } else false; 652 | 653 | if (have_next_or_type) { 654 | try self.writer.writeAll( 655 | \\ pub fn empty() @This() { 656 | \\ var value: @This() = undefined; 657 | ); 658 | 659 | for (container.fields) |field| { 660 | if (mem.eql(u8, field.name, "next") or mem.eql(u8, field.name, "type")) { 661 | try self.writer.writeAll("value."); 662 | try self.writeIdentifierWithCase(.snake, field.name); 663 | try self.renderContainerDefaultField(container, name, field); 664 | try self.writer.writeAll(";\n"); 665 | } 666 | } 667 | 668 | try self.writer.writeAll( 669 | \\ return value; 670 | \\ } 671 | ); 672 | } 673 | } 674 | 675 | try self.writer.writeAll("};\n"); 676 | } 677 | 678 | fn renderContainerDefaultField(self: *Self, container: reg.Container, container_name: []const u8, field: reg.Container.Field) !void { 679 | if (mem.eql(u8, field.name, "next")) { 680 | try self.writer.writeAll(" = null"); 681 | } else if (mem.eql(u8, field.name, "type")) { 682 | if (container.stype == null) { 683 | return; 684 | } 685 | 686 | const stype = container.stype.?; 687 | if (!mem.startsWith(u8, stype, "XR_TYPE_")) { 688 | return error.InvalidRegistry; 689 | } 690 | 691 | try self.writer.writeAll(" = ."); 692 | try self.writeIdentifierWithCase(.snake, stype["XR_TYPE_".len..]); 693 | } else if (mem.eql(u8, field.name, "w") and mem.eql(u8, container_name, "XrQuaternionf")) { 694 | try self.writer.writeAll(" = 1"); 695 | } else if (field.is_optional) { 696 | if (field.field_type == .name) { 697 | const field_type_name = field.field_type.name; 698 | if (self.resolveDeclaration(field_type_name)) |decl_type| { 699 | if (decl_type == .handle) { 700 | try self.writer.writeAll(" = .null_handle"); 701 | } else if (decl_type == .bitmask) { 702 | try self.writer.writeAll(" = .{}"); 703 | } else if (decl_type == .typedef and decl_type.typedef == .command_ptr) { 704 | try self.writer.writeAll(" = null"); 705 | } else if ((decl_type == .typedef and builtin_types.has(decl_type.typedef.name)) or 706 | (decl_type == .foreign and builtin_types.has(field_type_name))) 707 | { 708 | try self.writer.writeAll(" = 0"); 709 | } 710 | } 711 | } else if (field.field_type == .pointer) { 712 | try self.writer.writeAll(" = null"); 713 | } 714 | } else if (field.field_type == .pointer and field.field_type.pointer.is_optional) { 715 | // pointer nullability could be here or above 716 | try self.writer.writeAll(" = null"); 717 | } else if (initialized_types.get(container_name)) |value| { 718 | try self.writer.writeAll(" = "); 719 | try self.writer.writeAll(value); 720 | } 721 | } 722 | 723 | fn renderEnumFieldName(self: *Self, name: []const u8, field_name: []const u8) !void { 724 | try self.writeIdentifierWithCase(.snake, try self.extractEnumFieldName(name, field_name)); 725 | } 726 | 727 | fn renderEnumeration(self: *Self, name: []const u8, enumeration: reg.Enum) !void { 728 | if (enumeration.is_bitmask) { 729 | try self.renderBitmaskBits(name, enumeration); 730 | return; 731 | } 732 | 733 | try self.writer.writeAll("pub const "); 734 | try self.renderName(name); 735 | try self.writer.writeAll(" = enum(i32) {"); 736 | 737 | for (enumeration.fields) |field| { 738 | if (field.value == .alias) 739 | continue; 740 | 741 | try self.renderEnumFieldName(name, field.name); 742 | switch (field.value) { 743 | .int => |int| try self.writer.print(" = {}, ", .{int}), 744 | .bitpos => |pos| try self.writer.print(" = 1 << {}, ", .{pos}), 745 | .bit_vector => |bv| try self.writer.print("= 0x{X}, ", .{bv}), 746 | .alias => unreachable, 747 | } 748 | } 749 | 750 | try self.writer.writeAll("_,"); 751 | 752 | for (enumeration.fields) |field| { 753 | if (field.value != .alias or field.value.alias.is_compat_alias) 754 | continue; 755 | 756 | try self.writer.writeAll("pub const "); 757 | try self.renderEnumFieldName(name, field.name); 758 | try self.writer.writeAll(" = "); 759 | try self.renderName(name); 760 | try self.writer.writeByte('.'); 761 | try self.renderEnumFieldName(name, field.value.alias.name); 762 | try self.writer.writeAll(";\n"); 763 | } 764 | 765 | try self.writer.writeAll("};\n"); 766 | } 767 | 768 | fn renderBitmaskBits(self: *Self, name: []const u8, bits: reg.Enum) !void { 769 | try self.writer.writeAll("pub const "); 770 | try self.renderName(name); 771 | try self.writer.writeAll(" = packed struct {"); 772 | 773 | if (bits.fields.len == 0) { 774 | try self.writer.writeAll("_reserved_bits: Flags64 = 0,"); 775 | } else { 776 | var flags_by_bitpos = [_]?[]const u8{null} ** 64; 777 | for (bits.fields) |field| { 778 | if (field.value == .bitpos) { 779 | flags_by_bitpos[field.value.bitpos] = field.name; 780 | } 781 | } 782 | 783 | for (flags_by_bitpos, 0..) |opt_flag_name, bitpos| { 784 | if (opt_flag_name) |flag_name| { 785 | try self.renderEnumFieldName(name, flag_name); 786 | } else { 787 | try self.writer.print("_reserved_bit_{}", .{bitpos}); 788 | } 789 | 790 | try self.writer.writeAll(":bool = false, "); 791 | } 792 | } 793 | try self.writer.writeAll("pub usingnamespace FlagsMixin("); 794 | try self.renderName(name); 795 | try self.writer.writeAll(");\n};\n"); 796 | } 797 | 798 | fn renderHandle(self: *Self, name: []const u8, handle: reg.Handle) !void { 799 | const backing_type: []const u8 = if (handle.is_dispatchable) "usize" else "u64"; 800 | 801 | try self.writer.writeAll("pub const "); 802 | try self.renderName(name); 803 | try self.writer.print(" = enum({s}) {{null_handle = 0, _}};\n", .{backing_type}); 804 | } 805 | 806 | fn renderAlias(self: *Self, name: []const u8, alias: reg.Alias) !void { 807 | if (alias.target == .other_command) { 808 | return; 809 | } else if (self.extractBitflagName(name) != null) { 810 | // Don't make aliases of the bitflag names, as those are replaced by just the flags type 811 | return; 812 | } 813 | 814 | try self.writer.writeAll("pub const "); 815 | try self.renderName(name); 816 | try self.writer.writeAll(" = "); 817 | try self.renderName(alias.name); 818 | try self.writer.writeAll(";\n"); 819 | } 820 | 821 | fn renderExternal(self: *Self, name: []const u8) !void { 822 | try self.writer.writeAll("pub const "); 823 | try self.renderName(name); 824 | try self.writer.writeAll(" = opaque {};\n"); 825 | } 826 | 827 | fn renderForeign(self: *Self, name: []const u8, foreign: reg.Foreign) !void { 828 | if (mem.eql(u8, foreign.depends, "openxr_platform_defines")) { 829 | return; // Skip built-in types, they are handled differently 830 | } 831 | 832 | try self.writer.writeAll("pub const "); 833 | try self.writeIdentifier(name); 834 | try self.writer.print(" = if (@hasDecl(root, \"{s}\")) root.", .{name}); 835 | try self.writeIdentifier(name); 836 | try self.writer.writeAll(" else "); 837 | 838 | if (foreign_types.get(name)) |default| { 839 | try self.writer.writeAll(default); 840 | try self.writer.writeAll(";\n"); 841 | } else { 842 | try self.writer.print("@compileError(\"Missing type definition of '{s}'\");\n", .{name}); 843 | } 844 | } 845 | 846 | fn renderTypedef(self: *Self, name: []const u8, type_info: reg.TypeInfo) !void { 847 | try self.writer.writeAll("pub const "); 848 | try self.renderName(name); 849 | try self.writer.writeAll(" = "); 850 | try self.renderTypeInfo(type_info); 851 | try self.writer.writeAll(";\n"); 852 | } 853 | 854 | fn renderCommandPtrName(self: *Self, name: []const u8) !void { 855 | try self.writeIdentifierFmt("Pfn{s}", .{trimXrNamespace(name)}); 856 | } 857 | 858 | fn renderCommandPtrs(self: *Self) !void { 859 | for (self.registry.decls) |decl| { 860 | switch (decl.decl_type) { 861 | .command => { 862 | try self.writer.writeAll("pub const "); 863 | try self.renderCommandPtrName(decl.name); 864 | try self.writer.writeAll(" = "); 865 | try self.renderCommandPtr(decl.decl_type.command, false); 866 | try self.writer.writeAll(";\n"); 867 | }, 868 | .alias => |alias| if (alias.target == .other_command) { 869 | try self.writer.writeAll("pub const "); 870 | try self.renderCommandPtrName(decl.name); 871 | try self.writer.writeAll(" = "); 872 | try self.renderCommandPtrName(alias.name); 873 | try self.writer.writeAll(";\n"); 874 | }, 875 | else => {}, 876 | } 877 | } 878 | } 879 | 880 | fn renderExtensionInfo(self: *Self) !void { 881 | try self.writer.writeAll( 882 | \\pub const extension_info = struct { 883 | \\ const Info = struct { 884 | \\ name: [:0]const u8, 885 | \\ version: u32, 886 | \\ }; 887 | ); 888 | for (self.registry.extensions) |ext| { 889 | try self.writer.writeAll("pub const "); 890 | try self.writeIdentifierWithCase(.snake, trimXrNamespace(ext.name)); 891 | try self.writer.writeAll("= Info {\n"); 892 | try self.writer.print(".name = \"{s}\", .version = {},", .{ ext.name, ext.version }); 893 | try self.writer.writeAll("};\n"); 894 | } 895 | try self.writer.writeAll("};\n"); 896 | } 897 | 898 | fn renderWrappers(self: *Self) !void { 899 | try self.writer.writeAll( 900 | \\pub fn CommandFlagsMixin(comptime CommandFlags: type) type { 901 | \\ return struct { 902 | \\ pub fn merge(lhs: CommandFlags, rhs: CommandFlags) CommandFlags { 903 | \\ var result: CommandFlags = .{}; 904 | \\ inline for (@typeInfo(CommandFlags).@"struct".fields) |field| { 905 | \\ @field(result, field.name) = @field(lhs, field.name) or @field(rhs, field.name); 906 | \\ } 907 | \\ return result; 908 | \\ } 909 | \\ pub fn intersect(lhs: CommandFlags, rhs: CommandFlags) CommandFlags { 910 | \\ var result: CommandFlags = .{}; 911 | \\ inline for (@typeInfo(CommandFlags).@"struct".fields) |field| { 912 | \\ @field(result, field.name) = @field(lhs, field.name) and @field(rhs, field.name); 913 | \\ } 914 | \\ return result; 915 | \\ } 916 | \\ pub fn complement(self: CommandFlags) CommandFlags { 917 | \\ var result: CommandFlags = .{}; 918 | \\ inline for (@typeInfo(CommandFlags).@"struct".fields) |field| { 919 | \\ @field(result, field.name) = !@field(self, field.name); 920 | \\ } 921 | \\ return result; 922 | \\ } 923 | \\ pub fn subtract(lhs: CommandFlags, rhs: CommandFlags) CommandFlags { 924 | \\ var result: CommandFlags = .{}; 925 | \\ inline for (@typeInfo(CommandFlags).@"struct".fields) |field| { 926 | \\ @field(result, field.name) = @field(lhs, field.name) and !@field(rhs, field.name); 927 | \\ } 928 | \\ return result; 929 | \\ } 930 | \\ pub fn contains(lhs: CommandFlags, rhs: CommandFlags) bool { 931 | \\ inline for (@typeInfo(CommandFlags).@"struct".fields) |field| { 932 | \\ if (!@field(lhs, field.name) and @field(rhs, field.name)) { 933 | \\ return false; 934 | \\ } 935 | \\ } 936 | \\ return true; 937 | \\ } 938 | \\ }; 939 | \\} 940 | \\ 941 | ); 942 | try self.renderWrappersOfDispatchType(.base); 943 | try self.renderWrappersOfDispatchType(.instance); 944 | } 945 | 946 | fn renderWrappersOfDispatchType(self: *Self, dispatch_type: CommandDispatchType) !void { 947 | const name = switch (dispatch_type) { 948 | .base => "Base", 949 | .instance => "Instance", 950 | }; 951 | 952 | try self.writer.print( 953 | \\pub const {0s}CommandFlags = packed struct {{ 954 | \\ 955 | , .{name}); 956 | for (self.registry.decls) |decl| { 957 | // If the target type does not exist, it was likely an empty enum - 958 | // assume spec is correct and that this was not a function alias. 959 | const decl_type = self.resolveAlias(decl.decl_type) catch continue; 960 | const command = switch (decl_type) { 961 | .command => |cmd| cmd, 962 | else => continue, 963 | }; 964 | 965 | if (classifyCommandDispatch(decl.name, command) == dispatch_type) { 966 | try self.writer.writeAll(" "); 967 | try self.writeIdentifierWithCase(.camel, trimXrNamespace(decl.name)); 968 | try self.writer.writeAll(": bool = false,\n"); 969 | } 970 | } 971 | 972 | try self.writer.print( 973 | \\pub fn CmdType(comptime tag: std.meta.FieldEnum({0s}CommandFlags)) type {{ 974 | \\ return switch (tag) {{ 975 | \\ 976 | , .{name}); 977 | for (self.registry.decls) |decl| { 978 | // If the target type does not exist, it was likely an empty enum - 979 | // assume spec is correct and that this was not a function alias. 980 | const decl_type = self.resolveAlias(decl.decl_type) catch continue; 981 | const command = switch (decl_type) { 982 | .command => |cmd| cmd, 983 | else => continue, 984 | }; 985 | 986 | if (classifyCommandDispatch(decl.name, command) == dispatch_type) { 987 | try self.writer.writeAll((" " ** 8) ++ "."); 988 | try self.writeIdentifierWithCase(.camel, trimXrNamespace(decl.name)); 989 | try self.writer.writeAll(" => "); 990 | try self.renderCommandPtrName(decl.name); 991 | try self.writer.writeAll(",\n"); 992 | } 993 | } 994 | try self.writer.writeAll(" };\n}"); 995 | 996 | try self.writer.print( 997 | \\pub fn cmdName(tag: std.meta.FieldEnum({0s}CommandFlags)) [:0]const u8 {{ 998 | \\ return switch(tag) {{ 999 | \\ 1000 | , .{name}); 1001 | for (self.registry.decls) |decl| { 1002 | // If the target type does not exist, it was likely an empty enum - 1003 | // assume spec is correct and that this was not a function alias. 1004 | const decl_type = self.resolveAlias(decl.decl_type) catch continue; 1005 | const command = switch (decl_type) { 1006 | .command => |cmd| cmd, 1007 | else => continue, 1008 | }; 1009 | 1010 | if (classifyCommandDispatch(decl.name, command) == dispatch_type) { 1011 | try self.writer.writeAll((" " ** 8) ++ "."); 1012 | try self.writeIdentifierWithCase(.camel, trimXrNamespace(decl.name)); 1013 | try self.writer.print( 1014 | \\ => "{s}", 1015 | \\ 1016 | , .{decl.name}); 1017 | } 1018 | } 1019 | try self.writer.writeAll(" };\n}"); 1020 | 1021 | try self.writer.print( 1022 | \\ pub usingnamespace CommandFlagsMixin({s}CommandFlags); 1023 | \\}}; 1024 | \\ 1025 | , .{name}); 1026 | 1027 | try self.writer.print( 1028 | \\pub fn {0s}Wrapper(comptime cmds: {0s}CommandFlags) type {{ 1029 | \\ return struct {{ 1030 | \\ dispatch: Dispatch, 1031 | \\ 1032 | \\ const Self = @This(); 1033 | \\ pub const commands = cmds; 1034 | \\ pub const Dispatch = blk: {{ 1035 | \\ @setEvalBranchQuota(10_000); 1036 | \\ const Type = std.builtin.Type; 1037 | \\ const fields_len = fields_len: {{ 1038 | \\ var fields_len = 0; 1039 | \\ for (@typeInfo({0s}CommandFlags).@"struct".fields) |field| {{ 1040 | \\ fields_len += @intFromBool(@field(cmds, field.name)); 1041 | \\ }} 1042 | \\ break :fields_len fields_len; 1043 | \\ }}; 1044 | \\ var fields: [fields_len]Type.StructField = undefined; 1045 | \\ var i: usize = 0; 1046 | \\ for (@typeInfo({0s}CommandFlags).@"struct".fields) |field| {{ 1047 | \\ if (@field(cmds, field.name)) {{ 1048 | \\ const field_tag = std.enums.nameCast(std.meta.FieldEnum({0s}CommandFlags), field.name); 1049 | \\ const PfnType = {0s}CommandFlags.CmdType(field_tag); 1050 | \\ fields[i] = .{{ 1051 | \\ .name = {0s}CommandFlags.cmdName(field_tag), 1052 | \\ .type = PfnType, 1053 | \\ .default_value_ptr = null, 1054 | \\ .is_comptime = false, 1055 | \\ .alignment = @alignOf(PfnType), 1056 | \\ }}; 1057 | \\ i += 1; 1058 | \\ }} 1059 | \\ }} 1060 | \\ break :blk @Type(.{{ 1061 | \\ .@"struct" = .{{ 1062 | \\ .layout = .auto, 1063 | \\ .fields = &fields, 1064 | \\ .decls = &[_]std.builtin.Type.Declaration{{}}, 1065 | \\ .is_tuple = false, 1066 | \\ }}, 1067 | \\ }}); 1068 | \\ }}; 1069 | \\ 1070 | , .{name}); 1071 | 1072 | try self.renderWrapperLoader(dispatch_type); 1073 | 1074 | for (self.registry.decls) |decl| { 1075 | // If the target type does not exist, it was likely an empty enum - 1076 | // assume spec is correct and that this was not a function alias. 1077 | const decl_type = self.resolveAlias(decl.decl_type) catch continue; 1078 | const command = switch (decl_type) { 1079 | .command => |cmd| cmd, 1080 | else => continue, 1081 | }; 1082 | 1083 | if (classifyCommandDispatch(decl.name, command) != dispatch_type) { 1084 | continue; 1085 | } 1086 | // Note: If this decl is an alias, generate a full wrapper instead of simply an 1087 | // alias like `const old = new;`. This ensures that OpenXR bindings generated 1088 | // for newer versions of openxr can still invoke extension behavior on older 1089 | // implementations. 1090 | try self.renderWrapper(decl.name, command); 1091 | } 1092 | 1093 | try self.writer.writeAll("};}\n"); 1094 | } 1095 | 1096 | fn renderWrapperLoader(self: *Self, dispatch_type: CommandDispatchType) !void { 1097 | const params = switch (dispatch_type) { 1098 | .base => "loader: anytype", 1099 | .instance => "instance: Instance, loader: anytype", 1100 | }; 1101 | 1102 | const loader_first_arg = switch (dispatch_type) { 1103 | .base => "Instance.null_handle", 1104 | .instance => "instance", 1105 | }; 1106 | 1107 | @setEvalBranchQuota(2000); 1108 | 1109 | try self.writer.print( 1110 | \\pub fn load({[params]s}) error{{CommandLoadFailure}}!Self {{ 1111 | \\ var self: Self = undefined; 1112 | \\ inline for (std.meta.fields(Dispatch)) |field| {{ 1113 | \\ const name: [*:0]const u8 = @ptrCast(field.name ++ "\x00"); 1114 | \\ var cmd_ptr: PfnVoidFunction = undefined; 1115 | \\ const result: Result = loader({[first_arg]s}, name, &cmd_ptr); 1116 | \\ if (result != .success) return error.CommandLoadFailure; 1117 | \\ @field(self.dispatch, field.name) = @ptrCast(cmd_ptr); 1118 | \\ }} 1119 | \\ return self; 1120 | \\}} 1121 | \\pub fn loadNoFail({[params]s}) Self {{ 1122 | \\ var self: Self = undefined; 1123 | \\ inline for (std.meta.fields(Dispatch)) |field| {{ 1124 | \\ const name: [*:0]const u8 = @ptrCast(field.name ++ "\x00"); 1125 | \\ var cmd_ptr: PfnVoidFunction = undefined; 1126 | \\ if (loader({[first_arg]s}, name, &cmd_ptr)) {{ 1127 | \\ @field(self.dispatch, field.name) = @ptrCast(cmd_ptr); 1128 | \\ }} 1129 | \\ }} 1130 | \\ return self; 1131 | \\}} 1132 | , .{ .params = params, .first_arg = loader_first_arg }); 1133 | } 1134 | 1135 | fn derefName(name: []const u8) []const u8 { 1136 | var it = id_render.SegmentIterator.init(name); 1137 | return if (mem.eql(u8, it.next().?, "p")) 1138 | name[1..] 1139 | else 1140 | name; 1141 | } 1142 | 1143 | fn renderWrapperPrototype(self: *Self, name: []const u8, command: reg.Command, returns: []const ReturnValue) !void { 1144 | try self.writer.writeAll("pub fn "); 1145 | try self.writeIdentifierWithCase(.camel, trimXrNamespace(name)); 1146 | try self.writer.writeAll("(self: Self, "); 1147 | 1148 | for (command.params) |param| { 1149 | // This parameter is returned instead. 1150 | if ((try self.classifyParam(param)) == .out_pointer) { 1151 | continue; 1152 | } 1153 | 1154 | try self.writeIdentifierWithCase(.snake, param.name); 1155 | try self.writer.writeAll(": "); 1156 | try self.renderTypeInfo(param.param_type); 1157 | try self.writer.writeAll(", "); 1158 | } 1159 | 1160 | try self.writer.writeAll(") "); 1161 | 1162 | const returns_xr_result = command.return_type.* == .name and mem.eql(u8, command.return_type.name, "XrResult"); 1163 | if (returns_xr_result) { 1164 | try self.renderErrorSetName(name); 1165 | try self.writer.writeByte('!'); 1166 | } 1167 | 1168 | if (returns.len == 1) { 1169 | try self.renderTypeInfo(returns[0].return_value_type); 1170 | } else if (returns.len > 1) { 1171 | try self.renderReturnStructName(name); 1172 | } else { 1173 | try self.writer.writeAll("void"); 1174 | } 1175 | } 1176 | 1177 | fn renderWrapperCall(self: *Self, name: []const u8, command: reg.Command, returns: []const ReturnValue) !void { 1178 | try self.writer.writeAll("self.dispatch."); 1179 | try self.writeIdentifier(name); 1180 | try self.writer.writeAll("("); 1181 | 1182 | for (command.params) |param| { 1183 | switch (try self.classifyParam(param)) { 1184 | .out_pointer => { 1185 | try self.writer.writeByte('&'); 1186 | if (returns.len > 1) { 1187 | try self.writer.writeAll("return_values."); 1188 | } 1189 | try self.writeIdentifierWithCase(.snake, derefName(param.name)); 1190 | }, 1191 | .bitflags, .in_pointer, .in_out_pointer, .buffer_len, .mut_buffer_len, .other => { 1192 | try self.writeIdentifierWithCase(.snake, param.name); 1193 | }, 1194 | } 1195 | 1196 | try self.writer.writeAll(", "); 1197 | } 1198 | try self.writer.writeAll(")"); 1199 | } 1200 | 1201 | fn extractReturns(self: *Self, command: reg.Command) ![]const ReturnValue { 1202 | var returns = std.ArrayList(ReturnValue).init(self.allocator); 1203 | 1204 | if (command.return_type.* == .name) { 1205 | const return_name = command.return_type.name; 1206 | if (!mem.eql(u8, return_name, "void") and !mem.eql(u8, return_name, "XrResult")) { 1207 | try returns.append(.{ 1208 | .name = "return_value", 1209 | .return_value_type = command.return_type.*, 1210 | .origin = .inner_return_value, 1211 | }); 1212 | } 1213 | } 1214 | 1215 | if (command.success_codes.len > 1) { 1216 | if (command.return_type.* != .name or !mem.eql(u8, command.return_type.name, "XrResult")) { 1217 | return error.InvalidRegistry; 1218 | } 1219 | 1220 | try returns.append(.{ 1221 | .name = "result", 1222 | .return_value_type = command.return_type.*, 1223 | .origin = .inner_return_value, 1224 | }); 1225 | } else if (command.success_codes.len == 1 and !mem.eql(u8, command.success_codes[0], "XR_SUCCESS")) { 1226 | return error.InvalidRegistry; 1227 | } 1228 | 1229 | for (command.params) |param| { 1230 | if ((try self.classifyParam(param)) == .out_pointer) { 1231 | try returns.append(.{ 1232 | .name = derefName(param.name), 1233 | .return_value_type = param.param_type.pointer.child.*, 1234 | .origin = .parameter, 1235 | }); 1236 | } 1237 | } 1238 | 1239 | return try returns.toOwnedSlice(); 1240 | } 1241 | 1242 | fn renderReturnStructName(self: *Self, command_name: []const u8) !void { 1243 | try self.writeIdentifierFmt("{s}Result", .{trimXrNamespace(command_name)}); 1244 | } 1245 | 1246 | fn renderErrorSetName(self: *Self, name: []const u8) !void { 1247 | try self.writeIdentifierWithCase(.title, trimXrNamespace(name)); 1248 | try self.writer.writeAll("Error"); 1249 | } 1250 | 1251 | fn renderReturnStruct(self: *Self, command_name: []const u8, returns: []const ReturnValue) !void { 1252 | try self.writer.writeAll("pub const "); 1253 | try self.renderReturnStructName(command_name); 1254 | try self.writer.writeAll(" = struct {\n"); 1255 | for (returns) |ret| { 1256 | try self.writeIdentifierWithCase(.snake, ret.name); 1257 | try self.writer.writeAll(": "); 1258 | try self.renderTypeInfo(ret.return_value_type); 1259 | try self.writer.writeAll(", "); 1260 | } 1261 | try self.writer.writeAll("};\n"); 1262 | } 1263 | 1264 | fn renderWrapper(self: *Self, name: []const u8, command: reg.Command) !void { 1265 | const returns_xr_result = command.return_type.* == .name and mem.eql(u8, command.return_type.name, "XrResult"); 1266 | const returns_void = command.return_type.* == .name and mem.eql(u8, command.return_type.name, "void"); 1267 | 1268 | const returns = try self.extractReturns(command); 1269 | 1270 | if (returns.len > 1) { 1271 | try self.renderReturnStruct(name, returns); 1272 | } 1273 | 1274 | if (returns_xr_result) { 1275 | try self.writer.writeAll("pub const "); 1276 | try self.renderErrorSetName(name); 1277 | try self.writer.writeAll(" = "); 1278 | try self.renderErrorSet(command.error_codes); 1279 | try self.writer.writeAll(";\n"); 1280 | } 1281 | 1282 | try self.renderWrapperPrototype(name, command, returns); 1283 | 1284 | if (returns.len == 1 and returns[0].origin == .inner_return_value) { 1285 | try self.writer.writeAll("{\n\n"); 1286 | 1287 | if (returns_xr_result) { 1288 | try self.writer.writeAll("const result = "); 1289 | try self.renderWrapperCall(name, command, returns); 1290 | try self.writer.writeAll(";\n"); 1291 | 1292 | try self.renderErrorSwitch("result", command); 1293 | try self.writer.writeAll("return result;\n"); 1294 | } else { 1295 | try self.writer.writeAll("return "); 1296 | try self.renderWrapperCall(name, command, returns); 1297 | try self.writer.writeAll(";\n"); 1298 | } 1299 | 1300 | try self.writer.writeAll("\n}\n"); 1301 | return; 1302 | } 1303 | 1304 | try self.writer.writeAll("{\n"); 1305 | if (returns.len == 1) { 1306 | try self.writer.writeAll("var "); 1307 | try self.writeIdentifierWithCase(.snake, returns[0].name); 1308 | try self.writer.writeAll(": "); 1309 | try self.renderTypeInfo(returns[0].return_value_type); 1310 | try self.writer.writeAll(" = undefined;\n"); 1311 | } else if (returns.len > 1) { 1312 | try self.writer.writeAll("var return_values: "); 1313 | try self.renderReturnStructName(name); 1314 | try self.writer.writeAll(" = undefined;\n"); 1315 | } 1316 | 1317 | if (returns_xr_result) { 1318 | try self.writer.writeAll("const result = "); 1319 | try self.renderWrapperCall(name, command, returns); 1320 | try self.writer.writeAll(";\n"); 1321 | 1322 | try self.renderErrorSwitch("result", command); 1323 | if (command.success_codes.len > 1) { 1324 | try self.writer.writeAll("return_values.result = result;\n"); 1325 | } 1326 | } else { 1327 | if (!returns_void) { 1328 | try self.writer.writeAll("return_values.return_value = "); 1329 | } 1330 | try self.renderWrapperCall(name, command, returns); 1331 | try self.writer.writeAll(";\n"); 1332 | } 1333 | 1334 | if (returns.len == 1) { 1335 | try self.writer.writeAll("return "); 1336 | try self.writeIdentifierWithCase(.snake, returns[0].name); 1337 | try self.writer.writeAll(";\n"); 1338 | } else if (returns.len > 1) { 1339 | try self.writer.writeAll("return return_values;\n"); 1340 | } 1341 | 1342 | try self.writer.writeAll("}\n"); 1343 | } 1344 | 1345 | fn renderErrorSwitch(self: *Self, result_var: []const u8, command: reg.Command) !void { 1346 | try self.writer.writeAll("switch ("); 1347 | try self.writeIdentifier(result_var); 1348 | try self.writer.writeAll(") {\n"); 1349 | 1350 | for (command.success_codes) |success| { 1351 | try self.writer.writeAll("Result."); 1352 | try self.renderEnumFieldName("XrResult", success); 1353 | try self.writer.writeAll(" => {},"); 1354 | } 1355 | 1356 | for (command.error_codes) |err| { 1357 | try self.writer.writeAll("Result."); 1358 | try self.renderEnumFieldName("XrResult", err); 1359 | try self.writer.writeAll(" => return error."); 1360 | try self.renderResultAsErrorName(err); 1361 | try self.writer.writeAll(", "); 1362 | } 1363 | 1364 | try self.writer.writeAll("else => return error.Unknown,}\n"); 1365 | } 1366 | 1367 | fn renderErrorSet(self: *Self, errors: []const []const u8) !void { 1368 | try self.writer.writeAll("error{"); 1369 | for (errors) |name| { 1370 | try self.renderResultAsErrorName(name); 1371 | try self.writer.writeAll(", "); 1372 | } 1373 | try self.writer.writeAll("Unknown, }"); 1374 | } 1375 | 1376 | fn renderResultAsErrorName(self: *Self, name: []const u8) !void { 1377 | const error_prefix = "XR_ERROR_"; 1378 | if (mem.startsWith(u8, name, error_prefix)) { 1379 | try self.writeIdentifierWithCase(.title, name[error_prefix.len..]); 1380 | } else { 1381 | // Apparently some commands (XrAcquireProfilingLockInfoKHR) return 1382 | // success codes as error... 1383 | try self.writeIdentifierWithCase(.title, trimXrNamespace(name)); 1384 | } 1385 | } 1386 | }; 1387 | } 1388 | 1389 | pub fn render(writer: anytype, allocator: Allocator, registry: *const reg.Registry, id_renderer: *IdRenderer) !void { 1390 | var renderer = try Renderer(@TypeOf(writer)).init(writer, allocator, registry, id_renderer); 1391 | defer renderer.deinit(); 1392 | try renderer.render(); 1393 | } 1394 | -------------------------------------------------------------------------------- /generator/xml.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const testing = std.testing; 4 | const Allocator = mem.Allocator; 5 | const ArenaAllocator = std.heap.ArenaAllocator; 6 | 7 | pub const Attribute = struct { 8 | name: []const u8, 9 | value: []const u8, 10 | }; 11 | 12 | pub const Content = union(enum) { 13 | char_data: []const u8, 14 | comment: []const u8, 15 | element: *Element, 16 | }; 17 | 18 | pub const Element = struct { 19 | tag: []const u8, 20 | attributes: []Attribute = &.{}, 21 | children: []Content = &.{}, 22 | 23 | pub fn getAttribute(self: Element, attrib_name: []const u8) ?[]const u8 { 24 | for (self.attributes) |child| { 25 | if (mem.eql(u8, child.name, attrib_name)) { 26 | return child.value; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | 33 | pub fn getCharData(self: Element, child_tag: []const u8) ?[]const u8 { 34 | const child = self.findChildByTag(child_tag) orelse return null; 35 | if (child.children.len != 1) { 36 | return null; 37 | } 38 | 39 | return switch (child.children[0]) { 40 | .char_data => |char_data| char_data, 41 | else => null, 42 | }; 43 | } 44 | 45 | pub fn iterator(self: Element) ChildIterator { 46 | return .{ 47 | .items = self.children, 48 | .i = 0, 49 | }; 50 | } 51 | 52 | pub fn elements(self: Element) ChildElementIterator { 53 | return .{ 54 | .inner = self.iterator(), 55 | }; 56 | } 57 | 58 | pub fn findChildByTag(self: Element, tag: []const u8) ?*Element { 59 | var it = self.findChildrenByTag(tag); 60 | return it.next(); 61 | } 62 | 63 | pub fn findChildrenByTag(self: Element, tag: []const u8) FindChildrenByTagIterator { 64 | return .{ 65 | .inner = self.elements(), 66 | .tag = tag, 67 | }; 68 | } 69 | 70 | pub const ChildIterator = struct { 71 | items: []Content, 72 | i: usize, 73 | 74 | pub fn next(self: *ChildIterator) ?*Content { 75 | if (self.i < self.items.len) { 76 | self.i += 1; 77 | return &self.items[self.i - 1]; 78 | } 79 | 80 | return null; 81 | } 82 | }; 83 | 84 | pub const ChildElementIterator = struct { 85 | inner: ChildIterator, 86 | 87 | pub fn next(self: *ChildElementIterator) ?*Element { 88 | while (self.inner.next()) |child| { 89 | if (child.* != .element) { 90 | continue; 91 | } 92 | 93 | return child.*.element; 94 | } 95 | 96 | return null; 97 | } 98 | }; 99 | 100 | pub const FindChildrenByTagIterator = struct { 101 | inner: ChildElementIterator, 102 | tag: []const u8, 103 | 104 | pub fn next(self: *FindChildrenByTagIterator) ?*Element { 105 | while (self.inner.next()) |child| { 106 | if (!mem.eql(u8, child.tag, self.tag)) { 107 | continue; 108 | } 109 | 110 | return child; 111 | } 112 | 113 | return null; 114 | } 115 | }; 116 | }; 117 | 118 | pub const Document = struct { 119 | arena: ArenaAllocator, 120 | xml_decl: ?*Element, 121 | root: *Element, 122 | 123 | pub fn deinit(self: Document) void { 124 | var arena = self.arena; // Copy to stack so self can be taken by value. 125 | arena.deinit(); 126 | } 127 | }; 128 | 129 | const Parser = struct { 130 | source: []const u8, 131 | offset: usize, 132 | line: usize, 133 | column: usize, 134 | 135 | fn init(source: []const u8) Parser { 136 | return .{ 137 | .source = source, 138 | .offset = 0, 139 | .line = 0, 140 | .column = 0, 141 | }; 142 | } 143 | 144 | fn peek(self: *Parser) ?u8 { 145 | return if (self.offset < self.source.len) self.source[self.offset] else null; 146 | } 147 | 148 | fn consume(self: *Parser) !u8 { 149 | if (self.offset < self.source.len) { 150 | return self.consumeNoEof(); 151 | } 152 | 153 | return error.UnexpectedEof; 154 | } 155 | 156 | fn consumeNoEof(self: *Parser) u8 { 157 | std.debug.assert(self.offset < self.source.len); 158 | const c = self.source[self.offset]; 159 | self.offset += 1; 160 | 161 | if (c == '\n') { 162 | self.line += 1; 163 | self.column = 0; 164 | } else { 165 | self.column += 1; 166 | } 167 | 168 | return c; 169 | } 170 | 171 | fn eat(self: *Parser, char: u8) bool { 172 | self.expect(char) catch return false; 173 | return true; 174 | } 175 | 176 | fn expect(self: *Parser, expected: u8) !void { 177 | if (self.peek()) |actual| { 178 | if (expected != actual) { 179 | return error.UnexpectedCharacter; 180 | } 181 | 182 | _ = self.consumeNoEof(); 183 | return; 184 | } 185 | 186 | return error.UnexpectedEof; 187 | } 188 | 189 | fn eatStr(self: *Parser, text: []const u8) bool { 190 | self.expectStr(text) catch return false; 191 | return true; 192 | } 193 | 194 | fn expectStr(self: *Parser, text: []const u8) !void { 195 | if (self.source.len < self.offset + text.len) { 196 | return error.UnexpectedEof; 197 | } else if (mem.startsWith(u8, self.source[self.offset..], text)) { 198 | var i: usize = 0; 199 | while (i < text.len) : (i += 1) { 200 | _ = self.consumeNoEof(); 201 | } 202 | 203 | return; 204 | } 205 | 206 | return error.UnexpectedCharacter; 207 | } 208 | 209 | fn eatWs(self: *Parser) bool { 210 | var ws = false; 211 | 212 | while (self.peek()) |ch| { 213 | switch (ch) { 214 | ' ', '\t', '\n', '\r' => { 215 | ws = true; 216 | _ = self.consumeNoEof(); 217 | }, 218 | else => break, 219 | } 220 | } 221 | 222 | return ws; 223 | } 224 | 225 | fn expectWs(self: *Parser) !void { 226 | if (!self.eatWs()) return error.UnexpectedCharacter; 227 | } 228 | 229 | fn currentLine(self: Parser) []const u8 { 230 | var begin: usize = 0; 231 | if (mem.lastIndexOfScalar(u8, self.source[0..self.offset], '\n')) |prev_nl| { 232 | begin = prev_nl + 1; 233 | } 234 | 235 | const end = mem.indexOfScalarPos(u8, self.source, self.offset, '\n') orelse self.source.len; 236 | return self.source[begin..end]; 237 | } 238 | }; 239 | 240 | test "xml: Parser" { 241 | { 242 | var parser = Parser.init("I like pythons"); 243 | try testing.expectEqual(@as(?u8, 'I'), parser.peek()); 244 | try testing.expectEqual(@as(u8, 'I'), parser.consumeNoEof()); 245 | try testing.expectEqual(@as(?u8, ' '), parser.peek()); 246 | try testing.expectEqual(@as(u8, ' '), try parser.consume()); 247 | 248 | try testing.expect(parser.eat('l')); 249 | try testing.expectEqual(@as(?u8, 'i'), parser.peek()); 250 | try testing.expectEqual(false, parser.eat('a')); 251 | try testing.expectEqual(@as(?u8, 'i'), parser.peek()); 252 | 253 | try parser.expect('i'); 254 | try testing.expectEqual(@as(?u8, 'k'), parser.peek()); 255 | try testing.expectError(error.UnexpectedCharacter, parser.expect('a')); 256 | try testing.expectEqual(@as(?u8, 'k'), parser.peek()); 257 | 258 | try testing.expect(parser.eatStr("ke")); 259 | try testing.expectEqual(@as(?u8, ' '), parser.peek()); 260 | 261 | try testing.expect(parser.eatWs()); 262 | try testing.expectEqual(@as(?u8, 'p'), parser.peek()); 263 | try testing.expectEqual(false, parser.eatWs()); 264 | try testing.expectEqual(@as(?u8, 'p'), parser.peek()); 265 | 266 | try testing.expectEqual(false, parser.eatStr("aaaaaaaaa")); 267 | try testing.expectEqual(@as(?u8, 'p'), parser.peek()); 268 | 269 | try testing.expectError(error.UnexpectedEof, parser.expectStr("aaaaaaaaa")); 270 | try testing.expectEqual(@as(?u8, 'p'), parser.peek()); 271 | try testing.expectError(error.UnexpectedCharacter, parser.expectStr("pytn")); 272 | try testing.expectEqual(@as(?u8, 'p'), parser.peek()); 273 | try parser.expectStr("python"); 274 | try testing.expectEqual(@as(?u8, 's'), parser.peek()); 275 | } 276 | 277 | { 278 | var parser = Parser.init(""); 279 | try testing.expectEqual(parser.peek(), null); 280 | try testing.expectError(error.UnexpectedEof, parser.consume()); 281 | try testing.expectEqual(parser.eat('p'), false); 282 | try testing.expectError(error.UnexpectedEof, parser.expect('p')); 283 | } 284 | } 285 | 286 | pub const ParseError = error{ 287 | IllegalCharacter, 288 | UnexpectedEof, 289 | UnexpectedCharacter, 290 | UnclosedValue, 291 | UnclosedComment, 292 | InvalidName, 293 | InvalidEntity, 294 | InvalidStandaloneValue, 295 | NonMatchingClosingTag, 296 | InvalidDocument, 297 | OutOfMemory, 298 | }; 299 | 300 | pub fn parse(backing_allocator: Allocator, source: []const u8) !Document { 301 | var parser = Parser.init(source); 302 | return try parseDocument(&parser, backing_allocator); 303 | } 304 | 305 | fn parseDocument(parser: *Parser, backing_allocator: Allocator) !Document { 306 | var doc = Document{ 307 | .arena = ArenaAllocator.init(backing_allocator), 308 | .xml_decl = null, 309 | .root = undefined, 310 | }; 311 | 312 | errdefer doc.deinit(); 313 | 314 | const allocator = doc.arena.allocator(); 315 | 316 | try skipComments(parser, allocator); 317 | 318 | doc.xml_decl = try parseElement(parser, allocator, .xml_decl); 319 | _ = parser.eatWs(); 320 | _ = parser.eatStr(""); 321 | _ = parser.eatWs(); 322 | 323 | // xr.xml currently has 2 processing instruction tags, they're handled manually for now 324 | _ = try parseElement(parser, allocator, .xml_decl); 325 | _ = parser.eatWs(); 326 | _ = try parseElement(parser, allocator, .xml_decl); 327 | _ = parser.eatWs(); 328 | 329 | try skipComments(parser, allocator); 330 | 331 | doc.root = (try parseElement(parser, allocator, .element)) orelse return error.InvalidDocument; 332 | _ = parser.eatWs(); 333 | try skipComments(parser, allocator); 334 | 335 | if (parser.peek() != null) return error.InvalidDocument; 336 | 337 | return doc; 338 | } 339 | 340 | fn parseAttrValue(parser: *Parser, alloc: Allocator) ![]const u8 { 341 | const quote = try parser.consume(); 342 | if (quote != '"' and quote != '\'') return error.UnexpectedCharacter; 343 | 344 | const begin = parser.offset; 345 | 346 | while (true) { 347 | const c = parser.consume() catch return error.UnclosedValue; 348 | if (c == quote) break; 349 | } 350 | 351 | const end = parser.offset - 1; 352 | 353 | return try unescape(alloc, parser.source[begin..end]); 354 | } 355 | 356 | fn parseEqAttrValue(parser: *Parser, alloc: Allocator) ![]const u8 { 357 | _ = parser.eatWs(); 358 | try parser.expect('='); 359 | _ = parser.eatWs(); 360 | 361 | return try parseAttrValue(parser, alloc); 362 | } 363 | 364 | fn parseNameNoDupe(parser: *Parser) ![]const u8 { 365 | // XML's spec on names is very long, so to make this easier 366 | // we just take any character that is not special and not whitespace 367 | const begin = parser.offset; 368 | 369 | while (parser.peek()) |ch| { 370 | switch (ch) { 371 | ' ', '\t', '\n', '\r' => break, 372 | '&', '"', '\'', '<', '>', '?', '=', '/' => break, 373 | else => _ = parser.consumeNoEof(), 374 | } 375 | } 376 | 377 | const end = parser.offset; 378 | if (begin == end) return error.InvalidName; 379 | 380 | return parser.source[begin..end]; 381 | } 382 | 383 | fn parseCharData(parser: *Parser, alloc: Allocator) !?[]const u8 { 384 | const begin = parser.offset; 385 | 386 | while (parser.peek()) |ch| { 387 | switch (ch) { 388 | '<' => break, 389 | else => _ = parser.consumeNoEof(), 390 | } 391 | } 392 | 393 | const end = parser.offset; 394 | if (begin == end) return null; 395 | 396 | return try unescape(alloc, parser.source[begin..end]); 397 | } 398 | 399 | fn parseContent(parser: *Parser, alloc: Allocator) ParseError!Content { 400 | if (try parseCharData(parser, alloc)) |cd| { 401 | return Content{ .char_data = cd }; 402 | } else if (try parseComment(parser, alloc)) |comment| { 403 | return Content{ .comment = comment }; 404 | } else if (try parseElement(parser, alloc, .element)) |elem| { 405 | return Content{ .element = elem }; 406 | } else { 407 | return error.UnexpectedCharacter; 408 | } 409 | } 410 | 411 | fn parseAttr(parser: *Parser, alloc: Allocator) !?Attribute { 412 | const name = parseNameNoDupe(parser) catch return null; 413 | _ = parser.eatWs(); 414 | try parser.expect('='); 415 | _ = parser.eatWs(); 416 | const value = try parseAttrValue(parser, alloc); 417 | 418 | const attr = Attribute{ 419 | .name = try alloc.dupe(u8, name), 420 | .value = value, 421 | }; 422 | return attr; 423 | } 424 | 425 | const ElementKind = enum { 426 | xml_decl, 427 | element, 428 | }; 429 | 430 | fn parseElement(parser: *Parser, alloc: Allocator, comptime kind: ElementKind) !?*Element { 431 | const start = parser.offset; 432 | 433 | switch (kind) { 434 | .xml_decl => { 435 | if (!parser.eatStr(" { 438 | if (!parser.eat('<')) return null; 439 | }, 440 | } 441 | 442 | const tag = parseNameNoDupe(parser) catch { 443 | parser.offset = start; 444 | return null; 445 | }; 446 | 447 | var attributes = std.ArrayList(Attribute).init(alloc); 448 | defer attributes.deinit(); 449 | 450 | var children = std.ArrayList(Content).init(alloc); 451 | defer children.deinit(); 452 | 453 | while (parser.eatWs()) { 454 | const attr = (try parseAttr(parser, alloc)) orelse break; 455 | try attributes.append(attr); 456 | } 457 | 458 | switch (kind) { 459 | .xml_decl => try parser.expectStr("?>"), 460 | .element => { 461 | if (!parser.eatStr("/>")) { 462 | try parser.expect('>'); 463 | 464 | while (true) { 465 | if (parser.peek() == null) { 466 | return error.UnexpectedEof; 467 | } else if (parser.eatStr("'); 482 | } 483 | }, 484 | } 485 | 486 | const element = try alloc.create(Element); 487 | element.* = .{ 488 | .tag = try alloc.dupe(u8, tag), 489 | .attributes = try attributes.toOwnedSlice(), 490 | .children = try children.toOwnedSlice(), 491 | }; 492 | return element; 493 | } 494 | 495 | test "xml: parseElement" { 496 | var arena = ArenaAllocator.init(testing.allocator); 497 | defer arena.deinit(); 498 | const alloc = arena.allocator(); 499 | 500 | { 501 | var parser = Parser.init("<= a='b'/>"); 502 | try testing.expectEqual(@as(?*Element, null), try parseElement(&parser, alloc, .element)); 503 | try testing.expectEqual(@as(?u8, '<'), parser.peek()); 504 | } 505 | 506 | { 507 | var parser = Parser.init(""); 508 | const elem = try parseElement(&parser, alloc, .element); 509 | try testing.expectEqualSlices(u8, elem.?.tag, "python"); 510 | 511 | const size_attr = elem.?.attributes[0]; 512 | try testing.expectEqualSlices(u8, size_attr.name, "size"); 513 | try testing.expectEqualSlices(u8, size_attr.value, "15"); 514 | 515 | const color_attr = elem.?.attributes[1]; 516 | try testing.expectEqualSlices(u8, color_attr.name, "color"); 517 | try testing.expectEqualSlices(u8, color_attr.value, "green"); 518 | } 519 | 520 | { 521 | var parser = Parser.init("test"); 522 | const elem = try parseElement(&parser, alloc, .element); 523 | try testing.expectEqualSlices(u8, elem.?.tag, "python"); 524 | try testing.expectEqualSlices(u8, elem.?.children[0].char_data, "test"); 525 | } 526 | 527 | { 528 | var parser = Parser.init("bdf"); 529 | const elem = try parseElement(&parser, alloc, .element); 530 | try testing.expectEqualSlices(u8, elem.?.tag, "a"); 531 | try testing.expectEqualSlices(u8, elem.?.children[0].char_data, "b"); 532 | try testing.expectEqualSlices(u8, elem.?.children[1].element.tag, "c"); 533 | try testing.expectEqualSlices(u8, elem.?.children[2].char_data, "d"); 534 | try testing.expectEqualSlices(u8, elem.?.children[3].element.tag, "e"); 535 | try testing.expectEqualSlices(u8, elem.?.children[4].char_data, "f"); 536 | try testing.expectEqualSlices(u8, elem.?.children[5].comment, "g"); 537 | } 538 | } 539 | 540 | test "xml: parse prolog" { 541 | var arena = ArenaAllocator.init(testing.allocator); 542 | defer arena.deinit(); 543 | const a = arena.allocator(); 544 | 545 | { 546 | var parser = Parser.init(""); 547 | try testing.expectEqual(@as(?*Element, null), try parseElement(&parser, a, .xml_decl)); 548 | try testing.expectEqual(@as(?u8, '<'), parser.peek()); 549 | } 550 | 551 | { 552 | var parser = Parser.init(""); 553 | const decl = try parseElement(&parser, a, .xml_decl); 554 | try testing.expectEqualSlices(u8, "aa", decl.?.getAttribute("version").?); 555 | try testing.expectEqual(@as(?[]const u8, null), decl.?.getAttribute("encoding")); 556 | try testing.expectEqual(@as(?[]const u8, null), decl.?.getAttribute("standalone")); 557 | } 558 | 559 | { 560 | var parser = Parser.init(""); 561 | const decl = try parseElement(&parser, a, .xml_decl); 562 | try testing.expectEqualSlices(u8, "ccc", decl.?.getAttribute("version").?); 563 | try testing.expectEqualSlices(u8, "bbb", decl.?.getAttribute("encoding").?); 564 | try testing.expectEqualSlices(u8, "yes", decl.?.getAttribute("standalone").?); 565 | } 566 | } 567 | 568 | fn skipComments(parser: *Parser, alloc: Allocator) !void { 569 | while ((try parseComment(parser, alloc)) != null) { 570 | _ = parser.eatWs(); 571 | } 572 | } 573 | 574 | fn parseComment(parser: *Parser, alloc: Allocator) !?[]const u8 { 575 | if (!parser.eatStr("")) { 579 | _ = parser.consume() catch return error.UnclosedComment; 580 | } 581 | 582 | const end = parser.offset - "-->".len; 583 | return try alloc.dupe(u8, parser.source[begin..end]); 584 | } 585 | 586 | fn unescapeEntity(text: []const u8) !u8 { 587 | const EntitySubstition = struct { text: []const u8, replacement: u8 }; 588 | 589 | const entities = [_]EntitySubstition{ 590 | .{ .text = "<", .replacement = '<' }, 591 | .{ .text = ">", .replacement = '>' }, 592 | .{ .text = "&", .replacement = '&' }, 593 | .{ .text = "'", .replacement = '\'' }, 594 | .{ .text = """, .replacement = '"' }, 595 | }; 596 | 597 | for (entities) |entity| { 598 | if (mem.eql(u8, text, entity.text)) return entity.replacement; 599 | } 600 | 601 | return error.InvalidEntity; 602 | } 603 | 604 | fn unescape(arena: Allocator, text: []const u8) ![]const u8 { 605 | const unescaped = try arena.alloc(u8, text.len); 606 | 607 | var j: usize = 0; 608 | var i: usize = 0; 609 | while (i < text.len) : (j += 1) { 610 | if (text[i] == '&') { 611 | const entity_end = 1 + (mem.indexOfScalarPos(u8, text, i, ';') orelse return error.InvalidEntity); 612 | unescaped[j] = try unescapeEntity(text[i..entity_end]); 613 | i = entity_end; 614 | } else { 615 | unescaped[j] = text[i]; 616 | i += 1; 617 | } 618 | } 619 | 620 | return unescaped[0..j]; 621 | } 622 | 623 | test "xml: unescape" { 624 | var arena = ArenaAllocator.init(testing.allocator); 625 | defer arena.deinit(); 626 | const a = arena.allocator(); 627 | 628 | try testing.expectEqualSlices(u8, "test", try unescape(a, "test")); 629 | try testing.expectEqualSlices(u8, "ad\"e'f<", try unescape(a, "a<b&c>d"e'f<")); 630 | try testing.expectError(error.InvalidEntity, unescape(a, "python&")); 631 | try testing.expectError(error.InvalidEntity, unescape(a, "python&&")); 632 | try testing.expectError(error.InvalidEntity, unescape(a, "python&test;")); 633 | try testing.expectError(error.InvalidEntity, unescape(a, "python&boa")); 634 | } 635 | 636 | test "xml: top level comments" { 637 | var arena = ArenaAllocator.init(testing.allocator); 638 | defer arena.deinit(); 639 | const a = arena.allocator(); 640 | 641 | const doc = try parse(a, ""); 642 | try testing.expectEqualSlices(u8, "python", doc.root.tag); 643 | } 644 | --------------------------------------------------------------------------------