The native layer
Most users should use Box3D.NET and never read this page.
Box3D.NET.Native is the raw P/Invoke layer: a one-to-one mirror of the Box3D C
API, with the same names, the same signatures and no abstraction. Reach for it
when you need one of the roughly 580 exported functions the idiomatic surface
does not cover yet.
flowchart TB
app["Your game or application"]
high["<b>Box3D.NET</b><br/>PhysicsWorld · Body · Shape · Joint<br/>idiomatic, validated, allocation-free"]
interop["<b>Box3D.Interop</b><br/>ToNativeId · ToBody<br/><i>the marked door between layers</i>"]
native["<b>Box3D.NET.Native</b><br/>579 P/Invokes · blittable structs<br/>a literal mirror of the C API"]
c["<b>Box3D</b> (C)<br/>consumed unmodified as a submodule"]
app -->|"the normal path"| high
app -.->|"when you need<br/>something not wrapped"| interop
high --> native
interop --> native
native -->|"P/Invoke, no marshalling"| c
style high fill:#512BD4,color:#fff,stroke:#3a1f9e
style native fill:#b09ef5,color:#1a1a1a,stroke:#7d68c9
style interop fill:#fff,color:#1a1a1a,stroke:#512BD4,stroke-dasharray: 4 3
style c fill:#d6cdfa,color:#1a1a1a,stroke:#7d68c9
style app fill:#f4f4f5,color:#1a1a1a,stroke:#a1a1aa
The safety rules, once
Everything in Box3D.NET.Native and Box3D.Interop follows the C API's
contract, which assumes the caller knows what it is doing:
Box3D.NET |
Box3D.NET.Native |
|
|---|---|---|
| Validates handles | yes, throws | no, undefined behaviour |
| Rejects NaN and infinity | yes, throws | no |
| Manages lifetimes | the world owns its objects | you do |
| Guards world creation | yes, process-wide mutex | no |
Passing an invalid identifier crashes the process rather than raising an exception. Passing a NaN contaminates the world silently. Those two sentences are the whole difference, and they are not repeated on the individual functions.
Going down a level
Box3D.NET never names a Box3D.NET.Native type in public API. If it did,
every consumer touching a handle would take a compile-time dependency on the C
ABI, and the two packages could no longer version independently.
LayeringTests enforces that by reflection over the built assembly, because a
rule like this decays quietly — one convenient property and nothing fails.
Dropping down is still supported, because a thin wrapper should not be a
ceiling. It goes through Box3D.Interop:
using Box3D.Interop;
using Box3D.Native;
b3BodyId raw = body.ToNativeId();
B3.b3Body_SetName(raw, name);
Body back = raw.ToBody();
Importing that namespace is the point: the coupling shows up as a using in
your own source rather than being the path of least resistance.
API coverage lists every exported function and whether the
idiomatic layer reaches it. NATIVE_ONLY is not a to-do list — most of it is
machinery an idiomatic API should not surface: individual accessors a single
property reads several of at once, recording and replay, tree internals,
profiling counters.
Why callbacks are structs
flowchart LR
q["world.Raycast<TCallback>"] --> ctx["context on the stack:<br/>pointer to your struct<br/>+ managed function pointer"]
ctx --> thunk["static thunk<br/>[UnmanagedCallersOnly]"]
thunk --> gen["InvokeRaycast<TCallback><br/><i>generic, so it can be specialised</i>"]
gen --> your["your OnHit — inlined"]
style q fill:#512BD4,color:#fff
style your fill:#16a34a,color:#fff
style thunk fill:#b09ef5,color:#1a1a1a
Queries are generic over the callback type, so the JIT specialises them and
devirtualises the call: your OnHit is inlined into the dispatcher with no
delegate allocation and nothing to keep alive across the transition.
The one indirection exists because [UnmanagedCallersOnly] cannot be applied to
a generic method. The context carries a managed function pointer to a generic
helper — which may be generic precisely because it is not the
UnmanagedCallersOnly one — and the non-generic native thunk calls through it.
That is one indirect call per hit, against an allocation and a GC handle per
query for the delegate design.
Measured: a ray cast with a struct callback runs at 168.9 ns against 171.7 ns for the callback-free convenience method, and allocates nothing.
Marshalling, or the absence of it
The native assembly is compiled with DisableRuntimeMarshalling. Every P/Invoke
is a direct call with arguments passed as they already sit in memory, and any
accidentally non-blittable type is a compile error instead of a silent
field-by-field copy. Booleans cross the boundary as NativeBool, a one-byte
value type matching C's _Bool.
Vectors need no conversion either: b3Vec3 and System.Numerics.Vector3 have
the same layout, as do b3Quat and Quaternion. LayoutTests asserts that
rather than trusting it.
Single precision only
Box3D's BOX3D_DOUBLE_PRECISION "large world" mode changes the ABI rather than
being a runtime switch. This binding targets the default single-precision build
and asserts at test time that the loaded library agrees. Large-world support, if
it lands, will be a separate package.
Where the bindings come from
They are generated from the Box3D headers rather than written by hand, and the struct layouts are checked against what a C compiler reports for the same declarations. Architecture covers that pipeline.