can parse all major header files now tested with Dofficial and castholm repos

This commit is contained in:
peterino2 2026-01-25 13:43:46 -08:00
parent 942e577775
commit aaa8ffba47
10 changed files with 153 additions and 348 deletions

56
AGENTS.md Normal file
View File

@ -0,0 +1,56 @@
# SDL3 Header Parser - Agent Documentation
## Project Overview
This is an SDL3 header parser that converts C header files into Zig interfaces, JSON objects, and C mocks. Built specifically for SDL3 headers (developed against SDL 3.2.10), it focuses on generating Zig APIs.
**Key Points:**
- Not a real C parser - best-effort text transformation specifically for SDL3 headers
- Does not do proper AST elaboration or tokenization
- Simple to modify but brittle
- Requires Zig 0.15.2 and git
## Architecture
The codebase consists of several modules:
- `parser.zig` - Main entry point and orchestration
- `patterns.zig` - SDL3 pattern matching and declaration scanning
- `codegen.zig` - Zig code generation
- `mock_codegen.zig` - C mock generation
- `json_serializer.zig` - JSON output generation
- `dependency_resolver.zig` - Type dependency resolution
- `header_cache.zig` - Header caching for cross-file type resolution
- `io.zig` - Error handling utilities
- `types.zig` - Type mapping and conversions
- `naming.zig` - Naming conventions
## Developing
### Error Handling
**CRITICAL:** Whenever actual errors are encountered (parsing failures, invalid declarations, missing types, etc.), you MUST call `io.emitError` to report them properly. Do NOT use `std.debug.print` for actual errors.
```zig
// Good - proper error reporting
try io.emitError(allocator, "Failed to parse struct field: {s}", .{field_name});
// Bad - don't use for errors
std.debug.print("Error: Failed to parse...\n", .{});
```
### Validation Process
After generating Zig code:
1. Output written to `tmp/` directory
2. `zig ast-check` runs on the temporary file
3. If passes: `zig fmt` runs, then copied to final destination
4. If fails: debug output written to `debug/` with `_fmterror.zig` suffix
### Memory Management
- Two cleanup functions exist with identical logic:
- `freeDecls()` - frees a slice of declarations
- `freeDeclDeep()` - frees a single declaration
- Both free all nested allocations (names, types, doc comments, params/fields/values/flags)
### Main APIs (Priority)
Focus areas: gpu, video, gamepad, joystick, input, event
Anything beyond these is not actively maintained.

View File

@ -9,34 +9,6 @@
"unions": [],
"flags": [],
"functions": [
{
"name": "SDL_SetError",
"return_type": "bool",
"parameters": [
{
"name": "fmt",
"type": "const char *"
},
{
"name": "",
"type": "..."
}
]
},
{
"name": "SDL_SetErrorV",
"return_type": "bool",
"parameters": [
{
"name": "fmt",
"type": "const char *"
},
{
"name": "ap",
"type": "va_list"
}
]
},
{
"name": "SDL_OutOfMemory",
"return_type": "bool",

View File

@ -1638,32 +1638,6 @@
"type": "const char *"
}
]
},
{
"name": "SDL_RenderDebugTextFormat",
"return_type": "bool",
"parameters": [
{
"name": "renderer",
"type": "SDL_Renderer *"
},
{
"name": "x",
"type": "float"
},
{
"name": "y",
"type": "float"
},
{
"name": "fmt",
"type": "const char *"
},
{
"name": "",
"type": "..."
}
]
}
]
}

View File

@ -49,14 +49,6 @@ pub const DependencyResolver = struct {
return try missing.toOwnedSlice(allocator);
}
/// Get hardcoded declarations for special types that can't be resolved from headers
/// Currently unused - function pointer typedefs are now handled as c_type_alias in patterns
pub fn getHardcodedDeclarations(self: *DependencyResolver, allocator: Allocator) ![]Declaration {
_ = self;
var hardcoded = std.ArrayList(Declaration){};
return try hardcoded.toOwnedSlice(allocator);
}
fn collectDefinedTypes(self: *DependencyResolver, decls: []const Declaration) !void {
for (decls) |decl| {
const type_name = switch (decl) {

View File

@ -11,4 +11,14 @@ pub fn initWriter() void {
stdout = &stdout_writer.interface;
}
pub fn emitError(src: std.builtin.SourceLocation, errorMessage: []const u8) void {
std.debug.print("----- Error occured -----\n", .{});
std.debug.print("in {s}:{d} module: {s}", .{ src.file, src.line, src.module });
std.debug.print("function: {s}", .{src.fn_name});
std.debug.print("message: '{s}'", .{errorMessage});
std.debug.dumpCurrentStackTrace(null);
std.debug.print("---- /Error occured -----\n", .{});
}
pub const std = @import("std");
const buildOptions = @import("buildOptions");

View File

@ -6,6 +6,13 @@ const json_serializer = @import("json_serializer.zig");
const io = @import("io.zig");
const header_cache = @import("header_cache.zig");
pub fn freeDecls(allocator: std.mem.Allocator, decls: []patterns.Declaration) void {
for (decls) |decl| {
freeDeclDeep(allocator, decl);
}
allocator.free(decls);
}
pub fn main() !void {
const allocator = std.heap.smp_allocator;
@ -74,87 +81,7 @@ pub fn main() !void {
// Parse declarations
var scanner = patterns.Scanner.init(allocator, source);
const decls = try scanner.scan();
defer {
for (decls) |decl| {
switch (decl) {
.opaque_type => |opaque_decl| {
allocator.free(opaque_decl.name);
if (opaque_decl.doc_comment) |doc| allocator.free(doc);
},
.typedef_decl => |typedef_decl| {
allocator.free(typedef_decl.name);
allocator.free(typedef_decl.underlying_type);
if (typedef_decl.doc_comment) |doc| allocator.free(doc);
},
.function_pointer_decl => |func_ptr_decl| {
allocator.free(func_ptr_decl.name);
allocator.free(func_ptr_decl.return_type);
if (func_ptr_decl.doc_comment) |doc| allocator.free(doc);
for (func_ptr_decl.params) |param| {
allocator.free(param.name);
allocator.free(param.type_name);
}
allocator.free(func_ptr_decl.params);
},
.c_type_alias => |alias| {
allocator.free(alias.name);
if (alias.doc_comment) |doc| allocator.free(doc);
},
.enum_decl => |enum_decl| {
allocator.free(enum_decl.name);
if (enum_decl.doc_comment) |doc| allocator.free(doc);
for (enum_decl.values) |val| {
allocator.free(val.name);
if (val.value) |v| allocator.free(v);
if (val.comment) |c| allocator.free(c);
}
allocator.free(enum_decl.values);
},
.struct_decl => |struct_decl| {
allocator.free(struct_decl.name);
if (struct_decl.doc_comment) |doc| allocator.free(doc);
for (struct_decl.fields) |field| {
allocator.free(field.name);
allocator.free(field.type_name);
if (field.comment) |c| allocator.free(c);
}
allocator.free(struct_decl.fields);
},
.union_decl => |union_decl| {
allocator.free(union_decl.name);
if (union_decl.doc_comment) |doc| allocator.free(doc);
for (union_decl.fields) |field| {
allocator.free(field.name);
allocator.free(field.type_name);
if (field.comment) |c| allocator.free(c);
}
allocator.free(union_decl.fields);
},
.flag_decl => |flag_decl| {
allocator.free(flag_decl.name);
allocator.free(flag_decl.underlying_type);
if (flag_decl.doc_comment) |doc| allocator.free(doc);
for (flag_decl.flags) |flag| {
allocator.free(flag.name);
allocator.free(flag.value);
if (flag.comment) |c| allocator.free(c);
}
allocator.free(flag_decl.flags);
},
.function_decl => |func| {
allocator.free(func.name);
allocator.free(func.return_type);
if (func.doc_comment) |doc| allocator.free(doc);
for (func.params) |param| {
allocator.free(param.name);
allocator.free(param.type_name);
}
allocator.free(func.params);
},
}
}
allocator.free(decls);
}
defer freeDecls(allocator, decls);
std.debug.print("Found {d} declarations\n", .{decls.len});
@ -240,17 +167,17 @@ pub fn main() !void {
}
// Get hardcoded declarations for special types (like SDL_HitTest)
const hardcoded_decls = try resolver.getHardcodedDeclarations(allocator);
defer {
for (hardcoded_decls) |hd| {
freeDeclDeep(allocator, hd);
}
allocator.free(hardcoded_decls);
}
// const hardcoded_decls = try resolver.getHardcodedDeclarations(allocator);
// defer {
// for (hardcoded_decls) |hd| {
// freeDeclDeep(allocator, hd);
// }
// allocator.free(hardcoded_decls);
// }
if (hardcoded_decls.len > 0) {
std.debug.print("Adding {d} hardcoded type declarations\n", .{hardcoded_decls.len});
}
// if (hardcoded_decls.len > 0) {
// std.debug.print("Adding {d} hardcoded type declarations\n", .{hardcoded_decls.len});
// }
var dependency_decls = std.ArrayList(patterns.Declaration){};
defer {
@ -332,12 +259,12 @@ pub fn main() !void {
}
// Combine declarations (hardcoded first, then dependencies, then primary!)
std.debug.print("Combining {d} hardcoded + {d} dependency declarations with primary declarations...\n", .{ hardcoded_decls.len, dependency_decls.items.len });
std.debug.print("Combining {d} dependency declarations with primary declarations...\n", .{dependency_decls.items.len});
var all_decls = std.ArrayList(patterns.Declaration){};
defer all_decls.deinit(allocator);
try all_decls.appendSlice(allocator, hardcoded_decls);
// try all_decls.appendSlice(allocator, hardcoded_decls);
try all_decls.appendSlice(allocator, dependency_decls.items);
try all_decls.appendSlice(allocator, decls);

View File

@ -1,4 +1,5 @@
const std = @import("std");
const io = @import("io");
const Allocator = std.mem.Allocator;
fn fixupZigName(name: []const u8) []u8 {
@ -603,7 +604,7 @@ pub const Scanner = struct {
const body = try self.readBracedBlock();
defer self.allocator.free(body);
// Check if struct contains unions - C unions can't be represented in other languages
// Check if struct contains unions - C embedded unions can't be represented in other languages
const has_unions = std.mem.indexOf(u8, body, "union ") != null or
std.mem.indexOf(u8, body, "union{") != null or
std.mem.indexOf(u8, body, "union\n") != null or
@ -876,7 +877,7 @@ pub const Scanner = struct {
const type_parts = parts_list[0 .. parts_count - 1];
// Reconstruct type with array notation
var type_buf: [128]u8 = undefined;
var type_buf: [256]u8 = undefined;
var fbs = std.io.fixedBufferStream(&type_buf);
const writer = fbs.writer();
for (type_parts, 0..) |part, i| {
@ -889,6 +890,7 @@ pub const Scanner = struct {
return null;
};
}
writer.writeAll(bracket_part) catch {
if (comment) |c| self.allocator.free(c);
return null;
@ -1176,12 +1178,15 @@ pub const Scanner = struct {
"SDL_PRINTF_VARARG_FUNCV",
"SDL_WPRINTF_VARARG_FUNC",
"SDL_SCANF_VARARG_FUNC",
};
// these mean nothing in any other language than C, we just blow it away
const clang_acqrel = [_][]const u8{
"SDL_ACQUIRE",
"SDL_RELEASE",
};
for (vararg_macros) |macro| {
for (clang_acqrel) |macro| {
if (std.mem.indexOf(u8, text, macro)) |pos| {
// Find semicolon after this position
if (std.mem.indexOfScalarPos(u8, text, pos, ';')) |semi_pos| {
// Find the closing ) before semicolon
var paren_pos = semi_pos;
@ -1201,6 +1206,13 @@ pub const Scanner = struct {
}
}
for (vararg_macros) |macro| {
if (std.mem.indexOf(u8, text, macro)) |pos| {
_ = pos;
return null; // we dont attempt to generate vararg functions.
}
}
// Find SDLCALL to split return type and function name
const sdlcall_pos = std.mem.indexOf(u8, text, "SDLCALL ") orelse return null;
const return_type_str = std.mem.trim(u8, text[0..sdlcall_pos], " \t\n");
@ -1479,106 +1491,3 @@ pub const Scanner = struct {
return comment;
}
};
test "scan opaque typedef" {
const source = "typedef struct SDL_GPUDevice SDL_GPUDevice;";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var scanner = Scanner.init(allocator, source);
const decls = try scanner.scan();
try std.testing.expectEqual(@as(usize, 1), decls.len);
try std.testing.expect(decls[0] == .opaque_type);
try std.testing.expectEqualStrings("SDL_GPUDevice", decls[0].opaque_type.name);
}
test "scan function declaration" {
const source =
\\extern SDL_DECLSPEC bool SDLCALL SDL_GPUSupportsShaderFormats(
\\ SDL_GPUShaderFormat format_flags,
\\ const char *name);
;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var scanner = Scanner.init(allocator, source);
const decls = try scanner.scan();
try std.testing.expectEqual(@as(usize, 1), decls.len);
try std.testing.expect(decls[0] == .function_decl);
const func = decls[0].function_decl;
try std.testing.expectEqualStrings("SDL_GPUSupportsShaderFormats", func.name);
try std.testing.expectEqualStrings("bool", func.return_type);
}
test "scan flag typedef with newline before defines" {
const source =
\\typedef Uint32 SDL_GPUTextureUsageFlags;
\\
\\#define SDL_GPU_TEXTUREUSAGE_SAMPLER (1u << 0)
\\#define SDL_GPU_TEXTUREUSAGE_COLOR_TARGET (1u << 1)
\\#define SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET (1u << 2)
;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var scanner = Scanner.init(allocator, source);
const decls = try scanner.scan();
try std.testing.expectEqual(@as(usize, 1), decls.len);
try std.testing.expect(decls[0] == .flag_decl);
const flag = decls[0].flag_decl;
try std.testing.expectEqualStrings("SDL_GPUTextureUsageFlags", flag.name);
try std.testing.expectEqualStrings("Uint32", flag.underlying_type);
try std.testing.expectEqual(@as(usize, 3), flag.flags.len);
try std.testing.expectEqualStrings("SDL_GPU_TEXTUREUSAGE_SAMPLER", flag.flags[0].name);
try std.testing.expectEqualStrings("SDL_GPU_TEXTUREUSAGE_COLOR_TARGET", flag.flags[1].name);
try std.testing.expectEqualStrings("SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET", flag.flags[2].name);
}
test "scan flag typedef with multiple blank lines" {
const source =
\\typedef Uint32 SDL_GPUBufferUsageFlags;
\\
\\
\\#define SDL_GPU_BUFFERUSAGE_VERTEX (1u << 0)
\\#define SDL_GPU_BUFFERUSAGE_INDEX (1u << 1)
;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var scanner = Scanner.init(allocator, source);
const decls = try scanner.scan();
try std.testing.expectEqual(@as(usize, 1), decls.len);
try std.testing.expect(decls[0] == .flag_decl);
const flag = decls[0].flag_decl;
try std.testing.expectEqual(@as(usize, 2), flag.flags.len);
}
test "scan flag typedef with comments before defines" {
const source =
\\typedef Uint32 SDL_GPUColorComponentFlags;
\\
\\/* Comment here */
;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var scanner = Scanner.init(allocator, source);
const decls = try scanner.scan();
// Should still parse the typedef even if no #defines follow
try std.testing.expectEqual(@as(usize, 1), decls.len);
try std.testing.expect(decls[0] == .flag_decl);
const flag = decls[0].flag_decl;
try std.testing.expectEqualStrings("SDL_GPUColorComponentFlags", flag.name);
// No flags found, but that's ok
try std.testing.expectEqual(@as(usize, 0), flag.flags.len);
}

View File

@ -1,6 +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 {
@ -21,12 +23,32 @@ pub fn convertType(c_type: []const u8, allocator: Allocator) ![]const u8 {
// 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");
const array_part = trimmed[bracket_pos..]; // "[2]"
var array_part = trimmed[bracket_pos..]; // "[2]"
// Recursively convert the base type
const zig_base = try convertType(base_type, allocator);
defer allocator.free(zig_base);
const inner = array_part[1 .. array_part.len - 1];
if (inner.len > 0) {
var alias_to_c: bool = false;
_ = std.fmt.parseInt(u32, inner, 0) catch {
// not a real error
alias_to_c = true;
};
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});
}
}
// Return Zig array notation: [size]Type
return try std.fmt.allocPrint(allocator, "{s}{s}", .{ array_part, zig_base });
}
@ -271,41 +293,3 @@ pub const CastType = enum {
int_from_enum,
enum_from_int,
};
test "convert primitive types" {
const t1 = try convertType("float", std.testing.allocator);
defer std.testing.allocator.free(t1);
try std.testing.expectEqualStrings("f32", t1);
const t2 = try convertType("Uint32", std.testing.allocator);
defer std.testing.allocator.free(t2);
try std.testing.expectEqualStrings("u32", t2);
const t3 = try convertType("bool", std.testing.allocator);
defer std.testing.allocator.free(t3);
try std.testing.expectEqualStrings("bool", t3);
}
test "convert SDL types" {
const t1 = try convertType("SDL_GPUDevice", std.testing.allocator);
defer std.testing.allocator.free(t1);
try std.testing.expectEqualStrings("GPUDevice", t1);
const t2 = try convertType("SDL_GPUDevice *", std.testing.allocator);
defer std.testing.allocator.free(t2);
try std.testing.expectEqualStrings("?*GPUDevice", t2);
const t3 = try convertType("const SDL_GPUViewport *", std.testing.allocator);
defer std.testing.allocator.free(t3);
try std.testing.expectEqualStrings("*const GPUViewport", t3);
}
test "convert pointer types" {
const t1 = try convertType("const char *", std.testing.allocator);
defer std.testing.allocator.free(t1);
try std.testing.expectEqualStrings("[*c]const u8", t1);
const t2 = try convertType("void *", std.testing.allocator);
defer std.testing.allocator.free(t2);
try std.testing.expectEqualStrings("?*anyopaque", t2);
}

View File

@ -1,16 +1,6 @@
const std = @import("std");
pub const c = @import("c.zig").c;
pub inline fn setError(fmt: [*c]const u8, ...) bool {
return c.SDL_SetError(
fmt,
);
}
pub inline fn setErrorV(fmt: [*c]const u8, ap: std.builtin.VaList) bool {
return c.SDL_SetErrorV(fmt, ap);
}
pub inline fn outOfMemory() bool {
return c.SDL_OutOfMemory();
}

View File

@ -1039,15 +1039,6 @@ pub const Renderer = opaque {
pub inline fn renderDebugText(renderer: *Renderer, x: f32, y: f32, str: [*c]const u8) bool {
return c.SDL_RenderDebugText(renderer, x, y, str);
}
pub inline fn renderDebugTextFormat(renderer: *Renderer, x: f32, y: f32, fmt: [*c]const u8, ...) bool {
return c.SDL_RenderDebugTextFormat(
renderer,
x,
y,
fmt,
);
}
};
pub const Texture = opaque {