How the pieces fit, why the shim exists, and how to extend the binding.
- The three layers
- Why a C shim
- Handles and safety
- Coordinate systems
- The ABI version
- Extending the binding
your xTalk script
│ b2k… (pixels, degrees, screen coords)
┌─────▼──────────────────────────────────┐
│ box2dxt-kit.livecodescript (the Kit) │ pure xTalk; owns world + loop
└─────┬──────────────────────────────────┘
│ b2… (metres, radians, handles)
┌─────▼──────────────────────────────────┐
│ box2dxt.lcb (xTalk Builder extension) │ foreign handlers + public wrappers
└─────┬──────────────────────────────────┘
│ FFI: c:box2dxt> b2lc_* (ints & doubles)
┌─────▼──────────────────────────────────┐
│ box2d_lc.c (C shim) + Box2D v3.1.0 │ one shared library: box2dxt
└─────────────────────────────────────────┘
src/box2d_lc.c+ Box2D compile together into one shared library,box2dxt(libbox2dxt.so/.dylib/box2dxt.dll). The shim exports flat C functions prefixedb2lc_*. This library ships bundled inside the extension, insrc/code/<arch>-<platform>/box2dxt.{so,dll,dylib}(bare token, nolibprefix; platform-idsx86_64-linux,x86-linux,x86_64-win32,x86-win32,universal-mac— architecture first, Windows-win32for both bitnesses). Those libraries are committed (built and tested by CI, and attached to each Release);tools/package-extension.pyrefreshes the tree from a newer build.src/box2dxt.lcbis the xTalk Builder (LCB) extension. It declaresprivate foreign handlerbindings to theb2lc_*symbols (binds to "c:box2dxt>b2lc_…!cdecl") and wraps each in a friendly publicb2…handler that scripts call directly. The engine resolves thec:box2dxt>library throughthe revLibraryMapping— a name→path table the IDE populates by scanning the extension'scode/<arch>-<platform>/folder when the extension is installed, so the right library loads automatically per platform with no loose file to place. (The old "drop the.soon a search path /sudo cp /usr/lib/LD_LIBRARY_PATH" approach was a workaround for not packaging the library into the extension.) For quick dev without packaging, the Kit'sb2kEnsureNativeLibseeds the samerevLibraryMapping["box2dxt"]entry from abox2dxt.{so,dll,dylib}sitting next to a saved stack.src/box2dxt-kit.livecodescriptis optional pure-xTalk sugar (b2k…) on top of theb2…API.
Box2D v3 is already C, so why not bind to it directly? Two reasons:
- Identifiers are structs passed by value. Box2D v3 ids (
b2WorldId,b2BodyId, …) are small structs. The LCB FFI is happiest with plain scalars and pointers, and there is no 64-bit integer foreign type. The shim stores every Box2D id in a handle table and hands the script a positive 32-bit int instead (0= null/invalid). Treat handles as opaque tokens. - Scalar conventions. Every real number crosses the boundary as
double(xTalk numbers are doubles); the shim casts to/from Box2D'sfloat. Every boolean crosses asint(0/1).
The shim also flattens array-ish APIs into call sequences the FFI can express —
for example, polygons are built with b2PolyBegin() / b2PolyAddPoint(x, y) /
b2AddPolygon(...) instead of marshalling a vertex array.
Box2D v3 ids carry a generation counter, so the engine can tell a live id
from a stale one. The shim validates every handle with the generation-checked
b2*_IsValid() before use. The result: calling any handler with a stale,
destroyed, or never-created handle is a harmless no-op — getters return 0,
actions do nothing — instead of crashing the engine.
The integer handle of each body/shape is also stored in its Box2D userData, so
queries, ray casts, and contact events can hand a handle back to the script.
Handles are generation-tagged: each one packs a small generation counter above its table slot, bumped every time the slot is freed. So even after the slot is recycled by a new object, your stale handle stays dead (no-op) instead of silently addressing the new occupant. Still drop references on destroy — that's what keeps the tables small.
Box2D works in MKS units (metres, kilograms, seconds) with Y pointing up. OpenXTalk screens use pixels with Y pointing down. Conversion happens only at draw time:
- The core
b2…API is pure metres/radians — you convert. - The Kit does the conversion for you: it keeps a pixels-per-metre scale (default 40) and an origin, and flips Y so screen-space "down" maps to world-space "down".
Keep moving objects roughly 0.1–10 m (≈ 4–400 px at the default scale). Drive the simulation from a fixed timestep — the Kit and demo accumulate real elapsed time and step in 1/60 s chunks; variable steps make the solver jittery and non-deterministic.
The shim exports b2lc_abi_version(), surfaced to scripts as b2Version(). It
returns the integer LC_ABI_VERSION defined in src/box2d_lc.c (currently 4).
Use it as a load/version sanity check, and bump it whenever the exported ABI
changes so the .lcb and native library can't silently drift apart.
The exported symbols keep the historical
b2lc_prefix even though the library is now namedbox2dxt. This is deliberate: it keeps already-compiled binaries binding-compatible across the OpenXTalk rebrand.
Exposing more of Box2D is mechanical. To add a handler:
- C shim (
src/box2d_lc.c) — add aLC_API … b2lc_yourthing(…)function that calls the Box2D API. Store/look up any ids in the existing handle tables, and validate inputs with the relevantb2*_IsValid. - Extension (
src/box2dxt.lcb) — add a matchingprivate foreign handler … binds to "c:box2dxt>b2lc_yourthing!cdecl", then apublic handler b2YourThing(…)wrapper that calls it (and tolerates0handles like the rest). - Bump
LC_ABI_VERSIONin the shim if the exported ABI changed. - Rebuild the native library (see building.md), re-run
tools/package-extension.py --linux64 build/libbox2dxt.so(or the flag for your platform) to refresh the bundledsrc/code/<arch>-<platform>/copy, then re-Package (or Test) the extension so the new library loads. (For quick iteration, dropping the rebuiltbox2dxt.{so,dll,dylib}beside a saved stack picks it up viab2kEnsureNativeLibwithout repackaging.)
Add a smoke-test assertion in tests/smoke_test.c for anything non-trivial so CI
exercises it on every platform.
As of ABI 3 the binding already covers the full Box2D v3.1 live-object
surface. The newer additions reuse a few shared shim patterns worth knowing when
you extend further:
- A shape-def "pending overrides" struct lets the existing shape creators gain
sensors / filters / event-flags / materials without new variants — set options
with
b2lc_shapedef_*, andfill_shape_defapplies them to the next shape then resets (one-shot, like the polygon vertex builder). - A chains handle table (the same
DEFINE_TABLEmacro) plus a point-cloud builder mirrors the polygon path. - Queries are callback-based upstream; the shim runs them with a small C callback that pushes hits into one shared result buffer, then exposes a count + indexed getters — the same idea as the ray/contact stashes.
- Sensor / hit / body-move events reuse the growable snapshot pattern of the contact events.
What stays intentionally unwrapped: pre-solve / custom-filter / friction / restitution callbacks (there is no safe way to call back into xTalk mid-step over the LCB FFI), and Box2D's standalone math / geometry / TOI / manifold helpers (they operate on raw structs, which xTalk handles itself). Filter category/mask bits are exposed as 32-bit (xTalk doubles carry 32 unsigned bits cleanly), so scripts get 32 collision layers rather than Box2D's full 64.