diff --git a/README.md b/README.md index 7c7622b..12d2df8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ ## API Documentation +If you want a clearer architecture, you can check it out [here](https://deepwiki.com/webui-dev/zig-webui) + * [https://webui-dev.github.io/zig-webui/](https://webui-dev.github.io/zig-webui/) * [https://webui.me/docs/2.5/#/](https://webui.me/docs/2.5/#/) diff --git a/build.zig b/build.zig index af6bdfc..2f208e0 100644 --- a/build.zig +++ b/build.zig @@ -3,11 +3,13 @@ const builtin = @import("builtin"); const Build = std.Build; +// Minimum required Zig version for this project const min_zig_string = "0.12.0"; const current_zig = builtin.zig_version; // NOTE: we should note that when enable tls support we cannot compile with musl +// Compile-time check to ensure the Zig version meets the minimum requirement comptime { const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable; if (current_zig.order(min_zig) == .lt) { @@ -15,22 +17,27 @@ comptime { } } +// Define logger and useful type aliases const log = std.log.scoped(.WebUI); const OptimizeMode = std.builtin.OptimizeMode; const CrossTarget = std.Target.Query; const Compile = Build.Step.Compile; const Module = Build.Module; +// Default build configuration options const default_isStatic = true; const default_enableTLS = false; pub fn build(b: *Build) !void { + // Parse command-line options or use defaults const isStatic = b.option(bool, "is_static", "whether lib is static") orelse default_isStatic; const enableTLS = b.option(bool, "enable_tls", "whether lib enable tls") orelse default_enableTLS; + // Standard build options for target and optimization const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + // TLS support has some limitations if (enableTLS) { log.info("enable TLS support", .{}); if (!target.query.isNative()) { @@ -39,15 +46,16 @@ pub fn build(b: *Build) !void { } } - // create a options for command parameter + // Create build options that will be used as a module const flags_options = b.addOptions(); - // add option + // Configure compile-time options flags_options.addOption(bool, "enableTLS", enableTLS); - // create a new module for flags options + // Create a module that exposes the options const flags_module = flags_options.createModule(); + // Get the webui dependency with appropriate options const webui = b.dependency("webui", .{ .target = target, .optimize = optimize, @@ -56,6 +64,7 @@ pub fn build(b: *Build) !void { .verbose = .err, }); + // Create the webui module that applications can import const webui_module = b.addModule("webui", .{ .root_source_file = b.path(b.pathJoin(&.{ "src", "webui.zig" })), .imports = &.{ @@ -65,22 +74,26 @@ pub fn build(b: *Build) !void { }, }, }); + // Link against the webui library webui_module.linkLibrary(webui.artifact("webui")); if (!isStatic) { + // For dynamic libraries, install the shared library b.installArtifact(webui.artifact("webui")); } - // generate docs + // Setup documentation generation generate_docs(b, optimize, target, flags_module); - // build examples + // Build example applications build_examples(b, optimize, target, webui_module, webui.artifact("webui")) catch |err| { log.err("failed to build examples: {}", .{err}); std.process.exit(1); }; } +// Function to generate API documentation fn generate_docs(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarget, flags_module: *Module) void { + // Create a temporary object for documentation generation const webui_lib = b.addObject(.{ .name = "webui_lib", .root_source_file = b.path(b.pathJoin(&.{ "src", "webui.zig" })), @@ -90,8 +103,10 @@ fn generate_docs(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarget webui_lib.root_module.addImport("flags", flags_module); + // Create a build step for documentation const docs_step = b.step("docs", "Generate docs"); + // Setup documentation installation const docs_install = b.addInstallDirectory(.{ .source_dir = webui_lib.getEmittedDocs(), .install_dir = .prefix, @@ -101,15 +116,18 @@ fn generate_docs(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarget docs_step.dependOn(&docs_install.step); } +// Function to build all example applications fn build_examples(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarget, webui_module: *Module, webui_lib: *Compile) !void { - // we use lazyPath to get absolute path of package + // Get the absolute path to the examples directory var lazy_path = b.path("examples"); + // Create a step to build all examples const build_all_step = b.step("examples", "build all examples"); const examples_path = lazy_path.getPath(b); + // Open the examples directory for iteration var iter_dir = std.fs.openDirAbsolute( examples_path, .{ .iterate = true }, @@ -123,6 +141,7 @@ fn build_examples(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarge var itera = iter_dir.iterate(); + // Iterate through all subdirectories in the examples directory while (try itera.next()) |val| { if (val.kind != .directory) { continue; @@ -131,6 +150,7 @@ fn build_examples(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarge const example_name = val.name; const path = b.pathJoin(&.{ "examples", example_name, "main.zig" }); + // Create an executable for each example const exe = b.addExecutable(.{ .name = example_name, .root_source_file = b.path(path), @@ -138,25 +158,29 @@ fn build_examples(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarge .optimize = optimize, }); + // Add the webui module and link against the library exe.root_module.addImport("webui", webui_module); exe.linkLibrary(webui_lib); + // Setup installation const exe_install = b.addInstallArtifact(exe, .{}); build_all_step.dependOn(&exe_install.step); + // Create a run step for the example const exe_run = b.addRunArtifact(exe); exe_run.step.dependOn(&exe_install.step); + // Set the working directory for the run const cwd = b.path(b.pathJoin(&.{ "examples", example_name })); - exe_run.setCwd(cwd); + // Create a named step to run this specific example const step_name = try std.fmt.allocPrint(b.allocator, "run_{s}", .{example_name}); - const step_desc = try std.fmt.allocPrint(b.allocator, "run_{s}", .{example_name}); const exe_run_step = b.step(step_name, step_desc); exe_run_step.dependOn(&exe_run.step); } } + diff --git a/examples/call_zig_from_js/main.zig b/examples/call_zig_from_js/main.zig index a5b67e6..428ed23 100644 --- a/examples/call_zig_from_js/main.zig +++ b/examples/call_zig_from_js/main.zig @@ -6,20 +6,40 @@ const webui = @import("webui"); const html = @embedFile("index.html"); pub fn main() !void { + // Create a new WebUI window object var nwin = webui.newWindow(); + // Use binding function instead of standard bind function + // binding is an advanced API that automatically handles parameter type conversion and function signature adaptation + // It allows using native Zig function signatures without needing to handle Event pointers directly + // Here we bind the HTML/JS "my_function_string" to Zig function getString _ = try nwin.binding("my_function_string", getString); + // Equivalent using traditional bind function which requires manual Event handling // _ = try nwin.bind("my_function_string", my_function_string); + + // Bind integer handler function, binding automatically converts JS parameters to corresponding Zig types _ = try nwin.binding("my_function_integer", getInteger); // _ = try nwin.bind("my_function_integer", my_function_integer); - _ = try nwin.bind("my_function_boolean", my_function_boolean); - _ = try nwin.bind("my_function_with_response", my_function_with_response); - _ = try nwin.bind("my_function_raw_binary", my_function_raw_binary); - + + // Bind boolean handler function, also with automatic type conversion + _ = try nwin.binding("my_function_boolean", getBool); + // _ = try nwin.bind("my_function_boolean", my_function_boolean); + + // Bind function with response, binding supports using event object directly for responses + _ = try nwin.binding("my_function_with_response", getResponse); + // _ = try nwin.bind("my_function_with_response", my_function_with_response); + + // Bind function for handling binary data, binding supports raw binary data processing + _ = try nwin.binding("my_function_raw_binary", raw_binary); + // _ = try nwin.bind("my_function_raw_binary", my_function_raw_binary); + + // Show the window with embedded HTML content try nwin.show(html); + // Wait for all windows to close, this will block the current thread webui.wait(); + // Clean up all resources webui.clean(); } @@ -75,6 +95,12 @@ fn my_function_integer(e: *webui.Event) void { std.debug.print("my_function_integer 4: {}\n", .{float_1}); } +fn getBool(b1: bool, b2: bool) void { + std.debug.print("boolean is {},{}", .{ + b1, b2, + }); +} + fn my_function_boolean(e: *webui.Event) void { // JavaScript: // my_function_boolean(true, false); @@ -89,6 +115,13 @@ fn my_function_boolean(e: *webui.Event) void { std.debug.print("my_function_bool 2: {}\n", .{status_2}); } +fn getResponse(e: *webui.Event,n1: i64, n2: i64) void { + const res = n1 * n2; + std.debug.print("my_function_with_response: {} * {} = {}\n", .{ n1, n2, res }); + // Send back the response to JavaScript + e.returnValue(res); +} + fn my_function_with_response(e: *webui.Event) void { // JavaScript: // my_function_with_response(number, 2).then(...) @@ -104,6 +137,33 @@ fn my_function_with_response(e: *webui.Event) void { e.returnValue(res); } +fn raw_binary(e: *webui.Event, raw_1: [:0]const u8, raw_2: [*]const u8) void { + // Or e.getSizeAt(0); + const len_1 = e.getSize() catch return; + const len_2 = e.getSizeAt(1) catch return; + + // Print raw_1 + std.debug.print("my_function_raw_binary 1 ({} bytes): ", .{len_1}); + for (0..len_1) |i| { + std.debug.print("0x{x} ", .{raw_1[i]}); + } + std.debug.print("\n", .{}); + + // Check raw_2 (Big) + // [0xA1, 0x00..., 0xA2] + var vaild = false; + + if (raw_2[0] == 0xA1 and raw_2[len_2 - 1] == 0xA2) { + vaild = true; + } + + // Print raw_2 + std.debug.print("my_function_raw_binary 2 big ({} bytes): valid data? {s}\n", .{ + len_2, + if (vaild) "Yes" else "No", + }); +} + fn my_function_raw_binary(e: *webui.Event) void { // JavaScript: // my_function_raw_binary(new Uint8Array([0x41]), new Uint8Array([0x42, 0x43])); diff --git a/src/webui.zig b/src/webui.zig index 80c5891..c61541e 100644 --- a/src/webui.zig +++ b/src/webui.zig @@ -732,13 +732,37 @@ pub fn interfaceScriptClient(self: webui, event_number: usize, script_content: [ if (!success) return WebUIError.ScriptError; } -/// a very convenient function for binding callback. -/// you just need to pase a function to get param. -/// no need to care webui param api. +/// binding function: Creates a binding between an HTML element and a callback function +/// binding function: Creates a binding between an HTML element and a callback function +/// - element: A null-terminated string identifying the HTML element(s) to bind to +/// - callback: A function to be called when the bound event triggers +/// +/// This function performs compile-time validation on the callback function to ensure it: +/// 1. Is a proper function (not another type) +/// 2. Returns void +/// 3. Is not generic +/// 4. Does not use variable arguments +/// +/// The callback function can accept various parameter types: +/// - Event: Gets the full event object +/// - *Event: Gets a pointer to the event object +/// - Other parameters will be automatically converted from event arguments in order: +/// * bool: Converted from event argument bool value +/// * int types: Converted from event argument integer value +/// * float types: Converted from event argument float value +/// * [:0]const u8: For null-terminated string data from event +/// * [*]const u8: For raw pointer data from event +/// +/// Note: Event and *Event parameters do not consume argument indices from the event, +/// but all other parameter types will consume arguments in the order they appear. +/// +/// Returns: +/// - The binding ID that can be used to unbind later pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) !usize { const T = @TypeOf(callback); const TInfo = @typeInfo(T); + // Verify the callback is a function if (TInfo != .@"fn") { const err_msg = std.fmt.comptimePrint( "callback's type ({}), it must be a function!", @@ -748,6 +772,7 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! } const fnInfo = TInfo.@"fn"; + // Verify return type is void if (fnInfo.return_type != void) { const err_msg = std.fmt.comptimePrint( "callback's return type ({}), it must be void!", @@ -756,6 +781,7 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! @compileError(err_msg); } + // Verify function is not generic if (fnInfo.is_generic) { const err_msg = std.fmt.comptimePrint( "callback's type ({}), it can not be a generic function!", @@ -764,6 +790,7 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! @compileError(err_msg); } + // Verify function does not use varargs if (fnInfo.is_var_args) { const err_msg = std.fmt.comptimePrint( "callback's type ({}), it can not have variable args!", @@ -775,57 +802,71 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! const tmp_struct = struct { const tup_t = fnParamsToTuple(fnInfo.params); + // Event handler that will convert parameters and call the user's callback fn handle(e: *Event) void { var param_tup: tup_t = undefined; + 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| { const paramTInfo = @typeInfo(tt); switch (paramTInfo) { + // Handle struct type parameters (only Event is allowed) .@"struct" => { - if (tt != Event) { + if (tt == Event) { + param_tup[i] = e.*; + index += 1; + } else { const err_msg = std.fmt.comptimePrint( "the struct type is ({}), the struct type you can use only is Event in params!", .{tt}, ); @compileError(err_msg); } - param_tup[i] = e; }, + // Convert boolean values .bool => { - const res = e.getBoolAt(i); + const res = e.getBoolAt(i - index); param_tup[i] = res; }, + // Convert integer values with appropriate casting .int => { - const res = e.getIntAt(i); + const res = e.getIntAt(i - index); param_tup[i] = @intCast(res); }, + // Convert floating point values .float => { - const res = e.getFloatAt(i); - param_tup[i] = res; + const res = e.getFloatAt(i - index); + param_tup[i] = @floatCast(res); }, + // Handle pointer types with special cases .pointer => |pointer| { - if (pointer.size != .slice or pointer.child != u8 or pointer.is_const == false) { + // Handle null-terminated string slices + if (pointer.size == .slice and pointer.child == u8 and pointer.is_const == true) { + if (pointer.sentinel()) |sentinel| { + if (sentinel == 0) { + const str_ptr = e.getStringAt(i - index); + param_tup[i] = str_ptr; + } + } + // Handle Event pointers + } else if (pointer.size == .one and pointer.child == Event) { + 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) { + const raw_ptr = e.getRawAt(i - index); + param_tup[i] = raw_ptr; + } else { const err_msg = std.fmt.comptimePrint( - "the pointer type is ({}), not support other type for pointer param!", + "the pointer type is ({}), now we only support [:0]const u8 or [*]const u8 or *webui.Event !", .{tt}, ); @compileError(err_msg); } - if (pointer.sentinel()) |sentinel| { - if (sentinel != 0) { - const err_msg = std.fmt.comptimePrint( - "type is ({}), only support these types: Event, Bool, Int, Float, [:0]const u8, []u8!", - .{tt}, - ); - @compileError(err_msg); - } - const str_ptr = e.getStringAt(i); - param_tup[i] = str_ptr; - } else { - @compileError("not support []u8"); - } }, + // Reject unsupported types else => { const err_msg = std.fmt.comptimePrint( "type is ({}), only support these types: Event, Bool, Int, Float, []u8!", @@ -839,10 +880,12 @@ pub fn binding(self: webui, element: [:0]const u8, comptime callback: anytype) ! } } + // Call the user's callback with the properly converted parameters @call(.auto, callback, param_tup); } }; + // Create the actual binding with the webui backend return self.bind(element, tmp_struct.handle); }