const std = @import("std"); const Allocator = std.mem.Allocator; const patterns = @import("patterns.zig"); const naming = @import("naming.zig"); const resolved_decl = @import("resolved_decl.zig"); const types = @import("types.zig"); const EnumDecl = patterns.EnumDecl; const FlagDecl = patterns.FlagDecl; const OpaqueType = patterns.OpaqueType; const ResolvedDecl = resolved_decl.ResolvedDecl; const StructDecl = patterns.StructDecl; const ImportedModule = struct { module_name: []const u8, import_name: []const u8, }; const TypeAlias = struct { name: []const u8, import_name: []const u8, target_name: []const u8, }; 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)), import_modules: std.ArrayList(ImportedModule), aliases: std.ArrayList(TypeAlias), pub fn generate(allocator: Allocator, options: CodeGenOptions) ![]const u8 { var gen = CodeGen{ .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), .import_modules = .empty, .aliases = .empty, }; defer gen.deinit(); try gen.planDependencies(); try gen.categorizeDeclarations(); try gen.writeHeader(); try gen.writeDeclarations(); return try gen.output.toOwnedSlice(allocator); } fn deinit(self: *CodeGen) void { var method_it = self.opaque_methods.valueIterator(); while (method_it.next()) |methods| { methods.deinit(self.allocator); } self.opaque_methods.deinit(); for (self.import_modules.items) |module| { self.allocator.free(module.module_name); self.allocator.free(module.import_name); } self.import_modules.deinit(self.allocator); for (self.aliases.items) |alias_decl| { self.allocator.free(alias_decl.import_name); } self.aliases.deinit(self.allocator); self.output.deinit(self.allocator); } fn planDependencies(self: *CodeGen) !void { var seen_modules = std.StringHashMap(void).init(self.allocator); defer { var module_it = seen_modules.keyIterator(); while (module_it.next()) |key| { self.allocator.free(key.*); } seen_modules.deinit(); } var seen_aliases = std.StringHashMap(void).init(self.allocator); defer { var alias_it = seen_aliases.keyIterator(); while (alias_it.next()) |key| { self.allocator.free(key.*); } seen_aliases.deinit(); } for (self.decls) |resolved| { const type_name = resolved.typeName() orelse continue; const owner_module = try resolved_decl.headerToModuleNameAlloc(self.allocator, resolved.source_header); defer self.allocator.free(owner_module); if (std.mem.eql(u8, owner_module, self.module_name)) continue; if (!seen_modules.contains(owner_module)) { const module_name = try self.allocator.dupe(u8, owner_module); const import_name = try std.fmt.allocPrint(self.allocator, "{s}_api", .{owner_module}); try seen_modules.put(try self.allocator.dupe(u8, owner_module), {}); try self.import_modules.append(self.allocator, .{ .module_name = module_name, .import_name = import_name, }); } const alias_name = naming.typeNameToZig(type_name); if (!seen_aliases.contains(alias_name)) { try seen_aliases.put(try self.allocator.dupe(u8, alias_name), {}); const import_name = try std.fmt.allocPrint(self.allocator, "{s}_api", .{owner_module}); try self.aliases.append(self.allocator, .{ .name = alias_name, .import_name = import_name, .target_name = alias_name, }); } } } fn categorizeDeclarations(self: *CodeGen) !void { var opaque_names: std.ArrayList([]const u8) = .empty; defer opaque_names.deinit(self.allocator); for (self.decls) |resolved| { if (!try self.isOwnedDecl(resolved)) continue; if (resolved.decl == .opaque_type) { const zig_name = naming.typeNameToZig(resolved.decl.opaque_type.name); try opaque_names.append(self.allocator, zig_name); try self.opaque_methods.put(zig_name, .empty); } } for (self.decls) |resolved| { if (!try self.isOwnedDecl(resolved)) continue; if (resolved.decl != .function_decl) continue; const func = resolved.decl.function_decl; if (func.params.len == 0) continue; const first_param_type = try types.convertType(func.params[0].type_name, self.allocator); defer self.allocator.free(first_param_type); for (opaque_names.items) |opaque_name| { const opt_ptr = try std.fmt.allocPrint(self.allocator, "?*{s}", .{opaque_name}); defer self.allocator.free(opt_ptr); const ptr = try std.fmt.allocPrint(self.allocator, "*{s}", .{opaque_name}); defer self.allocator.free(ptr); if (std.mem.eql(u8, first_param_type, opt_ptr) or std.mem.eql(u8, first_param_type, ptr)) { var methods = self.opaque_methods.getPtr(opaque_name).?; try methods.append(self.allocator, func); break; } } } } fn isOwnedDecl(self: *CodeGen, resolved: ResolvedDecl) !bool { if (!resolved.is_primary_header_decl and resolved.typeName() == null) return true; const owner_module = try resolved_decl.headerToModuleNameAlloc(self.allocator, resolved.source_header); defer self.allocator.free(owner_module); return std.mem.eql(u8, owner_module, self.module_name); } fn writeHeader(self: *CodeGen) !void { try self.output.writer(self.allocator).print( \\const std = @import("std"); \\pub const c = @import("{s}").c; \\ , .{self.c_import_path}); for (self.import_modules.items) |module| { try self.output.writer(self.allocator).print("const {s} = @import(\"{s}.zig\");\n", .{ module.import_name, module.module_name, }); } if (self.import_modules.items.len > 0) { try self.output.appendSlice(self.allocator, "\n"); } for (self.aliases.items) |alias_decl| { try self.output.writer(self.allocator).print("pub const {s} = {s}.{s};\n", .{ alias_decl.name, alias_decl.import_name, alias_decl.target_name, }); } 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 { for (self.decls) |resolved| { if (!try self.isOwnedDecl(resolved)) continue; switch (resolved.decl) { .opaque_type => |opaque_decl| try self.writeOpaqueWithMethods(opaque_decl), .typedef_decl => |typedef_decl| try self.writeTypedef(typedef_decl), .function_pointer_decl => |func_ptr_decl| try self.writeFunctionPointer(func_ptr_decl), .c_type_alias => |alias| try self.writeCTypeAlias(alias), .enum_decl => |enum_decl| try self.writeEnum(enum_decl), .struct_decl => |struct_decl| try self.writeStruct(struct_decl), .union_decl => |union_decl| try self.writeUnion(union_decl), .flag_decl => |flag_decl| try self.writeFlags(flag_decl), .function_decl => |func| { if (try self.isStandaloneFunction(func)) { try self.writeFunction(func); } }, } } } fn isStandaloneFunction(self: *CodeGen, func: patterns.FunctionDecl) !bool { if (func.params.len == 0) return true; const first_param_type = try types.convertType(func.params[0].type_name, self.allocator); defer self.allocator.free(first_param_type); var it = self.opaque_methods.keyIterator(); while (it.next()) |opaque_name| { const opt_ptr = try std.fmt.allocPrint(self.allocator, "?*{s}", .{opaque_name.*}); defer self.allocator.free(opt_ptr); const ptr = try std.fmt.allocPrint(self.allocator, "*{s}", .{opaque_name.*}); defer self.allocator.free(ptr); if (std.mem.eql(u8, first_param_type, opt_ptr) or std.mem.eql(u8, first_param_type, ptr)) return false; } return true; } fn writeOpaqueWithMethods(self: *CodeGen, opaque_type: OpaqueType) !void { const zig_name = naming.typeNameToZig(opaque_type.name); if (opaque_type.doc_comment) |doc| try self.writeDocComment(doc); if (self.opaque_methods.get(zig_name)) |method_list| { if (method_list.items.len > 0) { try self.output.writer(self.allocator).print("pub const {s} = opaque {{\n", .{zig_name}); for (method_list.items) |func| { try self.writeFunctionAsMethod(func, zig_name); } try self.output.appendSlice(self.allocator, "};\n\n"); return; } } try self.output.writer(self.allocator).print("pub const {s} = opaque {{}};\n\n", .{zig_name}); } fn writeTypedef(self: *CodeGen, typedef_decl: patterns.TypedefDecl) !void { if (typedef_decl.doc_comment) |doc| try self.writeDocComment(doc); const zig_name = naming.typeNameToZig(typedef_decl.name); const zig_type = try types.convertType(typedef_decl.underlying_type, self.allocator); defer self.allocator.free(zig_type); try self.output.appendSlice(self.allocator, "pub const "); try self.output.appendSlice(self.allocator, zig_name); try self.output.appendSlice(self.allocator, " = "); try self.output.appendSlice(self.allocator, zig_type); try self.output.appendSlice(self.allocator, ";\n\n"); } fn writeFunctionPointer(self: *CodeGen, func_ptr_decl: patterns.FunctionPointerDecl) !void { if (func_ptr_decl.doc_comment) |doc| try self.writeDocComment(doc); const zig_name = naming.typeNameToZig(func_ptr_decl.name); const return_type = try types.convertType(func_ptr_decl.return_type, self.allocator); defer self.allocator.free(return_type); try self.output.writer(self.allocator).print("pub const {s} = *const fn(", .{zig_name}); for (func_ptr_decl.params, 0..) |param, i| { if (i > 0) try self.output.appendSlice(self.allocator, ", "); const param_type = try types.convertType(param.type_name, self.allocator); defer self.allocator.free(param_type); try self.output.writer(self.allocator).print("{s}: {s}", .{ param.name, param_type }); } try self.output.writer(self.allocator).print(") callconv(.C) {s};\n\n", .{return_type}); } fn writeCTypeAlias(self: *CodeGen, alias: patterns.CTypeAlias) !void { if (alias.doc_comment) |doc| try self.writeDocComment(doc); const zig_name = naming.typeNameToZig(alias.name); try self.output.writer(self.allocator).print("pub const {s} = c.{s};\n\n", .{ zig_name, alias.name }); } fn enumShouldBeFlags(self: *CodeGen, enum_decl: EnumDecl) bool { _ = self; for (enum_decl.values) |value| { if (value.value) |val| { if (std.mem.indexOf(u8, val, "|") != null) return true; } } return false; } fn convertEnumToFlags(self: *CodeGen, enum_decl: EnumDecl) !FlagDecl { var flags_list: std.ArrayList(patterns.FlagValue) = .empty; errdefer flags_list.deinit(self.allocator); var implicit_value: u32 = 0; for (enum_decl.values) |value| { const current_value: ?u32 = if (value.value) |val| blk: { const trimmed = std.mem.trim(u8, val, " \t"); if (std.mem.indexOf(u8, trimmed, "|") != null) break :blk null; const parsed = std.fmt.parseInt(u32, trimmed, 0) catch break :blk null; implicit_value = parsed + 1; break :blk parsed; } else blk: { const val = implicit_value; implicit_value += 1; break :blk val; }; if (current_value) |val| { if (val == 0) continue; if (val > 0 and (val & (val - 1)) == 0) { try flags_list.append(self.allocator, .{ .name = try self.allocator.dupe(u8, value.name), .value = try std.fmt.allocPrint(self.allocator, "(1u << {d})", .{@ctz(val)}), .comment = if (value.comment) |c| try self.allocator.dupe(u8, c) else null, }); } } } return .{ .name = try self.allocator.dupe(u8, enum_decl.name), .underlying_type = try self.allocator.dupe(u8, "Uint32"), .flags = try flags_list.toOwnedSlice(self.allocator), .constants = &.{}, .doc_comment = if (enum_decl.doc_comment) |doc| try self.allocator.dupe(u8, doc) else null, }; } fn writeEnum(self: *CodeGen, enum_decl: EnumDecl) !void { if (enum_decl.values.len == 0) return; if (self.enumShouldBeFlags(enum_decl)) { const flag_decl = try self.convertEnumToFlags(enum_decl); defer flag_decl.deinit(self.allocator); return self.writeFlags(flag_decl); } const zig_name = naming.typeNameToZig(enum_decl.name); if (enum_decl.doc_comment) |doc| try self.writeDocComment(doc); try self.output.writer(self.allocator).print("pub const {s} = enum(c_int) {{\n", .{zig_name}); const value_names = try self.allocator.alloc([]const u8, enum_decl.values.len); defer self.allocator.free(value_names); for (enum_decl.values, 0..) |value, i| value_names[i] = value.name; const prefix = try naming.detectCommonPrefix(value_names, self.allocator); defer self.allocator.free(prefix); var identifier_to_value = std.StringHashMap([]const u8).init(self.allocator); defer identifier_to_value.deinit(); var value_to_name = std.StringHashMap([]const u8).init(self.allocator); defer value_to_name.deinit(); for (enum_decl.values) |value| { if (value.value) |explicit_value| { const clean_value = if (std.mem.endsWith(u8, explicit_value, "u")) explicit_value[0 .. explicit_value.len - 1] else explicit_value; try identifier_to_value.put(value.name, clean_value); } } const resolved_values = try self.allocator.alloc(?[]const u8, enum_decl.values.len); defer self.allocator.free(resolved_values); for (enum_decl.values, 0..) |value, i| { if (value.value) |explicit_value| { const clean_value = if (std.mem.endsWith(u8, explicit_value, "u")) explicit_value[0 .. explicit_value.len - 1] else explicit_value; if (clean_value.len > 0 and !std.ascii.isDigit(clean_value[0])) { resolved_values[i] = identifier_to_value.get(clean_value) orelse clean_value; } else { resolved_values[i] = clean_value; } } else { resolved_values[i] = null; } } const EnumAlias = struct { name: []const u8, target: []const u8, comment: ?[]const u8 }; var aliases: std.ArrayList(EnumAlias) = .empty; defer aliases.deinit(self.allocator); for (enum_decl.values, 0..) |value, i| { const zig_value = try naming.enumValueToZig(value.name, prefix, self.allocator); defer self.allocator.free(zig_value); if (resolved_values[i]) |numeric_value| { if (value_to_name.get(numeric_value)) |first_name| { try aliases.append(self.allocator, .{ .name = try self.allocator.dupe(u8, zig_value), .target = try naming.enumValueToZig(first_name, prefix, self.allocator), .comment = if (value.comment) |c| try self.allocator.dupe(u8, c) else null, }); } else { try value_to_name.put(numeric_value, value.name); if (value.comment) |comment| { try self.output.writer(self.allocator).print(" {s} = {s}, //{s}\n", .{ zig_value, numeric_value, comment }); } else { try self.output.writer(self.allocator).print(" {s} = {s},\n", .{ zig_value, numeric_value }); } } } else if (value.comment) |comment| { try self.output.writer(self.allocator).print(" {s}, //{s}\n", .{ zig_value, comment }); } else { try self.output.writer(self.allocator).print(" {s},\n", .{zig_value}); } } if (aliases.items.len > 0) { try self.output.appendSlice(self.allocator, "\n"); for (aliases.items) |alias_decl| { if (alias_decl.comment) |comment| { try self.output.writer(self.allocator).print(" pub const {s} = .{s}; //{s}\n", .{ alias_decl.name, alias_decl.target, comment }); } else { try self.output.writer(self.allocator).print(" pub const {s} = .{s};\n", .{ alias_decl.name, alias_decl.target }); } self.allocator.free(alias_decl.name); self.allocator.free(alias_decl.target); if (alias_decl.comment) |comment| self.allocator.free(comment); } } try self.output.appendSlice(self.allocator, "};\n\n"); } fn writeStruct(self: *CodeGen, struct_decl: StructDecl) !void { const zig_name = naming.typeNameToZig(struct_decl.name); if (struct_decl.doc_comment) |doc| try self.writeDocComment(doc); if (struct_decl.has_unions) { try self.output.writer(self.allocator).print("pub const {s} = opaque {{}};\n\n", .{zig_name}); return; } try self.output.writer(self.allocator).print("pub const {s} = extern struct {{\n", .{zig_name}); for (struct_decl.fields) |field| { 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 }); } else { try self.output.writer(self.allocator).print(" {s}: {s},\n", .{ field.name, zig_type }); } } try self.output.appendSlice(self.allocator, "};\n\n"); } fn writeUnion(self: *CodeGen, union_decl: patterns.UnionDecl) !void { const zig_name = naming.typeNameToZig(union_decl.name); 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 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 }); } else { try self.output.writer(self.allocator).print(" {s}: {s},\n", .{ field.name, zig_type }); } } try self.output.appendSlice(self.allocator, "};\n\n"); } fn writeFlags(self: *CodeGen, flag_decl: FlagDecl) !void { const zig_name = naming.typeNameToZig(flag_decl.name); if (flag_decl.doc_comment) |doc| try self.writeDocComment(doc); const underlying_type = if (std.mem.eql(u8, flag_decl.underlying_type, "Uint8")) "u8" else if (std.mem.eql(u8, flag_decl.underlying_type, "Uint16")) "u16" else if (std.mem.eql(u8, flag_decl.underlying_type, "Uint64")) "u64" else "u32"; const type_bits: u32 = if (std.mem.eql(u8, underlying_type, "u8")) 8 else if (std.mem.eql(u8, underlying_type, "u16")) 16 else if (std.mem.eql(u8, underlying_type, "u64")) 64 else 32; try self.output.writer(self.allocator).print("pub const {s} = packed struct({s}) {{\n", .{ zig_name, underlying_type }); const flag_names = try self.allocator.alloc([]const u8, flag_decl.flags.len); defer self.allocator.free(flag_names); for (flag_decl.flags, 0..) |flag, i| flag_names[i] = flag.name; const prefix = try naming.detectCommonPrefix(flag_names, self.allocator); defer self.allocator.free(prefix); var used_bits = std.bit_set.IntegerBitSet(64).initEmpty(); for (flag_decl.flags) |flag| { const zig_flag = try naming.flagNameToZig(flag.name, prefix, self.allocator); defer self.allocator.free(zig_flag); const bit_pos = self.parseBitPosition(flag.value) catch continue; used_bits.set(bit_pos); if (flag.comment) |comment| { try self.output.writer(self.allocator).print(" {s}: bool = false, // {s}\n", .{ zig_flag, comment }); } else { try self.output.writer(self.allocator).print(" {s}: bool = false,\n", .{zig_flag}); } } const padding_bits = type_bits - used_bits.count() - 1; if (padding_bits > 0) { try self.output.writer(self.allocator).print(" pad0: u{d} = 0,\n", .{padding_bits}); } try self.output.appendSlice(self.allocator, " rsvd: bool = false,\n\n"); try self.output.writer(self.allocator).print(" pub const None = {s}{{}};\n", .{zig_name}); for (flag_decl.constants) |constant| { const zig_const_camel = try naming.flagNameToZig(constant.name, prefix, self.allocator); defer self.allocator.free(zig_const_camel); const zig_const = try self.allocator.dupe(u8, zig_const_camel); defer self.allocator.free(zig_const); if (zig_const.len > 0) zig_const[0] = std.ascii.toUpper(zig_const[0]); if (constant.comment) |comment| { try self.output.writer(self.allocator).print(" pub const {s}: {s} = @bitCast(@as({s}, {s})); // {s}\n", .{ zig_const, zig_name, underlying_type, constant.value, comment }); } else { try self.output.writer(self.allocator).print(" pub const {s}: {s} = @bitCast(@as({s}, {s}));\n", .{ zig_const, zig_name, underlying_type, constant.value }); } } try self.output.appendSlice(self.allocator, "};\n\n"); } fn writeFunctionAsMethod(self: *CodeGen, func: patterns.FunctionDecl, owner_type: []const u8) !void { const zig_name = try naming.functionNameToZig(func.name, self.allocator); defer self.allocator.free(zig_name); if (func.doc_comment) |doc| try self.writeDocComment(doc); const zig_return_type = try types.convertType(func.return_type, self.allocator); defer self.allocator.free(zig_return_type); try self.output.writer(self.allocator).print(" pub inline fn {s}(", .{zig_name}); for (func.params, 0..) |param, i| { const zig_type = try self.convertFunctionParamType(func, param); defer self.allocator.free(zig_type); if (i > 0) try self.output.appendSlice(self.allocator, ", "); if (i == 0) { const lower_name = try std.ascii.allocLowerString(self.allocator, owner_type); defer self.allocator.free(lower_name); const non_nullable = if (std.mem.startsWith(u8, zig_type, "?*")) zig_type[1..] else zig_type; try self.output.writer(self.allocator).print("{s}: {s}", .{ lower_name, non_nullable }); } else if (param.name.len > 0) { try self.output.writer(self.allocator).print("{s}: {s}", .{ param.name, zig_type }); } else { try self.output.writer(self.allocator).print("{s}", .{zig_type}); } } 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.getReturnCastType(zig_return_type) else .none; if (return_cast != .none) { try self.output.writer(self.allocator).print("{s}(", .{castTypeToString(return_cast)}); } try self.output.writer(self.allocator).print("c.{s}(", .{func.name}); for (func.params, 0..) |param, i| { if (i > 0) try self.output.appendSlice(self.allocator, ", "); if (param.name.len > 0 or i == 0) { const param_name = if (i == 0) blk: { const lower = try std.ascii.allocLowerString(self.allocator, owner_type); defer self.allocator.free(lower); break :blk try self.allocator.dupe(u8, lower); } else try self.allocator.dupe(u8, param.name); defer self.allocator.free(param_name); const zig_param_type = try self.convertFunctionParamType(func, param); defer self.allocator.free(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}); } else { try self.output.writer(self.allocator).print("{s}({s})", .{ castTypeToString(param_cast), param_name }); } } } if (return_cast != .none) { try self.output.appendSlice(self.allocator, "));\n"); } else { try self.output.appendSlice(self.allocator, ");\n"); } try self.output.appendSlice(self.allocator, " }\n\n"); } fn writeFunction(self: *CodeGen, func: patterns.FunctionDecl) !void { const zig_name = try naming.functionNameToZig(func.name, self.allocator); defer self.allocator.free(zig_name); if (func.doc_comment) |doc| try self.writeDocComment(doc); const zig_return_type = try types.convertType(func.return_type, self.allocator); defer self.allocator.free(zig_return_type); try self.output.writer(self.allocator).print("pub inline fn {s}(", .{zig_name}); for (func.params, 0..) |param, i| { 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) { try self.output.writer(self.allocator).print("{s}: {s}", .{ param.name, zig_type }); } else { try self.output.writer(self.allocator).print("{s}", .{zig_type}); } } 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.getReturnCastType(zig_return_type) else .none; if (return_cast != .none) { try self.output.writer(self.allocator).print("{s}(", .{castTypeToString(return_cast)}); } try self.output.writer(self.allocator).print("c.{s}(", .{func.name}); 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 self.convertFunctionParamType(func, param); defer self.allocator.free(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}); } else { try self.output.writer(self.allocator).print("{s}({s})", .{ castTypeToString(param_cast), param.name }); } } } if (return_cast != .none) { try self.output.appendSlice(self.allocator, "));\n"); } else { try self.output.appendSlice(self.allocator, ");\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 { return switch (cast_type) { .none => "none", .ptr_cast => "@ptrCast", .bit_cast => "@bitCast", .int_from_enum => "@intFromEnum", .enum_from_int => "@enumFromInt", }; } fn writeDocComment(self: *CodeGen, comment: []const u8) !void { _ = self; _ = comment; } fn parseBitPosition(self: *CodeGen, value: []const u8) !u6 { _ = self; var trimmed = std.mem.trim(u8, value, " \t()"); if (std.mem.startsWith(u8, trimmed, "SDL_UINT64_C(")) { trimmed = std.mem.trim(u8, trimmed["SDL_UINT64_C(".len..], " \t)"); } if (trimmed.len > 0 and (trimmed[trimmed.len - 1] == 'u' or trimmed[trimmed.len - 1] == 'U')) { trimmed = trimmed[0 .. trimmed.len - 1]; } if (std.mem.indexOf(u8, trimmed, "<<")) |shift_pos| { const after_shift = std.mem.trim(u8, trimmed[shift_pos + 2 ..], " \t)"); return try std.fmt.parseInt(u6, after_shift, 10); } if (std.mem.startsWith(u8, trimmed, "0x")) { const val = try std.fmt.parseInt(u64, trimmed[2..], 16); var bit: u7 = 0; while (bit < 64) : (bit += 1) { if (val == (@as(u64, 1) << @as(u6, @intCast(bit)))) return @intCast(bit); } } if (std.fmt.parseInt(u64, trimmed, 10)) |val| { if (val == 0) return error.InvalidBitPosition; var bit: u7 = 0; while (bit < 64) : (bit += 1) { if (val == (@as(u64, 1) << @as(u6, @intCast(bit)))) return @intCast(bit); } return error.InvalidBitPosition; } else |_| {} return error.InvalidBitPosition; } }; test "generated module aliases foreign shared types" { const allocator = std.testing.allocator; var decls: std.ArrayList(ResolvedDecl) = .empty; defer { for (decls.items) |decl| decl.deinit(allocator); decls.deinit(allocator); } const method_params = try allocator.alloc(patterns.ParamDecl, 2); method_params[0] = .{ .name = try allocator.dupe(u8, "device"), .type_name = try allocator.dupe(u8, "SDL_GPUDevice *") }; method_params[1] = .{ .name = try allocator.dupe(u8, "window"), .type_name = try allocator.dupe(u8, "SDL_Window *") }; try decls.append(allocator, .{ .decl = .{ .opaque_type = .{ .name = try allocator.dupe(u8, "SDL_GPUDevice"), .doc_comment = null } }, .source_header = try allocator.dupe(u8, "SDL_gpu.h"), .is_primary_header_decl = true, }); try decls.append(allocator, .{ .decl = .{ .function_decl = .{ .name = try allocator.dupe(u8, "SDL_WaitForWindow"), .return_type = try allocator.dupe(u8, "void"), .params = method_params, .doc_comment = null, } }, .source_header = try allocator.dupe(u8, "SDL_gpu.h"), .is_primary_header_decl = true, }); try decls.append(allocator, .{ .decl = .{ .opaque_type = .{ .name = try allocator.dupe(u8, "SDL_Window"), .doc_comment = null } }, .source_header = try allocator.dupe(u8, "SDL_video.h"), .is_primary_header_decl = false, }); 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, "const video_api = @import(\"video.zig\");") != null); try std.testing.expect(std.mem.indexOf(u8, output, "pub const Window = video_api.Window;") != null); try std.testing.expect(std.mem.indexOf(u8, output, "pub const c = @import(\"../c.zig\").c;") != null); try std.testing.expect(std.mem.indexOf(u8, output, "pub const Window = opaque") == null); } test "primary declarations are emitted without self imports" { 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_Window"), .doc_comment = null } }, .source_header = try allocator.dupe(u8, "SDL_video.h"), .is_primary_header_decl = true, }); const output = try CodeGen.generate(allocator, .{ .decls = decls.items, .module_name = "video", }); defer allocator.free(output); 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); }