PatchBukkit is a plugin for PumpkinMC that enables Paper, Spigot, and Bukkit plugins to run on the Pumpkin server. It bridges Pumpkin's Rust-native plugin API with Java's Bukkit/Paper ecosystem by embedding a JVM and reimplementing the server-side Bukkit API surface.
As such the project is split between two different components:
- a Rust dynamic library (the Pumpkin plugin)
- a Java library (the Bukkit API implementation)
They communicate at runtime through JNI and a Protocol Buffer-based FFI bridge.
When PatchBukkit is started, it performs the following steps:
- Creates a new thread for the Java virtual machine (JVM) and initializes the
JvmWorkerstruct. - Stores a handle to send commands to the
JvmWorker. - Pumpkin triggers the on_load function in PatchBukkit.
- As part of this process, PatchBukkit first discovers all of the JAR files in the
patchbukkit-plugins/directory. - It then loads each Jar file into the JVM by sending a
JvmCommand::LoadPlugincommand to theJvmWorker. - After that PatchBukkit then embeds all of the required libraries for the
paper-apiinto the Jassets directory, so it can be loaded by the JvmWorker. - PatchBukkit then tells the
JvmWorkerto start the JVM viaJvmCommand::Initialize. - Finally, we load the plugins and enable them all via sending a
JvmCommand::InstantiateAllPluginsandJvmCommand::EnableAllPluginscommand to theJvmWorker.
- As part of this process, PatchBukkit first discovers all of the JAR files in the
Now you might be wondering why we keep JvmWorker on its own thread. The reason we do this is because the Jvm is not thread-safe. By keeping JvmWorker on its own thread, we ensure that all interactions with the JVM are thread-safe and that the JVM is not accessed from multiple threads simultaneously.
What does the JvmWorker do during this?
- We then use the rust
PluginManagerto load all of the plugins uponJvmCommand::LoadPluginbeing called. - Then upon
JvmCommand::Initialize, the main idea is that we want to register all of the native Foreign Function and Memory (FFM) API calls with the JVM. Upon everything else being setup, we then create our first Java Object, thePatchBukkitServerinstance. - The details are too numerious to describe fully here but upon
JvmCommand::InstantiateAllPlugins, we compute the plugin load order using topological sorting based on dependencies, then create a Java Object for each plugin with theorg.patchbukkit.loader.PatchBukkitPluginLoaderclass and setup commands for each plugin. - Finally, we enable all plugins upon
JvmCommand::EnableAllPluginsby getting theorg.bukkit.Bukkitinstance'sgetPluginManager()method and then callingenablePlugin()on each plugin.
- We disable all plugins by sending the
JvmCommand::DisableAllPluginscommand to theJvmWorker. - We terminate the Jvm thread by sending the
JvmCommand::Shutdown.
Events are handled through a bidirectional bridge:
- Rust registers event handlers with Pumpkin using
PatchBukkitEventHandler. - When an event fires, Rust sends a
JvmCommand::FireEventto theJvmWorkerwith the event data serialized as a protobuf message. - The Java side deserializes the event, fires it through the Bukkit event system, and returns whether it was cancelled.
Upon a command being received, we send the command to the JvmWorker via JvmCommand::TriggerCommand and let it handle it.
We have two primary ways of communicating between Rust and Java:
- Using Java Native Interface (JNI) API to send data from Rust to Java.
- Using FFM API with Protocol Buffers to send data from Java to Rust, and finally back to Java.
We prefer to use FFM with protobufs, since it is generally faster and more efficient than JNI, and protobufs provide a type-safe, serialization format.
The FFI bridge uses .proto files in the proto/ directory to define message types and
service interfaces. During the build process:
rust/build/protobufs.rsgenerates Rust code for protobuf messages and FFI entry pointsjava/protoc-gen-ffiis a custom protoc plugin that generates Java FFI wrapper classes (e.g.,NativeBridgeFfi)
The generated code handles serialization/deserialization automatically, allowing type-safe communication between Rust and Java without manual struct layout management.
java/ contains all of the Java code for PatchBukkit.
rust/ contains all of the Rust code for PatchBukkit.
proto/ contains Protocol Buffer definitions for the FFI bridge.
In rust/src/java we contain the majority of the code needed to interact directly with the JVM.
Specific areas of interest are:
rust/src/java/native_callbacks- This directory contains all of the native FFM callback implementations that are invoked from Java via the generated FFI bridge.
rust/src/java/jvm/worker.rs- This file contains the
JvmWorkerstruct and its methods.
- This file contains the
rust/src/java/plugin- This folder contains all of the code needed to interact with plugins from Rust, including dependency resolution and load ordering.
rust/src/proto- This module includes the generated protobuf code for Rust.
On the java/patchbukkit/src/main/java/org/patchbukkit side, we have some of the following classes:
patchbukkit.bridge.NativeBridgeFfi(generated)- This generated class contains the native FFM methods that invoke Rust callbacks via protobuf serialization.
org.patchbukkit.bridge.BridgeUtils- Utility class for converting between Java and protobuf types (e.g., UUID conversion).
org.patchbukkit.PatchBukkitServer- This class creates and manages the PatchBukkit servers.
org.patchbukkit.loader.PatchBukkitPluginLoader- This class is the way we create plugins on the Java side.
org.patchbukkit.PatchBukkitPluginManager- This class is the way we help manage the PatchBukkit plugins.
org.patchbukkit.loader.LibraryResolver- Resolves Maven library dependencies for plugins at runtime.
org.patchbukkit.events.PatchBukkitEventManager- Manages event registration and firing, bridging Pumpkin events to Bukkit events.
Now it might seem unusual to have the build.rs file as an important part of the architecture,
but in this project, it is very important. The build process is split into modules under rust/build/:
rust/build/main.rs- Entry point that orchestrates the buildrust/build/java.rs- Embeds all transitive dependencies of thepaper-apiJava library and the PatchBukkit jarrust/build/protobufs.rs- Generates Rust protobuf code and FFI entry points from.protodefinitions