Skip to content

Remove usingnamespace, add new interface system, rework IDs, and misc fixes #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 24, 2025

Conversation

kcbanner
Copy link
Collaborator

@kcbanner kcbanner commented Jul 20, 2025

usingnamespace has been removed in zig master, which motivated the bulk of this change.

  • Introduce a new system for interfaces:
    • They are defined as structs that are composed as the first member of the implementation
    • initInterface is used to build VTable default values at comptime automatically
    • By convention, interface definitions contain an init function which calls initInterface with the appropriate types
    • This allows method to be created on the interface struct, that call into the vtable. However, in practice most of these functions are called by Jolt itself, and so I've omitted the equivalent of what Methods was doing before in most cases.
  • Use structs for JPC_BodyID and JPC_SubShapeID
  • Use non-exhaustive enums for BodyId and SubShapeId
  • Add activateBodies/deactivateBodies
  • Enhance the DebugRenderer test to verify that the render functions are called
  • Change the signature of DestroyTriangleBatch to pass non-const user pointer

The ID change was motivated by needing to implement toJph for JPC_BodyID, which wasn't possible without making the IDs distinct types.

I spent some time working out different ways to replace the usingnamespace usage, and I'm pretty happy with what I landed on (it's somewhat similar to the Mixin example here: ziglang/zig#20663). If there are existing projects where this new system isn't workable, please post your use case here.

Upgrade guide:

Calling Super-class Methods

                    const decorated_settings = try zphys.DecoratedShapeSettings.createRotatedTranslated(=
                        @ptrCast(shape_settings),
                        zm.qidentity(),
                        translation,
                    );

-                   defer decorated_settings.release();
+                   defer decorated_settings.asShapeSettings().release();

-                  break :shape try decorated_settings.createShape();
+                  break :shape try decorated_settings.asShapeSettings().createShape();

Interfaces

Before:

/// Definition
pub const StreamOut = extern struct {
    __v: *const VTable,

    pub fn Methods(comptime T: type) type {
        return extern struct {
            pub inline fn writeBytes(self: *T, data: [*]const u8, num_bytes: usize) u32 {
                return @as(*StreamOut.VTable, @ptrCast(self.__v))
                    .writeBytes(@as(*StreamOut, @ptrCast(self)), data, num_bytes);
            }
            pub inline fn isFailed(self: *const T) bool {
                return @as(*const StreamOut.VTable, @ptrCast(self.__v))
                    .isFailed(@as(*const StreamOut, @ptrCast(self)));
            }
        };
    }

    pub const VTable = extern struct {
        __header: VTableHeader = .{},
        writeBytes: *const fn (self: *StreamOut, data: [*]const u8, num_bytes: usize) callconv(.C) void,
        isFailed: *const fn (self: *StreamOut) callconv(.C) bool,
    };

    comptime {
        assert(@sizeOf(VTable) == @sizeOf(c.JPC_StreamOutVTable));
    }
};

/// Implementation
pub const AnyWriterStreamOut = extern struct {
    usingnamespace StreamOut.Methods(@This());
    __v: *const StreamOut.VTable = &vtable,
    writer: *const std.io.AnyWriter,
    failed: bool = false,

    const vtable = StreamOut.VTable{
        .writeBytes = _writeBytes,
        .isFailed = _isFailed,
    };

    pub fn init(writer: *const std.io.AnyWriter) AnyWriterStreamOut {
        return .{ .writer = writer };
    }

    fn _writeBytes(iself: *StreamOut, data: [*]const u8, num_bytes: usize) callconv(.C) void {
        const self = @as(*AnyWriterStreamOut, @ptrCast(iself));
        self.writer.writeAll(data[0..num_bytes]) catch {
            self.failed = true;
        };
    }

    fn _isFailed(iself: *StreamOut) callconv(.C) bool {
        const self = @as(*AnyWriterStreamOut, @ptrCast(iself));
        return self.failed;
    }
};

After:

/// Definition
pub const StreamOut = extern struct {
    __v: *const VTable,

    const VTable = extern struct {
        __header: VTableHeader = .{},
        writeBytes: *const fn (self: *StreamOut, data: [*]const u8, num_bytes: usize) callconv(.c) void,
        isFailed: *const fn (self: *StreamOut) callconv(.c) bool,
    };

    pub fn init(comptime T: type) StreamOut {
        return .{ .__v = initInterface(T, VTable) };
    }

    pub fn writeBytes(self: *StreamOut, data: [*]const u8, num_bytes: usize) void {
        self.__v.writeBytes(self, data, num_bytes);
    }

    pub fn isFailed(self: *StreamOut) bool {
        return self.__v.isFailed(self);
    }

    comptime {
        assert(@sizeOf(VTable) == @sizeOf(c.JPC_StreamOutVTable));
    }
};

/// Implementation
pub const AnyWriterStreamOut = extern struct {
    stream_out: StreamOut = .init(@This()),
    writer: *const std.io.AnyWriter,
    failed: bool = false,

    pub fn init(writer: *const std.io.AnyWriter) AnyWriterStreamOut {
        return .{ .writer = writer };
    }

    pub fn writeBytes(stream_out: *StreamOut, data: [*]const u8, num_bytes: usize) callconv(.c) void {
        const self: *AnyWriterStreamOut = @alignCast(@fieldParentPtr("stream_out", stream_out));
        self.writer.writeAll(data[0..num_bytes]) catch {
            self.failed = true;
        };
    }

    pub fn isFailed(stream_out: *StreamOut) callconv(.c) bool {
        const self: *AnyWriterStreamOut = @alignCast(@fieldParentPtr("stream_out", stream_out));
        return self.failed;
    }
};

There is no longer a need to build the vtable yourself, as this is done at comptime by iterating the methods on the implementation.

You can also pass the first member of the implementation directly to the functions that take that interface, without any additional casting.

If you forget / typo the name of a non-optional vtable function, you get a helpful compile-time error:

src\zphysics.zig:143:25: error: zphysics.test_cb1.MyBroadphaseLayerInterface is missing `pub fn getBroadPhaseLayer`: *const fn (*const zphysics.BroadPhaseLayerInterface, u16) callconv(.c) u8
                        @compileError(@typeName(T) ++ " is missing `pub fn " ++ field.name ++ "`: " ++ @typeName(@TypeOf(@field(vtable, field.name))));
                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src\zphysics.zig:258:39: note: called at comptime here
        return .{ .__v = initInterface(T, VTable) };
                         ~~~~~~~~~~~~~^~~~~~~~~~~
src\zphysics.zig:4608:52: note: called at comptime here
        interface: BroadPhaseLayerInterface = .init(@This()),
                                              ~~~~~^~~~~~~~~

- Use non-exhaustive enums for BodyId and SubShapeId
- Add activateBodies/deactivateBodies

The ID change was motivated by needing to implement toJph for
JPC_BodyID, which wasn't possible without making the IDs distinct
types.
Copilot

This comment was marked as outdated.

Copy link
Member

@hazeycode hazeycode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated physics_test_wgpu sample app to test. Works well. Nice!

@hazeycode
Copy link
Member

I've also updated the monolith sample app. It works great but I tripped up on something for a while...
If we implement a method but forget to mark it pub then it silently fails and results in non-obvious runtime effects.

kcbanner and others added 3 commits July 21, 2025 22:59
- Introduce a new system for interfaces:
  - They are defined as structs that are composed as the first member of the implementation
  - `initInterface` is used to build VTable default values at comptime automatically
- Enhance the DebugRenderer test to verify that the render functions are called
- Change the signature of DestroyTriangleBatch to pass non-const user pointer
- Make all vtable functions non-optional, to improve error messages when `pub` is missing
- Fix up test output when PRINT_OUTPUT is enabled
- Move defines to a common build function
- Add -Dverbose to set PRINT_OUTPUT
…lease enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.

- Remove the vtable header from ContactListener (it's called manually, not used as an actual vtable)
@hazeycode hazeycode requested a review from Copilot July 24, 2025 00:01
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR modernizes the codebase by removing the deprecated usingnamespace feature and introducing a new interface system based on struct composition, while also reworking IDs to use distinct struct types and adding various fixes and enhancements.

  • Replaces usingnamespace with a new interface system using struct composition and vtable initialization
  • Changes JPC_BodyID and JPC_SubShapeID from simple typedefs to struct wrappers with an id field
  • Adds new functions for batch body activation/deactivation and enhances the debug renderer test coverage

Reviewed Changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
libs/JoltC/JoltPhysicsC_Tests.c Updates test code to use new struct-based ID system and adds debug renderer destroy callback
libs/JoltC/JoltPhysicsC_Extensions.cpp Modifies ID assignment to use struct field access
libs/JoltC/JoltPhysicsC.h Defines new ID struct types and adds batch activation/deactivation functions
libs/JoltC/JoltPhysicsC.cpp Implements ID struct conversions and new batch body operations
build.zig.zon Sets minimum Zig version requirement
build.zig Refactors build configuration and adds verbose test option
.zigversion Removes fixed version file
.github/workflows/main.yml Updates CI to use latest Zig setup action

@hazeycode
Copy link
Member

hazeycode commented Jul 24, 2025

I tested this against the sample apps and now I get helpful errors as expected for missing or non-pub methods. Nice work!

@hazeycode hazeycode merged commit cfa9464 into zig-gamedev:main Jul 24, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants