C ABI and host services
The complete contract between module and host. If you use the base classes and registration macros from script.hpp, the SDK handles all of these details for you โ this page is for those who want to understand the boundary, write test drivers, or debug 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 it was compiled against (currently 13); the host rejects incompatible versions โ the same version-mismatch reload policy JCM applies to 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; constructs State with placement-new (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 success, -1 null input, -2 ABI mismatch, -3 wrong resource type. The host calls them per instance, serially, on a single background thread per module |
Frame input and frame output
JcmFrameInput (host โ module)
typedef struct JcmFrameInput {
uint32_t abi_version; /* Negotiated ABI version */
int32_t resource_kind; /* MtrResourceKind: vehicle/pids/eyecandy/lift/psd */
const void* snapshot; /* The matching 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 convention: every *_offset field is a byte offset relative to the snapshot's first byte; string_pool_offset/len describe where the shared UTF-8 string pool lives. The SDK wrappers do all of this addressing for you.
JcmFrameOutput (module โ host)
The output is a flat record stream + five arenas; the host reads it and reclaims the frame as soon as mtrRender returns (double buffering between modules is done by the host):
| Arena | Contents | Capacity (SDK default) |
|---|---|---|
records | Draw records in sequence (variable length; walk them in record_size steps) | 256 records |
matrix_arena | Matrix stream (column-major 4ร4, 16 floats per matrix) | 128 sets |
string_arena | UTF-8 string stream (texture ids, text, sound ids, โฆ) | 32 KB |
pixel_arena | BGRA8 pixel stream (texture uploads) | 1 MB initially, grows on demand up to 16 MB, decays back when idle |
float_arena | Float stream (voxel collision boxes, etc.) | 1024 floats |
Since ABI 9 the output also reports each arena's full capacity โ the host creates a single ByteBuffer per arena and reuses it every frame by moving the limit, instead of allocating five wrapper objects per frame.
Draw records
Each record starts with 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 โ take the world light value instead of the max render light). The byte was promoted in place from padding: modules compiled against older SDKs always write 0 and older hosts ignore it โ binary compatible in both directions, no ABI bump. Eight kinds:
| kind | Struct | Purpose / JS call mirrored |
|---|---|---|
JCM_DRAW_TEXT | JcmDrawText | PIDS / general text (field-by-field alignment 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 sounds (car-local coordinate offset) |
JCM_DRAW_LOCAL_SOUND | JcmDrawSound | 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 capped at 1.5 blocks) |
JCM_DRAW_BLOCK_EVENT | โ | Eyecandy onBlockUse event handshake |
The host replays these records through 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, newer slots are null; null-check before calling to 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 rasterization callback (CJK etc.), writes BGRA8 into the caller's buffer; -1 means the font doesn't 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 to the texture's top-left, y points down. Headless drivers return -1; skip drawing then |
measure_font_text / draw_font_text | ABI 7 | Resource-pack font metrics and baseline rasterization (same font as JS Graphics2D, same compression and clipping semantics; dirty bounds as out-parameters) |
invoke_api | ABI 8 | The transport under mtr::Api: UTF-8 JSON request in, result out into 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) |
ABI version history
The host accepts ABI 6โ13 modules (each is fed with its era's layout 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 (zero-filled blocks are undefined behavior for non-trivial types) |
| 7 | Resource-pack font metrics + baseline text rasterization joined 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 (MAGIC MTR extensions); the host stopped copying snapshots every frame, arena capacities became out-parameters |
| 10 | The defer_free deferred-reclamation hook (freeing the script's 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, color (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 will call slots that an old host doesn't have. The host picks the struct layout and feeding scheme matching the ABI number the module reports, and refuses to load on mismatch โ a deliberate fail-fast strategy.
Transport layer (host-internal, 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; the bridge discovers regions through a filesystem handshake using a poller thread started from the constructor, and everything is served over shared pages). Script code is completely unaware of this โ modules only see the C ABI above.
Up next: Performance and debugging โ keeping your scripts fast and stable.