C ABI and host services
The complete contract between module and host. When you use the base classes and registration macros from script.hpp, the SDK takes care of all these details for you—this page is written for those who want to understand the boundary, write test drivers or troubleshoot loading problems.
Module exports
Every script library exports 7 C functions (mtr_native.h; the registration macros generate them all):
| Export | Description |
|---|---|
uint32_t mtrNativeAbiVersion() | Returns the ABI version compiled against (currently 13); the host rejects incompatible versions—matching JCM's version-mismatch reload policy for JS scripts |
const char* mtrScriptType() | "vehicle" / "pids" / "eyecandy" / "lift" / "psd" |
const char* mtrScriptId() | The script id (must match the resource pack declaration) |
size_t mtrStateSize() | Per-instance state block size; the host allocates one block per instance |
void mtrInit(const JcmFrameInput*) | Called once right after the host allocates the state block; placement-new constructs the State (required since ABI 6; if the host forgets to call it, the module falls back to lazy construction) |
int32_t mtrCreate / mtrRender / mtrDispose(const JcmFrameInput*, …) | Lifecycle entry points; return 0 for success, -1 for a null argument, -2 for an ABI mismatch, -3 for a resource kind mismatch. The host calls them serially per instance on a single background thread per module |
Frame input and frame output
JcmFrameInput (host → module)
typedef struct JcmFrameInput {
uint32_t abi_version; /* The negotiated ABI version */
int32_t resource_kind; /* MtrResourceKind: vehicle/pids/eyecandy/lift/psd */
const void* snapshot; /* The corresponding Jcm*Snapshot */
void* state; /* Host-allocated per-instance state block */
size_t state_size;
const struct JcmHostServices* host;/* Host services callable on the render thread */
} JcmFrameInput;
Snapshot internals: every *_offset field is a byte offset relative to the first byte of the snapshot; string_pool_offset/len describes the location of the shared UTF-8 string pool. The SDK wrappers do all of this addressing for you.
JcmFrameOutput (module → host)
The output is a flat record stream + five arenas; once mtrRender returns, the host reads it immediately and reclaims the frame (double buffering between modules is handled by the host):
| Arena | Contents | Capacity (SDK default) |
|---|---|---|
records | Draw records laid out in sequence (variable length; walk them in record_size steps) | 256 records |
matrix_arena | A stream of matrices (column-major 4×4, 16 floats each) | 128 sets |
string_arena | A stream of UTF-8 strings (texture ids, text, sound ids, …) | 32 KB |
pixel_arena | A stream of BGRA8 pixels (texture uploads) | Starts at 1 MB, grows on demand up to 16 MB, decays back when idle |
float_arena | A stream of floats (voxel collision boxes and so on) | 1024 floats |
Since ABI 9 the output also carries each arena's full capacity—the host creates just one ByteBuffer per arena and reuses it by changing the limit each frame, instead of allocating five wrapper objects every frame.
Draw records
Every record starts with a JcmRecordHeader{kind, flags, record_size} and is laid out sequentially with variable length. flags carries record-level switches: currently MTR_RECORD_FLAG_NATURAL_LIGHT (the natural_light() option of Text / Texture / Rect — world light instead of max render light). The byte was promoted in place from padding: older-SDK modules always write 0 and older hosts ignore it — binary compatible in both directions, no ABI bump. The 8 kinds:
| kind | Structure | Purpose / JS call mirrored |
|---|---|---|
JCM_DRAW_TEXT | JcmDrawText | PIDS / general text (the fields align one-to-one with TextWrapper: alignment, overflow mode, marquee, z-order, layer, font id) |
JCM_DRAW_TEXTURE | JcmDrawTexture | Texture quads (UV, tint, z-order) |
JCM_DRAW_MODEL | JcmDrawModel | Vehicle cars / bogies / eyecandy models (host model handle + pose matrix set) |
JCM_DRAW_SOUND | JcmDrawSound | Positioned sound effects (car-local coordinate offset) |
JCM_DRAW_LOCAL_SOUND | JcmDrawSound | In-car announcements (audible only to riding players) |
JCM_DRAW_TEXTURE_UPLOAD | JcmDrawTextureUpload | GraphicsTexture pixel upload (BGRA8 + dirty rectangle) |
JCM_DRAW_OUTLINE_SHAPE | JcmDrawShape | Eyecandy outline / collision boxes (6 floats per box; collision boxes are capped at 1.5 blocks) |
JCM_DRAW_BLOCK_EVENT | — | Eyecandy onBlockUse event handshake |
The host replays these records along exactly the same rendering code path as JS scripts (PIDSDrawCall / ScriptRenderManager)—what gets drawn is the same thing.
Host services
JcmHostServices is a table of function pointers (in->host) callable on the render thread. On older hosts the newer slots are null; test for null before calling and degrade gracefully:
| Service | Since | Description |
|---|---|---|
acquire_model / release_model | ABI 1 | Model JSON handle lifecycle (mirrors ModelJS handles) |
create_texture / release_texture | ABI 1 | GraphicsTexture lifecycle (mirrors the Java class) |
rasterize_text | ABI 1 | Host TTF rasterisation callback (CJK and more), writing BGRA8 into the caller's buffer; -1 means the font does not cover that code point (fall back to the built-in bitmap font) |
log | ABI 1 | The console.log / console.error channel |
acquire_quad_model | ABI 4 | The host builds a textured quad (4 vertices + UV + render stage + texture handle)—the native equivalent of the JS DisplayHelper slot model; vertices are in model space, UVs are anchored at the texture's top-left corner, and y points down. Headless drivers return -1; skip drawing then |
measure_font_text / draw_font_text | ABI 7 | Metric measurement and baseline rasterisation for resource fonts (the same font as JS Graphics2D, the same compression and clipping semantics; dirty bounds as out-parameters) |
invoke_api | ABI 8 | The channel underneath mtr::Api: a UTF-8 JSON request goes in, the result lands in a thread-local buffer (valid until the next call) |
defer_free | ABI 10 | Hands already-dead large memory blocks to the host's background reclamation thread (see Performance and debugging for details) |
ABI version history
The host accepts ABI 6–13 modules (each is fed the layout of its era and reports its own ABI number). Compiling with the current SDK produces ABI 13:
| ABI | What it introduced |
|---|---|
| 6 | mtrInit(): the host must construct the state object (a zero-filled block is undefined behaviour for non-trivial types) |
| 7 | Resource-font metrics + baseline text rasterisation join the host services |
| 8 | invoke_api (the full Java API channel), the lift snapshot, typed interchange modes; ABI 8 libraries require a JCM Yanyang.7 or newer host |
| 9 | Canonical transport type ids for interchanges / vehicles (the MAGIC MTR extensions); the host stops copying the snapshot every frame, arena capacities become out-parameters |
| 10 | The defer_free deferred-reclamation hook (freeing scripts' own large buffers off the JVM threads) |
| 11 | door_direction on the vehicle snapshot (door-opening side 0..3 — the former _pad0 byte promoted in place; layout and size unchanged, older modules keep working) |
| 12 | MTR_RESOURCE_PSD (the 5th script kind) and the JcmPsdSnapshot / JcmPsdArrival / JcmPsdRoute / JcmPsdDoor snapshots — purely additive, every pre-existing struct unchanged; ABI 12 modules need a Yanyang hotfix.2+ host |
| 13 | JcmPsdSnapshot.station_offset (byte offset 124; -1 = none — the field was padding in ABI 12) and the 96-byte JcmPsdStation record behind psd.stop() / mtr::Psd::stop(): station name, colour (ARGB + hex), fare zones 1–3, every route calling at the station, connecting-station route groups, physically connected station names, exits with destinations, platform numbers — purely additive, every pre-existing struct unchanged; ABI 6–12 modules keep loading |
The function pointer table grows with the ABI: a new module would call slots that an old host does not have. The host picks the matching structure layout and feeding strategy from the ABI number the module reports and refuses to load on mismatch—this fail-fast policy is deliberate.
Transport layer (internal to the host, invisible to scripts)
Since 2.3.0-beta-2.yanyang.6 (layout v2) the mod contains no JNI at all: two calling planes carry all cross-language traffic—the Panama FFM plane (JDK 22+, libraryLookup + jcm_* downcalls + an upcall vtable) and the shared-memory polling plane (any JDK ≥ 16, where the background poller thread started by the bridge's constructor discovers regions via a file-system handshake and everything is served through shared pages). Script code is completely unaware of this—modules only ever see the C ABI above.
Up next: Performance and debugging — keeping your scripts fast and stable.