diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c73332b..4f89f33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - version: [0.16.0] + version: [0.16.0, 0.17.0] fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/README.md b/README.md index d766ad3..d4c9615 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ Like `zig build run_minimal`, this will build and run the `minimal` example. ### Zig `0.16.0` and later -This package targets Zig `0.16.0` and up. Nightly is -still not recommended — the build-system API can change between dev builds -and break the binding without warning. +This package supports Zig `0.16.0` and `0.17.0`; CI builds and tests both. +Nightly is still not recommended — the build-system API can change between +dev builds and break the binding without warning. 1. Add to `build.zig.zon` diff --git a/build.zig b/build.zig index 434bf2b..fdbd09e 100644 --- a/build.zig +++ b/build.zig @@ -93,13 +93,23 @@ pub fn addEmbeddedDir(b: *Build, module: *Module, options: EmbedDirOptions) !voi var dir = try root.openDir(io, options.path, .{ .iterate = true }); defer dir.close(io); + // Zig 0.17 caches the configure phase. Declare every scanned directory so + // adding, deleting, or renaming a file re-runs this scan; file contents are + // already tracked by `addCopyFile`. The API is not recursive. + const track_dirs = @hasDecl(Build, "dependOnDirectoryContents"); + if (track_dirs) b.dependOnDirectoryContents(b.path(options.path)); + var files: std.ArrayList([]const u8) = .empty; defer files.deinit(b.allocator); var walker = try dir.walk(b.allocator); defer walker.deinit(); while (try walker.next(io)) |entry| { + if (track_dirs and entry.kind == .directory) { + b.dependOnDirectoryContents(b.path(b.pathJoin(&.{ options.path, entry.path }))); + } if (entry.kind != .file) continue; - const path = b.dupe(entry.path); + // `b.dupe` returns `[]const u8` on Zig 0.17; the path is rewritten in place on Windows. + const path = try b.allocator.dupe(u8, entry.path); if (builtin.os.tag == .windows) { for (path) |*char| { if (char.* == '\\') char.* = '/'; @@ -376,6 +386,9 @@ fn buildExamples(b: *Build, options: BuildExamplesOptions) !void { }; defer examples_dir.close(io); + // Re-run configuration when an example directory is added or removed. + b.dependOnDirectoryContents(b.path(examples_path)); + var iter = examples_dir.iterate(); while (try iter.next(io)) |entry| { if (entry.kind != .directory) continue; diff --git a/build.zig.zon b/build.zig.zon index 4cb0622..fe61afa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.16.0", .dependencies = .{ .webui = .{ - .hash = "webui-2.5.0-beta.4-pxqD5aCPRABtMDhL6OiX-rWtAXSBTzb1iY0YYfKxR-rF", - .url = "https://github.com/webui-dev/webui/archive/52f9e75b92faf9a23fd150b3c60051c4ec85fc69.tar.gz", + .hash = "webui-2.5.0-beta.4-pxqD5SCVRAAXiIiARY9CWyEhzl_sHMmRW8tzspnErwfT", + .url = "https://github.com/webui-dev/webui/archive/f1b28eeeecfc2d63bdb4d4857c6f137468c6d6f6.tar.gz", }, }, .paths = .{ diff --git a/examples/compat.zig b/examples/compat.zig index 8155dcc..4d94e53 100644 --- a/examples/compat.zig +++ b/examples/compat.zig @@ -65,6 +65,13 @@ pub const FixedBufferStream = struct { } }; +/// Format into `buffer` with a NUL terminator. `std.fmt.bufPrintZ` was removed +/// in Zig 0.17, and its replacement `std.mem.printSentinel` does not exist on 0.16. +pub fn bufPrintZ(buffer: []u8, comptime format: []const u8, args: anytype) ![:0]u8 { + if (@hasDecl(std.mem, "printSentinel")) return std.mem.printSentinel(buffer, format, args, 0); + return std.fmt.bufPrintSentinel(buffer, format, args, 0); +} + // ===== Allocator compat ====================================================== /// `std.heap.GeneralPurposeAllocator` was renamed to `std.heap.DebugAllocator` diff --git a/examples/comprehensive/main.zig b/examples/comprehensive/main.zig index aee84d1..10c42af 100644 --- a/examples/comprehensive/main.zig +++ b/examples/comprehensive/main.zig @@ -154,7 +154,7 @@ fn getAppStatus(e: *webui.Event) void { const url = win.getUrl() catch "unknown"; var buffer: [1024]u8 = undefined; - const json = std.fmt.bufPrintZ(buffer[0..], + const json = compat.bufPrintZ(buffer[0..], \\{{"status":"running","users":{},"messages":{},"files":{},"port":{},"url":"{s}","clientId":{},"timestamp":{}}} , .{ app_state.users_online, app_state.messages_sent, app_state.files_uploaded, port, url, e.client_id, compat.timestamp() }) catch "{\"error\":\"format_error\"}"; @@ -172,7 +172,7 @@ fn userAction(e: *webui.Event, action: [:0]const u8, data: [:0]const u8) void { if (std.mem.eql(u8, action, "login")) { // Check if user is already online if (online_users.contains(data)) { - result = std.fmt.bufPrintZ(response[0..], "User '{s}' is already online. Online users: {}", .{ data, app_state.users_online }) catch "Error"; + result = compat.bufPrintZ(response[0..], "User '{s}' is already online. Online users: {}", .{ data, app_state.users_online }) catch "Error"; } else { // Add new user const username_copy = allocator.dupe(u8, data) catch { @@ -187,24 +187,24 @@ fn userAction(e: *webui.Event, action: [:0]const u8, data: [:0]const u8) void { return; }; app_state.users_online += 1; - result = std.fmt.bufPrintZ(response[0..], "User '{s}' logged in. Online users: {}", .{ data, app_state.users_online }) catch "Error"; + result = compat.bufPrintZ(response[0..], "User '{s}' logged in. Online users: {}", .{ data, app_state.users_online }) catch "Error"; } } else if (std.mem.eql(u8, action, "logout")) { // Check if user is online if (online_users.fetchRemove(data)) |kv| { allocator.free(kv.key); if (app_state.users_online > 0) app_state.users_online -= 1; - result = std.fmt.bufPrintZ(response[0..], "User '{s}' logged out. Online users: {}", .{ data, app_state.users_online }) catch "Error"; + result = compat.bufPrintZ(response[0..], "User '{s}' logged out. Online users: {}", .{ data, app_state.users_online }) catch "Error"; } else { - result = std.fmt.bufPrintZ(response[0..], "User '{s}' was not online. Online users: {}", .{ data, app_state.users_online }) catch "Error"; + result = compat.bufPrintZ(response[0..], "User '{s}' was not online. Online users: {}", .{ data, app_state.users_online }) catch "Error"; } } else if (std.mem.eql(u8, action, "message")) { app_state.messages_sent += 1; - result = std.fmt.bufPrintZ(response[0..], "Message sent. Total messages: {}", .{app_state.messages_sent}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Message sent. Total messages: {}", .{app_state.messages_sent}) catch "Error"; } else if (std.mem.eql(u8, action, "upload")) { // For simulation purposes, just acknowledge the upload request const filename = if (data.len > 0) data else "demo_file.txt"; - result = std.fmt.bufPrintZ(response[0..], "Upload request received for '{s}'. Use the file input for actual upload.", .{filename}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Upload request received for '{s}'. Use the file input for actual upload.", .{filename}) catch "Error"; } else { result = "Unknown action"; } @@ -217,7 +217,7 @@ fn sendNotification(e: *webui.Event, message: [:0]const u8, level: [:0]const u8) // Send notification to all clients var js_code: [512]u8 = undefined; - const script = std.fmt.bufPrintZ(js_code[0..], "showNotification('{s}', '{s}');", .{ message, level }) catch return; + const script = compat.bufPrintZ(js_code[0..], "showNotification('{s}', '{s}');", .{ message, level }) catch return; win.run(script); @@ -238,7 +238,7 @@ fn processData(e: *webui.Event, operation: [:0]const u8, input_data: [:0]const u while (i < input_data.len and i < 512) : (i += 1) { reversed[i] = input_data[input_data.len - 1 - i]; } - output = std.fmt.bufPrintZ(result[0..], "Reversed: {s}", .{reversed[0..i]}) catch "Error"; + output = compat.bufPrintZ(result[0..], "Reversed: {s}", .{reversed[0..i]}) catch "Error"; } else if (std.mem.eql(u8, operation, "uppercase")) { // Convert to uppercase var upper: [512]u8 = undefined; @@ -246,14 +246,14 @@ fn processData(e: *webui.Event, operation: [:0]const u8, input_data: [:0]const u if (i >= 512) break; upper[i] = std.ascii.toUpper(c); } - output = std.fmt.bufPrintZ(result[0..], "Uppercase: {s}", .{upper[0..@min(input_data.len, 512)]}) catch "Error"; + output = compat.bufPrintZ(result[0..], "Uppercase: {s}", .{upper[0..@min(input_data.len, 512)]}) catch "Error"; } else if (std.mem.eql(u8, operation, "hash")) { // Simple hash (sum of bytes) var hash: u32 = 0; for (input_data) |c| { hash = hash *% 31 +% c; } - output = std.fmt.bufPrintZ(result[0..], "Hash: {X}", .{hash}) catch "Error"; + output = compat.bufPrintZ(result[0..], "Hash: {X}", .{hash}) catch "Error"; } else { output = "Unknown operation"; } @@ -268,17 +268,17 @@ fn executeCommand(e: *webui.Event, command: [:0]const u8, args: [:0]const u8) vo var result: [:0]const u8 = ""; if (std.mem.eql(u8, command, "echo")) { - result = std.fmt.bufPrintZ(response[0..], "Echo: {s}", .{args}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Echo: {s}", .{args}) catch "Error"; } else if (std.mem.eql(u8, command, "time")) { const timestamp = compat.timestamp(); - result = std.fmt.bufPrintZ(response[0..], "Current time: {}", .{timestamp}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Current time: {}", .{timestamp}) catch "Error"; } else if (std.mem.eql(u8, command, "random")) { var prng = std.Random.DefaultPrng.init(@intCast(compat.timestamp())); const random_num = prng.random().int(u32); - result = std.fmt.bufPrintZ(response[0..], "Random number: {}", .{random_num}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Random number: {}", .{random_num}) catch "Error"; } else if (std.mem.eql(u8, command, "memory")) { // Simple memory info (simulated) - result = std.fmt.bufPrintZ(response[0..], "Memory usage: {}MB", .{50 + @rem(compat.timestamp(), 100)}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Memory usage: {}MB", .{50 + @rem(compat.timestamp(), 100)}) catch "Error"; } else { result = "Unknown command"; } @@ -290,7 +290,7 @@ fn getSystemInfo(e: *webui.Event) void { const builtin = @import("builtin"); var buffer: [1024]u8 = undefined; - const info = std.fmt.bufPrintZ(buffer[0..], + const info = compat.bufPrintZ(buffer[0..], \\{{"os":"{s}","arch":"{s}","zigVersion":"{s}","webuiVersion":"2.5.0","timestamp":{}}} , .{ @tagName(builtin.os.tag), @tagName(builtin.cpu.arch), @import("builtin").zig_version_string, compat.timestamp() }) catch "{}"; @@ -318,7 +318,7 @@ fn testPerformance(e: *webui.Event, iterations: i64, operation: [:0]const u8) vo const duration_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0; var response: [256]u8 = undefined; - const msg = std.fmt.bufPrintZ(response[0..], "Performance test completed: {} iterations of {s} in {d:.2}ms", .{ iterations, operation, duration_ms }) catch "Error"; + const msg = compat.bufPrintZ(response[0..], "Performance test completed: {} iterations of {s} in {d:.2}ms", .{ iterations, operation, duration_ms }) catch "Error"; e.returnString(msg); std.debug.print("Performance test: {} iterations in {d:.2}ms\n", .{ iterations, duration_ms }); @@ -357,22 +357,22 @@ fn manageSettings(e: *webui.Event, action: [:0]const u8, key: [:0]const u8, valu return; }; - result = std.fmt.bufPrintZ(response[0..], "Setting '{s}' set to '{s}'", .{ key, value }) catch "Error"; + result = compat.bufPrintZ(response[0..], "Setting '{s}' set to '{s}'", .{ key, value }) catch "Error"; } else if (std.mem.eql(u8, action, "get")) { // Get setting from memory if (settings_map.get(key)) |stored_value| { - result = std.fmt.bufPrintZ(response[0..], "Setting '{s}' = '{s}'", .{ key, stored_value }) catch "Error"; + result = compat.bufPrintZ(response[0..], "Setting '{s}' = '{s}'", .{ key, stored_value }) catch "Error"; } else { - result = std.fmt.bufPrintZ(response[0..], "Setting '{s}' not found (no value set)", .{key}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Setting '{s}' not found (no value set)", .{key}) catch "Error"; } } else if (std.mem.eql(u8, action, "delete")) { // Delete setting from memory if (settings_map.fetchRemove(key)) |kv| { allocator.free(kv.key); allocator.free(kv.value); - result = std.fmt.bufPrintZ(response[0..], "Setting '{s}' deleted", .{key}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Setting '{s}' deleted", .{key}) catch "Error"; } else { - result = std.fmt.bufPrintZ(response[0..], "Setting '{s}' not found (nothing to delete)", .{key}) catch "Error"; + result = compat.bufPrintZ(response[0..], "Setting '{s}' not found (nothing to delete)", .{key}) catch "Error"; } } else { result = "Unknown settings action"; @@ -468,7 +468,7 @@ fn uploadFile(e: *webui.Event, filename: [:0]const u8, content: [:0]const u8) vo // Write content to file compat.writeFile(file_path, content) catch |err| { std.debug.print("Failed to write file {s}: {}\n", .{ file_path, err }); - result = std.fmt.bufPrintZ(response[0..], "Error: Failed to write file '{s}' ({s})", .{ filename, @errorName(err) }) catch "Error"; + result = compat.bufPrintZ(response[0..], "Error: Failed to write file '{s}' ({s})", .{ filename, @errorName(err) }) catch "Error"; e.returnString(result); return; }; @@ -477,6 +477,6 @@ fn uploadFile(e: *webui.Event, filename: [:0]const u8, content: [:0]const u8) vo app_state.files_uploaded += 1; // Return success message - result = std.fmt.bufPrintZ(response[0..], "File '{s}' uploaded successfully as '{s}'. Size: {} bytes. Total files: {}", .{ filename, safe_filename[0..safe_len :0], content.len, app_state.files_uploaded }) catch "Error"; + result = compat.bufPrintZ(response[0..], "File '{s}' uploaded successfully as '{s}'. Size: {} bytes. Total files: {}", .{ filename, safe_filename[0..safe_len :0], content.len, app_state.files_uploaded }) catch "Error"; e.returnString(result); } diff --git a/examples/custom_spa_server_on_free_port/main.zig b/examples/custom_spa_server_on_free_port/main.zig index 3f55dad..2026143 100644 --- a/examples/custom_spa_server_on_free_port/main.zig +++ b/examples/custom_spa_server_on_free_port/main.zig @@ -36,8 +36,8 @@ pub fn main() !void { // now use the port: var buf1: [64]u8 = undefined; var buf2: [64]u8 = undefined; - const port_argument1: []u8 = try std.fmt.bufPrintZ(&buf1, "{d}", .{backend_port}); - const port_argument2: []u8 = try std.fmt.bufPrintZ(&buf2, "{d}", .{webui_port}); + const port_argument1: []u8 = try compat.bufPrintZ(&buf1, "{d}", .{backend_port}); + const port_argument2: []u8 = try compat.bufPrintZ(&buf2, "{d}", .{webui_port}); const argv = [_][]const u8{ "python", "./free_port_web_server.py", port_argument1, port_argument2 }; // start the SPA web server: @@ -45,7 +45,7 @@ pub fn main() !void { // Show a new window served by our custom web server (spawned above): var buf: [64]u8 = undefined; - home_url = try std.fmt.bufPrintZ(&buf, "http://localhost:{d}/index.html", .{backend_port}); + home_url = try compat.bufPrintZ(&buf, "http://localhost:{d}/index.html", .{backend_port}); try nwin.show(home_url); // Wait until all windows get closed diff --git a/examples/event_handling/main.zig b/examples/event_handling/main.zig index 5910cf2..c3a96da 100644 --- a/examples/event_handling/main.zig +++ b/examples/event_handling/main.zig @@ -141,7 +141,8 @@ pub fn main() !void { std.debug.print(" Windows: ipconfig | findstr IPv4\n", .{}); std.debug.print(" Mac/Linux: ifconfig | grep inet\n", .{}); std.debug.print(" Or check in network settings\n", .{}); - std.debug.print("\n" ++ "=" ** 60 ++ "\n", .{}); + const separator: [60]u8 = @splat('='); + std.debug.print("\n{s}\n", .{&separator}); std.debug.print("\nšŸ“‹ How to test multi-client functionality:\n", .{}); std.debug.print("\nšŸ”— Multi-Client Connection Methods:\n", .{}); std.debug.print(" 1. SAME COMPUTER - New Tab/Window:\n", .{}); diff --git a/examples/public_network_access/main.zig b/examples/public_network_access/main.zig index f538c3b..3494a1e 100644 --- a/examples/public_network_access/main.zig +++ b/examples/public_network_access/main.zig @@ -1,6 +1,7 @@ //! Public Network Access Example const std = @import("std"); const webui = @import("webui"); +const compat = @import("compat"); // embed the html const private_html = @embedFile("private.html"); @@ -32,9 +33,9 @@ fn private_window_events(e: *webui.Event) void { const public_win_url: [:0]const u8 = public_window.getUrl() catch return; var buf = std.mem.zeroes([1024]u8); - const js_1 = std.fmt.bufPrintZ(&buf, "document.getElementById('urlSpan1').innerHTML = 'http://localhost:{}';", .{public_win_port}) catch unreachable; + const js_1 = compat.bufPrintZ(&buf, "document.getElementById('urlSpan1').innerHTML = 'http://localhost:{}';", .{public_win_port}) catch unreachable; private_window.run(js_1); - const js_2 = std.fmt.bufPrintZ(&buf, "document.getElementById('urlSpan2').innerHTML = '{s}';", .{public_win_url}) catch unreachable; + const js_2 = compat.bufPrintZ(&buf, "document.getElementById('urlSpan2').innerHTML = '{s}';", .{public_win_url}) catch unreachable; private_window.run(js_2); } } diff --git a/examples/web_app_multi_client/main.zig b/examples/web_app_multi_client/main.zig index 5a5c625..76908b4 100644 --- a/examples/web_app_multi_client/main.zig +++ b/examples/web_app_multi_client/main.zig @@ -87,7 +87,7 @@ fn events(e: *webui.Event) void { // userNumber { var buffer = std.mem.zeroes([2048]u8); - const js = std.fmt.bufPrintZ( + const js = compat.bufPrintZ( &buffer, "document.getElementById(\"userNumber\").innerText = \"{}\";", .{client_id}, @@ -98,7 +98,7 @@ fn events(e: *webui.Event) void { // connectionNumber { var buffer = std.mem.zeroes([2048]u8); - const js = std.fmt.bufPrintZ( + const js = compat.bufPrintZ( &buffer, "document.getElementById(\"connectionNumber\").innerText = \"{}\";", .{connection_id}, @@ -111,7 +111,7 @@ fn events(e: *webui.Event) void { const val = if (private_input_arr[client_id]) |val| val else ""; var buffer = std.mem.zeroes([2048]u8); - const js = std.fmt.bufPrintZ( + const js = compat.bufPrintZ( &buffer, "document.getElementById(\"privateInput\").value = \"{s}\";", .{val}, @@ -123,7 +123,7 @@ fn events(e: *webui.Event) void { { const val = if (public_input) |val| val else ""; var buffer = std.mem.zeroes([2048]u8); - const js = std.fmt.bufPrintZ( + const js = compat.bufPrintZ( &buffer, "document.getElementById(\"publicInput\").value = \"{s}\";", .{val}, @@ -137,7 +137,7 @@ fn events(e: *webui.Event) void { // userCount { var buffer = std.mem.zeroes([2048]u8); - const js = std.fmt.bufPrintZ( + const js = compat.bufPrintZ( &buffer, "document.getElementById(\"userCount\").innerText = \"{}\";", .{users_count}, @@ -148,7 +148,7 @@ fn events(e: *webui.Event) void { // tabCount { var buffer = std.mem.zeroes([2048]u8); - const js = std.fmt.bufPrintZ( + const js = compat.bufPrintZ( &buffer, "document.getElementById(\"tabCount\").innerText = \"{}\";", .{tab_count}, diff --git a/src/tests.zig b/src/tests.zig index babb55a..015452d 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -142,20 +142,14 @@ test "WebUIError contains expected variants" { } test "tuple.fnParamsToTuple synthesizes correct tuple" { - const Type = std.builtin.Type; - const params = [_]Type.Fn.Param{ - .{ .is_generic = false, .is_noalias = false, .type = i32 }, - .{ .is_generic = false, .is_noalias = false, .type = bool }, - .{ .is_generic = false, .is_noalias = false, .type = f64 }, - }; - const Tup = tuple.fnParamsToTuple(¶ms); + const Tup = tuple.fnParamsToTuple(&.{ i32, bool, f64 }); const info = @typeInfo(Tup).@"struct"; try std.testing.expect(info.is_tuple); - try std.testing.expectEqual(@as(usize, 3), info.fields.len); - try std.testing.expectEqual(i32, info.fields[0].type); - try std.testing.expectEqual(bool, info.fields[1].type); - try std.testing.expectEqual(f64, info.fields[2].type); + try std.testing.expectEqual(@as(usize, 3), std.meta.fieldNames(Tup).len); + try std.testing.expectEqual(i32, @FieldType(Tup, "0")); + try std.testing.expectEqual(bool, @FieldType(Tup, "1")); + try std.testing.expectEqual(f64, @FieldType(Tup, "2")); // We can build an instance and read it back. var t: Tup = undefined; @@ -188,6 +182,25 @@ test "newWindowWithId rejects 0 and out-of-range ids" { try std.testing.expectError(webui.WebUIError.CreateWindowError, webui.newWindowWithId(webui.WEBUI_MAX_IDS + 100)); } +fn typedCallback( + _: webui.Event, + _: *webui.Event, + _: bool, + _: i32, + _: i64, + _: f32, + _: f64, + _: [:0]const u8, + _: [*]const u8, +) void {} + +test "binding accepts every supported callback parameter type" { + // Instantiates the comptime type-reflection path used to decode arguments. + const win = webui.newWindow(); + defer win.destroy(); + try std.testing.expect(try win.binding("typedCallback", typedCallback) > 0); +} + test "newWindowWithId with explicit id" { const id = webui.getNewWindowId(); try std.testing.expect(id > 0); diff --git a/src/tuple.zig b/src/tuple.zig index fd1b237..ce52b62 100644 --- a/src/tuple.zig +++ b/src/tuple.zig @@ -1,14 +1,16 @@ //! Tuple-synthesis helper. //! //! Uses the `@Tuple` builtin (available on Zig 0.16+) to construct a tuple -//! type from a function's parameter list. Used by `webui.bind` to build the +//! type from a function's parameter types. Used by `webui.bind` to build the //! argument tuple passed to user callbacks. -const std = @import("std"); +//! +//! Takes plain parameter types rather than `std.builtin.Type.Fn.Param`, which +//! Zig 0.17 replaced with the `param_types` slice. -pub fn fnParamsToTuple(comptime params: []const std.builtin.Type.Fn.Param) type { - var types: [params.len]type = undefined; - for (params, 0..) |param, i| { - types[i] = param.type orelse @compileError("param must have type"); +pub fn fnParamsToTuple(comptime param_types: []const ?type) type { + var types: [param_types.len]type = undefined; + for (param_types, &types) |param_type, *field_type| { + field_type.* = param_type orelse @compileError("param must have type"); } return @Tuple(&types); } diff --git a/src/webui.zig b/src/webui.zig index 6b893ad..8cf6465 100644 --- a/src/webui.zig +++ b/src/webui.zig @@ -639,7 +639,7 @@ pub fn run(self: webui, script_content: [:0]const u8) void { /// Format JavaScript into `buffer`, then run it without waiting for a response. pub fn runFmt(self: webui, buffer: []u8, comptime fmt: []const u8, args: anytype) !void { - const script_content = try std.fmt.bufPrintZ(buffer, fmt, args); + const script_content = try bufPrintZ(buffer, fmt, args); self.run(script_content); } @@ -920,7 +920,7 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! } // Verify function does not use varargs - if (fnInfo.is_var_args) { + if (fnIsVarArgs(fnInfo)) { const err_msg = std.fmt.comptimePrint( "callback's type ({}), it can not have variable args!", .{T}, @@ -928,8 +928,10 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! @compileError(err_msg); } + const param_types = comptime fnParamTypes(fnInfo); + const tmp_struct = struct { - const tup_t = fnParamsToTuple(fnInfo.params); + const tup_t = fnParamsToTuple(param_types); // Event handler that will convert parameters and call the user's callback fn handle(e: *Event) void { @@ -937,8 +939,8 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! var index: usize = 0; // Process each parameter of the callback function - inline for (fnInfo.params, 0..fnInfo.params.len) |param, i| { - if (param.type) |tt| { + inline for (param_types, 0..) |param_type, i| { + if (param_type) |tt| { const paramTInfo = @typeInfo(tt); switch (paramTInfo) { // Handle struct type parameters (only Event is allowed) @@ -972,7 +974,7 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! // Handle pointer types with special cases .pointer => |pointer| { // Handle null-terminated string slices - if (pointer.size == .slice and pointer.child == u8 and pointer.is_const == true) { + if (pointer.size == .slice and pointer.child == u8 and ptrIsConst(pointer)) { if (pointer.sentinel()) |sentinel| { if (sentinel == 0) { const str_ptr = e.getStringAt(i - index); @@ -984,7 +986,7 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! param_tup[i] = e; index += 1; // Handle raw byte pointers - } else if (pointer.size == .many and pointer.child == u8 and pointer.is_const == true and pointer.sentinel() == null) { + } else if (pointer.size == .many and pointer.child == u8 and ptrIsConst(pointer) and pointer.sentinel() == null) { const raw_ptr = e.getRawAt(i - index); param_tup[i] = raw_ptr; } else { @@ -1021,6 +1023,33 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! /// this function will return a fn's params tuple const fnParamsToTuple = tuple.fnParamsToTuple; +// Zig 0.17 rewrote type reflection: `Fn.params` became the `param_types` slice, +// and `Fn.is_var_args` / `Pointer.is_const` moved into `attrs`. These helpers +// detect the layout so the bindings build on both 0.16 and 0.17. + +fn fnParamTypes(comptime info: std.builtin.Type.Fn) []const ?type { + if (@hasField(std.builtin.Type.Fn, "param_types")) return info.param_types; + var types: [info.params.len]?type = undefined; + for (info.params, &types) |param, *param_type| param_type.* = param.type; + const result = types; + return &result; +} + +inline fn fnIsVarArgs(comptime info: std.builtin.Type.Fn) bool { + return if (@hasField(std.builtin.Type.Fn, "attrs")) info.attrs.varargs else info.is_var_args; +} + +inline fn ptrIsConst(comptime info: std.builtin.Type.Pointer) bool { + return if (@hasField(std.builtin.Type.Pointer, "attrs")) info.attrs.@"const" else info.is_const; +} + +/// `std.fmt.bufPrintZ` was removed in Zig 0.17, and its replacement +/// `std.mem.printSentinel` does not exist on 0.16. +fn bufPrintZ(buffer: []u8, comptime fmt: []const u8, args: anytype) ![:0]u8 { + if (@hasDecl(std.mem, "printSentinel")) return std.mem.printSentinel(buffer, fmt, args, 0); + return std.fmt.bufPrintSentinel(buffer, fmt, args, 0); +} + pub const WEBUI_VERSION: std.SemanticVersion = .{ .major = 2, .minor = 5, @@ -1190,7 +1219,7 @@ pub const Event = extern struct { /// Format JavaScript into `buffer`, then run it on the event client. pub fn runClientFmt(self: *Event, buffer: []u8, comptime fmt: []const u8, args: anytype) !void { - const script_content = try std.fmt.bufPrintZ(buffer, fmt, args); + const script_content = try bufPrintZ(buffer, fmt, args); self.runClient(script_content); } @@ -1240,7 +1269,7 @@ pub const Event = extern struct { /// Format a string response into `buffer`, then return it to JavaScript. pub fn returnFmt(e: *Event, buffer: []u8, comptime fmt: []const u8, args: anytype) !void { - const response = try std.fmt.bufPrintZ(buffer, fmt, args); + const response = try bufPrintZ(buffer, fmt, args); e.returnString(response); }