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.
Scope boundary
This crate is a generic JNI/JVMTI binding and agent framework (published on
crates.io — current release 2.2.1). Bytecode instrumentation engines, spec
transforms, stackmap-aware BCI, and related policy live in the separate
bytecode-instrument project. Do not add that instrumentation technology here
unless there is an explicit decision to merge or port it. Agents can depend on
both crates independently.
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
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
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 by default — Core JNI/JVMTI bindings need no external Rust crates; optional helpers are feature-gated
- 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; optional helper deps are feature-gated |
| Long-term compatibility | Verified against OpenJDK headers, JDK 8 through 27 |
Safety and FFI
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.
Public API
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.
Raw FFI Access
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::*_with_jvmticallback variants when a callback needs the exactjvmtiEnv*supplied by the JVM.
Attach and Threading Rules
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.- For bytecode transforms and live metadata collection, prefer
class_file_load_hook_with_jvmti,vm_init_with_jvmti, or the other*_with_jvmtimethods over rediscovering JVMTI fromJavaVM.
ClassLoader and JPMS Helpers
JniEnv includes neutral helpers for modern agent work:
define_classfor target-loader helper injection.class_loader_parentandsystem_class_loaderfor loader graph discovery.module_name,module_packages, andmodule_class_loaderfor JPMS metadata.module_can_read,module_is_exported_to, andmodule_is_open_tofor visibility preflight.
These are generic JNI conveniences. Higher-level instrumentation policy, helper deployment, and compatibility planning remain outside this crate.
Compatibility
See docs/COMPATIBILITY.md for a full JDK 8-27 matrix.
The event callback ABI has dedicated offset tests and a live multi-JDK agent
proof. Run cargo test --test jvmti_event_abi for the portable checks or
scripts/prove-event-callback-matrix.sh to exercise callback delivery on the
installed JVMs.
Advanced Helpers
Feature-gated helpers live under advanced:
heap-graphfor heap tagging and reference edge extraction.
Enable with:
[]
= { = "2", = ["heap-graph"] }
Quick Start
1. Create your crate
2. Configure Cargo.toml
[]
= ["cdylib"]
[]
= "2"
3. Implement your agent
use *;
;
export_agent!;
4. Build and run
# Linux
# macOS
# Windows
Dynamic Attach (Optional)
If you want to attach to an already running JVM, implement Agent::on_attach and load the agent with the JVM Attach API:
use *;
;
export_agent!;
Attach it with jcmd (example):
JVMTI.agent_load expects an absolute path to the native agent and an optional option string.
Class File Parsing
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 ClassFile;
Nested attributes are preserved and exposed (method Code attributes, record component attributes, and more). You can traverse them like this:
use ;
Embedding A JVM (Optional)
If you want to embed a JVM inside a Rust process (not just build an agent), enable the embed feature and use JavaVmBuilder:
use *;
let builder = default
.option?
.option?
.option?;
let vm = builder.create?; // uses JAVA_HOME or JVM_LIB_PATH
let env = unsafe ; // only valid on the creating thread
// ... call JNI through env ...
scope;
vm.destroy?;
The embed feature uses libloading; the core crate remains dependency-free by default. See docs/EMBEDDING.md and examples/embed.rs for details.
Examples
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)
Agent Starter Template
See templates/agent-starter/ for a ready-to-copy agent crate.
CI
The repository includes a GitHub Actions workflow that builds and tests on Linux, macOS, and Windows.
What export_agent! Does
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
- Register callbacks or enable events
- Prevent JVM crashes from invalid JVMTI usage
For callbacks that need the raw JVMTI callback environment, implement the
corresponding *_with_jvmti method, for example vm_init_with_jvmti or
class_file_load_hook_with_jvmti.
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 | High-level JVMTI methods deallocate buffers they allocate |
| Errors are explicit | JVMTI methods return Result, JNI helpers use Option/Result |
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 only pure-Java instrumentation (
java.lang.instrumentmay be simpler) - 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 (150+ methods) │
│ JniEnv - JNI operations plus ClassLoader/JPMS │
│ 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) │
└─────────────────────────────────────────────────────────┘
Enabling Events
Events require three steps — capabilities, callbacks, then enable:
use *;
Lower-level helpers are available when you need explicit control:
let caps = for_method_trace;
jvmti_env.add_capabilities?;
jvmti_env.set_default_agent_callbacks?;
jvmti_env.enable_method_entry_exit_events?;
For diagnostics:
eprintln!;
eprintln!;
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 | SemVer 2.x; breaking changes require a major release |
| JVMTI coverage | 156/156 (100%) |
| JNI coverage | 236/236 (100%) |
| Dependencies | Zero by default; optional embed and bench-tools features |
| Testing | Classfile parser, doctests, all-feature builds, example agents |
Examples
# Minimal agent — lifecycle events only
# Method counter — counts all method entries/exits
# Class logger — logs every class load
Documentation
- Your First Production Agent — Step-by-step guide with production hardening
- Public API Surface — What is stable and supported
- API Stability Checklist — 2.x 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
License
MIT OR Apache-2.0