A tool for bridging C/Rust native code to the Java Foreign Function & Memory (FFM) API. Its main goal is to automate the generation of a high-performance interface between native code and the JVM. I originally built it to bridge native physics engines to the JVM, but it generalizes to any native code.
The central feature is a deferred command buffer: commands are serialized into a shared per-thread
buffer and executed in batch when you call flush() or when the buffer runs out of space. That
said, you can also use it to generate a plain bridge with no buffering and only direct calls, or
any mix of the two.
This crate is in active development. The interface may change, documentation is incomplete, and there's a fair amount of refactoring still to do. If you find a bug, please open an issue.
- Rust (stable)
- Java 22+
- jextract — must be installed and on your
PATH
cargo install --path .or add it as a local dependency in your workspace.
The best way to get a feel for it is to look at the examples/ folder alongside this explanation.
You give Jconduit two C headers:
functions_header— the function declarations you want to exposetypedef_header— the struct and type definitions shared between Java and native. All types referenced in your function signatures must be defined directly in this file — no#includechaining.
From those, Jconduit runs the following pipeline:
- Runs bindgen to generate a Rust FFI interface to your native code.
- Generates a Rust middleman: a command dispatcher that reads the JVM's command buffer and calls into native, plus direct-call wrappers for functions that return values or are marked as direct.
- Generates a
_java_bindings.hheader describing the middleman's interface. - Runs jextract on that header to produce the low-level Java bindings.
- Generates a Proxy class — this is what you actually use from Java. It writes commands into
a per-thread buffer, handles flushing, resizing, and alignment, and exposes direct calls for
queries. The buffer is shared across instances on the same thread, so a
flush()call will dispatch everything in the buffer regardless of which instance wrote it.
Jconduit is configured via a jconduit.toml file. Once it's set up:
cargo jconduit generateor point it at a specific config:
cargo jconduit generate --config path/to/jconduit.tomlCore metadata and I/O paths.
[generator]
crate_name = "your_crate_name" # Name of the generated Rust crate.
proxy_class_name = "YourProxyClass" # Name of the generated Java proxy class.
package = "com.example" # Java package for generated code.
functions_header = "./path/to/functions.h" # Header with the function declarations to expose.
typedef_header = "./path/to/types.h" # Header with shared type definitions (structs, enums).
prefix = "engine_" # Prefix used to allowlist functions automatically.
version = "0.1.0" # Your bridge version.
output_dir = "./generated/jconduit" # Output directory. Defaults to "./generated/jconduit".Controls what bindgen picks up. See bindgen's docs for details.
[filters]
allowlist_functions = []
allowlist_types = []
allowlist_vars = []
allowlist_items = []
allowlist_files = []
# These defaults already block compiler internals and common POSIX noise.
blocklist_functions = []
blocklist_types = ["^__.*", "^va_list.*"]
blocklist_vars = ["^__.*", "^COBJMACROS$"]
blocklist_items = ["^__.*"]
blocklist_files = []
opaque_types = ["^opaque_.*"]
no_copy_types = []
no_debug_types = []
no_default_types = []
no_hash_types = []Fine-tunes code generation behavior.
[options]
strip_prefix = "" # Strip this prefix from C function names in the generated Rust code.
# Bindgen options
derive_copy = true
derive_default = false
use_core = false
c_naming_conversion = false
layout_tests = true
# Raw lines injected at the top of the generated Rust file.
raw_lines = ["#![allow(dead_code, non_camel_case_types, non_snake_case, non_upper_case_globals)]"]
# Functions matching any of these prefixes or containing any of these keywords are routed to
# the direct (synchronous) execution path instead of the command buffer.
direct_prefixes = ["get_", "fetch_", "jconduit_direct_"]
direct_keywords = ["_get_"]
# If a void function's last parameter matches one of these suffixes or prefixes, Jconduit
# generates a convenience overload that allocates a scratch buffer, calls the function, and
# returns the result directly — so you don't have to manage the output pointer yourself.
# Example: `void body_get_rotation(uint32_t id, Quaternion *rotation_out)` becomes
# `MemorySegment bodyGetRotation(int id)`.
# Only one output parameter per function is supported, and it must be the last parameter.
output_param_prefix = []
output_param_suffix = ["_out", "_dest"]Command buffer sizing and lifecycle policy.
[proxy_settings]
min_buffer_size = 65536 # Minimum buffer size in bytes (64KB).
max_buffer_size = 2097152 # Maximum buffer size in bytes (2MB).
# The buffer shrinks if usage stays below `decaying_usage_threshold` for
# `decaying_frames_threshold` consecutive flush() calls.
decaying_frames_threshold = 300 # Flushes to wait before shrinking.
decaying_usage_threshold = 0.5 # Shrink if usage stays below 50%.
shrink_rate = 0.75 # Shrink the buffer to 75% of its current size.
# When a command doesn't fit, the buffer is flushed first, then grown by this factor.
growth_rate = 1.50 # Grow by 150% on overflow.
# If true, Jconduit manages the Arena lifecycle automatically (GC-managed).
# Disable this if you manage your own threads and want deterministic cleanup.
auto_arena = true