migration to generated gpu sdl3 bindings and sdl 3.2.30

This commit is contained in:
peterino2 2026-03-07 13:30:32 -08:00
parent 44e7928c6d
commit 45ee35163c
5 changed files with 574 additions and 96 deletions

View File

@ -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 { 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 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"); 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; path_prep_step = basedir_step;
} }
// All public SDL3 API headers (53 total) const output_root = if (effective_output_dir) |dir| b.fmt("{s}/api", .{dir}) else "api";
// Skipped: assert, thread, hidapi, mutex, tray (not core APIs or problematic) 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 }{ const headers_to_generate = [_]struct { header: []const u8, output: []const u8 }{
// .{ .header = "SDL_asyncio.h", .output = "asyncio" }, // .{ .header = "SDL_asyncio.h", .output = "asyncio" },
// .{ .header = "SDL_atomic.h", .output = "atomic" }, // .{ .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_hidapi.h", .output = "hidapi" }, // Skipped: not core API
.{ .header = "SDL_hints.h", .output = "hints" }, .{ .header = "SDL_hints.h", .output = "hints" },
.{ .header = "SDL_init.h", .output = "init" }, .{ .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_joystick.h", .output = "joystick" },
.{ .header = "SDL_keyboard.h", .output = "keyboard" }, .{ .header = "SDL_keyboard.h", .output = "keyboard" },
.{ .header = "SDL_keycode.h", .output = "keycode" }, .{ .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.? }) b.fmt("{s}/{s}", .{ dir, header_root_suffix.? })
else else
"sdl3/include/SDL3"; "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}); const timestamp_arg = b.fmt("--timestamp={d}", .{timestamp});
for (headers_to_generate) |header_info| { 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| { if (basedir) |dir| {
regenerate.addArg(b.fmt("--basedir={s}", .{dir})); regenerate.addArg(b.fmt("--basedir={s}", .{dir}));
} }
if (c_import_path) |path| { regenerate.addArg(b.fmt("--c-import={s}", .{effective_c_import_path}));
regenerate.addArg(b.fmt("--c-import={s}", .{path}));
}
// regenerate.addArg(b.fmt("--output=api/{s}.zig --mocks=mocks/{s}.c", .{ header_info.output, header_info.output })); // 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(path_prep_step);
regenerate_step.dependOn(&regenerate.step); regenerate_step.dependOn(&regenerate.step);

View File

@ -26,12 +26,14 @@ pub const CodeGenOptions = struct {
decls: []const ResolvedDecl, decls: []const ResolvedDecl,
module_name: []const u8, module_name: []const u8,
c_import_path: []const u8 = "c.zig", c_import_path: []const u8 = "c.zig",
skipped: []const patterns.SkippedDecl = &.{},
}; };
pub const CodeGen = struct { pub const CodeGen = struct {
decls: []const ResolvedDecl, decls: []const ResolvedDecl,
module_name: []const u8, module_name: []const u8,
c_import_path: []const u8, c_import_path: []const u8,
skipped: []const patterns.SkippedDecl,
allocator: Allocator, allocator: Allocator,
output: std.ArrayList(u8), output: std.ArrayList(u8),
opaque_methods: std.StringHashMap(std.ArrayList(patterns.FunctionDecl)), opaque_methods: std.StringHashMap(std.ArrayList(patterns.FunctionDecl)),
@ -43,6 +45,7 @@ pub const CodeGen = struct {
.decls = options.decls, .decls = options.decls,
.module_name = options.module_name, .module_name = options.module_name,
.c_import_path = options.c_import_path, .c_import_path = options.c_import_path,
.skipped = options.skipped,
.allocator = allocator, .allocator = allocator,
.output = try std.ArrayList(u8).initCapacity(allocator, 4096), .output = try std.ArrayList(u8).initCapacity(allocator, 4096),
.opaque_methods = std.StringHashMap(std.ArrayList(patterns.FunctionDecl)).init(allocator), .opaque_methods = std.StringHashMap(std.ArrayList(patterns.FunctionDecl)).init(allocator),
@ -200,6 +203,14 @@ pub const CodeGen = struct {
if (self.aliases.items.len > 0) { if (self.aliases.items.len > 0) {
try self.output.appendSlice(self.allocator, "\n"); 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 { 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}); try self.output.writer(self.allocator).print("pub const {s} = extern struct {{\n", .{zig_name});
for (struct_decl.fields) |field| { 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); defer self.allocator.free(zig_type);
if (field.comment) |comment| { if (field.comment) |comment| {
try self.output.writer(self.allocator).print(" {s}: {s}, // {s}\n", .{ field.name, zig_type, 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); 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}); try self.output.writer(self.allocator).print("pub const {s} = extern union {{\n", .{zig_name});
for (union_decl.fields) |field| { 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); defer self.allocator.free(zig_type);
if (field.comment) |comment| { if (field.comment) |comment| {
try self.output.writer(self.allocator).print(" {s}: {s}, // {s}\n", .{ field.name, zig_type, 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}); try self.output.writer(self.allocator).print(" pub inline fn {s}(", .{zig_name});
for (func.params, 0..) |param, i| { 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); defer self.allocator.free(zig_type);
if (i > 0) try self.output.appendSlice(self.allocator, ", "); 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.writer(self.allocator).print(") {s} {{\n", .{zig_return_type});
try self.output.appendSlice(self.allocator, " return "); 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) { if (return_cast != .none) {
try self.output.writer(self.allocator).print("{s}(", .{castTypeToString(return_cast)}); 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); } else try self.allocator.dupe(u8, param.name);
defer self.allocator.free(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); 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) { if (param_cast == .none) {
try self.output.writer(self.allocator).print("{s}", .{param_name}); 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}); try self.output.writer(self.allocator).print("pub inline fn {s}(", .{zig_name});
for (func.params, 0..) |param, i| { 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); defer self.allocator.free(zig_type);
if (i > 0) try self.output.appendSlice(self.allocator, ", "); if (i > 0) try self.output.appendSlice(self.allocator, ", ");
if (param.name.len > 0) { 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.writer(self.allocator).print(") {s} {{\n", .{zig_return_type});
try self.output.appendSlice(self.allocator, " return "); 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) { if (return_cast != .none) {
try self.output.writer(self.allocator).print("{s}(", .{castTypeToString(return_cast)}); 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| { for (func.params, 0..) |param, i| {
if (i > 0) try self.output.appendSlice(self.allocator, ", "); if (i > 0) try self.output.appendSlice(self.allocator, ", ");
if (param.name.len > 0) { 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); 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) { if (param_cast == .none) {
try self.output.writer(self.allocator).print("{s}", .{param.name}); 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"); 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 { fn castTypeToString(cast_type: types.CastType) []const u8 {
return switch (cast_type) { return switch (cast_type) {
.none => "none", .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, "const video_api = @import(\"video.zig\");") == null);
try std.testing.expect(std.mem.indexOf(u8, output, "pub const Window = opaque") != 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);
}

View File

@ -85,10 +85,21 @@ pub fn main() !void {
// Parse declarations // Parse declarations
var scanner = patterns.Scanner.init(allocator, source); var scanner = patterns.Scanner.init(allocator, source);
defer scanner.deinit();
const decls = try scanner.scan(); const decls = try scanner.scan();
defer freeDecls(allocator, decls); 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}); 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 // Count each type
var opaque_count: usize = 0; var opaque_count: usize = 0;
@ -281,6 +292,7 @@ pub fn main() !void {
.decls = all_resolved_decls.items, .decls = all_resolved_decls.items,
.module_name = module_name, .module_name = module_name,
.c_import_path = c_import_path, .c_import_path = c_import_path,
.skipped = skipped,
}); });
defer allocator.free(output); defer allocator.free(output);

View File

@ -202,11 +202,22 @@ pub const ParamDecl = struct {
type_name: []const u8, // SDL_GPUShaderFormat 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 { pub const Scanner = struct {
source: []const u8, source: []const u8,
pos: usize, pos: usize,
allocator: Allocator, allocator: Allocator,
pending_doc_comment: ?[]const u8, pending_doc_comment: ?[]const u8,
skipped: std.ArrayList(SkippedDecl),
pub fn init(allocator: Allocator, source: []const u8) Scanner { pub fn init(allocator: Allocator, source: []const u8) Scanner {
return .{ return .{
@ -214,9 +225,17 @@ pub const Scanner = struct {
.pos = 0, .pos = 0,
.allocator = allocator, .allocator = allocator,
.pending_doc_comment = null, .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 { pub fn scan(self: *Scanner) ![]Declaration {
var decls = try std.ArrayList(Declaration).initCapacity(self.allocator, 100); var decls = try std.ArrayList(Declaration).initCapacity(self.allocator, 100);
@ -226,6 +245,10 @@ pub const Scanner = struct {
self.pending_doc_comment = comment; self.pending_doc_comment = comment;
} }
if (try self.scanUnsupportedMacro()) {
continue;
}
// Try each pattern - order matters! // Try each pattern - order matters!
// Try opaque first (typedef struct SDL_X SDL_X;) // Try opaque first (typedef struct SDL_X SDL_X;)
if (try self.scanOpaque()) |opaque_decl| { if (try self.scanOpaque()) |opaque_decl| {
@ -260,6 +283,50 @@ pub const Scanner = struct {
return try decls.toOwnedSlice(self.allocator); 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; // Pattern: typedef struct SDL_Foo SDL_Foo;
fn scanOpaque(self: *Scanner) !?OpaqueType { fn scanOpaque(self: *Scanner) !?OpaqueType {
const start = self.pos; const start = self.pos;
@ -1692,6 +1759,15 @@ pub const Scanner = struct {
for (vararg_macros) |macro| { for (vararg_macros) |macro| {
if (std.mem.indexOf(u8, text, macro)) |pos| { if (std.mem.indexOf(u8, text, macro)) |pos| {
_ = 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. return null; // we dont attempt to generate vararg functions.
} }
} }

View File

@ -1,11 +1,8 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const io = @import("io");
var fmtBuffer: [256]u8 = undefined;
/// Convert C type to Zig type /// Convert C type to Zig type
/// Simple table-based conversion for SDL3 types /// 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"); const trimmed = std.mem.trim(u8, c_type, " \t");
// Handle opaque struct pointers: "struct X *" -> "*anyopaque" // 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"); 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) { if (std.mem.indexOf(u8, trimmed, "(SDLCALL *") != null or std.mem.indexOf(u8, trimmed, "(*") != null) {
// TODO: Implement full function pointer conversion return try convertFunctionPointerType(trimmed, allocator);
// For now, return a placeholder type
return try std.fmt.allocPrint(allocator, "?*const anyopaque", .{});
} }
// Handle array types: "Uint8[2]" -> "[2]u8" // Handle array types: "Uint8[2]" -> "[2]u8"
if (std.mem.indexOf(u8, trimmed, "[")) |bracket_pos| { if (std.mem.indexOf(u8, trimmed, "[")) |bracket_pos| {
const base_type = std.mem.trim(u8, trimmed[0..bracket_pos], " \t"); const base_type = std.mem.trim(u8, trimmed[0..bracket_pos], " \t");
var array_part = trimmed[bracket_pos..]; // "[2]" 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 // Recursively convert the base type
const zig_base = try convertType(base_type, allocator); 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) { if (alias_to_c) {
var b = std.io.fixedBufferStream(&fmtBuffer); owned_array_part = try std.fmt.allocPrint(allocator, "[c.{s}]", .{inner});
_ = try b.write("["); array_part = owned_array_part.?;
_ = try b.write("c.");
_ = try b.write(inner);
_ = try b.write("]");
array_part = b.getWritten();
std.debug.print("arrya_part = {s}\n", .{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) // Match "SDL_Type *const *" (no space before const)
if (std.mem.indexOf(u8, trimmed, " *const *")) |pos| { if (std.mem.indexOf(u8, trimmed, " *const *")) |pos| {
const base_type = trimmed[4..pos]; // Remove SDL_ prefix and get type 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) // Match "SDL_Type * const *" (space before const)
if (std.mem.indexOf(u8, trimmed, " * const *")) |pos| { if (std.mem.indexOf(u8, trimmed, " * const *")) |pos| {
const base_type = trimmed[4..pos]; // Remove SDL_ prefix and get type 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| { if (std.mem.indexOf(u8, trimmed, " **")) |pos| {
const base_type = trimmed[4..pos]; // Remove SDL_ prefix and get type 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, "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, "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, "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.eql(u8, trimmed, "const bool *")) return try allocator.dupe(u8, "*const bool");
if (std.mem.startsWith(u8, trimmed, "const ")) { if (std.mem.startsWith(u8, trimmed, "const ")) {
const rest = trimmed[6..]; const rest = trimmed[6..];
if (std.mem.startsWith(u8, rest, "SDL_")) { 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| { if (std.mem.indexOfScalar(u8, rest, '*')) |star_pos| {
const base_type = std.mem.trim(u8, rest[0..star_pos], " \t"); const base_type = std.mem.trim(u8, rest[0..star_pos], " \t");
const star_count = std.mem.count(u8, rest[star_pos..], "*"); 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}); 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, "*")) { if (std.mem.endsWith(u8, rest, " *") or std.mem.endsWith(u8, rest, "*")) {
const base_type = if (std.mem.endsWith(u8, rest, " *")) const base_type = if (std.mem.endsWith(u8, rest, " *"))
rest[0 .. rest.len - 2] 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); return try allocator.dupe(u8, trimmed);
} }
/// Determine the appropriate cast for a given type when calling C functions pub fn convertStructFieldType(c_type: []const u8, allocator: Allocator) Allocator.Error![]const u8 {
pub fn getCastType(zig_type: []const u8) CastType { 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 // Bool needs @bitCast
if (std.mem.eql(u8, zig_type, "bool")) { if (std.mem.eql(u8, zig_type, "bool")) {
return .bit_cast; return .bit_cast;
} }
// Opaque pointers need @ptrCast (both ?*Type and *Type) // Opaque pointers need @ptrCast (both ?*Type and *Type)
// Skip [*c] (C pointers) and *anyopaque if (isOpaquePointerLike(zig_type)) {
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;
}
return .ptr_cast; return .ptr_cast;
} }
// Enums need @intFromEnum if (isEnumLikeType(zig_type)) {
// 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)
{
return .int_from_enum; return .int_from_enum;
} }
// Flags (packed structs) need @bitCast if (isPackedFlagsLikeType(zig_type)) {
if (std.mem.endsWith(u8, zig_type, "Flags") or return .bit_cast;
std.mem.endsWith(u8, zig_type, "Format")) }
{
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; 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 /// Convert C function pointer type to Zig function pointer syntax
/// Example: Sint64 (SDLCALL *size)(void *userdata) -> *const fn (?*anyopaque) callconv(.C) i64 /// 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) // Pattern: ReturnType (SDLCALL *name)(params) or ReturnType (*name)(params)
// Find the return type (everything before the opening paren) // 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); defer allocator.free(zig_return);
// Convert parameters // Convert parameters
var params_list = std.ArrayList([]const u8).init(allocator); var params_list: std.ArrayList([]const u8) = .empty;
defer { defer {
for (params_list.items) |param| { for (params_list.items) |param| {
allocator.free(param); allocator.free(param);
} }
params_list.deinit(); params_list.deinit(allocator);
} }
// Parse comma-separated parameters // 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"); const trimmed_param = std.mem.trim(u8, param, " \t");
if (trimmed_param.len == 0) continue; if (trimmed_param.len == 0) continue;
// Extract just the type (remove parameter name if present) const param_type = stripParamName(trimmed_param);
// 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");
}
}
// Don't recursively convert function pointers in params // Don't recursively convert function pointers in params
const zig_param = if (std.mem.indexOf(u8, param_type, "(") != null) const zig_param = if (std.mem.indexOf(u8, param_type, "(") != null)
try allocator.dupe(u8, param_type) try allocator.dupe(u8, param_type)
else else
try convertType(param_type, allocator); try convertType(param_type, allocator);
try params_list.append(zig_param); try params_list.append(allocator, zig_param);
} }
// Build Zig function pointer type // Build Zig function pointer type
var result = std.ArrayList(u8).init(allocator); var result: std.ArrayList(u8) = .empty;
defer result.deinit(); defer result.deinit(allocator);
try result.appendSlice("?*const fn ("); try result.appendSlice(allocator, "?*const fn(");
for (params_list.items, 0..) |param, i| { for (params_list.items, 0..) |param, i| {
if (i > 0) try result.appendSlice(", "); if (i > 0) try result.appendSlice(allocator, ", ");
try result.appendSlice(param); try result.appendSlice(allocator, param);
} }
try result.appendSlice(") callconv(.C) "); try result.appendSlice(allocator, ") callconv(.C) ");
try result.appendSlice(zig_return); 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 { pub const CastType = enum {
@ -352,3 +434,46 @@ test "convert const SDL double pointer" {
try std.testing.expectEqualStrings("[*c]?*const TrayEntry", zig_type); 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);
}