
FerricOxide
Native WebView UI for Minecraft, driven by Rust.
FerricOxide renders modern web UI (HTML/CSS/JS) inside Minecraft by calling the operating system's native WebView through a thin Rust JNI bridge — no bundled browser engines, no embedded Chromium. It is a lightweight, high-performance, cross-platform GUI foundation for Minecraft mod developers.
Preview

How it works
┌─────────────────────────── Minecraft (Java) ───────────────────────────┐
│ WebUi (public API) ──► NativeWebView (JNI stubs) │
└───────────────────────────────────┬────────────────────────────────────┘
│ JNI calls (fire-and-forget commands)
┌─────────────────────────── Rust (cdylib) ─────────────────────────────┐
│ ferric_oxide_native: jni + wry + tao │
│ dedicated event-loop thread owns every WebView/window │
└───────────────────────────────────┬────────────────────────────────────┘
│ OS native WebView
Windows: WebView2 (Chromium) │ macOS: WKWebView Linux: WebKitGTK
- Java exposes a small, mod-developer-friendly API (
WebUi), backed by thin JNI stubs (NativeWebView). - Rust (
rust/, crateferric_oxide_native) useswryto drive the platform's native WebView. All window/WebView work happens on one dedicated event-loop thread; JNI calls become messages sent through anEventLoopProxy, so no game thread is ever blocked. - Embedding: on Windows the WebView is created as an HWND child of the Minecraft window
(
WebViewBuilder::build_as_child), so the web UI is pinned to the game window and follows its size. Other platforms currently fall back to a standalone window. - Two-way bridge: events and queries flow in both directions over one JSON protocol.
Java uses
WebUi.bridge(), the page usesferric.emit / on / call / handle— the runtime is injected before any page script, so no feature detection is needed. Seedocs/webui-bridge.md.
Requirements
- Minecraft / NeoForge dev environment (see
gradle.propertiesfor the pinned versions) - JDK 25
- Rust toolchain (
cargo, stable) — needed to build the native library locally - Node.js 22+ — runs the page-side bridge tests during
./gradlew check - Windows: WebView2 Runtime (preinstalled on Windows 11 / modern Edge)
- Linux: WebKitGTK 4.1 and D-Bus runtime libraries (for example,
libwebkit2gtk-4.1-0andlibdbus-1-3on Ubuntu)
Building
./gradlew build
A normal local build invokes cargo build --release via the buildRustNative task, copies the
host library into the jar at natives/<platform>/<arch>/..., and configures dev runs with the
ferricoxide.native.path system property. The NativeLoader first honours that property and
otherwise extracts the matching library from the mod jar at runtime.
The reusable GitHub Actions build compiles these eight native targets in parallel and packages them into one mod JAR:
| Platform | Rust target | JAR resource |
|---|---|---|
| Windows x86 | i686-pc-windows-msvc |
natives/windows/x86/ferric_oxide_native.dll |
| Windows x86_64 | x86_64-pc-windows-msvc |
natives/windows/x86_64/ferric_oxide_native.dll |
| Windows aarch64 | aarch64-pc-windows-msvc |
natives/windows/aarch64/ferric_oxide_native.dll |
| Linux x86 | i686-unknown-linux-gnu |
natives/linux/x86/libferric_oxide_native.so |
| Linux x86_64 | x86_64-unknown-linux-gnu |
natives/linux/x86_64/libferric_oxide_native.so |
| Linux aarch64 | aarch64-unknown-linux-gnu |
natives/linux/aarch64/libferric_oxide_native.so |
| macOS x86_64 | x86_64-apple-darwin |
natives/macos/x86_64/libferric_oxide_native.dylib |
| macOS aarch64 | aarch64-apple-darwin |
natives/macos/aarch64/libferric_oxide_native.dylib |
The packaging job supplies the merged resource directory with
-PprebuiltNativesDir=/path/to/natives. In this mode Gradle copies every prebuilt native and
does not run Cargo; without the property, local and development-run behavior remains unchanged.
CI versions and publishing
The baseline version is mod_version in gradle.properties. CI passes an override to Gradle, so
the JAR filename and the embedded NeoForge metadata always carry the same version:
- A push that changes build inputs on
releases/**publishes an alpha to Modrinth and CurseForge as<mod_version>+build.<GitHub run number>, for example0.0.1+build.123. - Pushing a
v<mod_version>tag, for examplev0.0.1, validates that it exactly matchesgradle.properties, then publishes a stable release to Modrinth, CurseForge, and GitHub. - Pull requests only run the eight-target build and test checks; they cannot publish.
Repository secrets required for publishing are MODRINTH_TOKEN and CURSEFORGE_TOKEN. The
workflow fails visibly if either platform rejects an upload; it never silently skips a release.
The x86 libraries are built and packaged for completeness, but current Minecraft, JDK 25, and LWJGL distributions generally do not provide a complete 32-bit runtime. Loading an x86 native still requires an x86 JVM and an otherwise architecture-compatible game environment. Packaging the Linux libraries also does not bundle their WebKitGTK, GTK, or D-Bus system dependencies.
Using the API
Java side — payloads are plain records, converted with Gson:
import dev.anvilcraft.oxide.ferric.webui.WebUi;
record Clicked(int value) {}
record Data(int foo) {}
record PlayerInfo(String name, float health) {}
// Load your page from the mod's assets, embed it into the Minecraft window:
WebUi ui = WebUi.embedded(
"My Mod UI", "my_mod", "webui/index.html",
minecraft.getWindow().getWidth(),
minecraft.getWindow().getHeight()
);
ui.bridge()
// JS -> Java event (handlers always run on the render thread):
.on("my_mod.clicked", Clicked.class, clicked -> doSomething(clicked.value()))
// JS -> Java query — whatever you return is sent back to the page:
.handle("my_mod.player", Void.class, ignored -> new PlayerInfo("Steve", 20.0F));
// Java -> JS event:
ui.bridge().emit("my_mod.data", new Data(42));
// Java -> JS query:
ui.bridge().call("my_mod.form", null, FormValues.class).thenAccept(this::save);
// WebUi is AutoCloseable; close() destroys the native window (also reclaimed by GC).
ui.close();
Page side — the mirror image, no setup required:
ferric.emit('my_mod.clicked', {value: 42});
const player = await ferric.call('my_mod.player');
ferric.on('my_mod.data', (data) => render(data.foo));
ferric.handle('my_mod.form', () => collectFormValues());
// Game resources, without worrying about the platform's URL scheme:
img.src = ferric.resource('item/minecraft:apple', {size: 48});
Open a standalone (non-embedded) window with WebUi.window(...) instead. The raw lower-level
API (NativeWebView.Builder, MessageHandler, CreationCallback) is available under
dev.anvilcraft.oxide.ferric.webui for advanced use.
Demo
Run /ferric ui demo in-game to open the bundled demo UI. It exercises every direction of the
bridge: it pings the game chat (JS event), queries the player's name and health with
Who am I? (JS query), receives the world time once per second and rendered entity frames
(Java events). Press Esc in the WebView to close it and return mouse control to Minecraft.
Set FERRICOXIDE_AUTO_OPEN=1 to open the demo UI automatically after launch (used for
smoke-testing).
Project layout
rust/ Native JNI bridge (wry + tao + jni)
src/lib.rs JNI entry points, event-loop thread, embedded/standalone WebViews
src/main/java/dev/anvilcraft/oxide/ferric/
FerricOxide.java Mod entry point
client/FerricOxideClient.java Client command + demo UI wiring
webui/WebUi.java Public high-level API
webui/NativeWebView.java Low-level JNI wrapper
webui/NativeLoader.java Library loading (system property / jar extraction)
webui/bridge/ Two-way event/query bridge (WebBridge + protocol)
src/main/resources/assets/ferric_oxide/webui/
bridge.js Page-side bridge runtime (injected before page scripts)
demo.html Demo page
src/test/java/ JUnit tests for the Java bridge half
src/test/js/bridge.test.js Node tests for the page-side bridge half
docs/webui-bridge.md Bridge protocol and API design
License
GNU LGPL 3.0

