Skip to content

Rust for Java developers: the minimum

Changing. Follow: #321

You do not need to learn all of Rust to write a plugin. This page shows the part that the tutorials use, next to the Java you already know, and ends with links for when you want more. The Java snippets are illustrations and are not part of mcrs. The Rust snippets are short sketches too, and CI does not compile them.

Rust Java idea
struct, enum class with fields, enum that can carry data
impl block Methods of a class
trait interface
Option<T> A value that may be null
Result<T, E> and ? Checked exceptions
match switch that must cover every case
Closure |x| ... Lambda
Vec, HashMap, String and &str List, Map, String
#[derive(Clone)] Generated copy and equals code
&T and &mut T Many readers or one writer, checked at compile time
Modules, use, Cargo.toml Packages, imports, build.gradle

You can leave unsafe, procedural macros and async for later. The same goes for lifetimes, except that you will meet them when you read signatures that other people wrote.

A Java class becomes two things in Rust: a struct for the data and an impl block for the methods.

// a class with data and a method
class Counter {
private int ticks;
void tick() { ticks++; }
}

A Paper plugin extends JavaPlugin, and an mcrs plugin implements the Plugin trait. The #[derive(Clone)] line is there because every dimension gets its own copy of the plugin.

// a plugin is an interface implementation
public class MyPlugin extends JavaPlugin {
@Override
public void onEnable() { /* register things */ }
}

Where Java returns null or throws an exception, Rust returns an Option or a Result, so both cases are visible in the type. The ? operator returns the error to the caller.

// null for "no value", exceptions for errors
Player p = lookup(name);
if (p == null) return;
int n = Integer.parseInt(text); // may throw

A closure does the job of a lambda. In the Rust tab the closure is an observer, which is a function that runs when an event is triggered.

// a lambda as a handler
events.on(ChatEvent.class, e -> e.setCancelled(true));

The next example is a real plugin from the repository. It has a resource and a system that runs in FixedUpdate.

CountTicks: a plugin with a resource and a system
#[derive(Resource, Default)]
struct Ticks<const ALL: bool>(u32);
#[derive(Message)]
struct Ticked;
#[derive(Clone)]
struct CountTicks<const ALL: bool>;
impl<const ALL: bool> Plugin for CountTicks<ALL> {
fn name(&self) -> &str {
if ALL {
"count every tick"
} else {
"count overworld ticks"
}
}
fn build(&self, app: &mut App) {
app.init_resource::<Ticks<ALL>>().add_systems(
FixedUpdate,
|mut ticks: ResMut<Ticks<ALL>>, mut ticked: MessageWriter<Ticked>| {
ticks.0 += 1;
if ALL {
ticked.write(Ticked);
}
},
);
app.add_message::<Ticked>();
}
}

crates/mcrs_minecraft_server/tests/it/dimension_plugins.rs:10-40

A value in Rust has one owner. The owner can lend it as &T to many readers or as &mut T to one writer, but never both at once. In a Bevy system you write this in the parameters, where Query<&T> reads and Query<&mut T> writes. You rarely write lifetimes.

Two habits change because of this. Keep an Entity id where you would keep a reference to a player, and call .clone() where you would share a value. Do not copy patterns from Rust servers built on Arc<Mutex<..>>, because the project avoids them in its library layers.1 The ownership chapter of the Rust Book explains the rules in full.

Error Cause Fix
Use of a moved value You passed a value by value and then used it again Pass &value or call .clone()
A function is not accepted as a system One parameter has a type that Bevy cannot supply Check that every parameter is Res, ResMut, Query, Commands, a message reader or writer, or Local
Two parameters of one system want the same data, one of them as &mut The scheduler reports this when it builds the graph at startup, and the compiler does not catch it Merge the queries or split the system

Next: your first plugin, then Paper and Minestom in mcrs terms.

  1. WIP#566 The direction of library code without shared ownership, locks, trait objects or hot-path allocation. ↩

  2. Undecided#321 Tracks making the protocol, network and game data crates publishable. ↩

Sources and verification

Verified: 26.4-snapshot-7, 51d45c26a, 2026-11-09

Tracking: #321