From be26c057d7e768af4fdbb05d251da6345c97baef Mon Sep 17 00:00:00 2001 From: Arran Ireland Date: Tue, 15 Jul 2025 11:28:06 +0100 Subject: [PATCH] core: decouple bindings generation from symbol exports --- build.zig | 8 +- core/bindings.zig | 320 ++++++++++++++++++++++++++++++++++++++++++++++ core/exports.zig | 114 ++--------------- 3 files changed, 340 insertions(+), 102 deletions(-) create mode 100644 core/bindings.zig diff --git a/build.zig b/build.zig index 637f43d..f7271e8 100644 --- a/build.zig +++ b/build.zig @@ -57,9 +57,13 @@ const Builder = struct { .linkage = .static, }); - const exports = @import("core/exports.zig"); const write_files = self.b.addWriteFiles(); - const header_path = write_files.add("rt_core.h", exports.generateHeader()); + const bindings = @import("core/bindings.zig"); + const data = bindings.data(.c, self.b.allocator) catch |err| { + std.debug.print("Failed to generate header: {any}", .{err}); + std.process.exit(1); + }; + const header_path = write_files.add("rt_core.h", data.items); const install_header_file = self.b.addInstallHeaderFile(header_path, "rt_core.h"); const install_static_lib = self.b.addInstallArtifact(static_lib, .{}); diff --git a/core/bindings.zig b/core/bindings.zig new file mode 100644 index 0000000..6faa5cd --- /dev/null +++ b/core/bindings.zig @@ -0,0 +1,320 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const Bytes = std.ArrayList(u8); + +const eql = std.mem.eql; + +const Language = enum { + c, + js, + rust, +}; + +pub fn data(language: Language, ally: Allocator) !Bytes { + var exports = parseExports(@embedFile("exports.zig"), ally); + + return switch (language) { + .c => try exports.c(), + .js => try exports.js(), + .rust => try exports.rust(), + }; +} + +fn parseExports(export_data: [:0]const u8, ally: Allocator) Exports { + const Ast = std.zig.Ast; + + const DocComment = struct { + fn search(ast: Ast, main_token: u32) []const u8 { + var t = main_token - 1; + + while (t > 0) : (t -= 1) { + switch (ast.tokens.items(.tag)[t]) { + .doc_comment => return ast.tokenSlice(t)["/// ".len..], + .keyword_pub => continue, + else => break, + } + } + + return ""; + } + }; + + var ast = Ast.parse(ally, export_data, .zig) catch return Exports.init(ally); + defer ast.deinit(ally); + + const root_declarations = ast.rootDecls(); + const nodes = ast.nodes; + const tokens = ast.tokens; + + var exports = Exports.init(ally); + + for (root_declarations) |declaration_index| { + const declaration_tag = nodes.items(.tag)[declaration_index]; + const main_token = nodes.items(.main_token)[declaration_index]; + + if (main_token <= 0 or tokens.items(.tag)[main_token - 1] != .keyword_pub) { + continue; + } + + if (declaration_tag == .fn_decl) { + var buffer: [1]std.zig.Ast.Node.Index = undefined; + + const function_prototype = ast.fullFnProto( + &buffer, + nodes.items(.data)[declaration_index].lhs, + ) orelse continue; + + const function = exports.functions.addOne() catch continue; + + function.* = .{ + .doc_comment = DocComment.search(ast, main_token), + .name = ast.tokenSlice(function_prototype.name_token orelse continue), + .parameters = std.ArrayList(Function.Parameter).init(ally), + .return_value = ast.getNodeSource(function_prototype.ast.return_type), + }; + + var parameters = function_prototype.iterate(&ast); + + while (parameters.next()) |parameter| { + const name = ast.tokenSlice(parameter.name_token orelse continue); + const @"type" = if (parameter.type_expr != 0) ast.getNodeSource(parameter.type_expr) else "unknown"; + + function.parameters.append(.{ .name = name, .type = @"type" }) catch continue; + } + } else if (declaration_tag == .simple_var_decl) { + const variable_declaration = ast.simpleVarDecl(declaration_index); + + if (variable_declaration.ast.init_node == 0) continue; + + switch (nodes.items(.tag)[variable_declaration.ast.init_node]) { + .container_decl_arg, .container_decl, .container_decl_arg_trailing, .container_decl_trailing => {}, + else => continue, + } + + var buffer: [2]std.zig.Ast.Node.Index = undefined; + const container_declaration = ast.fullContainerDecl(&buffer, variable_declaration.ast.init_node) orelse continue; + + if (tokens.items(.tag)[container_declaration.ast.main_token] != .keyword_enum) continue; + + const @"enum" = exports.enums.addOne() catch continue; + + @"enum".* = .{ + .doc_comment = DocComment.search(ast, main_token), + .name = ast.tokenSlice(main_token + 1), + .values = std.ArrayList(Enum.Value).init(ally), + }; + + for (container_declaration.ast.members) |member_index| { + if (nodes.items(.tag)[member_index] != .container_field_init) { + continue; + } + + const name = ast.tokenSlice(nodes.items(.main_token)[member_index]); + const member_data = nodes.items(.data)[member_index]; + const number = ast.getNodeSource(member_data.rhs); + + @"enum".values.append(.{ .name = name, .number = number }) catch continue; + } + } + } + + return exports; +} + +const Exports = struct { + const Self = @This(); + + ally: Allocator, + enums: std.ArrayList(Enum), + functions: std.ArrayList(Function), + + fn init(ally: Allocator) Self { + return .{ + .ally = ally, + .enums = std.ArrayList(Enum).init(ally), + .functions = std.ArrayList(Function).init(ally), + }; + } + + fn c(self: *Self) !Bytes { + var bytes = Bytes.init(self.ally); + + const b = &bytes; + try b.appendSlice( + \\#ifndef RT_CORE_H + \\#define RT_CORE_H + \\ + \\#include + \\#include + \\ + \\ + ); + + for (self.enums.items) |e| { + try b.appendSlice("// "); + try b.appendSlice(e.doc_comment); + try b.append('\n'); + + try b.appendSlice("typedef enum {\n"); + + for (e.values.items) |v| { + try b.appendSlice(" "); + try b.appendSlice(v.name); + try b.appendSlice(" = "); + try b.appendSlice(v.number); + try b.appendSlice(",\n"); + } + + try b.appendSlice("} "); + try deriveType(b, e.name); + try b.appendSlice(";\n\n"); + } + + for (self.functions.items) |f| { + try b.appendSlice("// "); + try b.appendSlice(f.doc_comment); + try b.append('\n'); + + try deriveType(b, f.return_value); + try b.append(' '); + try deriveName(b, f.name); + try b.append('('); + + for (f.parameters.items, 0..) |p, i| { + try parameter(b, p.name, p.type); + + if (i != f.parameters.items.len - 1) { + try b.appendSlice(", "); + } + } + + try b.appendSlice(");\n\n"); + } + + try b.appendSlice( + \\#endif + \\ + ); + + return bytes; + } + + fn rust(self: *Self) !Bytes { + return Bytes.init(self.ally); + } + + fn js(self: *Self) !Bytes { + return Bytes.init(self.ally); + } + + fn parameter(b: *Bytes, name: []const u8, @"type": []const u8) !void { + if (eql(u8, @"type", "lib.System.SimpleClock.Callback")) { + try b.appendSlice("int64_t (*"); + try b.appendSlice(name); + try b.appendSlice(")(void)"); + } else if (eql(u8, @"type", "lib.System.SimpleRng.Callback")) { + try b.appendSlice("void (*"); + try b.appendSlice(name); + try b.appendSlice(")(uint8_t* buf, "); + try deriveType(b, "usize"); + try b.appendSlice(" length)"); + } else if (eql(u8, @"type", "Error")) { + try reticulumType(b, @"type"); + } else { + try deriveType(b, @"type"); + } + } + + fn deriveType(b: *Bytes, string: []const u8) !void { + const translations = std.StaticStringMap([]const u8).initComptime(.{ + .{ "c_int", "int" }, + .{ "anyopaque", "void" }, + .{ "*anyopaque", "void*" }, + .{ "**anyopaque", "void**" }, + .{ "u16", "uint16_t" }, + .{ "u32", "uint32_t" }, + .{ "u64", "uint64_t" }, + .{ + "usize", switch (@sizeOf(usize)) { + 8 => "uint8_t", + 16 => "uint16_t", + 32 => "uint32_t", + 64 => "uint64_t", + 128 => "uint128_t", + else => "", + }, + }, + }); + + if (translations.get(string)) |translation| { + try b.appendSlice(translation); + } else { + try reticulumType(b, string); + } + } + + fn reticulumType(b: *Bytes, string: []const u8) !void { + try deriveName(b, string); + try b.appendSlice("_t"); + } + + fn deriveName(b: *Bytes, string: []const u8) !void { + try b.appendSlice("rt_core"); + + if (string.len > 0 and !std.ascii.isUpper(string[0])) { + try b.append('_'); + } + + for (string) |char| { + if (std.ascii.isUpper(char)) { + try b.append('_'); + try b.append(std.ascii.toLower(char)); + } else { + try b.append(char); + } + } + } +}; + +const Enum = struct { + const Value = struct { + name: []const u8, + number: []const u8, + }; + + doc_comment: []const u8, + name: []const u8, + values: std.ArrayList(Value), +}; + +const Function = struct { + const Parameter = struct { + name: []const u8, + type: []const u8, + }; + + doc_comment: []const u8, + name: []const u8, + parameters: std.ArrayList(Parameter), + return_value: []const u8, +}; + +pub fn derive(comptime name: []const u8) []const u8 { + if (!builtin.target.cpu.arch.isWasm()) { + var snake_case: []const u8 = "rt_core_"; + + inline for (name) |char| { + if (std.ascii.isUpper(char)) { + snake_case = snake_case ++ .{ '_', std.ascii.toLower(char) }; + } else { + snake_case = snake_case ++ .{char}; + } + } + + return snake_case; + } else { + return name; + } +} diff --git a/core/exports.zig b/core/exports.zig index 713fa28..105bb0e 100644 --- a/core/exports.zig +++ b/core/exports.zig @@ -1,12 +1,11 @@ -//! This file is not part of the core module and therefore can contain non-freestanding code. -//! Be aware that this file will be imported into the build executable in order to generate the header file. +//! These exports are currently unstable as the codebase develops. +const bindings = @import("bindings.zig"); const builtin = @import("builtin"); const std = @import("std"); const lib = @import("lib.zig"); -const is_wasm = (builtin.cpu.arch == .wasm32 or builtin.cpu.arch == .wasm64); - +const is_wasm = builtin.target.cpu.arch.isWasm(); const Gpa = if (is_wasm) void else std.heap.GeneralPurposeAllocator(.{}); const Allocator = std.mem.Allocator; @@ -18,8 +17,7 @@ var clock: lib.System.SimpleClock = undefined; var rng: lib.System.SimpleRng = undefined; var system: lib.System = undefined; -// Exported structs and functions. - +/// Error type. pub const Error = enum(c_int) { none = 0, already_initialized = 1, @@ -29,7 +27,8 @@ pub const Error = enum(c_int) { unknown = 255, }; -pub fn libInit( +/// Sets up the library. +pub fn init( monotonicMicros: lib.System.SimpleClock.Callback, rngFill: lib.System.SimpleRng.Callback, ) callconv(.c) Error { @@ -52,12 +51,11 @@ pub fn libInit( .rng = rng.rng(), }; - // std.debug.print("Hello from a wasm module!\n", .{}); - return .none; } -pub fn libDeinit() callconv(.c) Error { +/// Tears down the library. +pub fn deinit() callconv(.c) Error { allocator = null; if (is_wasm) { @@ -71,10 +69,11 @@ pub fn libDeinit() callconv(.c) Error { return .none; } +/// Makes a node. pub fn makeNode(node_ptr: **anyopaque) callconv(.c) Error { const ally = allocator orelse return .missing_allocator; - const node = ally.create(lib.Node) catch |e| return convertError(Allocator.Error, e); + const node = ally.create(lib.Node) catch |err| return convertError(Allocator.Error, err); errdefer ally.destroy(node); node_ptr.* = node; @@ -83,14 +82,14 @@ pub fn makeNode(node_ptr: **anyopaque) callconv(.c) Error { &system, null, .{}, - ) catch |e| return convertError(lib.Node.Error, e); + ) catch |err| return convertError(lib.Node.Error, err); return .none; } -fn convertError(comptime E: type, e: E) Error { +fn convertError(comptime E: type, err: E) Error { if (E == lib.Node.Error) { - return switch (e) { + return switch (err) { error.OutOfMemory => .out_of_memory, else => .unknown, }; @@ -99,104 +98,19 @@ fn convertError(comptime E: type, e: E) Error { return .unknown; } -// Perform exports. -// This is currently also adding these symbols to the build executable. -// As far as I can tell it won't cause any issues and can probably be changed later. comptime { for (@typeInfo(@This()).@"struct".decls) |declaration| { const field = @field(@This(), declaration.name); const info = @typeInfo(@TypeOf(field)); if (info != .@"fn") continue; - if (std.mem.eql(u8, declaration.name, "generateHeader")) continue; const function: *const anyopaque = @ptrCast(&field); const export_options = std.builtin.ExportOptions{ - .name = deriveName(declaration.name), + .name = bindings.derive(declaration.name), .linkage = .strong, }; @export(function, export_options); } } - -/// This needs to be public for use in the build c step. -pub fn generateHeader() []const u8 { - comptime var header: []const u8 = - \\#ifndef RT_CORE_H - \\#define RT_CORE_H - \\ - \\#include - \\#include - \\ - \\ - ; - - inline for (@typeInfo(Self).@"struct".decls) |declaration| { - const field = @field(Self, declaration.name); - const info = @typeInfo(@TypeOf(field)); - - if (info != .@"fn") continue; - if (comptime std.mem.eql(u8, declaration.name, "generateHeader")) continue; - - const function = info.@"fn"; - const name = comptime deriveName(declaration.name); - const return_type = switch (function.return_type.?) { - Error => "int", - *anyopaque => "void*", - c_int => "int", - else => @typeName(function.return_type.?), - }; - - comptime var forward_declaration: []const u8 = return_type ++ " " ++ name ++ "("; - - inline for (function.params, 0..) |param, i| { - const param_type = switch (param.type.?) { - lib.System.SimpleClock.Callback => "int64_t (*monotonic_micros)(void)", - lib.System.SimpleRng.Callback => "void (*rng_fill)(uint8_t* buf, size_t length)", - *anyopaque => "void*", - **anyopaque => "void**", - c_int => "int", - else => @typeName(param.type.?), - }; - - forward_declaration = forward_declaration ++ param_type; - - if (i != function.params.len - 1) { - forward_declaration = forward_declaration ++ ", "; - } - } - - forward_declaration = forward_declaration ++ ");\n"; - - header = header ++ forward_declaration; - } - - header = header ++ - \\ - \\#endif - \\ - ; - - return header; -} - -fn deriveName(comptime name: []const u8) []const u8 { - var c_name: []const u8 = "rt_"; - - inline for (name) |char| { - if (std.ascii.isUpper(char)) { - c_name = c_name ++ .{ '_', std.ascii.toLower(char) }; - } else { - c_name = c_name ++ .{char}; - } - } - - const arch = builtin.target.cpu.arch; - - if (arch == .wasm32 or arch == .wasm64) { - return name; - } else { - return c_name; - } -}