diff --git a/build.zig b/build.zig index da0fce5..6109544 100644 --- a/build.zig +++ b/build.zig @@ -155,6 +155,41 @@ const MakeDirStep = struct { } }; +const WriteStaticFileStep = struct { + step: std.Build.Step, + path: []const u8, + contents: []const u8, + + pub fn create(b: *std.Build, path: []const u8, contents: []const u8) *std.Build.Step { + const self = b.allocator.create(WriteStaticFileStep) catch @panic("OOM"); + self.* = .{ + .step = std.Build.Step.init(.{ + .id = .custom, + .name = b.fmt("write {s}", .{path}), + .owner = b, + .makeFn = make, + }), + .path = b.dupe(path), + .contents = b.dupe(contents), + }; + return &self.step; + } + + fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void { + _ = options; + const self: *WriteStaticFileStep = @fieldParentPtr("step", step); + + if (std.fs.path.dirname(self.path)) |dir| { + try std.fs.cwd().makePath(dir); + } + + try std.fs.cwd().writeFile(.{ + .sub_path = self.path, + .data = self.contents, + }); + } +}; + pub fn generateApi(b: *std.Build, parser_exe: *std.Build.Step.Compile, fetch_sdl_step: *std.Build.Step) void { const source_dir = b.option([]const u8, "sourceDir", "Parse SDL headers from an existing local directory instead of fetching git"); const output_dir = b.option([]const u8, "outputDir", "Directory where generated api/ and json/ folders should be written"); @@ -190,8 +225,30 @@ pub fn generateApi(b: *std.Build, parser_exe: *std.Build.Step.Compile, fetch_sdl path_prep_step = basedir_step; } - // All public SDL3 API headers (53 total) - // Skipped: assert, thread, hidapi, mutex, tray (not core APIs or problematic) + const output_root = if (effective_output_dir) |dir| b.fmt("{s}/api", .{dir}) else "api"; + const json_root = if (effective_output_dir) |dir| b.fmt("{s}/json", .{dir}) else "json"; + const effective_c_import_path = c_import_path orelse "c.zig"; + const c_bridge_target = if (c_import_path) |path| path else "../c.zig"; + const c_bridge_step = WriteStaticFileStep.create( + b, + b.fmt("{s}/c.zig", .{output_root}), + b.fmt("pub const c = @import(\"{s}\").c;\n", .{c_bridge_target}), + ); + c_bridge_step.dependOn(path_prep_step); + path_prep_step = c_bridge_step; + + const stdinc_bridge_step = WriteStaticFileStep.create( + b, + b.fmt("{s}/stdinc.zig", .{output_root}), + \\pub const c = @import("c.zig").c; + \\pub const Time = i64; + \\pub const FunctionPointer = c.SDL_FunctionPointer; + \\ + ); + stdinc_bridge_step.dependOn(path_prep_step); + path_prep_step = stdinc_bridge_step; + + // All public SDL3 API headers const headers_to_generate = [_]struct { header: []const u8, output: []const u8 }{ // .{ .header = "SDL_asyncio.h", .output = "asyncio" }, // .{ .header = "SDL_atomic.h", .output = "atomic" }, @@ -212,7 +269,7 @@ pub fn generateApi(b: *std.Build, parser_exe: *std.Build.Step.Compile, fetch_sdl // .{ .header = "SDL_hidapi.h", .output = "hidapi" }, // Skipped: not core API .{ .header = "SDL_hints.h", .output = "hints" }, .{ .header = "SDL_init.h", .output = "init" }, - // .{ .header = "SDL_iostream.h", .output = "iostream" }, // Skipped: complex I/O API + .{ .header = "SDL_iostream.h", .output = "iostream" }, .{ .header = "SDL_joystick.h", .output = "joystick" }, .{ .header = "SDL_keyboard.h", .output = "keyboard" }, .{ .header = "SDL_keycode.h", .output = "keycode" }, @@ -252,9 +309,6 @@ pub fn generateApi(b: *std.Build, parser_exe: *std.Build.Step.Compile, fetch_sdl b.fmt("{s}/{s}", .{ dir, header_root_suffix.? }) else "sdl3/include/SDL3"; - const output_root = if (effective_output_dir) |dir| b.fmt("{s}/api", .{dir}) else "api"; - const json_root = if (effective_output_dir) |dir| b.fmt("{s}/json", .{dir}) else "json"; - const timestamp_arg = b.fmt("--timestamp={d}", .{timestamp}); for (headers_to_generate) |header_info| { @@ -265,9 +319,7 @@ pub fn generateApi(b: *std.Build, parser_exe: *std.Build.Step.Compile, fetch_sdl if (basedir) |dir| { regenerate.addArg(b.fmt("--basedir={s}", .{dir})); } - if (c_import_path) |path| { - regenerate.addArg(b.fmt("--c-import={s}", .{path})); - } + regenerate.addArg(b.fmt("--c-import={s}", .{effective_c_import_path})); // regenerate.addArg(b.fmt("--output=api/{s}.zig --mocks=mocks/{s}.c", .{ header_info.output, header_info.output })); regenerate.step.dependOn(path_prep_step); regenerate_step.dependOn(®enerate.step); diff --git a/src/codegen.zig b/src/codegen.zig index 00f2850..87c144c 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -26,12 +26,14 @@ pub const CodeGenOptions = struct { decls: []const ResolvedDecl, module_name: []const u8, c_import_path: []const u8 = "c.zig", + skipped: []const patterns.SkippedDecl = &.{}, }; pub const CodeGen = struct { decls: []const ResolvedDecl, module_name: []const u8, c_import_path: []const u8, + skipped: []const patterns.SkippedDecl, allocator: Allocator, output: std.ArrayList(u8), opaque_methods: std.StringHashMap(std.ArrayList(patterns.FunctionDecl)), @@ -43,6 +45,7 @@ pub const CodeGen = struct { .decls = options.decls, .module_name = options.module_name, .c_import_path = options.c_import_path, + .skipped = options.skipped, .allocator = allocator, .output = try std.ArrayList(u8).initCapacity(allocator, 4096), .opaque_methods = std.StringHashMap(std.ArrayList(patterns.FunctionDecl)).init(allocator), @@ -200,6 +203,14 @@ pub const CodeGen = struct { if (self.aliases.items.len > 0) { try self.output.appendSlice(self.allocator, "\n"); } + + if (self.skipped.len > 0) { + try self.output.appendSlice(self.allocator, "pub const unsupported = struct {\n"); + for (self.skipped) |item| { + try self.output.writer(self.allocator).print(" pub const @\"{s}\" = \"{s}\";\n", .{ item.name, item.reason }); + } + try self.output.appendSlice(self.allocator, "};\n\n"); + } } fn writeDeclarations(self: *CodeGen) !void { @@ -446,7 +457,7 @@ pub const CodeGen = struct { try self.output.writer(self.allocator).print("pub const {s} = extern struct {{\n", .{zig_name}); for (struct_decl.fields) |field| { - const zig_type = try types.convertType(field.type_name, self.allocator); + const zig_type = try self.convertAggregateFieldType(field.type_name); defer self.allocator.free(zig_type); if (field.comment) |comment| { try self.output.writer(self.allocator).print(" {s}: {s}, // {s}\n", .{ field.name, zig_type, comment }); @@ -462,7 +473,7 @@ pub const CodeGen = struct { if (union_decl.doc_comment) |doc| try self.writeDocComment(doc); try self.output.writer(self.allocator).print("pub const {s} = extern union {{\n", .{zig_name}); for (union_decl.fields) |field| { - const zig_type = try types.convertType(field.type_name, self.allocator); + const zig_type = try self.convertAggregateFieldType(field.type_name); defer self.allocator.free(zig_type); if (field.comment) |comment| { try self.output.writer(self.allocator).print(" {s}: {s}, // {s}\n", .{ field.name, zig_type, comment }); @@ -550,7 +561,7 @@ pub const CodeGen = struct { try self.output.writer(self.allocator).print(" pub inline fn {s}(", .{zig_name}); for (func.params, 0..) |param, i| { - const zig_type = try types.convertType(param.type_name, self.allocator); + const zig_type = try self.convertFunctionParamType(func, param); defer self.allocator.free(zig_type); if (i > 0) try self.output.appendSlice(self.allocator, ", "); @@ -568,7 +579,7 @@ pub const CodeGen = struct { try self.output.writer(self.allocator).print(") {s} {{\n", .{zig_return_type}); try self.output.appendSlice(self.allocator, " return "); - const return_cast = if (!std.mem.eql(u8, zig_return_type, "void")) types.getCastType(zig_return_type) else .none; + const return_cast = if (!std.mem.eql(u8, zig_return_type, "void")) types.getReturnCastType(zig_return_type) else .none; if (return_cast != .none) { try self.output.writer(self.allocator).print("{s}(", .{castTypeToString(return_cast)}); } @@ -584,9 +595,9 @@ pub const CodeGen = struct { } else try self.allocator.dupe(u8, param.name); defer self.allocator.free(param_name); - const zig_param_type = try types.convertType(param.type_name, self.allocator); + const zig_param_type = try self.convertFunctionParamType(func, param); defer self.allocator.free(zig_param_type); - const param_cast = types.getCastType(zig_param_type); + const param_cast = self.getFunctionParamCastType(param, zig_param_type); if (param_cast == .none) { try self.output.writer(self.allocator).print("{s}", .{param_name}); @@ -614,7 +625,7 @@ pub const CodeGen = struct { try self.output.writer(self.allocator).print("pub inline fn {s}(", .{zig_name}); for (func.params, 0..) |param, i| { - const zig_type = try types.convertType(param.type_name, self.allocator); + const zig_type = try self.convertFunctionParamType(func, param); defer self.allocator.free(zig_type); if (i > 0) try self.output.appendSlice(self.allocator, ", "); if (param.name.len > 0) { @@ -626,7 +637,7 @@ pub const CodeGen = struct { try self.output.writer(self.allocator).print(") {s} {{\n", .{zig_return_type}); try self.output.appendSlice(self.allocator, " return "); - const return_cast = if (!std.mem.eql(u8, zig_return_type, "void")) types.getCastType(zig_return_type) else .none; + const return_cast = if (!std.mem.eql(u8, zig_return_type, "void")) types.getReturnCastType(zig_return_type) else .none; if (return_cast != .none) { try self.output.writer(self.allocator).print("{s}(", .{castTypeToString(return_cast)}); } @@ -635,9 +646,9 @@ pub const CodeGen = struct { for (func.params, 0..) |param, i| { if (i > 0) try self.output.appendSlice(self.allocator, ", "); if (param.name.len > 0) { - const zig_param_type = try types.convertType(param.type_name, self.allocator); + const zig_param_type = try self.convertFunctionParamType(func, param); defer self.allocator.free(zig_param_type); - const param_cast = types.getCastType(zig_param_type); + const param_cast = self.getFunctionParamCastType(param, zig_param_type); if (param_cast == .none) { try self.output.writer(self.allocator).print("{s}", .{param.name}); @@ -655,6 +666,118 @@ pub const CodeGen = struct { try self.output.appendSlice(self.allocator, "}\n\n"); } + fn convertAggregateFieldType(self: *CodeGen, c_type: []const u8) ![]const u8 { + if (self.isOpaqueFieldPointerType(c_type)) { + return try types.convertType(c_type, self.allocator); + } + return try types.convertStructFieldType(c_type, self.allocator); + } + + fn convertFunctionParamType(self: *CodeGen, func: patterns.FunctionDecl, param: patterns.ParamDecl) ![]const u8 { + const zig_type = try types.convertType(param.type_name, self.allocator); + errdefer self.allocator.free(zig_type); + + if (std.mem.startsWith(u8, zig_type, "?*") or std.mem.startsWith(u8, zig_type, "[*c]")) { + return zig_type; + } + + const should_make_nullable = + self.paramMayBeNull(func, param.name) or + (std.mem.startsWith(u8, zig_type, "*") and !std.mem.startsWith(u8, zig_type, "*const ")); + + if (should_make_nullable and std.mem.startsWith(u8, zig_type, "*")) { + const nullable = try std.fmt.allocPrint(self.allocator, "?{s}", .{zig_type}); + self.allocator.free(zig_type); + return nullable; + } + + return zig_type; + } + + fn getFunctionParamCastType(self: *CodeGen, param: patterns.ParamDecl, zig_param_type: []const u8) types.CastType { + if (self.isSDLPointerArrayParam(param.type_name)) { + return .ptr_cast; + } + return types.getParamCastType(zig_param_type); + } + + fn paramMayBeNull(self: *CodeGen, func: patterns.FunctionDecl, param_name: []const u8) bool { + _ = self; + if (param_name.len == 0) return false; + const doc = func.doc_comment orelse return false; + + const marker = std.fmt.allocPrint(std.heap.smp_allocator, "\\param {s}", .{param_name}) catch return false; + defer std.heap.smp_allocator.free(marker); + + const start = std.mem.indexOf(u8, doc, marker) orelse return false; + const tail = doc[start + marker.len ..]; + const next_param = std.mem.indexOf(u8, tail, "\\param ") orelse tail.len; + const next_returns = std.mem.indexOf(u8, tail, "\\returns") orelse tail.len; + const end = @min(next_param, next_returns); + const param_doc = tail[0..end]; + + return std.mem.indexOf(u8, param_doc, "may be NULL") != null or + std.mem.indexOf(u8, param_doc, "or NULL") != null; + } + + fn isOpaqueFieldPointerType(self: *CodeGen, c_type: []const u8) bool { + const trimmed = std.mem.trim(u8, c_type, " \t"); + if (std.mem.indexOf(u8, trimmed, "(SDLCALL *") != null or std.mem.indexOf(u8, trimmed, "(*") != null) { + return false; + } + if (std.mem.indexOfScalar(u8, trimmed, '*') == null) { + return false; + } + + var rest = trimmed; + if (std.mem.startsWith(u8, rest, "const ")) { + rest = std.mem.trim(u8, rest[6..], " \t"); + } + + const first_star = std.mem.indexOfScalar(u8, rest, '*') orelse return false; + const base_type = std.mem.trim(u8, rest[0..first_star], " \t"); + if (!std.mem.startsWith(u8, base_type, "SDL_")) { + return false; + } + + const zig_name = naming.typeNameToZig(base_type); + if (self.opaque_methods.contains(zig_name)) { + return true; + } + + for (self.decls) |resolved| { + if (resolved.decl != .opaque_type) continue; + if (std.mem.eql(u8, resolved.decl.opaque_type.name, base_type)) { + return true; + } + if (std.mem.eql(u8, naming.typeNameToZig(resolved.decl.opaque_type.name), zig_name)) { + return true; + } + } + + return false; + } + + fn isSDLPointerArrayParam(self: *CodeGen, c_type: []const u8) bool { + _ = self; + const trimmed = std.mem.trim(u8, c_type, " \t"); + if (std.mem.indexOf(u8, trimmed, "(SDLCALL *") != null or std.mem.indexOf(u8, trimmed, "(*") != null) { + return false; + } + if (std.mem.count(u8, trimmed, "*") < 2) { + return false; + } + + var rest = trimmed; + if (std.mem.startsWith(u8, rest, "const ")) { + rest = std.mem.trim(u8, rest[6..], " \t"); + } + + const first_star = std.mem.indexOfScalar(u8, rest, '*') orelse return false; + const base_type = std.mem.trim(u8, rest[0..first_star], " \t"); + return std.mem.startsWith(u8, base_type, "SDL_"); + } + fn castTypeToString(cast_type: types.CastType) []const u8 { return switch (cast_type) { .none => "none", @@ -775,3 +898,93 @@ test "primary declarations are emitted without self imports" { try std.testing.expect(std.mem.indexOf(u8, output, "const video_api = @import(\"video.zig\");") == null); try std.testing.expect(std.mem.indexOf(u8, output, "pub const Window = opaque") != null); } + +test "function params make plain out pointers nullable" { + const allocator = std.testing.allocator; + var gen = CodeGen{ + .decls = &.{}, + .module_name = "gpu", + .c_import_path = "c.zig", + .skipped = &.{}, + .allocator = allocator, + .output = .empty, + .opaque_methods = std.StringHashMap(std.ArrayList(patterns.FunctionDecl)).init(allocator), + .import_modules = .empty, + .aliases = .empty, + }; + defer gen.deinit(); + + var params = [_]patterns.ParamDecl{ + .{ .name = "swapchain_texture_width", .type_name = "Uint32 *" }, + }; + const func = patterns.FunctionDecl{ + .name = "SDL_WaitAndAcquireGPUSwapchainTexture", + .return_type = "bool", + .params = params[0..], + .doc_comment = null, + }; + + const zig_type = try gen.convertFunctionParamType(func, params[0]); + defer allocator.free(zig_type); + try std.testing.expectEqualStrings("?*u32", zig_type); +} + +test "opaque methods cast enum params to C ABI" { + const allocator = std.testing.allocator; + + var decls: std.ArrayList(ResolvedDecl) = .empty; + defer { + for (decls.items) |decl| decl.deinit(allocator); + decls.deinit(allocator); + } + + try decls.append(allocator, .{ + .decl = .{ .opaque_type = .{ .name = try allocator.dupe(u8, "SDL_GPURenderPass"), .doc_comment = null } }, + .source_header = try allocator.dupe(u8, "SDL_gpu.h"), + .is_primary_header_decl = true, + }); + try decls.append(allocator, .{ + .decl = .{ .struct_decl = .{ + .name = try allocator.dupe(u8, "SDL_GPUBufferBinding"), + .fields = &.{}, + .doc_comment = null, + .has_unions = false, + } }, + .source_header = try allocator.dupe(u8, "SDL_gpu.h"), + .is_primary_header_decl = true, + }); + const enum_values = try allocator.alloc(patterns.EnumValue, 1); + enum_values[0] = .{ .name = try allocator.dupe(u8, "SDL_GPU_INDEXELEMENTSIZE_16BIT"), .value = null, .comment = null }; + try decls.append(allocator, .{ + .decl = .{ .enum_decl = .{ + .name = try allocator.dupe(u8, "SDL_GPUIndexElementSize"), + .values = enum_values, + .doc_comment = null, + } }, + .source_header = try allocator.dupe(u8, "SDL_gpu.h"), + .is_primary_header_decl = true, + }); + const func_params = try allocator.alloc(patterns.ParamDecl, 3); + func_params[0] = .{ .name = try allocator.dupe(u8, "render_pass"), .type_name = try allocator.dupe(u8, "SDL_GPURenderPass *") }; + func_params[1] = .{ .name = try allocator.dupe(u8, "binding"), .type_name = try allocator.dupe(u8, "const SDL_GPUBufferBinding *") }; + func_params[2] = .{ .name = try allocator.dupe(u8, "index_element_size"), .type_name = try allocator.dupe(u8, "SDL_GPUIndexElementSize") }; + try decls.append(allocator, .{ + .decl = .{ .function_decl = .{ + .name = try allocator.dupe(u8, "SDL_BindGPUIndexBuffer"), + .return_type = try allocator.dupe(u8, "void"), + .params = func_params, + .doc_comment = null, + } }, + .source_header = try allocator.dupe(u8, "SDL_gpu.h"), + .is_primary_header_decl = true, + }); + + const output = try CodeGen.generate(allocator, .{ + .decls = decls.items, + .module_name = "gpu", + .c_import_path = "../c.zig", + }); + defer allocator.free(output); + + try std.testing.expect(std.mem.indexOf(u8, output, "@intFromEnum(index_element_size)") != null); +} diff --git a/src/parser.zig b/src/parser.zig index dec3ff1..e3abd22 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -85,10 +85,21 @@ pub fn main() !void { // Parse declarations var scanner = patterns.Scanner.init(allocator, source); + defer scanner.deinit(); const decls = try scanner.scan(); defer freeDecls(allocator, decls); + const skipped = try scanner.takeSkipped(); + defer { + for (skipped) |item| { + item.deinit(allocator); + } + allocator.free(skipped); + } std.debug.print("Found {d} declarations\n", .{decls.len}); + if (skipped.len > 0) { + std.debug.print("Skipped {d} unsupported declarations\n", .{skipped.len}); + } // Count each type var opaque_count: usize = 0; @@ -281,6 +292,7 @@ pub fn main() !void { .decls = all_resolved_decls.items, .module_name = module_name, .c_import_path = c_import_path, + .skipped = skipped, }); defer allocator.free(output); diff --git a/src/patterns.zig b/src/patterns.zig index a57c31c..381b827 100644 --- a/src/patterns.zig +++ b/src/patterns.zig @@ -202,11 +202,22 @@ pub const ParamDecl = struct { type_name: []const u8, // SDL_GPUShaderFormat }; +pub const SkippedDecl = struct { + name: []const u8, + reason: []const u8, + + pub fn deinit(self: SkippedDecl, allocator: Allocator) void { + allocator.free(self.name); + allocator.free(self.reason); + } +}; + pub const Scanner = struct { source: []const u8, pos: usize, allocator: Allocator, pending_doc_comment: ?[]const u8, + skipped: std.ArrayList(SkippedDecl), pub fn init(allocator: Allocator, source: []const u8) Scanner { return .{ @@ -214,9 +225,17 @@ pub const Scanner = struct { .pos = 0, .allocator = allocator, .pending_doc_comment = null, + .skipped = .empty, }; } + pub fn deinit(self: *Scanner) void { + for (self.skipped.items) |item| { + item.deinit(self.allocator); + } + self.skipped.deinit(self.allocator); + } + pub fn scan(self: *Scanner) ![]Declaration { var decls = try std.ArrayList(Declaration).initCapacity(self.allocator, 100); @@ -226,6 +245,10 @@ pub const Scanner = struct { self.pending_doc_comment = comment; } + if (try self.scanUnsupportedMacro()) { + continue; + } + // Try each pattern - order matters! // Try opaque first (typedef struct SDL_X SDL_X;) if (try self.scanOpaque()) |opaque_decl| { @@ -260,6 +283,50 @@ pub const Scanner = struct { return try decls.toOwnedSlice(self.allocator); } + pub fn takeSkipped(self: *Scanner) ![]SkippedDecl { + const items = try self.skipped.toOwnedSlice(self.allocator); + self.skipped = .empty; + return items; + } + + fn recordSkippedDecl(self: *Scanner, name: []const u8, reason: []const u8) !void { + try self.skipped.append(self.allocator, .{ + .name = try self.allocator.dupe(u8, name), + .reason = try self.allocator.dupe(u8, reason), + }); + } + + fn scanUnsupportedMacro(self: *Scanner) !bool { + if (!std.mem.startsWith(u8, self.source[self.pos..], "#define ")) { + return false; + } + + const start = self.pos; + const line = try self.readLine(); + defer self.allocator.free(line); + + var parts = std.mem.tokenizeScalar(u8, line, ' '); + const define_name = parts.next() orelse { + self.pos = start; + return false; + }; + + if (std.mem.indexOfScalar(u8, define_name, '(') == null) { + self.pos = start; + return false; + } + + const macro_name = define_name[0 .. std.mem.indexOfScalar(u8, define_name, '(').?]; + try self.recordSkippedDecl(macro_name, "function-like macro is not emitted"); + + if (self.pending_doc_comment) |comment| { + self.allocator.free(comment); + self.pending_doc_comment = null; + } + + return true; + } + // Pattern: typedef struct SDL_Foo SDL_Foo; fn scanOpaque(self: *Scanner) !?OpaqueType { const start = self.pos; @@ -1692,6 +1759,15 @@ pub const Scanner = struct { for (vararg_macros) |macro| { if (std.mem.indexOf(u8, text, macro)) |pos| { _ = pos; + if (std.mem.indexOf(u8, text, "SDLCALL ")) |sdlcall_pos| { + const after_sdlcall = text[sdlcall_pos + 8 ..]; + if (std.mem.indexOfScalar(u8, after_sdlcall, '(')) |paren_pos| { + const func_name = std.mem.trim(u8, after_sdlcall[0..paren_pos], " \t\n*"); + if (func_name.len > 0) { + try self.recordSkippedDecl(func_name, "varargs function is not representable in Zig"); + } + } + } return null; // we dont attempt to generate vararg functions. } } diff --git a/src/types.zig b/src/types.zig index 855921a..e61450d 100644 --- a/src/types.zig +++ b/src/types.zig @@ -1,11 +1,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const io = @import("io"); - -var fmtBuffer: [256]u8 = undefined; /// Convert C type to Zig type /// Simple table-based conversion for SDL3 types -pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 { +pub fn convertType(c_type: []const u8, allocator: Allocator) Allocator.Error![]const u8 { const trimmed = std.mem.trim(u8, c_type, " \t"); // Handle opaque struct pointers: "struct X *" -> "*anyopaque" @@ -13,17 +10,17 @@ pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 { return try allocator.dupe(u8, "*anyopaque"); } - // Handle function pointers: For now, just return as placeholder until we implement full conversion + // Handle function pointers used in callbacks and interface structs. if (std.mem.indexOf(u8, trimmed, "(SDLCALL *") != null or std.mem.indexOf(u8, trimmed, "(*") != null) { - // TODO: Implement full function pointer conversion - // For now, return a placeholder type - return try std.fmt.allocPrint(allocator, "?*const anyopaque", .{}); + return try convertFunctionPointerType(trimmed, allocator); } // Handle array types: "Uint8[2]" -> "[2]u8" if (std.mem.indexOf(u8, trimmed, "[")) |bracket_pos| { const base_type = std.mem.trim(u8, trimmed[0..bracket_pos], " \t"); var array_part = trimmed[bracket_pos..]; // "[2]" + var owned_array_part: ?[]const u8 = null; + defer if (owned_array_part) |slice| allocator.free(slice); // Recursively convert the base type const zig_base = try convertType(base_type, allocator); @@ -39,13 +36,8 @@ pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 { }; if (alias_to_c) { - var b = std.io.fixedBufferStream(&fmtBuffer); - _ = try b.write("["); - _ = try b.write("c."); - _ = try b.write(inner); - _ = try b.write("]"); - array_part = b.getWritten(); - std.debug.print("arrya_part = {s}\n", .{array_part}); + owned_array_part = try std.fmt.allocPrint(allocator, "[c.{s}]", .{inner}); + array_part = owned_array_part.?; } } @@ -97,12 +89,12 @@ pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 { // Match "SDL_Type *const *" (no space before const) if (std.mem.indexOf(u8, trimmed, " *const *")) |pos| { const base_type = trimmed[4..pos]; // Remove SDL_ prefix and get type - return std.fmt.allocPrint(allocator, "[*c]?*const {s}", .{base_type}); + return std.fmt.allocPrint(allocator, "[*c]const ?*{s}", .{base_type}); } // Match "SDL_Type * const *" (space before const) if (std.mem.indexOf(u8, trimmed, " * const *")) |pos| { const base_type = trimmed[4..pos]; // Remove SDL_ prefix and get type - return std.fmt.allocPrint(allocator, "[*c]?*const {s}", .{base_type}); + return std.fmt.allocPrint(allocator, "[*c]const ?*{s}", .{base_type}); } if (std.mem.indexOf(u8, trimmed, " **")) |pos| { const base_type = trimmed[4..pos]; // Remove SDL_ prefix and get type @@ -127,11 +119,23 @@ pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 { if (std.mem.eql(u8, trimmed, "Sint8 *")) return try allocator.dupe(u8, "*i8"); if (std.mem.eql(u8, trimmed, "Sint16 *")) return try allocator.dupe(u8, "*i16"); if (std.mem.eql(u8, trimmed, "Sint32 *")) return try allocator.dupe(u8, "*i32"); + if (std.mem.eql(u8, trimmed, "Sint64 *")) return try allocator.dupe(u8, "*i64"); + if (std.mem.eql(u8, trimmed, "const Sint64 *")) return try allocator.dupe(u8, "*const i64"); if (std.mem.eql(u8, trimmed, "const bool *")) return try allocator.dupe(u8, "*const bool"); if (std.mem.startsWith(u8, trimmed, "const ")) { const rest = trimmed[6..]; if (std.mem.startsWith(u8, rest, "SDL_")) { + if (std.mem.indexOf(u8, rest, " *const *")) |pos| { + const base_type = rest[0..pos]; + const zig_type = base_type[4..]; + return std.fmt.allocPrint(allocator, "[*c]const ?*const {s}", .{zig_type}); + } + if (std.mem.indexOf(u8, rest, " * const *")) |pos| { + const base_type = rest[0..pos]; + const zig_type = base_type[4..]; + return std.fmt.allocPrint(allocator, "[*c]const ?*const {s}", .{zig_type}); + } if (std.mem.indexOfScalar(u8, rest, '*')) |star_pos| { const base_type = std.mem.trim(u8, rest[0..star_pos], " \t"); const star_count = std.mem.count(u8, rest[star_pos..], "*"); @@ -152,20 +156,6 @@ pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 { return std.fmt.allocPrint(allocator, "[*c]?*const {s}", .{zig_type}); } } - if (std.mem.indexOf(u8, rest, " *const *")) |pos| { - const base_type = rest[0..pos]; - if (std.mem.startsWith(u8, base_type, "SDL_")) { - const zig_type = base_type[4..]; - return std.fmt.allocPrint(allocator, "[*c]?*const {s}", .{zig_type}); - } - } - if (std.mem.indexOf(u8, rest, " * const *")) |pos| { - const base_type = rest[0..pos]; - if (std.mem.startsWith(u8, base_type, "SDL_")) { - const zig_type = base_type[4..]; - return std.fmt.allocPrint(allocator, "[*c]?*const {s}", .{zig_type}); - } - } if (std.mem.endsWith(u8, rest, " *") or std.mem.endsWith(u8, rest, "*")) { const base_type = if (std.mem.endsWith(u8, rest, " *")) rest[0 .. rest.len - 2] @@ -220,39 +210,121 @@ pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 { return try allocator.dupe(u8, trimmed); } -/// Determine the appropriate cast for a given type when calling C functions -pub fn getCastType(zig_type: []const u8) CastType { +pub fn convertStructFieldType(c_type: []const u8, allocator: Allocator) Allocator.Error![]const u8 { + const trimmed = std.mem.trim(u8, c_type, " \t"); + + if (std.mem.indexOf(u8, trimmed, "(SDLCALL *") != null or std.mem.indexOf(u8, trimmed, "(*") != null) { + return try convertType(trimmed, allocator); + } + + if (std.mem.indexOfScalar(u8, trimmed, '[') != null) { + return try convertType(trimmed, allocator); + } + + if (std.mem.indexOfScalar(u8, trimmed, '*') == null) { + return try convertType(trimmed, allocator); + } + + var rest = trimmed; + var pointee_const = false; + if (std.mem.startsWith(u8, rest, "const ")) { + pointee_const = true; + rest = std.mem.trim(u8, rest[6..], " \t"); + } + + const first_star = std.mem.indexOfScalar(u8, rest, '*') orelse return try convertType(trimmed, allocator); + const base_type = std.mem.trim(u8, rest[0..first_star], " \t"); + const star_count = std.mem.count(u8, rest[first_star..], "*"); + + const zig_base = if (std.mem.eql(u8, base_type, "void")) + try allocator.dupe(u8, "anyopaque") + else + try convertType(base_type, allocator); + defer allocator.free(zig_base); + + var result: std.ArrayList(u8) = .empty; + defer result.deinit(allocator); + + for (0..star_count) |_| { + try result.appendSlice(allocator, "[*c]"); + } + if (pointee_const) { + try result.appendSlice(allocator, "const "); + } + try result.appendSlice(allocator, zig_base); + + return try result.toOwnedSlice(allocator); +} + +fn isOpaquePointerLike(zig_type: []const u8) bool { + if (!(std.mem.startsWith(u8, zig_type, "?*") or std.mem.startsWith(u8, zig_type, "*"))) { + return false; + } + + if (std.mem.indexOf(u8, zig_type, "anyopaque") != null or + std.mem.startsWith(u8, zig_type, "[*c]") or + std.mem.startsWith(u8, zig_type, "?[*c]")) + { + return false; + } + + return true; +} + +fn isEnumLikeType(zig_type: []const u8) bool { + if (isPackedFlagsLikeType(zig_type)) return false; + + return std.mem.indexOf(u8, zig_type, "Type") != null or + std.mem.indexOf(u8, zig_type, "Size") != null or + std.mem.indexOf(u8, zig_type, "Mode") != null or + std.mem.indexOf(u8, zig_type, "Op") != null or + std.mem.endsWith(u8, zig_type, "Format"); +} + +fn isPackedFlagsLikeType(zig_type: []const u8) bool { + return std.mem.endsWith(u8, zig_type, "Flags") or + std.mem.eql(u8, zig_type, "GPUShaderFormat") or + std.mem.eql(u8, zig_type, "FlipMode"); +} + +/// Determine the appropriate cast for a Zig parameter passed into a C function. +pub fn getParamCastType(zig_type: []const u8) CastType { // Bool needs @bitCast if (std.mem.eql(u8, zig_type, "bool")) { return .bit_cast; } // Opaque pointers need @ptrCast (both ?*Type and *Type) - // Skip [*c] (C pointers) and *anyopaque - if (std.mem.startsWith(u8, zig_type, "?*") or std.mem.startsWith(u8, zig_type, "*")) { - // Exclude anyopaque and C pointers - if (std.mem.indexOf(u8, zig_type, "anyopaque") != null or - std.mem.startsWith(u8, zig_type, "[*c]") or - std.mem.startsWith(u8, zig_type, "?[*c]")) { - return .none; - } + if (isOpaquePointerLike(zig_type)) { return .ptr_cast; } - // Enums need @intFromEnum - // We'll detect these by naming convention or explicit marking - // For now, assume types ending in certain patterns are enums - if (std.mem.indexOf(u8, zig_type, "Type") != null or - std.mem.indexOf(u8, zig_type, "Mode") != null or - std.mem.indexOf(u8, zig_type, "Op") != null) - { + if (isEnumLikeType(zig_type)) { return .int_from_enum; } - // Flags (packed structs) need @bitCast - if (std.mem.endsWith(u8, zig_type, "Flags") or - std.mem.endsWith(u8, zig_type, "Format")) - { + if (isPackedFlagsLikeType(zig_type)) { + return .bit_cast; + } + + return .none; +} + +/// Determine the appropriate cast for a C return value exposed as a Zig type. +pub fn getReturnCastType(zig_type: []const u8) CastType { + if (std.mem.eql(u8, zig_type, "bool")) { + return .bit_cast; + } + + if (isOpaquePointerLike(zig_type)) { + return .ptr_cast; + } + + if (isEnumLikeType(zig_type)) { + return .enum_from_int; + } + + if (isPackedFlagsLikeType(zig_type)) { return .bit_cast; } @@ -261,7 +333,7 @@ pub fn getCastType(zig_type: []const u8) CastType { /// Convert C function pointer type to Zig function pointer syntax /// Example: Sint64 (SDLCALL *size)(void *userdata) -> *const fn (?*anyopaque) callconv(.C) i64 -fn convertFunctionPointerType(c_type: []const u8, allocator: Allocator) ![]const u8 { +fn convertFunctionPointerType(c_type: []const u8, allocator: Allocator) Allocator.Error![]const u8 { // Pattern: ReturnType (SDLCALL *name)(params) or ReturnType (*name)(params) // Find the return type (everything before the opening paren) @@ -284,12 +356,12 @@ fn convertFunctionPointerType(c_type: []const u8, allocator: Allocator) ![]const defer allocator.free(zig_return); // Convert parameters - var params_list = std.ArrayList([]const u8).init(allocator); + var params_list: std.ArrayList([]const u8) = .empty; defer { for (params_list.items) |param| { allocator.free(param); } - params_list.deinit(); + params_list.deinit(allocator); } // Parse comma-separated parameters @@ -298,43 +370,53 @@ fn convertFunctionPointerType(c_type: []const u8, allocator: Allocator) ![]const const trimmed_param = std.mem.trim(u8, param, " \t"); if (trimmed_param.len == 0) continue; - // Extract just the type (remove parameter name if present) - // Pattern: "void *userdata" -> "void *" - // Pattern: "Sint64 offset" -> "Sint64" - var param_type: []const u8 = trimmed_param; - - // Find the last space that separates type from name - if (std.mem.lastIndexOf(u8, trimmed_param, " ")) |space_pos| { - // Check if what comes after is an identifier (not a *) - const after_space = trimmed_param[space_pos + 1 ..]; - if (after_space.len > 0 and after_space[0] != '*') { - param_type = std.mem.trimRight(u8, trimmed_param[0..space_pos], " \t"); - } - } + const param_type = stripParamName(trimmed_param); // Don't recursively convert function pointers in params const zig_param = if (std.mem.indexOf(u8, param_type, "(") != null) try allocator.dupe(u8, param_type) else try convertType(param_type, allocator); - try params_list.append(zig_param); + try params_list.append(allocator, zig_param); } // Build Zig function pointer type - var result = std.ArrayList(u8).init(allocator); - defer result.deinit(); + var result: std.ArrayList(u8) = .empty; + defer result.deinit(allocator); - try result.appendSlice("?*const fn ("); + try result.appendSlice(allocator, "?*const fn("); for (params_list.items, 0..) |param, i| { - if (i > 0) try result.appendSlice(", "); - try result.appendSlice(param); + if (i > 0) try result.appendSlice(allocator, ", "); + try result.appendSlice(allocator, param); } - try result.appendSlice(") callconv(.C) "); - try result.appendSlice(zig_return); + try result.appendSlice(allocator, ") callconv(.C) "); + try result.appendSlice(allocator, zig_return); - return try result.toOwnedSlice(); + return try result.toOwnedSlice(allocator); +} + +fn stripParamName(param: []const u8) []const u8 { + if (param.len == 0) return param; + if (std.mem.eql(u8, param, "...")) return param; + + var ident_start = param.len; + while (ident_start > 0) { + const c = param[ident_start - 1]; + if (std.ascii.isAlphanumeric(c) or c == '_') { + ident_start -= 1; + continue; + } + break; + } + + if (ident_start == param.len or ident_start == 0) return param; + + const before_name = std.mem.trimRight(u8, param[0..ident_start], " \t"); + if (before_name.len == 0) return param; + + return before_name; } pub const CastType = enum { @@ -352,3 +434,46 @@ test "convert const SDL double pointer" { try std.testing.expectEqualStrings("[*c]?*const TrayEntry", zig_type); } + +test "convert function pointer callback type" { + const allocator = std.testing.allocator; + const zig_type = try convertType("Sint64 (SDLCALL *size)(void *userdata)", allocator); + defer allocator.free(zig_type); + + try std.testing.expectEqualStrings("?*const fn(?*anyopaque) callconv(.C) i64", zig_type); +} + +test "convert SDL pointer arrays preserves const on outer sequence" { + const allocator = std.testing.allocator; + + const zig_type = try convertType("SDL_GPUBuffer *const *", allocator); + defer allocator.free(zig_type); + try std.testing.expectEqualStrings("[*c]const ?*GPUBuffer", zig_type); + + const const_zig_type = try convertType("const SDL_GPUBuffer *const *", allocator); + defer allocator.free(const_zig_type); + try std.testing.expectEqualStrings("[*c]const ?*const GPUBuffer", const_zig_type); +} + +test "enum cast direction differs for params and returns" { + try std.testing.expectEqual(.int_from_enum, getParamCastType("GPUTextureFormat")); + try std.testing.expectEqual(.enum_from_int, getReturnCastType("GPUTextureFormat")); + try std.testing.expectEqual(.int_from_enum, getParamCastType("GPUIndexElementSize")); + try std.testing.expectEqual(.enum_from_int, getReturnCastType("GPUIndexElementSize")); + try std.testing.expectEqual(.bit_cast, getParamCastType("GPUShaderFormat")); + try std.testing.expectEqual(.bit_cast, getReturnCastType("GPUShaderFormat")); + try std.testing.expectEqual(.bit_cast, getParamCastType("FlipMode")); + try std.testing.expectEqual(.bit_cast, getReturnCastType("FlipMode")); +} + +test "struct fields use C pointers" { + const allocator = std.testing.allocator; + + const single = try convertStructFieldType("const SDL_GPUColorTargetDescription *", allocator); + defer allocator.free(single); + try std.testing.expectEqualStrings("[*c]const GPUColorTargetDescription", single); + + const generic = try convertStructFieldType("const void *", allocator); + defer allocator.free(generic); + try std.testing.expectEqualStrings("[*c]const anyopaque", generic); +}