5.7 KiB
Go Minecraft Bridge
Go Minecraft Bridge is a Fabric server-side host for plugins written in Go. The Minecraft-facing code stays in Java, while plugin logic receives immutable tick snapshots and returns actions or named system calls.
The current MVP targets:
- Minecraft
26.1.2, Fabric Loader0.19.3, Fabric API0.149.1+26.1.2; - Java 25 and Go 1.24 or newer;
- native Go plugins built with
-buildmode=c-shared; - initialization, tick, chat, death, system-call-result, and deinitialization callbacks;
- entity snapshots and explicit subscriptions to block positions;
- chat broadcast/direct-message actions;
- extensible namespaced system calls;
- capture of Go
stdout,stderr, and the standardlogpackage.
The backend interface is deliberately independent from Fabric and native FFI so that a WASI/WASM backend can implement the same protocol later.
Build and try the example
Build the Fabric mod:
./gradlew build
Build the example Go plugin:
./examples/hello-native/build.sh
Copy the resulting library to either location:
mods/libhello_native.so
config/go-minecraft-bridge/plugins/libhello_native.so
Use .dll on Windows and .dylib on macOS. The plugin reports its own ID,
name, version, authors, and config schema through the ABI; no sidecar manifest is
required.
Native access must be enabled for the unnamed Java module:
--enable-native-access=ALL-UNNAMED
The development run configurations add this automatically. A production server must add it to its JVM arguments.
After joining a development server, send !go in chat. The example responds
directly to the player and requests minecraft:server.info through the system
call registry.
Plugin programming model
A Go project only imports the SDK and registers one value. The native C exports,
panic boundary, output allocation, and gmb_free implementation live inside the
SDK and are linked into the final library automatically:
package main
import "github.com/yawaflua/GoMinecraftBridge/sdk"
type myPlugin struct{}
func init() {
sdk.Register(myPlugin{})
}
func main() {}
Until the SDK is published as a tagged Go module, reference the local checkout:
require github.com/yawaflua/GoMinecraftBridge/sdk v0.0.0
replace github.com/yawaflua/GoMinecraftBridge/sdk => /path/to/GoMinecraftBridge/sdk
Build it normally:
go build -buildmode=c-shared -o dist/my_plugin.so .
It implements only the callbacks it needs:
func (myPlugin) Tick(ctx *sdk.Context, snapshot sdk.ServerSnapshot) error {
for _, entity := range snapshot.Entities {
// Read the immutable snapshot.
_ = entity
}
return nil
}
func (myPlugin) Chat(ctx *sdk.Context, event sdk.ChatEvent) error {
ctx.SendMessage(event.PlayerUUID, "Hello from Go")
return nil
}
See examples/hello-native/main.go for a
complete native entrypoint and every currently supported callback.
Snapshots
Entity snapshots are enabled by default. Blocks are opt-in because walking all loaded blocks every tick would be prohibitively expensive:
ctx.SubscribeSnapshot(true,
sdk.BlockReference{
Dimension: "minecraft:overworld",
X: 0,
Y: 64,
Z: 0,
},
)
The subscription returned by a callback applies to subsequent ticks for that plugin.
Actions and system calls
Actions are fire-and-forget operations implemented by the bridge:
minecraft:chat.broadcast;minecraft:chat.player.
System calls return a result to the plugin's SystemCallResult callback. Built-in
calls currently include:
minecraft:server.info;minecraft:player.get;minecraft:block.get.
Another Java mod can expose a custom call without changing the bridge:
GoMinecraftBridgeApi.systemCalls().register("example:claim.owner", (context, payload) -> {
JsonObject result = new JsonObject();
result.addProperty("owner", "player uuid");
return result;
});
Discovered metadata, including each plugin's config schema, is available through
GoMinecraftBridgeApi.plugins(). This is the integration point for a future
Mod Menu/configuration screen; Go plugins are intentionally not injected into
Fabric Loader's already-finalized mod list.
Failure model
The SDK converts callback errors into status=error and recovers ordinary Go
panics at the ABI boundary. A panic disables that plugin logically. Native Go
libraries remain loaded until JVM shutdown because unloading a live Go runtime is
unsafe.
Native memory corruption, a C/Go runtime fatal error, or a segmentation fault can still terminate Minecraft. The planned WASM backend is the isolated option for untrusted plugins.
Verification
Run all Java and Go tests, including the real Java-to-Go FFI test:
./examples/hello-native/build.sh
GMB_TEST_LIBRARY="$PWD/examples/hello-native/dist/libhello_native.so" ./gradlew test
(cd sdk && go test ./...)
The wire-level contract is documented in docs/native-abi.md.
Codec and performance
Protocol v1 deliberately uses JSON because it makes the ABI inspectable while the schema is still changing. JSON is not intended to remain the hot-path codec: it formats every number as text and allocates while decoding large snapshots.
The planned split is:
- JSON for metadata, configuration, logs, and arbitrary custom system calls;
- a generated binary schema for tick snapshots and action batches;
- codec negotiation through the plugin metadata, without changing the three C ABI functions.
FlatBuffers is the current preferred snapshot codec because Go can read the snapshot directly from the transferred buffer. Protocol Buffers remains a good alternative if schema evolution and tooling prove more important than avoiding decode allocations.