Skip to content

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.

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 services

The 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.

  • Blittable data only: fixed-width ints and floats, raw pointers, the POD structs of abi.hpp. No bool (use int32_t or uint8_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 checks struct_size, the catalogue hash and the layout of the mirrors at start.
  • McHostEcs functions take no context pointer, so the table goes to C# verbatim. Functions marked [fast] are called with SuppressGCTransition: 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 through supersedes.
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

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’s enums section.
  • Mutable fields: a field whose type is written with ! (compact form name: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.
  1. Event: append to events.json (never renumber), run the generator, and add the firing site in game code guarded by bus.has_subscribers<E>(): events::PlayerChatEvent e{.player = p, .message = to_mc_str(msg)}; if (!bus.fire(e)) return;.
  2. Host function: append to the function table and its C# mirror, bump MC_ABI_VERSION only for breaking changes, keep the thunk noexcept and validating.
  3. Add the XML documentation to the public C# member: the API reference is generated from it.