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
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 — 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 |
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.
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.
Compatibility
See docs/COMPATIBILITY.md for a full JDK 8-27 matrix.
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
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;
Examples
Included examples (build as cdylib agents):
examples/minimal.rsexamples/class_logger.rsexamples/profiler.rsexamples/tracer.rsexamples/heap_sampler.rs
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
- Obtain the JVMTI environment for you
- Register callbacks or enable events
- 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 | 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 dynamic attach (use
Agent::on_attach/Agent_OnAttach) - 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 (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) │
└─────────────────────────────────────────────────────────┘
Enabling Events
Events require three steps — capabilities, callbacks, then enable:
use *;
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
Documentation
- 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
- 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