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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

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.
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 zphysics library by removing deprecated usingnamespace usage and introducing a new interface system that uses struct composition with compile-time vtable generation. The changes also rework ID types to use structs instead of plain integers and add new body activation/deactivation functions.

  • Remove usingnamespace (deprecated in Zig master) and replace with a new interface system using struct composition
  • Change JPC_BodyID and JPC_SubShapeID from plain uint32_t to wrapper structs with an id field
  • Add new activateBodies/deactivateBodies functions and enhance debug renderer functionality

Reviewed Changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
libs/JoltC/JoltPhysicsC_Tests.c Update test code to use new struct-based ID format with .id field access
libs/JoltC/JoltPhysicsC_Extensions.cpp Modify body ID assignment to use struct field syntax
libs/JoltC/JoltPhysicsC.h Define new struct-based ID types and add new activation functions
libs/JoltC/JoltPhysicsC.cpp Implement ID conversion functions and new activation APIs
build.zig.zon Add minimum Zig version requirement
build.zig Modernize build script using newer Zig build system APIs

Comment on lines +305 to +306
#define JPC_ID_EQ(a, b) (a.id == b.id)

Copy link
Preview

Copilot AI Jul 20, 2025

Choose a reason for hiding this comment

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

The macro JPC_ID_EQ assumes both parameters have an 'id' field but lacks type safety. Consider using inline functions with proper parameter types to ensure type safety and provide better error messages when used incorrectly.

Suggested change
#define JPC_ID_EQ(a, b) (a.id == b.id)
static inline bool JPC_ID_EQ(JPC_BodyID a, JPC_BodyID b) {
return a.id == b.id;
}
static inline bool JPC_SubShapeID_EQ(JPC_SubShapeID a, JPC_SubShapeID b) {
return a.id == b.id;
}

Copilot uses AI. Check for mistakes.

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)
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