jvmti-bindings
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.
Why This Exists
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.
Comparison with Alternatives
| Feature | jvmti-bindings | jni | jvmti-rs | jni-simple |
|---|---|---|---|---|
| JVMTI support | ✅ 156/156 (100%) | ❌ None | ⚠️ Partial | ⚠️ Untested |
| JNI support | ✅ 236/236 (100%) | ✅ Complete | ⚠️ Partial | ✅ Complete |
| Agent trait | ✅ Yes | ❌ No | ❌ No | ❌ No |
| RAII guards | ✅ LocalRef, GlobalRef | ✅ Yes | ❌ No | ❌ No |
| Dependencies | 0 | 3+ | 1+ | 1 |
| Build-time JDK | ❌ Not needed | ❌ Not needed | ✅ Required | ❌ Not needed |
| Last updated | 2025 | 2024 | 2024 | 2024 |
| JDK range | 8–27 | 8–21 | Unknown | 8–21 |
When to use what:
- jvmti-bindings: Building JVMTI agents (profilers, tracers, debuggers)
- jni: Calling Java from Rust applications (not agents)
- jvmti-rs: Legacy projects already using it
- jni-simple: Minimal JNI with no frills
Why Rust for JVMTI?
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.
Design Goals
| 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 |
Quick Start
1. Create your crate
2. Configure Cargo.toml
[]
= ["cdylib"]
[]
= "0.1"
3. Implement your agent
use ;
use jni;
;
export_agent!;
4. Build and run
# Linux
# macOS
# Windows
What export_agent! Does
The macro generates the native entry points the JVM expects.
It does:
- Generate
Agent_OnLoad/Agent_OnUnloadFFI entry points - Initialize JNI and JVMTI environments
- Register callbacks before enabling events
- Convert JVMTI error codes into
Result<T, JvmtiError> - Store your agent instance globally (must be
Sync + Send)
It does not:
- Hide undefined JVMTI behavior
- Make callbacks re-entrant or async-safe
- Attach arbitrary native threads automatically
- Prevent JVM crashes from invalid JVMTI usage
The goal is clarity, not magic.
Safety Model
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 | Wrapper methods handle alloc/dealloc |
| Errors are never ignored | All methods return Result<T, jvmtiError> |
What Remains Unsafe
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.
Is This For You?
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 API (not yet wrapped)
- Want zero
unsafeanywhere
Architecture
┌─────────────────────────────────────────────────────────┐
│ Your Agent Code │
│ impl Agent for MyAgent { ... } │
├─────────────────────────────────────────────────────────┤
│ Agent Trait + Macros │
│ Agent, export_agent!, get_default_callbacks() │
├─────────────────────────────────────────────────────────┤
│ High-Level Wrappers (env module) │
│ Jvmti - JVMTI operations (153 methods) │
│ JniEnv - JNI operations (60+ methods) │
│ LocalRef - RAII guard, prevented from escaping │
│ GlobalRef - RAII guard, releases on drop │
├─────────────────────────────────────────────────────────┤
│ Raw FFI Bindings (sys module) │
│ sys::jni - Complete JNI vtable (236 functions) │
│ sys::jvmti - Complete JVMTI vtable (156 functions) │
└─────────────────────────────────────────────────────────┘
Enabling Events
Events require three steps — capabilities, callbacks, then enable:
use Jvmti;
use jvmti;
use get_default_callbacks;
Capabilities Reference
| 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 Compatibility
| 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.
Project Status
| 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 |
Examples
# Minimal agent — lifecycle events only
# Method counter — counts all method entries/exits
# Class logger — logs every class load
License
MIT OR Apache-2.0