Write JVM agents in Rust with explicit safety boundaries and production-grade ergonomics.
Complete JNI and JVMTI bindings plus higher-level abstractions designed for building profilers, tracers, debuggers, and runtime instrumentation — without writing C or C++.
This crate focuses on:
- Making ownership, lifetimes, and error handling explicit
- Reducing common JVMTI footguns
- Keeping unsafe behavior localized and auditable
It is intended for serious native JVM tooling, not just experimentation.
JVMTI is powerful — and notoriously easy to misuse.
Typical problems when writing agents:
- Unchecked error codes that silently corrupt state
- Invalid reference lifetimes causing segfaults
- Allocator mismatches leaking memory
- Thread-local
JNIEnvmisuse across callbacks - Undocumented callback constraints causing deadlocks
Most existing Rust options either:
- Expose raw bindings with little guidance
- Rely on build-time bindgen
- Are incomplete or unmaintained (7+ years)
- Optimize for JNI, not JVMTI agents
This crate was designed around how agents are actually written, not around mirroring C headers.
If you only need JNI to call into Java from Rust applications, crates like jni or jni-simple are often sufficient. This crate is purpose-built for JVMTI agents (profilers, tracers, debuggers, instrumentation) and emphasizes:
- Full JNI + JVMTI coverage (agent-first focus)
- Safe, owned return types in the high-level
envwrappers - Class file parsing with all standard Java 8-27 attributes
- A tiny but explicit public surface (
env,sys,classfile,prelude) - Safety guidance, pitfalls, and compatibility documentation
- Examples that mirror real JVMTI tooling patterns
C++ is the traditional choice, but Rust offers compelling advantages:
- Memory safety without GC — JVMTI agents run inside the JVM process; a segfault kills the application
- Fearless concurrency — JVMTI callbacks fire from multiple threads simultaneously
- Zero-cost abstractions — RAII guards and Result types add safety without runtime overhead
- No runtime dependencies — Deploy a single
.so/.dylib/.dllwith no external libraries - Modern tooling — Cargo, docs.rs, and crates.io beat Makefiles and manual distribution
Java agents (java.lang.instrument) are simpler but can't access low-level features like heap iteration, breakpoints, or raw bytecode hooks.
| Goal | How |
|---|---|
| Explicit safety model | Unsafe operations centralized; APIs return Result |
| Complete surface | All 236 JNI + 156 JVMTI functions, mapped to Rust types |
| Agent-first ergonomics | Structured callbacks, capability management, RAII resources |
| No hidden dependencies | No bindgen, no build-time JVM, no global allocators |
| Long-term compatibility | Verified against OpenJDK headers, JDK 8 through 27 |
This crate is built around explicit safety boundaries. See docs/SAFETY.md and docs/PITFALLS.md for the full checklist.
Key rules:
- Never use
JNIEnvacross threads. - Never panic across JNI/JVMTI callbacks.
- Always deallocate JVMTI buffers with
Deallocate. - Avoid JNI calls in GC callbacks.
The supported public surface is intentionally small. For most users:
- Use
envfor safe wrappers. - Use
preludefor standard imports. - Use
sysonly for raw FFI work.
Details: docs/PUBLIC_API.md.
If you need raw JNI/JVMTI functions, use:
jvmti_bindings::sys::jniandjvmti_bindings::sys::jvmtifor raw types and vtables.JniEnv::raw()andJvmti::raw()to access the underlying raw pointers.
Agent_OnAttachis supported via theexport_agent!macro andAgent::on_attach.JNIEnvis thread-local and must only be used on its originating thread.GlobalRefcleanup attaches to the JVM when needed, but you should still manage lifetimes explicitly.
See docs/COMPATIBILITY.md for a full JDK 8-27 matrix.
Feature-gated helpers live under advanced:
heap-graphfor heap tagging and reference edge extraction.
Enable with:
[dependencies]
jvmti-bindings = { version = "2", features = ["heap-graph"] }cargo new --lib my_agent
cd my_agent[lib]
crate-type = ["cdylib"]
[dependencies]
jvmti-bindings = "2"use jvmti_bindings::prelude::*;
#[derive(Default)]
struct MyAgent;
impl Agent for MyAgent {
fn on_load(&self, vm: *mut jni::JavaVM, options: &str) -> jni::jint {
println!("[MyAgent] Loaded with options: {}", options);
jni::JNI_OK
}
fn vm_init(&self, _jni: *mut jni::JNIEnv, _thread: jni::jthread) {
println!("[MyAgent] VM initialized");
}
fn vm_death(&self, _jni: *mut jni::JNIEnv) {
println!("[MyAgent] VM shutting down");
}
}
export_agent!(MyAgent);cargo build --release
# Linux
java -agentpath:./target/release/libmy_agent.so=myoptions MyApp
# macOS
java -agentpath:./target/release/libmy_agent.dylib=myoptions MyApp
# Windows
java -agentpath:./target/release/my_agent.dll=myoptions MyAppIf you want to attach to an already running JVM, implement Agent::on_attach and load the agent with the JVM Attach API:
use jvmti_bindings::prelude::*;
#[derive(Default)]
struct AttachLogger;
impl Agent for AttachLogger {
fn on_attach(&self, vm: *mut jni::JavaVM, options: &str) -> jni::jint {
println!("[AttachLogger] attached with options: {}", options);
let _jvmti = Jvmti::new(vm).expect("get JVMTI");
jni::JNI_OK
}
}
export_agent!(AttachLogger);Attach it with jcmd (example):
jcmd <pid> JVMTI.agent_load /abs/path/to/libattach_logger.so "opt1=val1"JVMTI.agent_load expects an absolute path to the native agent and an optional option string.
This crate now includes a zero-dependency class file parser that understands all standard attributes from Java 8 through Java 27. Use it inside ClassFileLoadHook to inspect or transform class metadata.
use jvmti_bindings::classfile::ClassFile;
fn parse_class(bytes: &[u8]) {
let classfile = ClassFile::parse(bytes).expect("valid class file");
println!("major version = {}", classfile.major_version);
println!("attributes = {}", classfile.attributes.len());
}Nested attributes are preserved and exposed (method Code attributes, record component attributes, and more). You can traverse them like this:
use jvmti_bindings::classfile::{AttributeInfo, ClassFile, RecordComponent};
fn walk_attributes(attrs: &[AttributeInfo]) {
for attr in attrs {
match attr {
AttributeInfo::Code(code) => walk_attributes(&code.attributes),
AttributeInfo::Record { components } => {
for RecordComponent { attributes, .. } in components {
walk_attributes(attributes);
}
}
_ => {}
}
}
}
fn parse_class(bytes: &[u8]) {
let classfile = ClassFile::parse(bytes).expect("valid class file");
walk_attributes(&classfile.attributes);
for field in &classfile.fields {
walk_attributes(&field.attributes);
}
for method in &classfile.methods {
walk_attributes(&method.attributes);
}
}If you want to embed a JVM inside a Rust process (not just build an agent), enable the embed feature and use JavaVmBuilder:
use jvmti_bindings::prelude::*;
let builder = JavaVmBuilder::new(jni::JNI_VERSION_1_8)
.option("-Xms64m")?
.option("-Xmx256m")?
.option("-Djava.class.path=./myapp.jar")?;
let vm = builder.create()?; // uses JAVA_HOME or JVM_LIB_PATH
let env = unsafe { vm.creator_env() }; // only valid on the creating thread
// ... call JNI through env ...
vm.destroy()?;This is feature-gated so the crate remains dependency-free by default. See docs/EMBEDDING.md and examples/embed.rs for details.
Included examples (build as cdylib agents):
examples/minimal.rsexamples/class_logger.rsexamples/profiler.rsexamples/tracer.rsexamples/heap_sampler.rsexamples/attach_logger.rs(dynamic attach viaAgent_OnAttach)
Embedding example (binary):
examples/embed.rs (run with cargo run --example embed --features embed)
See templates/agent-starter/ for a ready-to-copy agent crate.
The repository includes a GitHub Actions workflow that builds and tests on Linux, macOS, and Windows.
The macro generates the native entry points the JVM expects.
It does:
- Generate
Agent_OnLoad/Agent_OnUnload/Agent_OnAttachentry points - Create your agent instance and store it globally (must be
Sync + Send) - Pass the options string to your
on_load/on_attachimplementation
It does not:
- Hide undefined JVMTI behavior
- Make callbacks re-entrant or async-safe
- Attach arbitrary native threads automatically
- Obtain the JVMTI environment for you
- Register callbacks or enable events
- Prevent JVM crashes from invalid JVMTI usage
The goal is clarity, not magic.
This crate enforces the following invariants:
| Invariant | Enforcement |
|---|---|
JNIEnv is thread-local |
JniEnv wrapper is not Send |
| Local refs don't escape | LocalRef<'a> tied to JniEnv lifetime |
| Global refs are freed | GlobalRef releases on Drop |
| JVMTI memory properly freed | High-level JVMTI methods deallocate buffers they allocate |
| Errors are explicit | JVMTI methods return Result, JNI helpers use Option/Result |
Some things cannot be made safe by design:
- Bytecode transformation correctness — invalid bytecode crashes the JVM
- Callback timing assumptions — JVMTI events fire at specific phases
- Blocking in callbacks — long operations in GC callbacks deadlock
- Cross-thread reference sharing — JNI local refs are thread-local
Rust helps — but JVMTI is still a sharp tool.
Yes, if you are:
- Building profilers, tracers, debuggers, or instrumentation
- Want Rust's type system around JVMTI's sharp edges
- Need a single crate that works across JDK 8–27
- Comfortable reading JVMTI docs for advanced use cases
Probably not, if you:
- Only need basic JNI calls (consider the
jnicrate) - Are uncomfortable debugging native JVM crashes
- Need dynamic attach (use
Agent::on_attach/Agent_OnAttach) - Want zero
unsafeanywhere
┌─────────────────────────────────────────────────────────┐
│ Your Agent Code │
│ impl Agent for MyAgent { ... } │
├─────────────────────────────────────────────────────────┤
│ Agent Trait + Macros │
│ Agent, export_agent!, get_default_callbacks() │
├─────────────────────────────────────────────────────────┤
│ High-Level Wrappers (env module) │
│ Jvmti - JVMTI operations (150+ methods) │
│ JniEnv - JNI operations (60+ methods) │
│ LocalRef - RAII guard, prevented from escaping │
│ GlobalRef - RAII guard, releases on drop │
├─────────────────────────────────────────────────────────┤
│ Class File Parser (classfile) │
│ ClassFile - All standard Java 8-27 attributes │
├─────────────────────────────────────────────────────────┤
│ Convenience Imports (prelude) │
│ prelude::* - Agent, env, sys, helpers │
├─────────────────────────────────────────────────────────┤
│ Raw FFI Bindings (sys module) │
│ sys::jni - Complete JNI vtable (236 functions) │
│ sys::jvmti - Complete JVMTI vtable (156 functions) │
└─────────────────────────────────────────────────────────┘
Events require three steps — capabilities, callbacks, then enable:
use jvmti_bindings::prelude::*;
fn on_load(&self, vm: *mut jni::JavaVM, _options: &str) -> jni::jint {
let jvmti_env = Jvmti::new(vm).expect("Failed to get JVMTI");
// 1. Request capabilities (must happen in on_load)
let mut caps = jvmti::jvmtiCapabilities::default();
caps.set_can_generate_all_class_hook_events(true);
jvmti_env.add_capabilities(&caps).expect("capabilities");
// 2. Wire callbacks to your Agent impl
let callbacks = get_default_callbacks();
jvmti_env.set_event_callbacks(callbacks).expect("callbacks");
// 3. Enable specific events
jvmti_env.enable_event(
jvmti::JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
std::ptr::null_mut(),
).expect("enable");
jni::JNI_OK
}| Capability | Required For |
|---|---|
can_generate_all_class_hook_events |
class_file_load_hook |
can_generate_method_entry_events |
method_entry |
can_generate_method_exit_events |
method_exit |
can_generate_exception_events |
exception, exception_catch |
can_tag_objects |
Object tagging, heap iteration |
can_retransform_classes |
retransform_classes() |
can_redefine_classes |
redefine_classes() |
can_get_bytecodes |
get_bytecodes() |
can_get_line_numbers |
get_line_number_table() |
can_access_local_variables |
get_local_*(), set_local_*() |
| JDK | Status | Notable Additions |
|---|---|---|
| 8 | ✅ Tested | Baseline |
| 11 | ✅ Tested | SetHeapSamplingInterval |
| 17 | ✅ Tested | — |
| 21 | ✅ Tested | Virtual thread support |
| 27 | ✅ Verified | ClearAllFramePops |
Bindings generated from JDK 27 headers, backwards compatible to JDK 8.
| Aspect | Status |
|---|---|
| API stability | Pre-1.0, breaking changes possible |
| JVMTI coverage | 156/156 (100%) |
| JNI coverage | 236/236 (100%) |
| Dependencies | Zero |
| Testing | Header verification, example agents |
# Minimal agent — lifecycle events only
cargo build --release --example minimal
# Method counter — counts all method entries/exits
cargo build --release --example method_counter
# Class logger — logs every class load
cargo build --release --example class_logger- Your First Production Agent — Step-by-step guide with production hardening
- Public API Surface — What is stable and supported
- API Stability Checklist — Pre-1.0 stability rules
- Contributor Style Guide — Prelude-first and API consistency
- Public API Report — Snapshot of the public surface
- API Report Script — Regenerate the report with rustdoc JSON
- Changelog — Release notes and breaking changes
- Comparison With Alternatives — Feature parity and positioning
- Benchmarks — How to run and view Criterion reports
- Embedding A JVM — Start a JVM from Rust and attach threads
- Dynamic Attach — Agent_OnAttach example and notes
- Safety and FFI Checklist — Safety rules and audit checklist
- Pitfalls and Footguns — Common JVMTI/JNI traps
- Compatibility Matrix — JDK 8-27 coverage
- Versioning Policy — API stability and SemVer plan
- API Reference — Complete API documentation on docs.rs
MIT OR Apache-2.0