sdlparser-scrap/AGENTS.md

8.1 KiB

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

Usage

Building and Running

Generate all headers:

zig build generate

This processes all SDL3 headers and generates corresponding Zig files in the api/ directory.

Generate a single header:

zig build run -- <path-to-header> --output=<output-path>

Example:

zig build run -- sdl3\include\SDL3\SDL_gpu.h --output=api\gpu.zig

The parser converts C headers to Zig files:

  • SDL_gpu.hgpu.zig
  • SDL_video.hvideo.zig
  • SDL_events.hevents.zig

Additional options:

  • --mocks=<path> - Generate C mock implementations
  • --generate-json=<path> - Generate JSON representation
  • --timestamp=<timestamp> - Set custom timestamp
  • --basedir=<directory> - Set base directory for dependency resolution

Output Directories

  • api/ - Generated Zig API files (final output)
  • tmp/ - Temporary files during generation (before validation)
  • debug/ - Debug output when generation fails (with _fmterror.zig suffix)
  • archive/ - Archived previous debug outputs

Developing

Build System Patterns

Custom Build Steps: The project uses custom Zig build steps for operations that need to happen during the build process:

const MakeDirStep = struct {
    step: std.Build.Step,
    dir_path: []const u8,

    pub fn create(b: *std.Build, dir_path: []const u8) *std.Build.Step {
        const self = b.allocator.create(MakeDirStep) catch @panic("OOM");
        self.* = .{
            .step = std.Build.Step.init(.{
                .id = .custom,
                .name = b.fmt("mkdir {s}", .{dir_path}),
                .owner = b,
                .makeFn = make,
            }),
            .dir_path = dir_path,
        };
        return &self.step;
    }

    fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void {
        _ = options;
        const self: *MakeDirStep = @fieldParentPtr("step", step);
        try std.fs.cwd().makePath(self.dir_path);
    }
};

This pattern allows cross-platform directory creation and other filesystem operations during build without relying on shell commands.

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.

// 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", .{});

Type Casting in Generated Code

The code generator automatically adds appropriate casts when calling C functions:

Parameters:

  • Opaque pointers (*Window, ?*GPUDevice) → @ptrCast
  • Enums → @intFromEnum
  • Flags (packed structs) → @bitCast
  • Booleans → @bitCast

Return values:

  • Opaque pointers (?*Window) → @ptrCast
  • Booleans → @bitCast
  • Flags (packed structs) → @bitCast

Pointer Optionality:

  • All pointer parameters in standalone functions are optional (?*const Type) to allow null values
  • The first parameter in methods (the "self" parameter) is non-optional (*Type)
  • Other parameters in methods are optional (?*const Type)

Double Pointers to Opaque Types: C double pointers like SDL_Window ** are converted to [*c]?*Window instead of [*c][*c]Window because:

  1. Zig doesn't allow C pointers ([*c]) to point to opaque types
  2. The inner pointer should be optional since C pointers can be null

The outer pointer remains a C pointer for interop, while the inner pointer is an optional Zig pointer.

Similarly, const void * const * is converted to [*c]const *anyopaque (not [*c]const [*c]const anyopaque).

Example:

// Standalone function - all pointers optional
pub inline fn beginGPURenderPass(
    gpucommandbuffer: *GPUCommandBuffer,
    color_target_infos: ?*const GPUColorTargetInfo,  // optional
    num_color_targets: u32,
    depth_stencil_target_info: ?*const GPUDepthStencilTargetInfo  // optional
) ?*GPURenderPass {
    return @ptrCast(c.SDL_BeginGPURenderPass(...));
}

// Method - self is non-optional, other params are optional
pub inline fn createGPUTexture(
    gpudevice: *GPUDevice,  // non-optional "self"
    createinfo: ?*const GPUTextureCreateInfo  // optional
) ?*GPUTexture {
    return @ptrCast(c.SDL_CreateGPUTexture(@ptrCast(gpudevice), @ptrCast(createinfo)));
}

pub inline fn getWindows(count: *c_int) [*c]?*Window {
    //                                    ^^^^^^^^^^^^ C pointer to optional Zig pointer to opaque type
    return c.SDL_GetWindows(@ptrCast(count));
}

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

Testing all outputs: Use zig build generate to regenerate all SDL3 headers and verify no regressions.

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)

Zig 0.15 Specifics

IMPORTANT: In Zig 0.15, std.ArrayList is an alias to std.ArrayListUnmanaged. This means:

  • ArrayList methods require an explicit allocator parameter
  • Use list.append(allocator, item) instead of list.append(item)
  • Use list.toOwnedSlice(allocator) instead of list.toOwnedSlice()
  • Use list.deinit(allocator) instead of list.deinit()
  • Initialize with std.ArrayList(T){} (empty init)

Example:

var list = std.ArrayList(MyType){};
errdefer list.deinit(allocator);
try list.append(allocator, item);
const slice = try list.toOwnedSlice(allocator);

Main APIs (Priority)

Focus areas: gpu, video, gamepad, joystick, input, event

Anything beyond these is not actively maintained.

Code Generation Features

Enum-to-Flags Conversion

The parser automatically detects enums that contain bitwise OR operations and converts them to packed structs with bool flags.

Example: SDL_FlipMode in SDL_surface.h

// C enum with bitwise OR
typedef enum SDL_FlipMode {
    SDL_FLIP_NONE,                                           // 0
    SDL_FLIP_HORIZONTAL,                                     // 1
    SDL_FLIP_VERTICAL,                                       // 2
    SDL_FLIP_HORIZONTAL_AND_VERTICAL = (SDL_FLIP_HORIZONTAL | SDL_FLIP_VERTICAL)  // 3
} SDL_FlipMode;

Converts to:

// Zig packed struct with bool flags
pub const FlipMode = packed struct(u32) {
    flipHorizontal: bool = false,
    flipVertical: bool = false,
    pad0: u29 = 0,
    rsvd: bool = false,
    
    pub const None = FlipMode{};
};

Usage:

  • FlipMode.None - no flip (matches C's SDL_FLIP_NONE)
  • FlipMode{ .flipHorizontal = true } - horizontal flip
  • FlipMode{ .flipVertical = true } - vertical flip
  • FlipMode{ .flipHorizontal = true, .flipVertical = true } - both (equivalent to SDL_FLIP_HORIZONTAL_AND_VERTICAL)

This approach eliminates the need for explicit combined values and provides a type-safe way to combine flags.