Native ABI and versioning (advanced)
Plugin authors never touch this layer; it explains why the API looks the way it does, and what contributors must respect when adding to it. The engine’s internal design notes are not part of this site; this page is the summary of the contract.
Layers
Section titled “Layers” plugin (C#, collectible AssemblyLoadContext) │ compiles against Mc.Scripting.Api (one shared instance in the default context) ▼ Mc.Scripting.Api ── typed facade: Plugin, Events, Commands, World, Entity, Systems, Scheduler ... │ function pointers (blittable data only) ▼ McApi / McHostEcs / McHostServer tables ── src/scripting/abi.hpp (the C ABI) ▼ native: event bus, command registry, flecs host, engine servicesThe CLR is embedded through hostfxr (located at run time, no link-time dependency). Mc.Scripting.Api and Mc.Scripting.Runtime are loaded into the default context. The runtime’s Initialize(McApi*, McManagedApi*) entry point ([UnmanagedCallersOnly]) checks the ABI version, the struct sizes, the event catalogue and the layout of the C# mirrors, then exchanges the function tables. CoreCLR can start only once per process, so the scripting host is a process singleton.
ABI rules
Section titled “ABI rules”- Blittable data only: fixed-width ints and floats, raw pointers, the POD structs of
abi.hpp. Nobool(useint32_toruint8_t), no C++ types, no exceptions across the boundary: every managed entry point catches everything and logs it with the owning plugin’s name. - Strings are UTF-8 pointer plus length (
McStr), valid only for the duration of the call or dispatch. - Every struct has a C# mirror. C++
static_asserts pin the sizes, and the managed side checksstruct_size, the catalogue hash and the layout of the mirrors at start. McHostEcsfunctions take no context pointer, so the table goes to C# verbatim. Functions marked[fast]are called withSuppressGCTransition: they must be quick and never call back into managed code.- Owner tags: every registration carries the plugin’s per-load tag, so
owner_release(tag)drops everything a plugin left behind (events, commands, systems, queries, component bindings). A hot-reloaded successor takes over names throughsupersedes.
Version numbers
Section titled “Version numbers”| What | Where | Rule |
|---|---|---|
ABI version (MC_ABI_VERSION, currently 5) |
native-managed function tables | bumped on breaking changes to existing members; appended members are compatible (struct_size checked). The managed side refuses to start on a mismatch and prints both versions |
| Host API hash | the host function table | the native host passes its hash in McApi::host_api_hash; the managed side refuses to start on a mismatch |
| Event catalogue hash | src/scripting/events.json |
generated into C++ and C# with the same layout hash, passed in McApi::catalogue_hash and checked at start. Ids are never reused or renumbered; fields and enum values are only appended |
API version (ApiVersion.Current, currently 1.0) |
the C# surface | minor = additive, major = breaking. A plugin declares the version it was built for in plugin.json ("api"); a missing field is read as 1.0 with a warning. A server satisfies a plugin when the major versions match and the server’s minor version is at least the plugin’s |
The event catalogue
Section titled “The event catalogue”src/scripting/events.json is the single source of truth. tools/codegen/gen_scripting_events.py generates src/scripting/generated/events.hpp and Mc.Scripting.Api/Generated/Events.g.cs. The --check mode fails the scripting_codegen ctest when they are stale. The event reference on this site is generated from the same file.
- Field types:
bool i8 u8 i16 u16 i32 u32 i64 u64 f32 f64 entity player world uuid string component bytes vec3d blockpos blockstate item enum:<Name>. Enum types are declared in the file’senumssection. - Mutable fields: a field whose type is written with
!(compact formname:type!|doc) can be changed by handlers, and the firing system reads it back. Entity, player, uuid, bytes and item fields cannot be mutable. - Ids: catalogue events are 1 to 511 and stable. Plugin-defined (dynamic) events are registered at run time in 512 to 1023.
Adding a call or an event (contributors)
Section titled “Adding a call or an event (contributors)”- Event: append to
events.json(never renumber), run the generator, and add the firing site in game code guarded bybus.has_subscribers<E>():events::PlayerChatEvent e{.player = p, .message = to_mc_str(msg)}; if (!bus.fire(e)) return;. - Host function: append to the function table and its C# mirror, bump
MC_ABI_VERSIONonly for breaking changes, keep the thunknoexceptand validating. - Add the XML documentation to the public C# member: the API reference is generated from it.