├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── example ├── global.wasm ├── global.wat ├── test.zig └── wasm_src.zig ├── gyro.zzz ├── gyro_plugin.zig ├── src ├── c.zig ├── main.zig └── wasm3_extra.c └── submod_build_plugin.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | zig-cache/ 3 | zig-out/ 4 | .gyro/ 5 | deps.zig 6 | gyro.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Allison Pierson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-wasm3 2 | 3 | Zig bindings (and build system integration) for [Wasm3](https://github.com/wasm3/wasm3) 4 | 5 | ## Usage 6 | 7 | Building this project requires master-branch zig for now. 8 | You can obtain a master branch build of zig from https://ziglang.org/download 9 | 10 | zig-wasm3 is primarily targeted for the Gyro package manager, but you can also use it with git submodules. 11 | 12 | * [Using with Gyro](#using-with-gyro) 13 | * [Using with Git Submodules](#using-with-git-submodules) 14 | 15 | Both methods are pretty simple, and both include a build system to compile wasm3 from source. 16 | Gyro, however, builds a known-good version of wasm3 that has been tested with these bindings, while submodules will build whatever version you clone, so use caution! 17 | 18 | #### Using With Gyro 19 | To use with Gyro, add the following to your `gyro.zzz` file 20 | ``` 21 | build_deps: 22 | wasm3-build: 23 | src: 24 | github: 25 | user: alichay 26 | repo: zig-wasm3 27 | ref: main 28 | root: gyro_plugin.zig 29 | ``` 30 | You should be able to do the same on the command line with 31 | ``` 32 | $ gyro add -b -s github 'alichay/zig-wasm3' -r 'gyro_plugin.zig' -a 'wasm3-build' 33 | ``` 34 | but gyro's CLI behaved oddly when I tried to do this. 35 | 36 | Then, import the wasm3 builder into your `build.zig` file, and add the library to your project! 37 | 38 | `build.zig` 39 | ```rust 40 | pub const wasm3_build = @import("wasm3-build"); 41 | 42 | pub fn build(b: *std.build.Builder) void { 43 | // ... 44 | wasm3_build.addTo(exe); 45 | // Replace null with a string to override package name. 46 | // By default, it is "wasm3" 47 | exe.addPackage(wasm3_build.pkg(null)); 48 | // ... 49 | } 50 | ``` 51 | 52 | From there, you can just `@import("wasm3")` in your application! 53 | 54 | #### Using with Git Submodules 55 | 56 | Git submodules are the devil. Additionally, unlike Gyro, it's a little bit more difficult to version control submodules, as these bindings are written to specific versions of wasm3 that aren't always up-to-date (this is a hobby project that I don't use much anymore, so updates lag behind quite a bit). 57 | If you can, you should absolutely use gyro to ensure that you have a compatible version of the wasm3 library to use with these bindings. If that's not possible, however, submodules are better than nothing. 58 | 59 | To use wasm3 with submodules, you need to add two submodules to your project. 60 | In this example, I'm going to put them in `/libs`, but that's completely up to you. 61 | 62 | ```bash 63 | git submodule add 'https://github.com/alichay/zig-wasm3' 'libs/zig-wasm3' 64 | git submodule add 'https://github.com/wasm3/wasm3' 'libs/wasm3' 65 | ``` 66 | 67 | Then, in `build.zig`, reference the zig-wasm3 build script, and pass it the path to the wasm3 repository. 68 | 69 | ```rust 70 | pub const wasm3_build = @import("libs/zig-wasm3/submod_build_plugin.zig"); 71 | 72 | pub fn build(b: *std.build.Builder) void { 73 | // ... 74 | wasm3_build.addTo(exe, "libs/wasm3"); 75 | // Replace null with a string to override package name. 76 | // By default, it is "wasm3" 77 | exe.addPackage(wasm3_build.pkg(null)); 78 | // ... 79 | } 80 | ``` 81 | 82 | From there, you can just `@import("wasm3")` in your application! -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Builder = std.build.Builder; 3 | 4 | pub const pkgs = @import("gyro").pkgs; 5 | const self_plugin = @import("gyro_plugin.zig"); 6 | 7 | pub fn build(b: *Builder) void { 8 | 9 | // Standard target options allows the person running `zig build` to choose 10 | // what target to build for. Here we do not override the defaults, which 11 | // means any target is allowed, and the default is native. Other options 12 | // for restricting supported target set are available. 13 | const target = b.standardTargetOptions(.{}); 14 | 15 | // Standard release options allow the person running `zig build` to select 16 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 17 | const mode = b.standardReleaseOptions(); 18 | 19 | const exe = b.addExecutable("example", "example/test.zig"); 20 | exe.setBuildMode(mode); 21 | exe.setTarget(target); 22 | exe.install(); 23 | 24 | const wasm_build = b.addSharedLibrary("wasm_example", "example/wasm_src.zig", .unversioned); 25 | wasm_build.target = std.zig.CrossTarget { 26 | .cpu_arch = .wasm32, 27 | .os_tag = .wasi, 28 | }; 29 | wasm_build.out_filename = "wasm_example.wasm"; 30 | 31 | self_plugin.addTo(exe); 32 | exe.addPackage(self_plugin.pkg(null)); 33 | 34 | exe.install(); 35 | wasm_build.install(); 36 | 37 | const run_cmd = exe.run(); 38 | run_cmd.step.dependOn(b.getInstallStep()); 39 | run_cmd.addArtifactArg(wasm_build); 40 | if (b.args) |args| { 41 | run_cmd.addArgs(args); 42 | } 43 | 44 | const run_step = b.step("run", "Run the app"); 45 | run_step.dependOn(&run_cmd.step); 46 | } 47 | -------------------------------------------------------------------------------- /example/global.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alichay/zig-wasm3/dc3c4406d808774e66e3eee637821ae6d0c83b24/example/global.wasm -------------------------------------------------------------------------------- /example/global.wat: -------------------------------------------------------------------------------- 1 | ;; This file is grabbed from Wasmer's examples. 2 | ;; https://github.com/wasmerio/wasmer/blob/1b0c87034708e22adbfdd3725b25bf1810a21479/examples/exports_global.rs#L26-L33 3 | ;; Wasmer is licensed under the MIT license at the time that this example was retrieved. 4 | ;; https://github.com/wasmerio/wasmer/blob/1b0c87034708e22adbfdd3725b25bf1810a21479/LICENSE 5 | 6 | (module 7 | (global $one (export "one") f32 (f32.const 1)) 8 | (global $some (export "some") (mut f32) (f32.const 0)) 9 | (func (export "get_one") (result f32) (global.get $one)) 10 | (func (export "get_some") (result f32) (global.get $some)) 11 | (func (export "set_some") (param f32) (global.set $some (local.get 0)))) 12 | -------------------------------------------------------------------------------- /example/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const wasm3 = @import("wasm3"); 3 | 4 | const kib = 1024; 5 | const mib = 1024 * kib; 6 | const gib = 1024 * mib; 7 | 8 | pub fn main() !void { 9 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 10 | defer _ = gpa.deinit(); 11 | 12 | var a = gpa.allocator(); 13 | 14 | var args = try std.process.argsAlloc(a); 15 | defer std.process.argsFree(a, args); 16 | 17 | if (args.len < 2) { 18 | std.log.err("Please provide a wasm file on the command line!\n", .{}); 19 | std.os.exit(1); 20 | } 21 | 22 | std.log.info("Loading wasm file {s}!\n", .{args[1]}); 23 | 24 | 25 | var env = wasm3.Environment.init(); 26 | defer env.deinit(); 27 | 28 | var rt = env.createRuntime(16 * kib, null); 29 | defer rt.deinit(); 30 | errdefer rt.printError(); 31 | 32 | var mod_bytes = try std.fs.cwd().readFileAlloc(a, args[1], 512 * kib); 33 | defer a.free(mod_bytes); 34 | var mod = try env.parseModule(mod_bytes); 35 | try rt.loadModule(mod); 36 | try mod.linkWasi(); 37 | 38 | try mod.linkLibrary("native_helpers", struct { 39 | pub fn add(_: *std.mem.Allocator, lh: i32, rh: i32, mul: wasm3.SandboxPtr(i32)) callconv(.Inline) i32 { 40 | mul.write(lh * rh); 41 | return lh + rh; 42 | } 43 | pub fn getArgv0(allocator: *std.mem.Allocator, str: wasm3.SandboxPtr(u8), max_len: u32) callconv(.Inline) u32 { 44 | var in_buf = str.slice(max_len); 45 | 46 | // Version <= 9.0 has you pass the allocator to args_iter.next(), but 47 | // >= 10.0 has you pass the allocator to the args iterator. 48 | if (@hasDecl(std.process, "argsWithAllocator") and @typeInfo(@TypeOf(std.process.ArgIterator.next)).Fn.args.len == 1) { 49 | // Version >= 10.0, pass the allocator to the args iterator. 50 | 51 | var arg_iter = std.process.argsWithAllocator(allocator.*) catch return 0; 52 | defer arg_iter.deinit(); 53 | 54 | var first_arg = arg_iter.next() orelse return 0; 55 | 56 | if (first_arg.len > in_buf.len) return 0; 57 | std.mem.copy(u8, in_buf, first_arg); 58 | 59 | return @truncate(u32, first_arg.len); 60 | } else { 61 | // Version <= 9.0, pass the allocator to args_iter.next(). 62 | 63 | var arg_iter = std.process.args(); 64 | 65 | var first_arg = (arg_iter.next(allocator.*) orelse return 0) catch return 0; 66 | defer allocator.free(first_arg); 67 | 68 | if (first_arg.len > in_buf.len) return 0; 69 | std.mem.copy(u8, in_buf, first_arg); 70 | 71 | return @truncate(u32, first_arg.len); 72 | } 73 | } 74 | }, &a); 75 | 76 | var start_fn = try rt.findFunction("main"); 77 | start_fn.call(void, .{}) catch |e| switch (e) { 78 | error.TrapExit => {}, 79 | else => return e, 80 | }; 81 | 82 | var add_five_fn = try rt.findFunction("addFive"); 83 | const num: i32 = 7; 84 | std.debug.print("Adding 5 to {d}: got {d}!\n", .{ num, try add_five_fn.call(i32, .{num}) }); 85 | 86 | var alloc_fn = try rt.findFunction("allocBytes"); 87 | var print_fn = try rt.findFunction("printStringZ"); 88 | 89 | const my_string = "Hello, world!"; 90 | 91 | var buffer_np = try alloc_fn.call(wasm3.SandboxPtr(u8), .{@as(u32, my_string.len + 1)}); 92 | var buffer = buffer_np.slice(my_string.len + 1); 93 | 94 | std.debug.print("Allocated buffer!\n{any}\n", .{buffer}); 95 | 96 | std.mem.copy(u8, buffer, my_string); 97 | buffer[my_string.len] = 0; 98 | 99 | try print_fn.call(void, .{buffer_np}); 100 | 101 | var optionally_null_np: ?wasm3.SandboxPtr(u8) = null; 102 | try print_fn.call(void, .{optionally_null_np}); 103 | 104 | try test_globals(a); 105 | } 106 | 107 | /// This is in a separate file because I can't find any 108 | /// compiler toolchains that actually work with Wasm globals yet (lol) 109 | /// so we just ship a binary wasm file that works with them 110 | pub fn test_globals(a: std.mem.Allocator) !void { 111 | 112 | var env = wasm3.Environment.init(); 113 | defer env.deinit(); 114 | 115 | var rt = env.createRuntime(1 * kib, null); 116 | defer rt.deinit(); 117 | errdefer rt.printError(); 118 | 119 | var mod_bytes = try std.fs.cwd().readFileAlloc(a, "example/global.wasm", 512 * kib); 120 | defer a.free(mod_bytes); 121 | var mod = try env.parseModule(mod_bytes); 122 | try rt.loadModule(mod); 123 | 124 | var one = mod.findGlobal("one") orelse { 125 | std.debug.panic("Failed to find global \"one\"\n", .{}); 126 | }; 127 | var some = mod.findGlobal("some") orelse { 128 | std.debug.panic("Failed to find global \"some\"\n", .{}); 129 | }; 130 | 131 | std.debug.print("'one' value: {d}\n", .{(try one.get()).Float32}); 132 | std.debug.print("'some' value: {d}\n", .{(try some.get()).Float32}); 133 | 134 | std.debug.print("Trying to set 'one' value to 5.0, should fail.\n", .{}); 135 | 136 | one.set(.{.Float32 = 5.0}) catch |err| switch(err) { 137 | wasm3.Error.SettingImmutableGlobal => { 138 | std.debug.print("Failed successfully!\n", .{}); 139 | }, 140 | else => { 141 | std.debug.print("Unexpected error {any}\n", .{err}); 142 | } 143 | }; 144 | std.debug.print("'one' value: {d}\n", .{(try one.get()).Float32}); 145 | if((try one.get()).Float32 != 1.0) { 146 | std.log.err("Global 'one' has a different value. This is probably a wasm3 bug!\n", .{}); 147 | } 148 | 149 | var some_setter = try rt.findFunction("set_some"); 150 | try some_setter.call(void, .{@as(f32, 25.0)}); 151 | std.debug.print("'some' value: {d}\n", .{(try some.get()).Float32}); 152 | } -------------------------------------------------------------------------------- /example/wasm_src.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | extern "native_helpers" fn add(a: i32, b: i32, mul: *i32) i32; 4 | extern "native_helpers" fn getArgv0(str_buf: [*]u8, max_len: u32) u32; 5 | 6 | const max_arg_size = 256; 7 | 8 | export fn allocBytes(size: u32) [*]u8 { 9 | var mem = std.heap.page_allocator.alloc(u8, @intCast(usize, size)) catch { 10 | std.debug.panic("Memory allocation failed!\n", .{}); 11 | }; 12 | for (mem) |*v, i| v.* = @intCast(u8, i); 13 | return mem.ptr; 14 | } 15 | 16 | export fn printStringZ(str: ?[*:0]const u8) void { 17 | std.debug.print("printStringZ: ", .{}); 18 | if (str) |s| { 19 | std.debug.print("\"{s}\"\n", .{std.mem.span(s)}); 20 | } else { 21 | std.debug.print("null\n", .{}); 22 | } 23 | } 24 | 25 | export fn addFive(num: i32) i32 { 26 | return num + 5; 27 | } 28 | 29 | export fn main() void { 30 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 31 | defer arena.deinit(); 32 | var a = arena.allocator(); 33 | 34 | const a1 = 2; 35 | const a2 = 6; 36 | 37 | var mul_res: i32 = 0; 38 | const add_res = add(a1, a2, &mul_res); 39 | 40 | std.debug.print("{d} + {d} = {d} (multiplied, it's {d}!)\n", .{ a1, a2, add_res, mul_res }); 41 | 42 | var buf = a.alloc(u8, max_arg_size) catch std.debug.panic("Memory allocation failed!\n", .{}); 43 | var written = getArgv0(buf.ptr, buf.len); 44 | if (written != 0) { 45 | std.debug.print("Got string {s}!\n", .{buf[0..@intCast(usize, written)]}); 46 | } else { 47 | std.debug.print("Failed to write string! No bytes written.", .{}); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gyro.zzz: -------------------------------------------------------------------------------- 1 | pkgs: 2 | wasm3-build: 3 | version: 0.0.1 4 | description: "Zig bindings and build system for https://github.com/wasm3/wasm3" 5 | source_url: "https://github.com/alichay/zig-wasm3" 6 | root: gyro_plugin.zig 7 | deps: 8 | wasm3_csrc: 9 | git: 10 | url: "https://github.com/wasm3/wasm3.git" 11 | ref: fa18e9ece4dd45371deac69ded32838470a55c1b 12 | root: .gitignore 13 | -------------------------------------------------------------------------------- /gyro_plugin.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const submod_build_plugin = @import("submod_build_plugin.zig"); 3 | 4 | fn getWasm3Src() []const u8 { 5 | const sep = std.fs.path.sep_str; 6 | 7 | const gyro_dir = comptime get_gyro: { 8 | const parent_dir = std.fs.path.dirname(@src().file).?; 9 | const maybe_clone_hash_dir = std.fs.path.dirname(parent_dir); 10 | const maybe_gyro_dir = if(maybe_clone_hash_dir) |d| std.fs.path.dirname(d) else null; 11 | 12 | if(std.ascii.eqlIgnoreCase(std.fs.path.basename(parent_dir), "pkg") and maybe_gyro_dir != null and 13 | std.ascii.eqlIgnoreCase(std.fs.path.basename(maybe_gyro_dir.?), ".gyro")) { 14 | break :get_gyro maybe_gyro_dir.?; 15 | } else { 16 | break :get_gyro std.fs.path.dirname(@src().file).? ++ sep ++ ".gyro"; 17 | } 18 | }; 19 | const gyro_zzz_src = @embedFile("gyro.zzz"); 20 | const path = [_][]const u8{"deps", "wasm3_csrc", "git", "ref"}; 21 | 22 | var tree = zzz.ZTree(1, 100){}; 23 | var config_node = tree.appendText(gyro_zzz_src) catch unreachable; 24 | for(path) |key| { 25 | config_node = config_node.findNth(0, .{.String = key}) orelse unreachable; 26 | } 27 | return std.mem.join(std.heap.page_allocator, "", &.{gyro_dir, sep, "wasm3-wasm3-github.com-", config_node.child.?.value.String[0..8], sep, "pkg"}) catch unreachable; 28 | } 29 | 30 | fn repoDir(b: *std.build.Builder) []const u8 { 31 | return std.fs.path.resolve(b.allocator, &[_][]const u8{b.build_root, getWasm3Src()}) catch unreachable; 32 | } 33 | 34 | /// Queues a build job for the C code of Wasm3. 35 | /// This builds a static library that depends on libc, so make sure to link that into your exe! 36 | pub fn compile(b: *std.build.Builder, mode: std.builtin.Mode, target: std.zig.CrossTarget) *std.build.LibExeObjStep { 37 | return submod_build_plugin.compile(b, mode, target, repoDir(b)); 38 | } 39 | 40 | /// Compiles Wasm3 and links it into the provided exe. 41 | /// If you use this API, you do not need to also use the compile() function. 42 | pub fn addTo(exe: *std.build.LibExeObjStep) void { 43 | submod_build_plugin.addTo(exe, repoDir(exe.builder)); 44 | } 45 | 46 | pub const pkg = submod_build_plugin.pkg; 47 | 48 | 49 | // We used to ship this as a dev dependency, but that dependency wasn't 50 | // being resolved in client applications that used this library, 51 | // so we have to just bundle and ship it ourselves. 52 | const zzz = struct { 53 | //! zzz format serializer and deserializer. public domain. 54 | //! 55 | //! StreamingParser inspired by Zig's JSON parser. 56 | //! 57 | //! SPARSE SPEC 58 | //! (zzz text is escaped using Zig's multiline string: \\) 59 | //! 60 | //! zzz text describes a tree of strings. Special characters (and spaces) are used to go up and down 61 | //! the tree. The tree has an implicit null root node. 62 | //! 63 | //! Descending the tree: 64 | //! \\grandparent:parent:child:grandchild 65 | //! Output: 66 | //! null -> "grandparent" -> "parent" -> "child" -> "grandchild" 67 | //! 68 | //! Traversing the children of root (siblings): 69 | //! \\sibling1,sibling2,sibling3 70 | //! Output: 71 | //! null -> "sibling1" 72 | //! -> "sibling2" 73 | //! -> "sibling3" 74 | //! 75 | //! Going up to the parent: 76 | //! \\parent:child;anotherparent 77 | //! Output: 78 | //! null -> "parent" -> "child" 79 | //! -> "anotherparent" 80 | //! 81 | //! White space and newlines are significant. A newline will take you back to the root: 82 | //! \\parent:child 83 | //! \\anotherparent 84 | //! Output: 85 | //! null -> "parent" -> "child" 86 | //! -> "anotherparent" 87 | //! 88 | //! Exactly two spaces are used to to go down a level in the tree: 89 | //! \\parent:child 90 | //! \\ siblingtend 91 | //! null -> "parent" -> "child" 92 | //! -> "sibling" 93 | //! 94 | //! You can only go one level deeper than the previous line's depth. Anything more is an error: 95 | //! \\parent:child 96 | //! \\ sibling 97 | //! Output: Error! 98 | //! 99 | //! Trailing commas, semicolons, and colons are optional. So the above (correct one) can be written 100 | //! as: 101 | //! \\parent 102 | //! \\ child 103 | //! \\ sibling 104 | //! Output: 105 | //! null -> "parent" -> "child" 106 | //! -> "sibling" 107 | //! 108 | //! zzz can contain strings, integers (i32), floats (f32), boolean, and nulls: 109 | //! \\string:42:42.0:true:: 110 | //! Output: 111 | //! null -> "string" -> 42 -> 42.0 -> true -> null 112 | //! 113 | //! strings are trimmed, they may still contain spaces: 114 | //! \\parent: child: grand child ; 115 | //! Output: 116 | //! null -> "parent" -> "child" -> "grand child" 117 | //! 118 | //! strings can be quoted with double quotes or Lua strings: 119 | //! \\"parent":[[ child ]]:[==[grand child]=]]==]; 120 | //! Output: 121 | //! null -> "parent" -> " child " -> "grand child]=]" 122 | //! 123 | //! Lua strings will skip the first empty newline: 124 | //! \\[[ 125 | //! \\some text]] 126 | //! Output: 127 | //! null -> "some text" 128 | //! 129 | //! Strings are not escaped and taken "as-is". 130 | //! \\"\n\t\r" 131 | //! Output: 132 | //! null -> "\n\t\r" 133 | //! 134 | //! Comments begin with # and run up to the end of the line. Their indentation follows the same 135 | //! rules as nodes. 136 | //! \\# A comment 137 | //! \\a node 138 | //! \\ # Another comment 139 | //! \\ a sibling 140 | //! Output: 141 | //! null -> "a node" -> "a sibling" 142 | 143 | /// The only output of the tokenizer. 144 | pub const ZNodeToken = struct { 145 | const Self = @This(); 146 | /// 0 is root, 1 is top level children. 147 | depth: usize, 148 | /// The extent of the slice. 149 | start: usize, 150 | end: usize, 151 | }; 152 | 153 | /// Parses text outputting ZNodeTokens. Does not convert strings to numbers, and all strings are 154 | /// "as is", no escaping is performed. 155 | pub const StreamingParser = struct { 156 | const Self = @This(); 157 | state: State, 158 | start_index: usize, 159 | current_index: usize, 160 | // The maximum node depth. 161 | max_depth: usize, 162 | // The current line's depth. 163 | line_depth: usize, 164 | // The current node depth. 165 | node_depth: usize, 166 | /// Level of multiline string. 167 | open_string_level: usize, 168 | /// Current level of multiline string close. 169 | close_string_level: usize, 170 | /// Account for any extra spaces trailing at the end of a word. 171 | trailing_spaces: usize, 172 | 173 | pub const Error = error{ 174 | TooMuchIndentation, 175 | InvalidWhitespace, 176 | OddIndentationValue, 177 | InvalidQuotation, 178 | InvalidMultilineOpen, 179 | InvalidMultilineClose, 180 | InvalidNewLineInString, 181 | InvalidCharacterAfterString, 182 | SemicolonWentPastRoot, 183 | UnexpectedEof, 184 | }; 185 | 186 | pub const State = enum { 187 | /// Whether we're starting on an openline. 188 | OpenLine, 189 | ExpectZNode, 190 | Indent, 191 | OpenCharacter, 192 | Quotation, 193 | SingleLineCharacter, 194 | MultilineOpen0, 195 | MultilineOpen1, 196 | MultilineLevelOpen, 197 | MultilineLevelClose, 198 | MultilineClose0, 199 | MultilineCharacter, 200 | EndString, 201 | OpenComment, 202 | Comment, 203 | }; 204 | 205 | /// Returns a blank parser. 206 | pub fn init() Self { 207 | var self: StreamingParser = undefined; 208 | self.reset(); 209 | return self; 210 | } 211 | 212 | /// Resets the parser back to the beginning state. 213 | pub fn reset(self: *Self) void { 214 | self.state = .OpenLine; 215 | self.start_index = 0; 216 | self.current_index = 0; 217 | self.max_depth = 0; 218 | self.line_depth = 0; 219 | self.node_depth = 0; 220 | self.open_string_level = 0; 221 | self.close_string_level = 0; 222 | self.trailing_spaces = 0; 223 | } 224 | 225 | pub fn completeOrError(self: *const Self) !void { 226 | switch (self.state) { 227 | .ExpectZNode, .OpenLine, .EndString, .Comment, .OpenComment, .Indent => {}, 228 | else => return Error.UnexpectedEof, 229 | } 230 | } 231 | 232 | /// Feeds a character to the parser. May output a ZNode. Check "hasCompleted" to see if there 233 | /// are any unfinished strings. 234 | pub fn feed(self: *Self, c: u8) Error!?ZNodeToken { 235 | defer self.current_index += 1; 236 | //std.debug.print("FEED<{}> {} {} ({c})\n", .{self.state, self.current_index, c, c}); 237 | switch (self.state) { 238 | .OpenComment, .Comment => switch (c) { 239 | '\n' => { 240 | self.start_index = self.current_index + 1; 241 | // We're ending a line with nodes. 242 | if (self.state == .Comment) { 243 | self.max_depth = self.line_depth + 1; 244 | } 245 | self.node_depth = 0; 246 | self.line_depth = 0; 247 | self.state = .OpenLine; 248 | }, 249 | else => { 250 | // Skip. 251 | }, 252 | }, 253 | // All basically act the same except for a few minor differences. 254 | .ExpectZNode, .OpenLine, .EndString, .OpenCharacter => switch (c) { 255 | '#' => { 256 | if (self.state == .OpenLine) { 257 | self.state = .OpenComment; 258 | } else { 259 | defer self.state = .Comment; 260 | if (self.state == .OpenCharacter) { 261 | return ZNodeToken{ 262 | .depth = self.line_depth + self.node_depth + 1, 263 | .start = self.start_index, 264 | .end = self.current_index - self.trailing_spaces, 265 | }; 266 | } 267 | } 268 | }, 269 | // The tricky character (and other whitespace). 270 | ' ' => { 271 | if (self.state == .OpenLine) { 272 | if (self.line_depth >= self.max_depth) { 273 | return Error.TooMuchIndentation; 274 | } 275 | self.state = .Indent; 276 | } else if (self.state == .OpenCharacter) { 277 | self.trailing_spaces += 1; 278 | } else { 279 | 280 | // Skip spaces when expecting a node on a closed line, 281 | // including this one. 282 | self.start_index = self.current_index + 1; 283 | } 284 | }, 285 | ':' => { 286 | defer self.state = .ExpectZNode; 287 | const node = ZNodeToken{ 288 | .depth = self.line_depth + self.node_depth + 1, 289 | .start = self.start_index, 290 | .end = self.current_index - self.trailing_spaces, 291 | }; 292 | self.start_index = self.current_index + 1; 293 | self.node_depth += 1; 294 | // Only return when we're not at end of a string. 295 | if (self.state != .EndString) { 296 | return node; 297 | } 298 | }, 299 | ',' => { 300 | defer self.state = .ExpectZNode; 301 | const node = ZNodeToken{ 302 | .depth = self.line_depth + self.node_depth + 1, 303 | .start = self.start_index, 304 | .end = self.current_index - self.trailing_spaces, 305 | }; 306 | self.start_index = self.current_index + 1; 307 | // Only return when we're not at end of a string. 308 | if (self.state != .EndString) { 309 | return node; 310 | } 311 | }, 312 | ';' => { 313 | if (self.node_depth == 0) { 314 | return Error.SemicolonWentPastRoot; 315 | } 316 | defer self.state = .ExpectZNode; 317 | const node = ZNodeToken{ 318 | .depth = self.line_depth + self.node_depth + 1, 319 | .start = self.start_index, 320 | .end = self.current_index - self.trailing_spaces, 321 | }; 322 | self.start_index = self.current_index + 1; 323 | self.node_depth -= 1; 324 | // Only return when we're not at end of a string, or in semicolons 325 | // special case, when we don't have an empty string. 326 | if (self.state != .EndString and node.start < node.end) { 327 | return node; 328 | } 329 | }, 330 | '"' => { 331 | if (self.state == .EndString) { 332 | return Error.InvalidCharacterAfterString; 333 | } 334 | // Don't start another string. 335 | if (self.state == .OpenCharacter) { 336 | return null; 337 | } 338 | // We start here to account for the possibility of a string being "" 339 | self.start_index = self.current_index + 1; 340 | self.state = .Quotation; 341 | }, 342 | '[' => { 343 | if (self.state == .EndString) { 344 | return Error.InvalidCharacterAfterString; 345 | } 346 | // Don't start another string. 347 | if (self.state == .OpenCharacter) { 348 | return null; 349 | } 350 | self.open_string_level = 0; 351 | self.state = .MultilineOpen0; 352 | }, 353 | '\n' => { 354 | defer self.state = .OpenLine; 355 | const node = ZNodeToken{ 356 | .depth = self.line_depth + self.node_depth + 1, 357 | .start = self.start_index, 358 | .end = self.current_index - self.trailing_spaces, 359 | }; 360 | self.start_index = self.current_index + 1; 361 | // Only reset on a non open line. 362 | if (self.state != .OpenLine) { 363 | self.max_depth = self.line_depth + 1; 364 | self.line_depth = 0; 365 | } 366 | self.node_depth = 0; 367 | // Only return something if there is something. Quoted strings are good. 368 | if (self.state == .OpenCharacter) { 369 | return node; 370 | } 371 | }, 372 | '\t', '\r' => { 373 | return Error.InvalidWhitespace; 374 | }, 375 | else => { 376 | // We already have a string. 377 | if (self.state == .EndString) { 378 | return Error.InvalidCharacterAfterString; 379 | } 380 | // Don't reset if we're in a string. 381 | if (self.state != .OpenCharacter) { 382 | self.start_index = self.current_index; 383 | } 384 | self.trailing_spaces = 0; 385 | self.state = .OpenCharacter; 386 | }, 387 | }, 388 | .Indent => switch (c) { 389 | ' ' => { 390 | self.start_index = self.current_index + 1; 391 | self.line_depth += 1; 392 | self.state = .OpenLine; 393 | }, 394 | else => { 395 | return Error.OddIndentationValue; 396 | }, 397 | }, 398 | .Quotation => switch (c) { 399 | '"' => { 400 | self.state = .EndString; 401 | const node = ZNodeToken{ 402 | .depth = self.line_depth + self.node_depth + 1, 403 | .start = self.start_index, 404 | .end = self.current_index, 405 | }; 406 | // Reset because we're going to expecting nodes. 407 | self.start_index = self.current_index + 1; 408 | return node; 409 | }, 410 | else => { 411 | self.state = .SingleLineCharacter; 412 | }, 413 | }, 414 | .SingleLineCharacter => switch (c) { 415 | '"' => { 416 | self.state = .EndString; 417 | const node = ZNodeToken{ 418 | .depth = self.line_depth + self.node_depth + 1, 419 | .start = self.start_index, 420 | .end = self.current_index, 421 | }; 422 | // Reset because we're going to expecting nodes. 423 | self.start_index = self.current_index + 1; 424 | return node; 425 | }, 426 | '\n' => { 427 | return Error.InvalidNewLineInString; 428 | }, 429 | else => { 430 | // Consume. 431 | }, 432 | }, 433 | .MultilineOpen0, .MultilineLevelOpen => switch (c) { 434 | '=' => { 435 | self.open_string_level += 1; 436 | self.state = .MultilineLevelOpen; 437 | }, 438 | '[' => { 439 | self.start_index = self.current_index + 1; 440 | self.state = .MultilineOpen1; 441 | }, 442 | else => { 443 | return Error.InvalidMultilineOpen; 444 | }, 445 | }, 446 | .MultilineOpen1 => switch (c) { 447 | ']' => { 448 | self.state = .MultilineClose0; 449 | }, 450 | '\n' => { 451 | // Skip first newline. 452 | self.start_index = self.current_index + 1; 453 | }, 454 | else => { 455 | self.state = .MultilineCharacter; 456 | }, 457 | }, 458 | .MultilineCharacter => switch (c) { 459 | ']' => { 460 | self.close_string_level = 0; 461 | self.state = .MultilineClose0; 462 | }, 463 | else => { 464 | // Capture EVERYTHING. 465 | }, 466 | }, 467 | .MultilineClose0, .MultilineLevelClose => switch (c) { 468 | '=' => { 469 | self.close_string_level += 1; 470 | self.state = .MultilineLevelClose; 471 | }, 472 | ']' => { 473 | if (self.close_string_level == self.open_string_level) { 474 | self.state = .EndString; 475 | return ZNodeToken{ 476 | .depth = self.line_depth + self.node_depth + 1, 477 | .start = self.start_index, 478 | .end = self.current_index - self.open_string_level - 1, 479 | }; 480 | } 481 | self.state = .MultilineCharacter; 482 | }, 483 | else => { 484 | return Error.InvalidMultilineClose; 485 | }, 486 | }, 487 | } 488 | return null; 489 | } 490 | }; 491 | 492 | fn testNextTextOrError(stream: *StreamingParser, idx: *usize, text: []const u8) ![]const u8 { 493 | while (idx.* < text.len) { 494 | const node = try stream.feed(text[idx.*]); 495 | idx.* += 1; 496 | if (node) |n| { 497 | //std.debug.print("TOKEN {}\n", .{text[n.start..n.end]}); 498 | return text[n.start..n.end]; 499 | } 500 | } 501 | return error.ExhaustedLoop; 502 | } 503 | test "parsing slice output" { 504 | const testing = std.testing; 505 | 506 | const text = 507 | \\# woo comment 508 | \\mp:10 509 | \\[[sy]] 510 | \\ # another 511 | \\ : n : "en" , [[m]] 512 | \\ "sc" : [[10]] , g #inline 513 | \\ [[]]:[==[ 514 | \\hi]==] 515 | ; 516 | var idx: usize = 0; 517 | var stream = StreamingParser.init(); 518 | try testing.expectEqualSlices(u8, "mp", try testNextTextOrError(&stream, &idx, text)); 519 | try testing.expectEqualSlices(u8, "10", try testNextTextOrError(&stream, &idx, text)); 520 | try testing.expectEqualSlices(u8, "sy", try testNextTextOrError(&stream, &idx, text)); 521 | try testing.expectEqualSlices(u8, "", try testNextTextOrError(&stream, &idx, text)); 522 | try testing.expectEqualSlices(u8, "n", try testNextTextOrError(&stream, &idx, text)); 523 | try testing.expectEqualSlices(u8, "en", try testNextTextOrError(&stream, &idx, text)); 524 | try testing.expectEqualSlices(u8, "m", try testNextTextOrError(&stream, &idx, text)); 525 | try testing.expectEqualSlices(u8, "sc", try testNextTextOrError(&stream, &idx, text)); 526 | try testing.expectEqualSlices(u8, "10", try testNextTextOrError(&stream, &idx, text)); 527 | try testing.expectEqualSlices(u8, "g", try testNextTextOrError(&stream, &idx, text)); 528 | try testing.expectEqualSlices(u8, "", try testNextTextOrError(&stream, &idx, text)); 529 | try testing.expectEqualSlices(u8, "hi", try testNextTextOrError(&stream, &idx, text)); 530 | } 531 | 532 | fn testNextLevelOrError(stream: *StreamingParser, idx: *usize, text: []const u8) !usize { 533 | while (idx.* < text.len) { 534 | const node = try stream.feed(text[idx.*]); 535 | idx.* += 1; 536 | if (node) |n| { 537 | return n.depth; 538 | } 539 | } 540 | return error.ExhaustedLoop; 541 | } 542 | 543 | test "parsing depths" { 544 | const testing = std.testing; 545 | 546 | const text = 547 | \\# woo comment 548 | \\mp:10 549 | \\[[sy]] 550 | \\ # another 551 | \\ : n : "en" , [[m]] 552 | \\ # more 553 | \\ 554 | \\ # even more 555 | \\ 556 | \\ "sc" : [[10]] , g #inline 557 | \\ [[]]:[==[ 558 | \\hi]==] 559 | ; 560 | var idx: usize = 0; 561 | var stream = StreamingParser.init(); 562 | 563 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 1); 564 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 2); 565 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 1); 566 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 2); 567 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 3); 568 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 4); 569 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 4); 570 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 3); 571 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 4); 572 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 4); 573 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 2); 574 | try testing.expectEqual(try testNextLevelOrError(&stream, &idx, text), 3); 575 | } 576 | 577 | /// Parses the stream, outputting ZNodeTokens which reference the text. 578 | pub fn parseStream(stream: *StreamingParser, idx: *usize, text: []const u8) !?ZNodeToken { 579 | while (idx.* <= text.len) { 580 | // Insert an extra newline at the end of the stream. 581 | const node = if (idx.* == text.len) try stream.feed('\n') else try stream.feed(text[idx.*]); 582 | idx.* += 1; 583 | if (node) |n| { 584 | return n; 585 | } 586 | } 587 | return null; 588 | } 589 | 590 | /// A `ZNode`'s value. 591 | pub const ZValue = union(enum) { 592 | const Self = @This(); 593 | Null, 594 | String: []const u8, 595 | Int: i32, 596 | Float: f32, 597 | Bool: bool, 598 | 599 | /// Checks a ZValues equality. 600 | pub fn equals(self: Self, other: Self) bool { 601 | if (self == .Null and other == .Null) { 602 | return true; 603 | } 604 | if (self == .String and other == .String) { 605 | return std.mem.eql(u8, self.String, other.String); 606 | } 607 | if (self == .Int and other == .Int) { 608 | return self.Int == other.Int; 609 | } 610 | if (self == .Float and other == .Float) { 611 | return std.math.approxEqAbs(f32, self.Float, other.Float, std.math.f32_epsilon); 612 | } 613 | if (self == .Bool and other == .Bool) { 614 | return self.Bool == other.Bool; 615 | } 616 | return false; 617 | } 618 | 619 | /// Outputs a value to the `out_stream`. This output is parsable. 620 | pub fn stringify(self: Self, out_stream: anytype) @TypeOf(out_stream).Error!void { 621 | switch (self) { 622 | .Null => { 623 | // Skip. 624 | }, 625 | .String => { 626 | const find = std.mem.indexOfScalar; 627 | const chars = "\"\n\t\r,:;"; 628 | const chars_count = @sizeOf(@TypeOf(chars)); 629 | var need_escape = false; 630 | var found = [_]bool{false} ** chars_count; 631 | for ("\"\n\t\r,:;") |ch, i| { 632 | const f = find(u8, self.String, ch); 633 | if (f != null) { 634 | found[i] = true; 635 | need_escape = true; 636 | } 637 | } 638 | // TODO: Escaping ]] in string. 639 | if (need_escape) { 640 | // 0=" 1=\n 641 | if (found[0] or found[1]) { 642 | // Escape with Lua. 643 | try out_stream.writeAll("[["); 644 | const ret = try out_stream.writeAll(self.String); 645 | try out_stream.writeAll("]]"); 646 | return ret; 647 | } else { 648 | // Escape with basic quotes. 649 | try out_stream.writeAll("\""); 650 | const ret = try out_stream.writeAll(self.String); 651 | try out_stream.writeAll("\""); 652 | return ret; 653 | } 654 | } 655 | return try out_stream.writeAll(self.String); 656 | }, 657 | .Int => { 658 | return std.fmt.formatIntValue(self.Int, "", std.fmt.FormatOptions{}, out_stream); 659 | }, 660 | .Float => { 661 | return std.fmt.formatFloatScientific(self.Float, std.fmt.FormatOptions{}, out_stream); 662 | }, 663 | .Bool => { 664 | return out_stream.writeAll(if (self.Bool) "true" else "false"); 665 | }, 666 | } 667 | } 668 | 669 | /// 670 | pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 671 | _ = fmt; 672 | _ = options; 673 | switch (self) { 674 | .Null => try std.fmt.format(writer, ".Null", .{}), 675 | .String => try std.fmt.format(writer, ".String({s})", .{self.String}), 676 | .Int => try std.fmt.format(writer, ".Int({})", .{self.Int}), 677 | .Float => try std.fmt.format(writer, ".Float({})", .{self.Float}), 678 | .Bool => try std.fmt.format(writer, ".Bool({})", .{self.Bool}), 679 | } 680 | } 681 | }; 682 | 683 | /// Result of imprinting 684 | pub fn Imprint(comptime T: type) type { 685 | return struct { 686 | result: T, 687 | arena: std.heap.ArenaAllocator, 688 | }; 689 | } 690 | 691 | pub const ImprintError = error{ 692 | ExpectedBoolNode, 693 | ExpectedFloatNode, 694 | ExpectedUnsignedIntNode, 695 | ExpectedIntNode, 696 | ExpectedIntOrStringNode, 697 | ExpectedStringNode, 698 | 699 | FailedToConvertStringToEnum, 700 | FailedToConvertIntToEnum, 701 | 702 | FieldNodeDoesNotExist, 703 | ValueNodeDoesNotExist, 704 | ArrayElemDoesNotExist, 705 | 706 | OutOfMemory, 707 | 708 | InvalidPointerType, 709 | InvalidType, 710 | }; 711 | 712 | /// Represents a node in a static tree. Nodes have a parent, child, and sibling pointer 713 | /// to a spot in the array. 714 | pub const ZNode = struct { 715 | const Self = @This(); 716 | value: ZValue = .Null, 717 | parent: ?*ZNode = null, 718 | sibling: ?*ZNode = null, 719 | child: ?*ZNode = null, 720 | 721 | /// Returns the next Node in the tree. Will return Null after reaching root. For nodes further 722 | /// down the tree, they will bubble up, resulting in a negative depth. Self is considered to be 723 | /// at depth 0. 724 | pub fn next(self: *const Self, depth: *isize) ?*ZNode { 725 | if (self.child) |c| { 726 | depth.* += 1; 727 | return c; 728 | } else if (self.sibling) |c| { 729 | return c; 730 | } else { 731 | // Go up and forward. 732 | var iter: ?*const ZNode = self; 733 | while (iter != null) { 734 | iter = iter.?.parent; 735 | if (iter != null) { 736 | depth.* -= 1; 737 | if (iter.?.sibling) |c| { 738 | return c; 739 | } 740 | } 741 | } 742 | return null; 743 | } 744 | } 745 | 746 | /// Returns the next node in the tree until reaching root or the stopper node. 747 | pub fn nextUntil(self: *const Self, stopper: *const ZNode, depth: *isize) ?*ZNode { 748 | if (self.child) |c| { 749 | if (c == stopper) { 750 | return null; 751 | } 752 | depth.* += 1; 753 | return c; 754 | } else if (self.sibling) |c| { 755 | if (c == stopper) { 756 | return null; 757 | } 758 | return c; 759 | } else { 760 | // Go up and forward. 761 | var iter: ?*const ZNode = self; 762 | while (iter != null) { 763 | iter = iter.?.parent; 764 | // All these checks. :/ 765 | if (iter == stopper) { 766 | return null; 767 | } 768 | if (iter != null) { 769 | depth.* -= 1; 770 | if (iter.?.sibling) |c| { 771 | if (c == stopper) { 772 | return null; 773 | } 774 | return c; 775 | } 776 | } 777 | } 778 | return null; 779 | } 780 | } 781 | 782 | /// Iterates this node's children. Pass null to start. `iter = node.nextChild(iter);` 783 | pub fn nextChild(self: *const Self, iter: ?*const ZNode) ?*ZNode { 784 | if (iter) |it| { 785 | return it.sibling; 786 | } else { 787 | return self.child; 788 | } 789 | } 790 | 791 | /// Returns the nth child's value. Or null if neither the node or child exist. 792 | pub fn getChildValue(self: *const Self, nth: usize) ?ZValue { 793 | var count: usize = 0; 794 | var iter: ?*ZNode = self.child; 795 | while (iter) |n| { 796 | if (count == nth) { 797 | if (n.child) |c| { 798 | return c.value; 799 | } else { 800 | return null; 801 | } 802 | } 803 | count += 1; 804 | iter = n.sibling; 805 | } 806 | return null; 807 | } 808 | 809 | /// Returns the nth child. O(n) 810 | pub fn getChild(self: *const Self, nth: usize) ?*ZNode { 811 | var count: usize = 0; 812 | var iter: ?*ZNode = self.child; 813 | while (iter) |n| { 814 | if (count == nth) { 815 | return n; 816 | } 817 | count += 1; 818 | iter = n.sibling; 819 | } 820 | return null; 821 | } 822 | 823 | /// Returns the number of children. O(n) 824 | pub fn getChildCount(self: *const Self) usize { 825 | var count: usize = 0; 826 | var iter: ?*ZNode = self.child; 827 | while (iter) |n| { 828 | count += 1; 829 | iter = n.sibling; 830 | } 831 | return count; 832 | } 833 | 834 | /// Finds the next child after the given iterator. This is good for when you can guess the order 835 | /// of the nodes, which can cut down on starting from the beginning. Passing null starts over 836 | /// from the beginning. Returns the found node or null (it will loop back around). 837 | pub fn findNextChild(self: *const Self, start: ?*const ZNode, value: ZValue) ?*ZNode { 838 | var iter: ?*ZNode = self.child; 839 | if (start) |si| { 840 | iter = si.sibling; 841 | } 842 | while (iter != start) { 843 | if (iter) |it| { 844 | if (it.value.equals(value)) { 845 | return it; 846 | } 847 | iter = it.sibling; 848 | } else { 849 | // Loop back. 850 | iter = self.child; 851 | } 852 | } 853 | return null; 854 | } 855 | 856 | /// Finds the nth child node with a specific tag. 857 | pub fn findNthAny(self: *const Self, nth: usize, tag: std.meta.Tag(ZValue)) ?*ZNode { 858 | var count: usize = 0; 859 | var iter: ?*ZNode = self.child; 860 | while (iter) |n| { 861 | if (n.value == tag) { 862 | if (count == nth) { 863 | return n; 864 | } 865 | count += 1; 866 | } 867 | iter = n.sibling; 868 | } 869 | return null; 870 | } 871 | 872 | /// Finds the nth child node with a specific value. 873 | pub fn findNth(self: *const Self, nth: usize, value: ZValue) ?*ZNode { 874 | var count: usize = 0; 875 | var iter: ?*ZNode = self.child orelse return null; 876 | while (iter) |n| { 877 | if (n.value.equals(value)) { 878 | if (count == nth) { 879 | return n; 880 | } 881 | count += 1; 882 | } 883 | iter = n.sibling; 884 | } 885 | return null; 886 | } 887 | 888 | /// Traverses descendants until a node with the tag is found. 889 | pub fn findNthAnyDescendant(self: *const Self, nth: usize, tag: std.meta.Tag(ZValue)) ?*ZNode { 890 | var depth: isize = 0; 891 | var count: usize = 0; 892 | var iter: *const ZNode = self; 893 | while (iter.nextUntil(self, &depth)) |n| : (iter = n) { 894 | if (n.value == tag) { 895 | if (count == nth) { 896 | return n; 897 | } 898 | count += 1; 899 | } 900 | } 901 | return null; 902 | } 903 | 904 | /// Traverses descendants until a node with the specific value is found. 905 | pub fn findNthDescendant(self: *const Self, nth: usize, value: ZValue) ?*ZNode { 906 | var depth: isize = 0; 907 | var count: usize = 0; 908 | var iter: *const ZNode = self; 909 | while (iter.nextUntil(self, &depth)) |n| : (iter = n) { 910 | if (n.value.equals(value)) { 911 | if (count == nth) { 912 | return n; 913 | } 914 | count += 1; 915 | } 916 | } 917 | return null; 918 | } 919 | 920 | /// Converts strings to specific types. This just tries converting the string to an int, then 921 | /// float, then bool. Booleans are only the string values "true" or "false". 922 | pub fn convertStrings(self: *const Self) void { 923 | var depth: isize = 0; 924 | var iter: *const ZNode = self; 925 | while (iter.nextUntil(self, &depth)) |c| : (iter = c) { 926 | if (c.value != .String) { 927 | continue; 928 | } 929 | // Try to cast to numbers, then true/false checks, then string. 930 | const slice = c.value.String; 931 | const integer = std.fmt.parseInt(i32, slice, 10) catch { 932 | const float = std.fmt.parseFloat(f32, slice) catch { 933 | if (std.mem.eql(u8, "true", slice)) { 934 | c.value = ZValue{ .Bool = true }; 935 | } else if (std.mem.eql(u8, "false", slice)) { 936 | c.value = ZValue{ .Bool = false }; 937 | } else { 938 | // Keep the value. 939 | } 940 | continue; 941 | }; 942 | c.value = ZValue{ .Float = float }; 943 | continue; 944 | }; 945 | c.value = ZValue{ .Int = integer }; 946 | } 947 | } 948 | 949 | fn imprint_(self: *const Self, comptime T: type, allocator: ?*std.mem.Allocator) ImprintError!T { 950 | const TI = @typeInfo(T); 951 | 952 | switch (TI) { 953 | .Void => {}, 954 | .Bool => { 955 | return switch (self.value) { 956 | .Bool => |b| b, 957 | else => ImprintError.ExpectedBoolNode, 958 | }; 959 | }, 960 | .Float, .ComptimeFloat => { 961 | return switch (self.value) { 962 | .Float => |n| @floatCast(T, n), 963 | .Int => |n| @intToFloat(T, n), 964 | else => ImprintError.ExpectedFloatNode, 965 | }; 966 | }, 967 | .Int, .ComptimeInt => { 968 | const is_signed = (TI == .Int and TI.Int.signedness == .signed) or (TI == .ComptimeInt and TI.CompTimeInt.is_signed); 969 | switch (self.value) { 970 | .Int => |n| { 971 | if (is_signed) { 972 | return @intCast(T, n); 973 | } else { 974 | if (n < 0) { 975 | return ImprintError.ExpectedUnsignedIntNode; 976 | } 977 | return @intCast(T, n); 978 | } 979 | }, 980 | else => return ImprintError.ExpectedIntNode, 981 | } 982 | }, 983 | .Enum => { 984 | switch (self.value) { 985 | .Int => |int| { 986 | return std.meta.intToEnum(T, int) catch { 987 | return ImprintError.FailedToConvertIntToEnum; 988 | }; 989 | }, 990 | .String => { 991 | if (std.meta.stringToEnum(T, self.value.String)) |e| { 992 | return e; 993 | } else { 994 | return ImprintError.FailedToConvertStringToEnum; 995 | } 996 | }, 997 | else => return ImprintError.ExpectedIntOrStringNode, 998 | } 999 | }, 1000 | .Optional => |opt_info| { 1001 | const CI = @typeInfo(opt_info.child); 1002 | // Aggregate types have a null root, so could still exist. 1003 | if (self.value != .Null or CI == .Array or CI == .Struct or (CI == .Pointer and CI.Pointer.size == .Slice)) { 1004 | return try self.imprint_(opt_info.child, allocator); 1005 | } else { 1006 | return null; 1007 | } 1008 | }, 1009 | .Struct => |struct_info| { 1010 | var iter: ?*const ZNode = null; 1011 | var result: T = .{}; 1012 | 1013 | inline for (struct_info.fields) |field| { 1014 | // Skip underscores. 1015 | if (field.name[0] == '_') { 1016 | continue; 1017 | } 1018 | 1019 | const found = self.findNextChild(iter, .{ .String = field.name }); 1020 | if (found) |child_node| { 1021 | if (@typeInfo(field.field_type) == .Struct) { 1022 | @field(result, field.name) = try child_node.imprint_(field.field_type, allocator); 1023 | } else { 1024 | if (child_node.child) |value_node| { 1025 | @field(result, field.name) = try value_node.imprint_(field.field_type, allocator); 1026 | } 1027 | } 1028 | 1029 | // Found, set the iterator here. 1030 | iter = found; 1031 | } 1032 | } 1033 | return result; 1034 | }, 1035 | // Only handle [N]?T, where T is any other valid type. 1036 | .Array => |array_info| { 1037 | // Arrays are weird. They work on siblings. 1038 | // TODO: For some types this causes a crash like [N]fn() void types. 1039 | var r: T = std.mem.zeroes(T); 1040 | var iter: ?*const ZNode = self; 1041 | comptime var i: usize = 0; 1042 | inline while (i < array_info.len) : (i += 1) { 1043 | if (iter) |it| { 1044 | r[i] = try it.imprint_(array_info.child, allocator); 1045 | } 1046 | if (iter) |it| { 1047 | iter = it.sibling; 1048 | } 1049 | } 1050 | return r; 1051 | }, 1052 | .Pointer => |ptr_info| { 1053 | switch (ptr_info.size) { 1054 | .One => { 1055 | if (ptr_info.child == ZNode) { 1056 | // This is an odd case because we usually pass the child of a node 1057 | // for the value, but here since we explicitely asked for the node, 1058 | // it likely means the top. By taking the parent we force ZNodes 1059 | // only working when part of a large struct and not stand alone. 1060 | // 1061 | // Something like this wouldn't work: ``` 1062 | // root.imprint(*ZNode); 1063 | // ``` 1064 | return self.parent.?; 1065 | } else if (allocator) |alloc| { 1066 | var ptr = try alloc.create(ptr_info.child); 1067 | ptr.* = try self.imprint_(ptr_info.child, allocator); 1068 | return ptr; 1069 | } else { 1070 | return ImprintError.InvalidPointerType; 1071 | } 1072 | }, 1073 | .Slice => { 1074 | if (ptr_info.child == u8) { 1075 | switch (self.value) { 1076 | .String => { 1077 | if (allocator) |alloc| { 1078 | return try std.mem.dupe(alloc, u8, self.value.String); 1079 | } else { 1080 | return self.value.String; 1081 | } 1082 | }, 1083 | else => return ImprintError.ExpectedStringNode, 1084 | } 1085 | } else if (allocator) |alloc| { 1086 | // Same as pointer above. We take parent. 1087 | var ret = try alloc.alloc(ptr_info.child, self.parent.?.getChildCount()); 1088 | var iter: ?*const ZNode = self; 1089 | var i: usize = 0; 1090 | while (i < ret.len) : (i += 1) { 1091 | if (iter) |it| { 1092 | ret[i] = try it.imprint_(ptr_info.child, allocator); 1093 | } else { 1094 | if (@typeInfo(ptr_info.child) == .Optional) { 1095 | ret[i] = null; 1096 | } else { 1097 | return ImprintError.ArrayElemDoesNotExist; 1098 | } 1099 | } 1100 | if (iter) |it| { 1101 | iter = it.sibling; 1102 | } 1103 | } 1104 | return ret; 1105 | } else { 1106 | return ImprintError.InvalidType; 1107 | } 1108 | }, 1109 | else => return ImprintError.InvalidType, 1110 | } 1111 | }, 1112 | else => return ImprintError.InvalidType, 1113 | } 1114 | } 1115 | 1116 | pub fn imprint(self: *const Self, comptime T: type) ImprintError!T { 1117 | return try self.imprint_(T, null); 1118 | } 1119 | 1120 | pub fn imprintAlloc(self: *const Self, comptime T: type, allocator: *std.mem.Allocator) ImprintError!Imprint(T) { 1121 | var arena = std.heap.ArenaAllocator.init(allocator); 1122 | errdefer { 1123 | // Free everything. 1124 | arena.deinit(); 1125 | } 1126 | return Imprint(T){ 1127 | .result = try self.imprint_(T, &arena.allocator), 1128 | .arena = arena, 1129 | }; 1130 | } 1131 | 1132 | /// Outputs a `ZNode` and its children on a single line. This can be parsed back. 1133 | pub fn stringify(self: *const Self, out_stream: anytype) @TypeOf(out_stream).Error!void { 1134 | // Likely not root. 1135 | if (self.value != .Null) { 1136 | try self.value.stringify(out_stream); 1137 | try out_stream.writeAll(":"); 1138 | } 1139 | var depth: isize = 0; 1140 | var last_depth: isize = 1; 1141 | var iter = self; 1142 | while (iter.nextUntil(self, &depth)) |n| : (iter = n) { 1143 | if (depth > last_depth) { 1144 | last_depth = depth; 1145 | try out_stream.writeAll(":"); 1146 | } else if (depth < last_depth) { 1147 | while (depth < last_depth) { 1148 | try out_stream.writeAll(";"); 1149 | last_depth -= 1; 1150 | } 1151 | } else if (depth > 1) { 1152 | try out_stream.writeAll(","); 1153 | } 1154 | try n.value.stringify(out_stream); 1155 | } 1156 | } 1157 | 1158 | /// Returns true if node has more than one descendant (child, grandchild, etc). 1159 | fn _moreThanOneDescendant(self: *const Self) bool { 1160 | var depth: isize = 0; 1161 | var count: usize = 0; 1162 | var iter: *const ZNode = self; 1163 | while (iter.nextUntil(self, &depth)) |n| : (iter = n) { 1164 | count += 1; 1165 | if (count > 1) { 1166 | return true; 1167 | } 1168 | } 1169 | return false; 1170 | } 1171 | 1172 | fn _stringifyPretty(self: *const Self, out_stream: anytype) @TypeOf(out_stream).Error!void { 1173 | try self.value.stringify(out_stream); 1174 | try out_stream.writeAll(":"); 1175 | var depth: isize = 0; 1176 | var last_depth: isize = 1; 1177 | var iter = self; 1178 | while (iter.nextUntil(self, &depth)) |n| : (iter = n) { 1179 | if (depth > last_depth) { 1180 | last_depth = depth; 1181 | try out_stream.writeAll(":"); 1182 | // Likely an array. 1183 | if (n.parent.?.value == .Null) { 1184 | try out_stream.writeAll(" "); 1185 | } else if (n.parent.?._moreThanOneDescendant()) { 1186 | try out_stream.writeAll("\n"); 1187 | try out_stream.writeByteNTimes(' ', 2 * @bitCast(usize, depth)); 1188 | } else { 1189 | try out_stream.writeAll(" "); 1190 | } 1191 | } else if (depth < last_depth) { 1192 | while (depth < last_depth) { 1193 | last_depth -= 1; 1194 | } 1195 | try out_stream.writeAll("\n"); 1196 | try out_stream.writeByteNTimes(' ', 2 * @bitCast(usize, depth)); 1197 | } else { 1198 | try out_stream.writeAll("\n"); 1199 | try out_stream.writeByteNTimes(' ', 2 * @bitCast(usize, depth)); 1200 | } 1201 | try n.value.stringify(out_stream); 1202 | } 1203 | } 1204 | 1205 | /// Outputs a `ZNode`s children on multiple lines. Excludes this node as root. 1206 | /// Arrays with children that have: 1207 | /// - null elements, separate lines 1208 | /// - non-null, same line 1209 | pub fn stringifyPretty(self: *const Self, out_stream: anytype) @TypeOf(out_stream).Error!void { 1210 | // Assume root, so don't print this node. 1211 | var iter: ?*const ZNode = self.child; 1212 | while (iter) |n| { 1213 | try n._stringifyPretty(out_stream); 1214 | try out_stream.writeAll("\n"); 1215 | iter = n.sibling; 1216 | } 1217 | } 1218 | 1219 | /// Debug print the node. 1220 | pub fn show(self: *const Self) void { 1221 | std.debug.print("{}\n", .{self.value}); 1222 | var depth: isize = 0; 1223 | var iter: *const ZNode = self; 1224 | while (iter.nextUntil(self, &depth)) |c| : (iter = c) { 1225 | var i: isize = 0; 1226 | while (i < depth) : (i += 1) { 1227 | std.debug.print(" ", .{}); 1228 | } 1229 | std.debug.print("{}\n", .{c.value}); 1230 | } 1231 | } 1232 | }; 1233 | 1234 | pub const ZTreeError = error{ 1235 | TreeFull, 1236 | TooManyRoots, 1237 | }; 1238 | 1239 | /// ZTree errors. 1240 | pub const ZError = StreamingParser.Error || ZTreeError; 1241 | 1242 | /// Represents a static fixed-size zzz tree. Values are slices over the text passed. 1243 | pub fn ZTree(comptime R: usize, comptime S: usize) type { 1244 | return struct { 1245 | const Self = @This(); 1246 | roots: [R]*ZNode = undefined, 1247 | root_count: usize = 0, 1248 | nodes: [S]ZNode = [_]ZNode{.{}} ** S, 1249 | node_count: usize = 0, 1250 | 1251 | /// Appends correct zzz text to the tree, creating a new root. 1252 | pub fn appendText(self: *Self, text: []const u8) ZError!*ZNode { 1253 | const current_node_count = self.node_count; 1254 | var root = try self.addNode(null, .Null); 1255 | // Undo everything we did if we encounter an error. 1256 | errdefer { 1257 | // Undo adding root above. 1258 | self.root_count -= 1; 1259 | // Reset to node count before adding root. 1260 | self.node_count = current_node_count; 1261 | } 1262 | // If we error, undo adding any of this. 1263 | var current = root; 1264 | var current_depth: usize = 0; 1265 | 1266 | var stream = StreamingParser.init(); 1267 | var idx: usize = 0; 1268 | while (try parseStream(&stream, &idx, text)) |token| { 1269 | const slice = text[token.start..token.end]; 1270 | const value: ZValue = if (slice.len == 0) .Null else .{ .String = slice }; 1271 | const new_depth = token.depth; 1272 | if (new_depth <= current_depth) { 1273 | // Ascend. 1274 | while (current_depth > new_depth) { 1275 | current = current.parent orelse unreachable; 1276 | current_depth -= 1; 1277 | } 1278 | // Sibling. 1279 | const new = try self.addNode(current.parent, value); 1280 | current.sibling = new; 1281 | current = new; 1282 | } else if (new_depth == current_depth + 1) { 1283 | // Descend. 1284 | current_depth += 1; 1285 | const new = try self.addNode(current, value); 1286 | current.child = new; 1287 | current = new; 1288 | } else { 1289 | // Levels shouldn't increase by more than one. 1290 | unreachable; 1291 | } 1292 | } 1293 | 1294 | try stream.completeOrError(); 1295 | 1296 | return root; 1297 | } 1298 | 1299 | /// Clears the entire tree. 1300 | pub fn clear(self: *Self) void { 1301 | self.root_count = 0; 1302 | self.node_count = 0; 1303 | } 1304 | 1305 | /// Returns a slice of active roots. 1306 | pub fn rootSlice(self: *const Self) []const *ZNode { 1307 | return self.roots[0..self.root_count]; 1308 | } 1309 | 1310 | /// Adds a node given a parent. Null parent starts a new root. When adding nodes manually 1311 | /// care must be taken to ensure tree is left in known state after erroring from being full. 1312 | /// Either reset to root_count/node_count when an error occurs, or leave as is (unfinished). 1313 | pub fn addNode(self: *Self, parent: ?*ZNode, value: ZValue) ZError!*ZNode { 1314 | if (self.node_count >= S) { 1315 | return ZError.TreeFull; 1316 | } 1317 | var node = &self.nodes[self.node_count]; 1318 | if (parent == null) { 1319 | if (self.root_count >= R) { 1320 | return ZError.TooManyRoots; 1321 | } 1322 | self.roots[self.root_count] = node; 1323 | self.root_count += 1; 1324 | } 1325 | self.node_count += 1; 1326 | node.value = value; 1327 | node.parent = parent; 1328 | node.sibling = null; 1329 | node.child = null; 1330 | // Add to end. 1331 | if (parent) |p| { 1332 | if (p.child) |child| { 1333 | var iter = child; 1334 | while (iter.sibling) |sib| : (iter = sib) {} 1335 | iter.sibling = node; 1336 | } else { 1337 | p.child = node; 1338 | } 1339 | } 1340 | return node; 1341 | } 1342 | 1343 | /// Recursively copies a node from another part of the tree onto a new parent. Strings will 1344 | /// be by reference. 1345 | pub fn copyNode(self: *Self, parent: ?*ZNode, node: *const ZNode) ZError!*ZNode { 1346 | const current_root_count = self.root_count; 1347 | const current_node_count = self.node_count; 1348 | // Likely because tree was full. 1349 | errdefer { 1350 | self.root_count = current_root_count; 1351 | self.node_count = current_node_count; 1352 | } 1353 | var last_depth: isize = 1; 1354 | var depth: isize = 0; 1355 | var iter = node; 1356 | var piter: ?*ZNode = parent; 1357 | var plast: ?*ZNode = null; 1358 | var pfirst: ?*ZNode = null; 1359 | while (iter.next(&depth)) |child| : (iter = child) { 1360 | if (depth > last_depth) { 1361 | piter = plast; 1362 | last_depth = depth; 1363 | } else if (depth < last_depth) { 1364 | plast = piter; 1365 | while (last_depth != depth) { 1366 | piter = piter.?.parent; 1367 | last_depth -= 1; 1368 | } 1369 | } 1370 | plast = try self.addNode(piter, child.value); 1371 | if (pfirst == null) { 1372 | pfirst = plast; 1373 | } 1374 | } 1375 | return pfirst.?; 1376 | } 1377 | 1378 | /// Debug print the tree and all of its roots. 1379 | pub fn show(self: *const Self) void { 1380 | for (self.rootSlice()) |rt| { 1381 | rt.show(); 1382 | } 1383 | } 1384 | 1385 | /// Extract a struct's values onto a tree with a new root. Performs no allocations so any strings 1386 | /// are by reference. 1387 | pub fn extract(self: *Self, root: ?*ZNode, from_ptr: anytype) anyerror!void { 1388 | if (root == null) { 1389 | return self.extract(try self.addNode(null, .Null), from_ptr); 1390 | } 1391 | if (@typeInfo(@TypeOf(from_ptr)) != .Pointer) { 1392 | @compileError("Passed struct must be a pointer."); 1393 | } 1394 | const T = @typeInfo(@TypeOf(from_ptr)).Pointer.child; 1395 | const TI = @typeInfo(T); 1396 | switch (TI) { 1397 | .Void => { 1398 | // No need. 1399 | }, 1400 | .Bool => { 1401 | _ = try self.addNode(root, .{ .Bool = from_ptr.* }); 1402 | }, 1403 | .Float, .ComptimeFloat => { 1404 | _ = try self.addNode(root, .{ .Float = @floatCast(f32, from_ptr.*) }); 1405 | }, 1406 | .Int, .ComptimeInt => { 1407 | _ = try self.addNode(root, .{ .Int = @intCast(i32, from_ptr.*) }); 1408 | }, 1409 | .Enum => { 1410 | _ = try self.addNode(root, .{ .String = std.meta.tagName(from_ptr.*) }); 1411 | }, 1412 | .Optional => { 1413 | if (from_ptr.* != null) { 1414 | return self.extract(root, &from_ptr.*.?); 1415 | } 1416 | }, 1417 | .Struct => |struct_info| { 1418 | inline for (struct_info.fields) |field| { 1419 | if (field.name[field.name.len - 1] == '_') { 1420 | continue; 1421 | } 1422 | var field_node = try self.addNode(root, .{ .String = field.name }); 1423 | try self.extract(field_node, &@field(from_ptr.*, field.name)); 1424 | } 1425 | }, 1426 | .Array => |array_info| { 1427 | comptime var i: usize = 0; 1428 | inline while (i < array_info.len) : (i += 1) { 1429 | var null_node = try self.addNode(root, .Null); 1430 | try self.extract(null_node, &from_ptr.*[i]); 1431 | } 1432 | }, 1433 | .Pointer => |ptr_info| { 1434 | switch (ptr_info.size) { 1435 | .One => { 1436 | if (ptr_info.child == ZNode) { 1437 | _ = try self.copyNode(root, from_ptr.*); 1438 | } else { 1439 | try self.extract(root, &from_ptr.*.*); 1440 | } 1441 | }, 1442 | .Slice => { 1443 | if (ptr_info.child != u8) { 1444 | for (from_ptr.*) |_, i| { 1445 | var null_node = try self.addNode(root, .Null); 1446 | try self.extract(null_node, &from_ptr.*[i]); 1447 | } 1448 | } else { 1449 | _ = try self.addNode(root, .{ .String = from_ptr.* }); 1450 | } 1451 | return; 1452 | }, 1453 | else => return error.InvalidType, 1454 | } 1455 | }, 1456 | else => return error.InvalidType, 1457 | } 1458 | } 1459 | }; 1460 | } 1461 | 1462 | test "stable after error" { 1463 | const testing = std.testing; 1464 | 1465 | var tree = ZTree(2, 6){}; 1466 | // Using 1 root, 3 nodes (+1 for root). 1467 | _ = try tree.appendText("foo:bar"); 1468 | try testing.expectEqual(@as(usize, 1), tree.root_count); 1469 | try testing.expectEqual(@as(usize, 3), tree.node_count); 1470 | try testing.expectError(ZError.TreeFull, tree.appendText("bar:foo:baz:ha:ha")); 1471 | try testing.expectEqual(@as(usize, 1), tree.root_count); 1472 | try testing.expectEqual(@as(usize, 3), tree.node_count); 1473 | // Using +1 root, +2 node = 2 roots, 5 nodes. 1474 | _ = try tree.appendText("bar"); 1475 | try testing.expectEqual(@as(usize, 2), tree.root_count); 1476 | try testing.expectEqual(@as(usize, 5), tree.node_count); 1477 | try testing.expectError(ZError.TooManyRoots, tree.appendText("foo")); 1478 | try testing.expectEqual(@as(usize, 2), tree.root_count); 1479 | try testing.expectEqual(@as(usize, 5), tree.node_count); 1480 | } 1481 | 1482 | test "static tree" { 1483 | const testing = std.testing; 1484 | const text = 1485 | \\max_particles: 100 1486 | \\texture: circle 1487 | \\en: Foo 1488 | \\systems: 1489 | \\ : name:Emitter 1490 | \\ params: 1491 | \\ some,stuff,hehe 1492 | \\ : name:Fire 1493 | ; 1494 | 1495 | var tree = ZTree(1, 100){}; 1496 | const node = try tree.appendText(text); 1497 | node.convertStrings(); 1498 | 1499 | var iter = node.findNextChild(null, .{ .String = "max_particles" }); 1500 | try testing.expect(iter != null); 1501 | iter = node.findNextChild(iter, .{ .String = "texture" }); 1502 | try testing.expect(iter != null); 1503 | iter = node.findNextChild(iter, .{ .String = "max_particles" }); 1504 | try testing.expect(iter != null); 1505 | iter = node.findNextChild(iter, .{ .String = "systems" }); 1506 | try testing.expect(iter != null); 1507 | iter = node.findNextChild(iter, .{ .Int = 42 }); 1508 | try testing.expect(iter == null); 1509 | } 1510 | 1511 | test "node appending and searching" { 1512 | const testing = std.testing; 1513 | 1514 | var tree = ZTree(1, 100){}; 1515 | var root = try tree.addNode(null, .Null); 1516 | 1517 | _ = try tree.addNode(root, .Null); 1518 | _ = try tree.addNode(root, .{ .String = "Hello" }); 1519 | _ = try tree.addNode(root, .{ .String = "foo" }); 1520 | _ = try tree.addNode(root, .{ .Int = 42 }); 1521 | _ = try tree.addNode(root, .{ .Float = 3.14 }); 1522 | _ = try tree.addNode(root, .{ .Bool = true }); 1523 | 1524 | try testing.expectEqual(@as(usize, 6), root.getChildCount()); 1525 | try testing.expect(root.findNth(0, .Null) != null); 1526 | 1527 | try testing.expect(root.findNth(0, .{ .String = "Hello" }) != null); 1528 | try testing.expect(root.findNth(0, .{ .String = "foo" }) != null); 1529 | try testing.expect(root.findNth(1, .{ .String = "Hello" }) == null); 1530 | try testing.expect(root.findNth(1, .{ .String = "foo" }) == null); 1531 | try testing.expect(root.findNthAny(0, .String) != null); 1532 | try testing.expect(root.findNthAny(1, .String) != null); 1533 | try testing.expect(root.findNthAny(2, .String) == null); 1534 | 1535 | try testing.expect(root.findNth(0, .{ .Int = 42 }) != null); 1536 | try testing.expect(root.findNth(0, .{ .Int = 41 }) == null); 1537 | try testing.expect(root.findNth(1, .{ .Int = 42 }) == null); 1538 | try testing.expect(root.findNthAny(0, .Int) != null); 1539 | try testing.expect(root.findNthAny(1, .Int) == null); 1540 | 1541 | try testing.expect(root.findNth(0, .{ .Float = 3.14 }) != null); 1542 | try testing.expect(root.findNth(0, .{ .Float = 3.13 }) == null); 1543 | try testing.expect(root.findNth(1, .{ .Float = 3.14 }) == null); 1544 | try testing.expect(root.findNthAny(0, .Float) != null); 1545 | try testing.expect(root.findNthAny(1, .Float) == null); 1546 | 1547 | try testing.expect(root.findNthAny(0, .Bool) != null); 1548 | try testing.expect(root.findNth(0, .{ .Bool = true }) != null); 1549 | try testing.expect(root.findNthAny(1, .Bool) == null); 1550 | try testing.expect(root.findNth(1, .{ .Bool = true }) == null); 1551 | } 1552 | 1553 | test "node conforming imprint" { 1554 | const testing = std.testing; 1555 | 1556 | const ConformingEnum = enum { 1557 | Foo, 1558 | }; 1559 | 1560 | const ConformingSubStruct = struct { 1561 | name: []const u8 = "default", 1562 | params: ?*const ZNode = null, 1563 | }; 1564 | 1565 | const ConformingStruct = struct { 1566 | max_particles: ?i32 = null, 1567 | texture: []const u8 = "default", 1568 | systems: [20]?ConformingSubStruct = [_]?ConformingSubStruct{null} ** 20, 1569 | en: ?ConformingEnum = null, 1570 | exists: ?void = null, 1571 | }; 1572 | 1573 | const text = 1574 | \\max_particles: 100 1575 | \\texture: circle 1576 | \\en: Foo 1577 | \\systems: 1578 | \\ : name:Emitter 1579 | \\ params: 1580 | \\ some,stuff,hehe 1581 | \\ : name:Fire 1582 | \\ params 1583 | \\exists: anything here 1584 | ; 1585 | var tree = ZTree(1, 100){}; 1586 | var node = try tree.appendText(text); 1587 | node.convertStrings(); 1588 | 1589 | const example = try node.imprint(ConformingStruct); 1590 | try testing.expectEqual(@as(i32, 100), example.max_particles.?); 1591 | try testing.expectEqualSlices(u8, "circle", example.texture); 1592 | try testing.expect(null != example.systems[0]); 1593 | try testing.expect(null != example.systems[1]); 1594 | try testing.expectEqual(@as(?ConformingSubStruct, null), example.systems[2]); 1595 | try testing.expectEqual(ConformingEnum.Foo, example.en.?); 1596 | try testing.expectEqualSlices(u8, "params", example.systems[0].?.params.?.value.String); 1597 | } 1598 | 1599 | test "node nonconforming imprint" { 1600 | const testing = std.testing; 1601 | 1602 | const NonConformingStruct = struct { 1603 | max_particles: bool = false, 1604 | }; 1605 | 1606 | const text = 1607 | \\max_particles: 100 1608 | \\texture: circle 1609 | \\en: Foo 1610 | \\systems: 1611 | \\ : name:Emitter 1612 | \\ params: 1613 | \\ some,stuff,hehe 1614 | \\ : name:Fire 1615 | ; 1616 | var tree = ZTree(1, 100){}; 1617 | var node = try tree.appendText(text); 1618 | node.convertStrings(); 1619 | 1620 | try testing.expectError(ImprintError.ExpectedBoolNode, node.imprint(NonConformingStruct)); 1621 | } 1622 | 1623 | test "imprint allocations" { 1624 | const testing = std.testing; 1625 | 1626 | const Embedded = struct { 1627 | name: []const u8 = "", 1628 | count: u32 = 0, 1629 | }; 1630 | const SysAlloc = struct { 1631 | name: []const u8 = "", 1632 | params: ?*const ZNode = null, 1633 | }; 1634 | const FooAlloc = struct { 1635 | max_particles: ?*i32 = null, 1636 | texture: []const u8 = "", 1637 | systems: []SysAlloc = undefined, 1638 | embedded: Embedded = .{}, 1639 | }; 1640 | const text = 1641 | \\max_particles: 100 1642 | \\texture: circle 1643 | \\en: Foo 1644 | \\systems: 1645 | \\ : name:Emitter 1646 | \\ params: 1647 | \\ some,stuff,hehe 1648 | \\ : name:Fire 1649 | \\ params 1650 | \\embedded: 1651 | \\ name: creator 1652 | \\ count: 12345 1653 | \\ 1654 | ; 1655 | var tree = ZTree(1, 100){}; 1656 | var node = try tree.appendText(text); 1657 | node.convertStrings(); 1658 | var imprint = try node.imprintAlloc(FooAlloc, testing.allocator); 1659 | try testing.expectEqual(@as(i32, 100), imprint.result.max_particles.?.*); 1660 | for (imprint.result.systems) |sys, i| { 1661 | try testing.expectEqualSlices(u8, ([_][]const u8{ "Emitter", "Fire" })[i], sys.name); 1662 | } 1663 | imprint.arena.deinit(); 1664 | } 1665 | 1666 | test "extract" { 1667 | var text_tree = ZTree(1, 100){}; 1668 | var text_root = try text_tree.appendText("foo:bar:baz;;42"); 1669 | 1670 | const FooNested = struct { 1671 | a_bool: bool = true, 1672 | a_int: i32 = 42, 1673 | a_float: f32 = 3.14, 1674 | }; 1675 | const foo_struct = struct { 1676 | foo: ?i32 = null, 1677 | hi: []const u8 = "lol", 1678 | arr: [2]FooNested = [_]FooNested{.{}} ** 2, 1679 | slice: []const FooNested = &[_]FooNested{ .{}, .{}, .{} }, 1680 | ptr: *const FooNested = &FooNested{}, 1681 | a_node: *ZNode = undefined, 1682 | }{ 1683 | .a_node = text_root, 1684 | }; 1685 | 1686 | var tree = ZTree(1, 100){}; 1687 | try tree.extract(null, &foo_struct); 1688 | } 1689 | 1690 | /// A minimal factory for creating structs. The type passed should be an interface. Register structs 1691 | /// with special declarations and instantiate them with ZNodes. Required declarations: 1692 | /// - ZNAME: []const u8 // name of the struct referenced in zzz 1693 | /// - zinit: fn(allocator: *std.mem.Allocator, argz: *const ZNode) anyerror!*T // constructor called 1694 | pub fn ZFactory(comptime T: type) type { 1695 | return struct { 1696 | const Self = @This(); 1697 | 1698 | const Ctor = struct { 1699 | func: fn (allocator: *std.mem.Allocator, argz: *const ZNode) anyerror!*T, 1700 | }; 1701 | 1702 | registered: std.StringHashMap(Ctor), 1703 | 1704 | /// Create the factory. The allocator is for the internal HashMap. Instantiated objects 1705 | /// can have their own allocator. 1706 | pub fn init(allocator: *std.mem.Allocator) Self { 1707 | return Self{ 1708 | .registered = std.StringHashMap(Ctor).init(allocator), 1709 | }; 1710 | } 1711 | 1712 | /// 1713 | pub fn deinit(self: *Self) void { 1714 | self.registered.deinit(); 1715 | } 1716 | 1717 | /// Registers an implementor of the interface. Requires ZNAME and a zinit 1718 | /// method. 1719 | pub fn register(self: *Self, comptime S: anytype) !void { 1720 | const SI = @typeInfo(S); 1721 | if (SI != .Struct) { 1722 | @compileError("Expected struct got: " ++ @typeName(S)); 1723 | } 1724 | if (!@hasDecl(S, "zinit")) { 1725 | @compileError("Missing `zinit` on registered struct, it could be private: " ++ @typeName(S)); 1726 | } 1727 | if (!@hasDecl(S, "ZNAME")) { 1728 | @compileError("Missing `ZNAME` on registered struct, it could be private: " ++ @typeName(S)); 1729 | } 1730 | const ctor = Ctor{ 1731 | .func = S.zinit, 1732 | }; 1733 | try self.registered.put(S.ZNAME, ctor); 1734 | } 1735 | 1736 | /// Instantiates an object with ZNode. The ZNode's first child must have a string value of 1737 | /// "name" with the child node's value being the name of the registered struct. The node is 1738 | /// then passed to zinit. 1739 | /// 1740 | /// The caller is responsible for the memory. 1741 | pub fn instantiate(self: *Self, allocator: *std.mem.Allocator, node: *const ZNode) !*T { 1742 | const name = node.findNth(0, .{ .String = "name" }) orelse return error.ZNodeMissingName; 1743 | const value_node = name.getChild(0) orelse return error.ZNodeMissingValueUnderName; 1744 | if (value_node.value != .String) { 1745 | return error.ZNodeNameValueNotString; 1746 | } 1747 | const ctor = self.registered.get(value_node.value.String) orelse return error.StructNotFound; 1748 | return try ctor.func(allocator, node); 1749 | } 1750 | }; 1751 | } 1752 | 1753 | const FooInterface = struct { 1754 | const Self = @This(); 1755 | 1756 | allocator: ?*std.mem.Allocator = null, 1757 | default: i32 = 100, 1758 | fooFn: ?fn (*Self) void = null, 1759 | 1760 | deinitFn: ?fn (*const Self) void = null, 1761 | 1762 | pub fn foo(self: *Self) void { 1763 | return self.fooFn.?(self); 1764 | } 1765 | pub fn deinit(self: *const Self) void { 1766 | self.deinitFn.?(self); 1767 | } 1768 | }; 1769 | 1770 | const FooBar = struct { 1771 | const Self = @This(); 1772 | const ZNAME = "Foo"; 1773 | interface: FooInterface = .{}, 1774 | bar: i32 = 0, 1775 | 1776 | pub fn zinit(allocator: *std.mem.Allocator, _: *const ZNode) !*FooInterface { 1777 | var self = try allocator.create(Self); 1778 | self.* = .{ 1779 | .interface = .{ 1780 | .allocator = allocator, 1781 | .fooFn = foo, 1782 | .deinitFn = deinit, 1783 | }, 1784 | }; 1785 | //const imprint = try argz.imprint(FooBar); 1786 | //self.bar = imprint.bar; 1787 | return &self.interface; 1788 | } 1789 | 1790 | pub fn deinit(interface: *const FooInterface) void { 1791 | const self = @fieldParentPtr(Self, "interface", interface); 1792 | interface.allocator.?.destroy(self); 1793 | } 1794 | 1795 | pub fn foo(interface: *FooInterface) void { 1796 | _ = @fieldParentPtr(FooBar, "interface", interface); 1797 | } 1798 | }; 1799 | 1800 | test "factory" { 1801 | const testing = std.testing; 1802 | 1803 | const text = 1804 | \\name:Foo 1805 | \\bar:42 1806 | ; 1807 | 1808 | var tree = ZTree(1, 100){}; 1809 | var root = try tree.appendText(text); 1810 | root.convertStrings(); 1811 | 1812 | var factory = ZFactory(FooInterface).init(testing.allocator); 1813 | defer factory.deinit(); 1814 | 1815 | try factory.register(FooBar); 1816 | 1817 | const foobar = try factory.instantiate(testing.allocator, root); 1818 | foobar.foo(); 1819 | defer foobar.deinit(); 1820 | } 1821 | }; -------------------------------------------------------------------------------- /src/c.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | 3 | pub const M3Result = [*c]const u8; 4 | pub const IM3Environment = ?*opaque {}; 5 | pub const IM3Runtime = ?*opaque {}; 6 | pub const IM3Module = ?*opaque {}; 7 | pub const IM3Function = ?*opaque {}; 8 | pub const IM3Global = ?*opaque {}; 9 | 10 | pub const M3ErrorInfo = extern struct { 11 | result: M3Result, 12 | runtime: IM3Runtime, 13 | module: IM3Module, 14 | function: IM3Function, 15 | file: [*c]const u8, 16 | line: u32, 17 | message: [*c]const u8, 18 | }; 19 | 20 | pub const M3BacktraceFrame = extern struct { 21 | moduleOffset: u32, 22 | function: IM3Function, 23 | next: ?*M3BacktraceFrame, 24 | }; 25 | 26 | pub const M3BacktraceInfo = extern struct { 27 | frames: ?*M3BacktraceFrame, 28 | lastFrame: ?*M3BacktraceFrame, 29 | 30 | pub fn lastFrameTruncated(self: *M3BacktraceInfo) callconv(.Inline) bool { 31 | const std = @import("std"); 32 | const last_frame = @ptrToInt(self.lastFrame); 33 | 34 | // M3_BACKTRACE_TRUNCATED is defined as (void*)(SIZE_MAX) 35 | return last_frame == std.math.maxInt(usize); 36 | } 37 | }; 38 | 39 | pub const M3ValueType = enum(c_int) { 40 | None = 0, 41 | Int32 = 1, 42 | Int64 = 2, 43 | Float32 = 3, 44 | Float64 = 4, 45 | Unknown = 5, 46 | }; 47 | 48 | pub const M3TaggedValue = extern struct { 49 | kind: M3ValueType, 50 | value: extern union { 51 | // The wasm3 API has the integers as unsigned, 52 | // but the spec and naming convention seems to imply 53 | // that they're actually signed. I can't find an example 54 | // of wasm working with global integers, so we're just winging it 55 | // and if it breaks something we'll just have to fix it unfortunately. 56 | int32: i32, 57 | int64: i64, 58 | float32: f32, 59 | float64: f64, 60 | } 61 | }; 62 | 63 | pub const M3ImportInfo = extern struct { 64 | moduleUtf8: [*c]const u8, 65 | fieldUtf8: [*c]const u8, 66 | }; 67 | 68 | pub const M3ImportContext = extern struct { 69 | userdata: ?*anyopaque, 70 | function: IM3Function, 71 | }; 72 | 73 | pub extern var m3Err_none: M3Result; 74 | 75 | // general errors 76 | pub extern var m3Err_mallocFailed: M3Result; 77 | 78 | // parse errors 79 | pub extern var m3Err_incompatibleWasmVersion: M3Result; 80 | pub extern var m3Err_wasmMalformed: M3Result; 81 | pub extern var m3Err_misorderedWasmSection: M3Result; 82 | pub extern var m3Err_wasmUnderrun: M3Result; 83 | pub extern var m3Err_wasmOverrun: M3Result; 84 | pub extern var m3Err_wasmMissingInitExpr: M3Result; 85 | pub extern var m3Err_lebOverflow: M3Result; 86 | pub extern var m3Err_missingUTF8: M3Result; 87 | pub extern var m3Err_wasmSectionUnderrun: M3Result; 88 | pub extern var m3Err_wasmSectionOverrun: M3Result; 89 | pub extern var m3Err_invalidTypeId: M3Result; 90 | pub extern var m3Err_tooManyMemorySections: M3Result; 91 | pub extern var m3Err_tooManyArgsRets: M3Result; 92 | 93 | // link errors 94 | pub extern var m3Err_moduleNotLinked: M3Result; 95 | pub extern var m3Err_moduleAlreadyLinked: M3Result; 96 | pub extern var m3Err_functionLookupFailed: M3Result; 97 | pub extern var m3Err_functionImportMissing: M3Result; 98 | 99 | pub extern var m3Err_malformedFunctionSignature: M3Result; 100 | 101 | // compilation errors 102 | pub extern var m3Err_noCompiler: M3Result; 103 | pub extern var m3Err_unknownOpcode: M3Result; 104 | pub extern var m3Err_restrictedOpcode: M3Result; 105 | pub extern var m3Err_functionStackOverflow: M3Result; 106 | pub extern var m3Err_functionStackUnderrun: M3Result; 107 | pub extern var m3Err_mallocFailedCodePage: M3Result; 108 | pub extern var m3Err_settingImmutableGlobal: M3Result; 109 | pub extern var m3Err_typeMismatch: M3Result; 110 | pub extern var m3Err_typeCountMismatch: M3Result; 111 | 112 | // runtime errors 113 | pub extern var m3Err_missingCompiledCode: M3Result; 114 | pub extern var m3Err_wasmMemoryOverflow: M3Result; 115 | pub extern var m3Err_globalMemoryNotAllocated: M3Result; 116 | pub extern var m3Err_globaIndexOutOfBounds: M3Result; 117 | pub extern var m3Err_argumentCountMismatch: M3Result; 118 | pub extern var m3Err_argumentTypeMismatch: M3Result; 119 | pub extern var m3Err_globalLookupFailed: M3Result; 120 | pub extern var m3Err_globalTypeMismatch: M3Result; 121 | pub extern var m3Err_globalNotMutable: M3Result; 122 | 123 | // traps 124 | pub extern var m3Err_trapOutOfBoundsMemoryAccess: M3Result; 125 | pub extern var m3Err_trapDivisionByZero: M3Result; 126 | pub extern var m3Err_trapIntegerOverflow: M3Result; 127 | pub extern var m3Err_trapIntegerConversion: M3Result; 128 | pub extern var m3Err_trapIndirectCallTypeMismatch: M3Result; 129 | pub extern var m3Err_trapTableIndexOutOfRange: M3Result; 130 | pub extern var m3Err_trapTableElementIsNull: M3Result; 131 | pub extern var m3Err_trapExit: M3Result; 132 | pub extern var m3Err_trapAbort: M3Result; 133 | pub extern var m3Err_trapUnreachable: M3Result; 134 | pub extern var m3Err_trapStackOverflow: M3Result; 135 | 136 | pub extern fn m3_NewEnvironment() IM3Environment; 137 | pub extern fn m3_FreeEnvironment(i_environment: IM3Environment) void; 138 | pub const M3SectionHandler = 139 | if (builtin.zig_backend == .stage1) 140 | ?fn (IM3Module, name: [*:0]const u8, start: [*]const u8, end: *const u8) callconv(.C) M3Result 141 | else 142 | ?*const fn (IM3Module, name: [*:0]const u8, start: [*]const u8, end: *const u8) callconv(.C) M3Result; 143 | pub extern fn m3_SetCustomSectionHandler(i_environment: IM3Environment, i_handler: M3SectionHandler) void; 144 | pub extern fn m3_NewRuntime(io_environment: IM3Environment, i_stackSizeInBytes: u32, i_userdata: ?*anyopaque) IM3Runtime; 145 | pub extern fn m3_FreeRuntime(i_runtime: IM3Runtime) void; 146 | pub extern fn m3_GetMemory(i_runtime: IM3Runtime, o_memorySizeInBytes: [*c]u32, i_memoryIndex: u32) [*c]u8; 147 | /// This is used internally by Raw Function helpers 148 | pub extern fn m3_GetMemorySize(i_runtime: IM3Runtime) u32; 149 | pub extern fn m3_GetUserData(i_runtime: IM3Runtime) ?*anyopaque; 150 | pub extern fn m3_ParseModule(i_environment: IM3Environment, o_module: *IM3Module, i_wasmBytes: [*]const u8, i_numWasmBytes: u32) M3Result; 151 | pub extern fn m3_FreeModule(i_module: IM3Module) void; 152 | /// LoadModule transfers ownership of a module to the runtime. Do not free modules once successfully loaded into the runtime 153 | pub extern fn m3_LoadModule(io_runtime: IM3Runtime, io_module: IM3Module) M3Result; 154 | /// Optional, compiles all functions in the module 155 | pub extern fn m3_CompileModule(io_module: IM3Module) M3Result; 156 | pub extern fn m3_RunStart(i_module: IM3Module) M3Result; 157 | /// Arguments and return values are passed in and out through the stack pointer _sp. 158 | /// Placeholder return value slots are first and arguments after. So, the first argument is at _sp [numReturns] 159 | /// Return values should be written into _sp [0] to _sp [num_returns - 1] 160 | pub const M3RawCall = 161 | if (builtin.zig_backend == .stage1) 162 | ?fn (IM3Runtime, ctx: *M3ImportContext, [*c]u64, ?*anyopaque) callconv(.C) ?*const anyopaque 163 | else 164 | ?*const fn (IM3Runtime, ctx: *M3ImportContext, [*c]u64, ?*anyopaque) callconv(.C) ?*const anyopaque; 165 | pub extern fn m3_LinkRawFunction(io_module: IM3Module, i_moduleName: [*:0]const u8, i_functionName: [*:0]const u8, i_signature: [*c]const u8, i_function: M3RawCall) M3Result; 166 | pub extern fn m3_LinkRawFunctionEx(io_module: IM3Module, i_moduleName: [*:0]const u8, i_functionName: [*:0]const u8, i_signature: [*c]const u8, i_function: M3RawCall, i_userdata: ?*const anyopaque) M3Result; 167 | /// Returns "" on failure, but this behavior isn't described in the API so could be subject to change. 168 | pub extern fn m3_GetModuleName(i_module: IM3Module) [*:0]u8; 169 | pub extern fn m3_SetModuleName(i_module: IM3Module, name: [*:0]const u8) void; 170 | pub extern fn m3_GetModuleRuntime(i_module: IM3Module) IM3Runtime; 171 | pub extern fn m3_FindGlobal(io_module: IM3Module, i_globalName: [*:0]const u8) IM3Global; 172 | pub extern fn m3_GetGlobal(i_global: IM3Global, i_value: *M3TaggedValue) M3Result; 173 | pub extern fn m3_SetGlobal(i_global: IM3Global, i_value: *const M3TaggedValue) M3Result; 174 | pub extern fn m3_GetGlobalType(i_global: IM3Global) M3ValueType; 175 | pub extern fn m3_Yield() M3Result; 176 | pub extern fn m3_FindFunction(o_function: [*c]IM3Function, i_runtime: IM3Runtime, i_functionName: [*c]const u8) M3Result; 177 | pub extern fn m3_GetArgCount(i_function: IM3Function) u32; 178 | pub extern fn m3_GetRetCount(i_function: IM3Function) u32; 179 | pub extern fn m3_GetArgType(i_function: IM3Function, index: u32) M3ValueType; 180 | pub extern fn m3_GetRetType(i_function: IM3Function, index: u32) M3ValueType; 181 | pub extern fn m3_CallV(i_function: IM3Function, ...) M3Result; 182 | pub extern fn m3_Call(i_function: IM3Function, i_argc: u32, i_argptrs: [*c]?*const anyopaque) M3Result; 183 | pub extern fn m3_CallArgV(i_function: IM3Function, i_argc: u32, i_argv: [*c][*c]const u8) M3Result; 184 | pub extern fn m3_GetResults(i_function: IM3Function, i_retc: u32, ret_ptrs: [*c]?*anyopaque) M3Result; 185 | pub extern fn m3_GetErrorInfo(i_runtime: IM3Runtime, info: [*c]M3ErrorInfo) void; 186 | pub extern fn m3_ResetErrorInfo(i_runtime: IM3Runtime) void; 187 | /// Returns "" on failure, but this behavior isn't described in the API so could be subject to change. 188 | pub extern fn m3_GetFunctionName(i_function: IM3Function) [*:0]const u8; 189 | pub extern fn m3_GetFunctionModule(i_function: IM3Function) IM3Module; 190 | pub extern fn m3_PrintRuntimeInfo(i_runtime: IM3Runtime) void; 191 | pub extern fn m3_PrintM3Info() void; 192 | pub extern fn m3_PrintProfilerInfo() void; 193 | pub extern fn m3_GetBacktrace(i_runtime: IM3Runtime) ?*M3BacktraceInfo; 194 | 195 | pub extern fn m3_LinkWASI(io_module: IM3Module) M3Result; 196 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const c = @import("c.zig"); 5 | const builtin = @import("builtin"); 6 | 7 | fn createErrorMappingFunctions() type { 8 | 9 | @setEvalBranchQuota(50000); 10 | const match_list = comptime get_results: { 11 | const Declaration = std.builtin.Type.Declaration; 12 | var result_values: []const [2][]const u8 = &[0][2][]const u8{}; 13 | for (@typeInfo(c).Struct.decls) |decl| { 14 | const d: Declaration = decl; 15 | if (std.mem.startsWith(u8, d.name, "m3Err_")) { 16 | if (!std.mem.eql(u8, d.name, "m3Err_none")) { 17 | var error_name: []const u8 = d.name[("m3Err_").len..]; 18 | 19 | error_name = get: for (std.meta.fieldNames(Error)) |f| { 20 | if (std.ascii.eqlIgnoreCase(error_name, f)) { 21 | break :get f; 22 | } 23 | } else { 24 | @compileError("Failed to find matching error for code " ++ d.name); 25 | }; 26 | 27 | result_values = result_values ++ [1][2][]const u8{[2][]const u8{ d.name, error_name }}; 28 | } 29 | } 30 | } 31 | break :get_results result_values; 32 | }; 33 | 34 | return struct { 35 | 36 | /// Map an M3Result to the matching Error value. 37 | pub fn mapError(result: c.M3Result) Error!void { 38 | 39 | if (result == c.m3Err_none) return; 40 | inline for (match_list) |pair| { 41 | if (result == @field(c, pair[0])) return @field(Error, pair[1]); 42 | } 43 | unreachable; 44 | } 45 | pub fn mapErrorReverse(result: Error!void) c.M3Result { 46 | if (result) { 47 | return c.m3Err_none; 48 | } else |err| { 49 | inline for (match_list) |pair| { 50 | if (err == @field(Error, pair[1])) return @field(c, pair[0]); 51 | } 52 | } 53 | unreachable; 54 | } 55 | }; 56 | } 57 | 58 | const ErrorMapping = createErrorMappingFunctions(); 59 | 60 | pub const Error = error{ 61 | // general errors 62 | MallocFailed, 63 | 64 | // parse errors 65 | IncompatibleWasmVersion, 66 | WasmMalformed, 67 | MisorderedWasmSection, 68 | WasmUnderrun, 69 | WasmOverrun, 70 | WasmMissingInitExpr, 71 | LebOverflow, 72 | MissingUTF8, 73 | WasmSectionUnderrun, 74 | WasmSectionOverrun, 75 | InvalidTypeId, 76 | TooManyMemorySections, 77 | TooManyArgsRets, 78 | 79 | // link errors 80 | ModuleNotLinked, 81 | ModuleAlreadyLinked, 82 | FunctionLookupFailed, 83 | FunctionImportMissing, 84 | 85 | MalformedFunctionSignature, 86 | 87 | // compilation errors 88 | NoCompiler, 89 | UnknownOpcode, 90 | RestrictedOpcode, 91 | FunctionStackOverflow, 92 | FunctionStackUnderrun, 93 | MallocFailedCodePage, 94 | SettingImmutableGlobal, 95 | TypeMismatch, 96 | TypeCountMismatch, 97 | 98 | // runtime errors 99 | MissingCompiledCode, 100 | WasmMemoryOverflow, 101 | GlobalMemoryNotAllocated, 102 | GlobaIndexOutOfBounds, 103 | ArgumentCountMismatch, 104 | ArgumentTypeMismatch, 105 | GlobalLookupFailed, 106 | GlobalTypeMismatch, 107 | GlobalNotMutable, 108 | 109 | // traps 110 | TrapOutOfBoundsMemoryAccess, 111 | TrapDivisionByZero, 112 | TrapIntegerOverflow, 113 | TrapIntegerConversion, 114 | TrapIndirectCallTypeMismatch, 115 | TrapTableIndexOutOfRange, 116 | TrapTableElementIsNull, 117 | TrapExit, 118 | TrapAbort, 119 | TrapUnreachable, 120 | TrapStackOverflow, 121 | }; 122 | 123 | pub const Runtime = struct { 124 | impl: c.IM3Runtime, 125 | 126 | pub fn deinit(this: Runtime) callconv(.Inline) void { 127 | c.m3_FreeRuntime(this.impl); 128 | } 129 | pub fn getMemory(this: Runtime, memory_index: u32) callconv(.Inline) ?[]u8 { 130 | var size: u32 = 0; 131 | var mem = c.m3_GetMemory(this.impl, &size, memory_index); 132 | if (mem) |valid| { 133 | return valid[0..@intCast(usize, size)]; 134 | } 135 | return null; 136 | } 137 | 138 | pub fn getMemorySize(this: Runtime) callconv(.Inline) u32 { 139 | return c.m3_GetMemorySize(this.impl); 140 | } 141 | 142 | pub fn getUserData(this: Runtime) callconv(.Inline) ?*anyopaque { 143 | return c.m3_GetUserData(this.impl); 144 | } 145 | 146 | pub fn loadModule(this: Runtime, module: Module) callconv(.Inline) !void { 147 | try ErrorMapping.mapError(c.m3_LoadModule(this.impl, module.impl)); 148 | } 149 | 150 | pub fn findFunction(this: Runtime, function_name: [:0]const u8) callconv(.Inline) !Function { 151 | var func = Function{ .impl = undefined }; 152 | try ErrorMapping.mapError(c.m3_FindFunction(&func.impl, this.impl, function_name.ptr)); 153 | return func; 154 | } 155 | pub fn printRuntimeInfo(this: Runtime) callconv(.Inline) void { 156 | c.m3_PrintRuntimeInfo(this.impl); 157 | } 158 | pub const ErrorInfo = c.M3ErrorInfo; 159 | pub fn getErrorInfo(this: Runtime) callconv(.Inline) ErrorInfo { 160 | var info: ErrorInfo = undefined; 161 | c.m3_GetErrorInfo(this.impl, &info); 162 | return info; 163 | } 164 | fn span(strz: ?[*:0]const u8) callconv(.Inline) []const u8 { 165 | if (strz) |s| return std.mem.span(s); 166 | return "nullptr"; 167 | } 168 | pub fn printError(this: Runtime) callconv(.Inline) void { 169 | var info = this.getErrorInfo(); 170 | this.resetErrorInfo(); 171 | std.log.err("Wasm3 error: {s} @ {s}:{d}\n", .{ span(info.message), span(info.file), info.line }); 172 | } 173 | pub fn resetErrorInfo(this: Runtime) callconv(.Inline) void { 174 | c.m3_ResetErrorInfo(this.impl); 175 | } 176 | }; 177 | 178 | pub const Function = struct { 179 | impl: c.IM3Function, 180 | 181 | pub fn getArgCount(this: Function) callconv(.Inline) u32 { 182 | return c.m3_GetArgCount(this.impl); 183 | } 184 | pub fn getRetCount(this: Function) callconv(.Inline) u32 { 185 | return c.m3_GetRetCount(this.impl); 186 | } 187 | pub fn getArgType(this: Function, idx: u32) callconv(.Inline) c.M3ValueType { 188 | return c.m3_GetArgType(this.impl, idx); 189 | } 190 | pub fn getRetType(this: Function, idx: u32) callconv(.Inline) c.M3ValueType { 191 | return c.m3_GetRetType(this.impl, idx); 192 | } 193 | /// Call a function, using a provided tuple for arguments. 194 | /// TYPES ARE NOT VALIDATED. Be careful 195 | /// TDOO: Test this! Zig has weird symbol export issues with wasm right now, 196 | /// so I can't verify that arguments or return values are properly passes! 197 | pub fn call(this: Function, comptime RetType: type, args: anytype) callconv(.Inline) !RetType { 198 | if (this.getRetCount() > 1) { 199 | return error.TooManyReturnValues; 200 | } 201 | 202 | const ArgsType = @TypeOf(args); 203 | if (@typeInfo(ArgsType) != .Struct) { 204 | @compileError("Expected tuple or struct argument, found " ++ @typeName(ArgsType)); 205 | } 206 | const fields_info = std.meta.fields(ArgsType); 207 | 208 | const count = fields_info.len; 209 | comptime var ptr_i: comptime_int = 0; 210 | const num_pointers = comptime ptr_count: { 211 | var num_ptrs: comptime_int = 0; 212 | var i: comptime_int = 0; 213 | inline while (i < count) : (i += 1) { 214 | const ArgType = @TypeOf(args[i]); 215 | if (isSandboxPtr(ArgType) or isOptSandboxPtr(ArgType)) { 216 | num_ptrs += 1; 217 | } 218 | } 219 | break :ptr_count num_ptrs; 220 | }; 221 | var pointer_values: [num_pointers]u32 = undefined; 222 | 223 | var arg_arr: [count]?*const anyopaque = undefined; 224 | comptime var i: comptime_int = 0; 225 | inline while (i < count) : (i += 1) { 226 | const ArgType = @TypeOf(args[i]); 227 | if (comptime (isSandboxPtr(ArgType) or isOptSandboxPtr(ArgType))) { 228 | if(pointer_values.len > 0) { 229 | pointer_values[ptr_i] = toLocalPtr(args[i]); 230 | arg_arr[i] = @ptrCast(?*const anyopaque, &pointer_values[ptr_i]); 231 | ptr_i += 1; 232 | } else { 233 | unreachable; 234 | } 235 | } else { 236 | arg_arr[i] = @ptrCast(?*const anyopaque, &args[i]); 237 | } 238 | } 239 | try ErrorMapping.mapError(c.m3_Call(this.impl, @intCast(u32, count), if (count == 0) null else &arg_arr)); 240 | 241 | if (RetType == void) return; 242 | 243 | const Extensions = struct { 244 | pub extern fn wasm3_addon_get_runtime_mem_ptr(rt: c.IM3Runtime) [*c]u8; 245 | pub extern fn wasm3_addon_get_fn_rt(func: c.IM3Function) c.IM3Runtime; 246 | }; 247 | 248 | const runtime_ptr = Extensions.wasm3_addon_get_fn_rt(this.impl); 249 | var return_data_buffer: u64 = undefined; 250 | var return_ptr: *anyopaque = @ptrCast(*anyopaque, &return_data_buffer); 251 | try ErrorMapping.mapError(c.m3_GetResults(this.impl, 1, &[1]?*anyopaque{return_ptr})); 252 | 253 | if (comptime (isSandboxPtr(RetType) or isOptSandboxPtr(RetType))) { 254 | const mem_ptr = Extensions.wasm3_addon_get_runtime_mem_ptr(runtime_ptr); 255 | return fromLocalPtr( 256 | RetType, 257 | @ptrCast(*u32, @alignCast(@alignOf(u32), return_ptr)).*, 258 | @ptrToInt(mem_ptr), 259 | ); 260 | } else { 261 | switch (RetType) { 262 | i8, i16, i32, i64, u8, u16, u32, u64, f32, f64 => { 263 | return @ptrCast(*RetType, @alignCast(@alignOf(RetType), return_ptr)).*; 264 | }, 265 | else => { 266 | @compileLog("Erroring anyway, is this wrong?", isSandboxPtr(RetType) or isOptSandboxPtr(RetType)); 267 | @compileError("Invalid WebAssembly return type " ++ @typeName(RetType) ++ "!"); 268 | }, 269 | } 270 | } 271 | } 272 | 273 | /// Don't free this, it's a member of the Function. 274 | /// Returns a generic name if the module is unnamed, such as "" 275 | pub fn getName(this: Function) callconv(.Inline) ![:0]const u8 { 276 | const name = try ErrorMapping.mapError(c.m3_GetFunctionName(this.impl)); 277 | return std.mem.span(name); 278 | } 279 | 280 | pub fn getModule(this: Function) callconv(.Inline) Module { 281 | return .{.impl = c.m3_GetFunctionModule(this.impl)}; 282 | } 283 | 284 | }; 285 | 286 | fn isSandboxPtr(comptime T: type) bool { 287 | return switch (@typeInfo(T)) { 288 | .Struct => @hasDecl(T, "_is_wasm3_local_ptr"), 289 | else => false, 290 | }; 291 | } 292 | 293 | fn isOptSandboxPtr(comptime T: type) bool { 294 | return switch (@typeInfo(T)) { 295 | .Optional => |opt| isSandboxPtr(opt.child), 296 | else => false, 297 | }; 298 | } 299 | 300 | pub fn SandboxPtr(comptime T: type) type { 301 | comptime { 302 | switch (T) { 303 | i8, i16, i32, i64 => {}, 304 | u8, u16, u32, u64 => {}, 305 | else => @compileError("Invalid type for a SandboxPtr. Must be an integer!"), 306 | } 307 | } 308 | return struct { 309 | pub const _is_wasm3_local_ptr = true; 310 | pub const Base = T; 311 | local_heap: usize, 312 | host_ptr: *T, 313 | const Self = @This(); 314 | 315 | pub fn localPtr(this: Self) callconv(.Inline) u32 { 316 | return @intCast(u32, @ptrToInt(this.host_ptr) - this.local_heap); 317 | } 318 | pub fn write(this: Self, val: T) callconv(.Inline) void { 319 | std.mem.writeIntLittle(T, std.mem.asBytes(this.host_ptr), val); 320 | } 321 | pub fn read(this: Self) callconv(.Inline) T { 322 | return std.mem.readIntLittle(T, std.mem.asBytes(this.host_ptr)); 323 | } 324 | fn offsetBy(this: Self, offset: i64) callconv(.Inline) *T { 325 | return @intToPtr(*T, get_ptr: { 326 | if (offset > 0) { 327 | break :get_ptr @ptrToInt(this.host_ptr) + @intCast(usize, offset); 328 | } else { 329 | break :get_ptr @ptrToInt(this.host_ptr) - @intCast(usize, -offset); 330 | } 331 | }); 332 | } 333 | /// Offset is in bytes, NOT SAFETY CHECKED. 334 | pub fn writeOffset(this: Self, offset: i64, val: T) callconv(.Inline) void { 335 | std.mem.writeIntLittle(T, std.mem.asBytes(this.offsetBy(offset)), val); 336 | } 337 | /// Offset is in bytes, NOT SAFETY CHECKED. 338 | pub fn readOffset(this: Self, offset: i64) callconv(.Inline) T { 339 | std.mem.readIntLittle(T, std.mem.asBytes(this.offsetBy(offset))); 340 | } 341 | pub usingnamespace if (T == u8) 342 | struct { 343 | /// NOT SAFETY CHECKED. 344 | pub fn slice(this: Self, len: u32) callconv(.Inline) []T { 345 | return @ptrCast([*]u8, this.host_ptr)[0..@intCast(usize, len)]; 346 | } 347 | } 348 | else 349 | struct {}; 350 | }; 351 | } 352 | 353 | fn fromLocalPtr(comptime T: type, localptr: u32, local_heap: usize) T { 354 | if (comptime isOptSandboxPtr(T)) { 355 | const Child = std.meta.Child(T); 356 | if (localptr == 0) return null; 357 | return Child{ 358 | .local_heap = local_heap, 359 | .host_ptr = @intToPtr(*Child.Base, local_heap + @intCast(usize, localptr)), 360 | }; 361 | } else if (comptime isSandboxPtr(T)) { 362 | std.debug.assert(localptr != 0); 363 | return T{ 364 | .local_heap = local_heap, 365 | .host_ptr = @intToPtr(*T.Base, local_heap + @intCast(usize, localptr)), 366 | }; 367 | } else { 368 | @compileError("Expected a SandboxPtr or a ?SandboxPtr, got " ++ @typeName(T)); 369 | } 370 | } 371 | 372 | fn toLocalPtr(sandbox_ptr: anytype) u32 { 373 | const T = @TypeOf(sandbox_ptr); 374 | if (comptime isOptSandboxPtr(T)) { 375 | if (sandbox_ptr) |np| { 376 | const lp = np.localPtr(); 377 | std.debug.assert(lp != 0); 378 | return lp; 379 | } else return 0; 380 | } else if (comptime isSandboxPtr(T)) { 381 | const lp = sandbox_ptr.localPtr(); 382 | std.debug.assert(lp != 0); 383 | return lp; 384 | } else { 385 | @compileError("Expected a SandboxPtr or a ?SandboxPtr"); 386 | } 387 | } 388 | 389 | pub const Module = struct { 390 | impl: c.IM3Module, 391 | 392 | pub fn deinit(this: Module) void { 393 | c.m3_FreeModule(this.impl); 394 | } 395 | 396 | fn mapTypeToChar(comptime T: type) u8 { 397 | switch (T) { 398 | void => return 'v', 399 | u32, i32 => return 'i', 400 | u64, i64 => return 'I', 401 | f32 => return 'f', 402 | f64 => return 'F', 403 | else => {}, 404 | } 405 | if (comptime (isSandboxPtr(T) or isOptSandboxPtr(T))) { 406 | return '*'; 407 | } 408 | switch (@typeInfo(T)) { 409 | .Pointer => |ptrti| { 410 | if (ptrti.size == .One) { 411 | @compileError("Please use a wasm3.SandboxPtr instead of raw pointers!"); 412 | } 413 | }, 414 | } 415 | @compileError("Invalid type " ++ @typeName(T) ++ " for WASM interop!"); 416 | } 417 | 418 | pub fn linkWasi(this: Module) !void { 419 | return ErrorMapping.mapError(c.m3_LinkWASI(this.impl)); 420 | } 421 | 422 | /// Links all functions in a struct to the module. 423 | /// library_name: the name of the library this function should belong to. 424 | /// library: a struct containing functions that should be added to the module. 425 | /// See linkRawFunction(...) for information about valid function signatures. 426 | /// userdata: A single-item pointer passed to the function as the first argument when called. 427 | /// Not accessible from within wasm, handled by the interpreter. 428 | /// If you don't want userdata, pass a void literal {}. 429 | pub fn linkLibrary(this: Module, library_name: [:0]const u8, comptime library: type, userdata: anytype) !void { 430 | inline for (@typeInfo(library).Struct.decls) |decl| { 431 | if (decl.is_pub) { 432 | const fn_name_z = comptime get_name: { 433 | var name_buf: [decl.name.len:0]u8 = undefined; 434 | std.mem.copy(u8, &name_buf, decl.name); 435 | break :get_name name_buf; 436 | }; 437 | try this.linkRawFunction(library_name, &fn_name_z, @field(library, decl.name), userdata); 438 | } 439 | } 440 | } 441 | 442 | /// Links a native function into the module. 443 | /// library_name: the name of the library this function should belong to. 444 | /// function_name: the name the function should have in module-space. 445 | /// function: a zig function (not function pointer!). 446 | /// Valid argument and return types are: 447 | /// i32, u32, i64, u64, f32, f64, void, and pointers to basic types. 448 | /// Userdata, if provided, is the first argument to the function. 449 | /// userdata: A single-item pointer passed to the function as the first argument when called. 450 | /// Not accessible from within wasm, handled by the interpreter. 451 | /// If you don't want userdata, pass a void literal {}. 452 | pub fn linkRawFunction(this: Module, library_name: [:0]const u8, function_name: [:0]const u8, comptime function: anytype, userdata: anytype) !void { 453 | errdefer { 454 | std.log.err("Failed to link proc {s}.{s}!\n", .{ library_name, function_name }); 455 | } 456 | comptime var has_userdata = @TypeOf(userdata) != void; 457 | comptime validate_userdata: { 458 | if (has_userdata) { 459 | switch (@typeInfo(@TypeOf(userdata))) { 460 | .Pointer => |ptrti| { 461 | if (ptrti.size == .One) { 462 | break :validate_userdata; 463 | } 464 | }, 465 | else => {}, 466 | } 467 | @compileError("Expected a single-item pointer for the userdata, got " ++ @typeName(@TypeOf(userdata))); 468 | } 469 | } 470 | const UserdataType = @TypeOf(userdata); 471 | const sig = comptime generate_signature: { 472 | switch (@typeInfo(@TypeOf(function))) { 473 | .Fn => |fnti| { 474 | const sub_data = if (has_userdata) 1 else 0; 475 | var arg_str: [fnti.args.len + 3 - sub_data:0]u8 = undefined; 476 | arg_str[0] = mapTypeToChar(fnti.return_type orelse void); 477 | arg_str[1] = '('; 478 | arg_str[arg_str.len - 1] = ')'; 479 | for (fnti.args[sub_data..]) |arg, i| { 480 | if (arg.is_generic) { 481 | @compileError("WASM does not support generic arguments to native functions!"); 482 | } 483 | arg_str[2 + i] = mapTypeToChar(arg.arg_type.?); 484 | } 485 | break :generate_signature arg_str; 486 | }, 487 | else => @compileError("Expected a function, got " ++ @typeName(@TypeOf(function))), 488 | } 489 | unreachable; 490 | }; 491 | const lambda = struct { 492 | pub fn l(_: c.IM3Runtime, import_ctx: *c.M3ImportContext, sp: [*c]u64, _mem: ?*anyopaque) callconv(.C) ?*const anyopaque { 493 | comptime var type_arr: []const type = &[0]type{}; 494 | if (has_userdata) { 495 | type_arr = type_arr ++ @as([]const type, &[1]type{UserdataType}); 496 | } 497 | std.debug.assert(_mem != null); 498 | var mem = @ptrToInt(_mem); 499 | var stack = @ptrToInt(sp); 500 | const stride = @sizeOf(u64) / @sizeOf(u8); 501 | 502 | switch (@typeInfo(@TypeOf(function))) { 503 | .Fn => |fnti| { 504 | const RetT = fnti.return_type orelse void; 505 | 506 | comptime var return_pointer = isSandboxPtr(RetT) or isOptSandboxPtr(RetT); 507 | 508 | const RetPtr = comptime if (RetT == void) void else if (return_pointer) *u32 else *RetT; 509 | var ret_val: RetPtr = undefined; 510 | if (RetT != void) { 511 | ret_val = @intToPtr(RetPtr, stack); 512 | stack += stride; 513 | } 514 | 515 | const sub_data = if (has_userdata) 1 else 0; 516 | inline for (fnti.args[sub_data..]) |arg| { 517 | 518 | if (arg.is_generic) unreachable; 519 | type_arr = type_arr ++ @as([]const type, &[1]type{arg.arg_type.?}); 520 | } 521 | 522 | var args: std.meta.Tuple(type_arr) = undefined; 523 | 524 | comptime var idx: usize = 0; 525 | if (has_userdata) { 526 | args[idx] = @ptrCast(UserdataType, @alignCast(@alignOf(std.meta.Child(UserdataType)), import_ctx.userdata)); 527 | idx += 1; 528 | } 529 | inline for (fnti.args[sub_data..]) |arg| { 530 | if (arg.is_generic) unreachable; 531 | 532 | const ArgT = arg.arg_type.?; 533 | 534 | if (comptime (isSandboxPtr(ArgT) or isOptSandboxPtr(ArgT))) { 535 | const vm_arg_addr: u32 = @intToPtr(*u32, stack).*; 536 | args[idx] = fromLocalPtr(ArgT, vm_arg_addr, mem); 537 | } else { 538 | args[idx] = @intToPtr(*ArgT, stack).*; 539 | } 540 | idx += 1; 541 | stack += stride; 542 | } 543 | 544 | if (RetT == void) { 545 | @call(.{ .modifier = .always_inline }, function, args); 546 | } else { 547 | const returned_value = @call(.{ .modifier = .always_inline }, function, args); 548 | if (return_pointer) { 549 | ret_val.* = toLocalPtr(returned_value); 550 | } else { 551 | ret_val.* = returned_value; 552 | } 553 | } 554 | 555 | return c.m3Err_none; 556 | }, 557 | else => unreachable, 558 | } 559 | } 560 | }.l; 561 | try ErrorMapping.mapError(c.m3_LinkRawFunctionEx(this.impl, library_name, function_name, @as([*]const u8, &sig), lambda, if (has_userdata) userdata else null)); 562 | } 563 | 564 | /// Optional, compiles all functions in the module 565 | pub fn compile(this: Module) callconv(.Inline) !void { 566 | return ErrorMapping.mapError(c.m3_CompileModule(this.impl)); 567 | } 568 | 569 | /// This is optional. 570 | pub fn runStart(this: Module) callconv(.Inline) !void { 571 | return ErrorMapping.mapError(c.m3_RunStart(this.impl)); 572 | } 573 | 574 | /// Don't free this, it's a member of the Module. 575 | /// Returns a generic name if the module is unnamed, such as "" 576 | pub fn getName(this: Module) callconv(.Inline) ![:0]const u8 { 577 | const name = try ErrorMapping.mapError(c.m3_GetModuleName(this.impl)); 578 | return std.mem.span(name); 579 | } 580 | 581 | /// Assumes that name will last as long as the module, does not copy 582 | pub fn setName(this: Module, name: [:0]const u8) callconv(.Inline) void { 583 | c.m3_SetModuleName(this.impl, name); 584 | } 585 | 586 | pub fn getRuntime(this: Module) callconv(.Inline) Runtime { 587 | return .{.impl = c.m3_GetModuleRuntime(this.impl)}; 588 | } 589 | 590 | pub fn findGlobal(this: Module, global_name: [:0]const u8) callconv(.Inline) ?Global { 591 | if(c.m3_FindGlobal(this.impl, global_name)) |global_ptr| { 592 | return Global {.impl = global_ptr}; 593 | } 594 | return null; 595 | } 596 | }; 597 | 598 | pub const Global = struct { 599 | pub const Value = union(enum) { 600 | Int32: i32, 601 | Int64: i64, 602 | Float32: f32, 603 | Float64: f64, 604 | }; 605 | pub const Type = c.M3ValueType; 606 | impl: c.IM3Global, 607 | pub fn getType(this: Global) callconv(.Inline) Type { 608 | return c.m3_GetGlobalType(this.impl); 609 | } 610 | pub fn get(this: Global) !Value { 611 | var tagged_union: c.M3TaggedValue = undefined; 612 | tagged_union.kind = .None; 613 | try ErrorMapping.mapError(c.m3_GetGlobal(this.impl, &tagged_union)); 614 | return switch(tagged_union.kind) { 615 | .None => Error.GlobalTypeMismatch, 616 | .Unknown => Error.GlobalTypeMismatch, 617 | .Int32 => Value {.Int32 = tagged_union.value.int32}, 618 | .Int64 => Value {.Int64 = tagged_union.value.int64}, 619 | .Float32 => Value {.Float32 = tagged_union.value.float32}, 620 | .Float64 => Value {.Float64 = tagged_union.value.float64}, 621 | }; 622 | } 623 | pub fn set(this: Global, value_union: Value) !void { 624 | var tagged_union: c.M3TaggedValue = switch(value_union) { 625 | .Int32 => |value| .{.kind = .Int32, .value = .{.int32 = value}}, 626 | .Int64 => |value| .{.kind = .Int64, .value = .{.int64 = value}}, 627 | .Float32 => |value| .{.kind = .Float32, .value = .{.float32 = value}}, 628 | .Float64 => |value| .{.kind = .Float64, .value = .{.float64 = value}}, 629 | }; 630 | return ErrorMapping.mapError(c.m3_SetGlobal(this.impl, &tagged_union)); 631 | } 632 | }; 633 | 634 | pub const Environment = struct { 635 | impl: c.IM3Environment, 636 | 637 | pub fn init() callconv(.Inline) Environment { 638 | return .{ .impl = c.m3_NewEnvironment() }; 639 | } 640 | pub fn deinit(this: Environment) callconv(.Inline) void { 641 | c.m3_FreeEnvironment(this.impl); 642 | } 643 | pub fn setCustomSectionHandler(this: Environment, comptime handler: fn(module: Module, name: []const u8, bytes: []const u8) Error!void) callconv(.Inline) void { 644 | const handler_adapter = struct { 645 | pub fn l(module: c.IM3Module, name: [*:0]const u8, start: [*]const u8, end: *const u8) callconv(.C) c.M3Result { 646 | var result = handler(.{.impl = module}, std.mem.span(name), start[0..(@ptrToInt(end) - @ptrToInt(start))]); 647 | return ErrorMapping.mapErrorReverse(result); 648 | } 649 | }.l; 650 | c.m3_SetCustomSectionHandler(this.impl, handler_adapter); 651 | } 652 | pub fn createRuntime(this: Environment, stack_size: u32, userdata: ?*anyopaque) callconv(.Inline) Runtime { 653 | return .{ .impl = c.m3_NewRuntime(this.impl, stack_size, userdata) }; 654 | } 655 | pub fn parseModule(this: Environment, wasm: []const u8) callconv(.Inline) !Module { 656 | var mod = Module{ .impl = undefined }; 657 | var res = c.m3_ParseModule(this.impl, &mod.impl, wasm.ptr, @intCast(u32, wasm.len)); 658 | try ErrorMapping.mapError(res); 659 | return mod; 660 | } 661 | }; 662 | 663 | pub fn yield() callconv(.Inline) !void { 664 | return ErrorMapping.mapError(c.m3_Yield()); 665 | } 666 | pub fn printM3Info() callconv(.Inline) void { 667 | c.m3_PrintM3Info(); 668 | } 669 | pub fn printProfilerInfo() callconv(.Inline) void { 670 | c.m3_PrintProfilerInfo(); 671 | } 672 | 673 | // HACK: Even though we're linking with libc, there's some disconnect between what wasm3 wants to link to 674 | // and what the platform's libc provides. 675 | // These functions stll exist, but for various reason, the C code in wasm3 expects functions with 676 | // different symbol names than the ones the system provides. 677 | // This isn't wasm3's fault, but I don't really know *where* blame lies, so we'll just work around it. 678 | // We can just reexport these functions. It's a bit hacky, but it gets things running. 679 | pub usingnamespace if (builtin.target.abi.isGnu() and builtin.target.os.tag != .windows) 680 | struct { 681 | export fn getrandom(buf: [*c]u8, len: usize, _: c_uint) i64 { 682 | std.os.getrandom(buf[0..len]) catch return 0; 683 | return @intCast(i64, len); 684 | } 685 | } 686 | else 687 | struct {}; 688 | -------------------------------------------------------------------------------- /src/wasm3_extra.c: -------------------------------------------------------------------------------- 1 | // Defines a public API for some of Wasm3's internals that are sort of important 2 | 3 | // ReleaseFast has LTO now, so this should just optimize away... 4 | // Hopefully! 5 | 6 | #include 7 | #include 8 | 9 | u8 *wasm3_addon_get_runtime_mem_ptr(M3Runtime *runtime) { 10 | return m3MemData(runtime->memory.mallocated); 11 | } 12 | 13 | M3Runtime *wasm3_addon_get_fn_rt(M3Function *func) { 14 | return func->module->runtime; 15 | } -------------------------------------------------------------------------------- /submod_build_plugin.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const root = @import("root"); 3 | 4 | /// Queues a build job for the C code of Wasm3. 5 | /// This builds a static library that depends on libc, so make sure to link that into your exe! 6 | pub fn compile(b: *std.build.Builder, mode: std.builtin.Mode, target: std.zig.CrossTarget, wasm3_root: []const u8) *std.build.LibExeObjStep { 7 | 8 | const lib = b.addStaticLibrary("wasm3", null); 9 | lib.setBuildMode(mode); 10 | lib.setTarget(target); 11 | lib.linkLibC(); 12 | lib.disable_sanitize_c = true; 13 | 14 | lib.defineCMacro("d_m3HasWASI", "1"); 15 | 16 | const src_dir = std.fs.path.join(b.allocator, &.{wasm3_root, "source"}) catch unreachable; 17 | 18 | var src_dir_handle = std.fs.cwd().openIterableDir(src_dir, .{}) catch unreachable; 19 | defer src_dir_handle.close(); 20 | 21 | lib.c_std = .C99; 22 | 23 | const cflags = [_][]const u8 { 24 | "-Wall", "-Wextra", "-Wparentheses", "-Wundef", "-Wpointer-arith", "-Wstrict-aliasing=2", 25 | "-Werror=implicit-function-declaration", 26 | "-Wno-unused-function", "-Wno-unused-variable", "-Wno-unused-parameter", "-Wno-missing-field-initializers", 27 | }; 28 | const cflags_with_windows_posix_aliases = cflags ++ [_][]const u8 { 29 | "-Dlseek(fd,off,whence)=_lseek(fd,off,whence)", 30 | "-Dfileno(stream)=_fileno(stream)", 31 | "-Dsetmode(fd,mode)=_setmode(fd,mode)", 32 | }; 33 | 34 | var core_src_file: ?[]const u8 = undefined; 35 | 36 | var iter = src_dir_handle.iterate(); 37 | while(iter.next() catch unreachable) |ent| { 38 | if(ent.kind == .File) { 39 | if(std.ascii.endsWithIgnoreCase(ent.name, ".c")) { 40 | const path = std.fs.path.join(b.allocator, &[_][]const u8{src_dir, ent.name}) catch unreachable; 41 | if(std.ascii.eqlIgnoreCase(ent.name, "m3_core.c")) { 42 | core_src_file = path; 43 | continue; 44 | } 45 | if( 46 | target.isWindows() and 47 | std.ascii.eqlIgnoreCase(ent.name, "m3_api_wasi.c") 48 | ) { 49 | lib.addCSourceFile(path, &cflags_with_windows_posix_aliases); 50 | } else { 51 | lib.addCSourceFile(path, &cflags); 52 | } 53 | } 54 | } 55 | } 56 | 57 | std.debug.assert(core_src_file != null); 58 | 59 | { // Patch source files. 60 | 61 | // wasm3 has a built-in limit for what it thinks should be the maximum sane length for a utf-8 string 62 | // It's 2000 characters, which seems reasonable enough. 63 | // 64 | // Here's the thing - C++ is not reasonable. 65 | // libc++'s rtti symbols exceed four-freakin'-thousand characters sometimes. 66 | // In order to support compiled C++ programs, we patch this value. 67 | // 68 | // It's kind of ugly, but it works! 69 | 70 | var build_root_handle = std.fs.cwd().openDir(wasm3_root, .{}) catch unreachable; 71 | defer build_root_handle.close(); 72 | 73 | std.fs.cwd().copyFile(core_src_file.?, build_root_handle, "m3_core.c", .{}) catch unreachable; 74 | lib.addCSourceFile(std.fs.path.join(b.allocator, &[_][]const u8{wasm3_root, "m3_core.c"}) catch unreachable, &cflags); 75 | 76 | build_root_handle.writeFile("m3_core.h", "#include \n" ++ 77 | "#undef d_m3MaxSaneUtf8Length\n" ++ 78 | "#define d_m3MaxSaneUtf8Length 10000\n") catch unreachable; 79 | 80 | } 81 | 82 | lib.addIncludeDir(src_dir); 83 | 84 | lib.addCSourceFile(std.fs.path.join(b.allocator, &[_][]const u8{ 85 | std.fs.path.dirname(@src().file).?, 86 | "src", "wasm3_extra.c" 87 | }) catch unreachable, &cflags); 88 | 89 | 90 | return lib; 91 | } 92 | 93 | /// Compiles Wasm3 and links it into the provided exe. 94 | /// If you use this API, you do not need to also use the compile() function. 95 | pub fn addTo(exe: *std.build.LibExeObjStep, wasm3_root: []const u8) void { 96 | 97 | var lib = compile(exe.builder, exe.build_mode, exe.target, wasm3_root); 98 | exe.linkLibC(); 99 | exe.linkLibrary(lib); 100 | } 101 | 102 | var file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; 103 | 104 | pub fn pkg(name: ?[]const u8) std.build.Pkg { 105 | var fba = std.heap.FixedBufferAllocator.init(&file_buf); 106 | return .{ 107 | .name = name orelse "wasm3", 108 | .source = std.build.FileSource { 109 | .path = std.fs.path.join(fba.allocator(), &[_][]const u8{std.fs.path.dirname(@src().file).?, "src", "main.zig"}) catch unreachable, 110 | } 111 | }; 112 | } 113 | --------------------------------------------------------------------------------