jvmti-bindings 2.2.1

Complete JNI and JVMTI bindings for Rust with zero dependencies by default. Build JVM agents in pure Rust.
Documentation

jvmti-bindings

Crates.io Docs.rs CI

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 JNIEnv misuse 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:

  1. Full JNI + JVMTI coverage (agent-first focus)
  2. Safe, owned return types in the high-level env wrappers
  3. Class file parsing with all standard Java 8-27 attributes
  4. A tiny but explicit public surface (env, sys, classfile, prelude)
  5. Safety guidance, pitfalls, and compatibility documentation
  6. 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:

  1. Never use JNIEnv across threads.
  2. Never panic across JNI/JVMTI callbacks.
  3. Always deallocate JVMTI buffers with Deallocate.
  4. Avoid JNI calls in GC callbacks.

Public API

The supported public surface is intentionally small. For most users:

  1. Use env for safe wrappers.
  2. Use prelude for standard imports.
  3. Use sys only for raw FFI work.

Details: docs/PUBLIC_API.md.

Raw FFI Access

If you need raw JNI/JVMTI functions, use:

  1. jvmti_bindings::sys::jni and jvmti_bindings::sys::jvmti for raw types and vtables.
  2. JniEnv::raw() and Jvmti::raw() to access the underlying raw pointers.
  3. Agent::*_with_jvmti callback variants when a callback needs the exact jvmtiEnv* supplied by the JVM.

Attach and Threading Rules

  1. Agent_OnAttach is supported via the export_agent! macro and Agent::on_attach.
  2. JNIEnv is thread-local and must only be used on its originating thread.
  3. GlobalRef cleanup attaches to the JVM when needed, but you should still manage lifetimes explicitly.
  4. For bytecode transforms and live metadata collection, prefer class_file_load_hook_with_jvmti, vm_init_with_jvmti, or the other *_with_jvmti methods over rediscovering JVMTI from JavaVM.

ClassLoader and JPMS Helpers

JniEnv includes neutral helpers for modern agent work:

  • define_class for target-loader helper injection.
  • class_loader_parent and system_class_loader for loader graph discovery.
  • module_name, module_packages, and module_class_loader for JPMS metadata.
  • module_can_read, module_is_exported_to, and module_is_open_to for 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:

  1. heap-graph for heap tagging and reference edge extraction.

Enable with:

[dependencies]
jvmti-bindings = { version = "2", features = ["heap-graph"] }

Quick Start

1. Create your crate

cargo new --lib my_agent
cd my_agent

2. Configure Cargo.toml

[lib]
crate-type = ["cdylib"]

[dependencies]
jvmti-bindings = "2"

3. Implement your agent

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);

4. Build and run

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 MyApp

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 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.

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 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);
    }
}

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 jvmti_bindings::prelude::*;

let builder = JavaVmBuilder::default()
    .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 ...

std::thread::scope(|s| {
    s.spawn(|| {
        vm.with_attached_current_thread(|env| {
            let _string = env.find_class("java/lang/String");
        })
        .expect("attach current thread");
    });
});

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):

  1. examples/minimal.rs
  2. examples/class_logger.rs
  3. examples/profiler.rs
  4. examples/tracer.rs
  5. examples/heap_sampler.rs
  6. examples/attach_logger.rs (dynamic attach via Agent_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_OnAttach entry points
  • Create your agent instance and store it globally (must be Sync + Send)
  • Pass the options string to your on_load / on_attach implementation

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 jni crate)
  • Are uncomfortable debugging native JVM crashes
  • Need only pure-Java instrumentation (java.lang.instrument may be simpler)
  • Want zero unsafe anywhere

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 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");

    // Requests capabilities, wires callbacks, and enables ClassFileLoadHook.
    jvmti_env.configure_class_file_load_hook_agent().expect("configure");

    jni::JNI_OK
}

Lower-level helpers are available when you need explicit control:

let caps = jvmti::jvmtiCapabilities::for_method_trace();
jvmti_env.add_capabilities(&caps)?;
jvmti_env.set_default_agent_callbacks()?;
jvmti_env.enable_method_entry_exit_events()?;

For diagnostics:

eprintln!("jni={}", describe_jni_result(jni::JNI_EDETACHED));
eprintln!("jvmti={}", jvmti::error_name(jvmti::jvmtiError::MUST_POSSESS_CAPABILITY));

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
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

Documentation

License

MIT OR Apache-2.0