CratonVM
Contributing to CratonVM
Contributing to CratonVM
Thank you for your interest in contributing to CratonVM! This document provides guidelines and information to help you get started.
Code of Conduct
This project follows the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.
Getting Started
- Fork the repository and clone your fork
- Install prerequisites (see BUILD_GUIDE.md):
- Rust 1.80+ via rustup.rs
- JDK 17+ (for compiling test Java classes)
- Visual Studio Build Tools (Windows only)
- Build and run tests:
cargo build --all-targets cargo test --all - Optional, recommended — enable the repository's hooks:
Currently onegit config core.hooksPath .githookspre-pushhook, running the ~1.8 s flag-surface guards. CI already runs them, but branches here are merged intodevand pushed directly, so CI reports a red surface rather than preventing one. See docs/contributing/flag-surface-hook.md.
Development Workflow
Before Submitting
Run the following release-quality checks before submitting and include any
known failures in the PR notes. CI (.github/workflows/ci.yml) is configured to
run them on every push and pull request:
- Format —
cargo fmt --all --check - Lint —
cargo clippy --workspace --all-targets -- -D warnings - Build —
cargo build --workspace - Test —
cargo test --workspace
CI runs these on ubuntu-latest and windows-latest, together with the
synthetic-jdk and experimental-* feature gates, the exact synthetic-stub
ratchet, the Markdown link check, the semantic differential gate, the fuzz build
smoke, coverage generation, and a Miri job over the core representation crate.
All of those are blocking — including the exact synthetic-stub ratchet, which
runs in the ordinary build-and-test job and carries no continue-on-error.
Two things are deliberately advisory: the Test vm (synthetic-jdk) step,
because the harness aborts mid-run and cannot report a result at all, and the
JDK-only mode job, because wave 1 is measurement and a --jdk-only run is
still expected to fail on real workloads. Each carries its own comment with
the measurement and the conditions for re-promoting it. Nothing else in the
workflow is advisory; do not add to that list to make a branch green.
Steps 1 and 4 are not green today. cargo fmt --all --check reports over a
thousand diffs tree-wide, and cargo test --workspace has a residual failure
set that predates any given change and is tracked internally. Compare your run
against that known set rather than against zero, and note in the PR which
entries you saw — a new name in the output is the signal.
Code Style
- Follow
rustfmtdefaults withmax_width = 100(seerustfmt.toml) - Keep
cargo clippy --workspace --all-targets -- -D warningsclean under the workspace[lints]config (the rootCargo.tomlallowsdead_code/unused_*and a few rustdoc lints), not the full default lint set - Use
thiserrorfor error types - Use
tracingfor logging (notprintln!oreprintln!in library crates) - Add
// SAFETY:comments to allunsafeblocks explaining the invariant
Project Structure
| Crate | Purpose |
|---|---|
reader | Java .class file parser |
types | Shared types (Value, ClassId, ObjectRef) |
native-api | NativeContext trait & FD table |
native-builtins | java.lang.* native methods |
native-collections | java.util.* native methods |
native-io | java.io/nio native methods |
native-awt | AWT/Swing/Java2D native peer implementation |
jit-api | JIT compiler API types |
jit | x86-64 / AArch64 JIT compiler |
jit-cuda | Java bytecode -> PTX lowering for GPU offload |
cuda-bridge | Thin CUDA Driver API bridge for GPU offload |
craton-gpu | Build-time Java annotation sources (@Parallel etc.) for GPU offload |
classloading | Class loading & bytecode verification |
gc | Generational GC default (young/old; Cheney moving + non-moving sweep); opt-in G1 region collector (-XX:+UseG1GC, experimental); ZgcRealHeap, a real memory-backed STW non-moving mark-sweep that -XX:+UseZGC genuinely selects, but compiled in only behind the default-off zgc feature, so absent from a stock build |
jfr | Java Flight Recorder |
vm | VM runtime engine |
vm-cli | Command-line entry point |
libcratonvm | C-ABI shared library for embedding (cdylib/staticlib libjvm substitute, JNI Invocation API) |
cratonvm-embed | Curated, semver-stable Rust facade for embedding CratonVM |
Writing Tests
- Add unit tests in the same file as the code (
#[cfg(test)]module) - Integration tests that require
javacshould skip gracefully if it's not available - Use
RUST_MIN_STACK=8388608for tests involving deep recursion
Commit Messages
- Use concise, descriptive commit messages
- Prefix with the affected area when useful:
reader: fix constant pool bounds check - Reference issue numbers where applicable:
Fix #42: handle empty switch tables
Updating the Changelog
When your PR adds a feature, fixes a bug, or changes behavior:
- Add an entry under
## [Unreleased]in CHANGELOG.md - Use the appropriate subsection:
Added,Changed,Fixed,Removed, orPerformance - Keep entries concise (one line per change)
Code Review Process
- Open a pull request against
main - Fill out the PR template
- A maintainer will review your PR, typically within a few days
- Address any feedback — push follow-up commits rather than force-pushing
- Once approved and CI passes, a maintainer will merge the PR
Areas for Contribution
Good First Issues
- Adding missing
java.lang.Mathmethods - Improving error messages in the bytecode verifier
- Adding tests for edge cases in existing native method implementations
Larger Projects
- ARM64 JIT backend (
jit/src/aarch64.rs) - Concurrent garbage collector
- Full JNI implementation
- Module system support
How to Add a New Bytecode Opcode
-
Define the opcode — add a variant to
Instructioninreader/src/instruction.rs. Include the opcode byte value and any operands. -
Parse it — in
reader/src/class_reader.rs, add a decode arm inread_instruction()that reads the operand bytes and constructs yourInstructionvariant. -
Verify it — in
classloading/src/verify_insn.rs, add a verification case that checks the expected stack/local types and produces the correct output type. -
Interpret it — in
vm/src/runtime/interpreter.rs, add a match arm in the main dispatch loop. Follow the existing patterns for stack manipulation. -
JIT-compile it (optional) — in
jit/src/x64.rs, add code generation for the new opcode incompile_instruction(). -
Test it — add unit tests in the interpreter module and a Java test class in
test_classes/that exercises the opcode.
How to Add a New Native Method
-
Choose the crate —
native-builtinsforjava.lang.*,native-collectionsforjava.util.*,native-ioforjava.io.*/java.nio.*. -
Register the method — in the appropriate crate's registration function, add:
registry.register( "java/lang/MyClass", "myMethod", "(Ljava/lang/String;)I", // descriptor |ctx, args| { // args[0] = this (for instance methods) // args[1..] = parameters let s = ctx.get_string_value(args[1].as_object().unwrap().unwrap())?; Ok(Some(Value::Int(s.len() as i32))) }, ); -
Declare an accurate
NativeKind— this is mandatory, and the trap is that it is invisible at the call site.register()takes four arguments and none of them is the kind. The kind is ambient: it comes from whateverset_category/with_categoryscope the enclosing registrar happens to be in, and the registry'scurrent_categorydefaults toNativeKind::SyntheticStub. So a genuine bridge registered outside awith_category(Bridge, …)scope — or after aset_categorythat was never restored — is silently recorded as a stub. Wrap every new registration:registry.with_category(NativeKind::Bridge, |r| { r.register("java/lang/MyClass", "myMethod", "(Ljava/lang/String;)I", …); });Prefer the scoped
with_categoryover bareset_category, and check the category actually in force at your call site rather than assuming the function you are editing sets one.This is not hypothetical.
native-api/src/registry.rscarries a permanent diagnostic (CRATONVM_DBG_DROPPED_STUBS) added while chasing a real-JDK boot regression —InternalError: null property: java.home— that traced to a wholeregister_*function's worth of permanentjava.util.Propertiesbridges inheriting the wrong ambient category at one of its call sites. Mis-tagging does not merely mislabel: underCRATONVM_NO_STUBS, and under--jdk-only,register()refuses aSyntheticStuboutright, so a mis-tagged bridge is never registered at all and the failure surfaces far from its cause. The ambient default no longer decides anything: reclassification work closed out into five slack-free ratchets scored byregression-suite/bridge-ratchet.shrather than a number in a document.One class of mis-tag is now decided centrally rather than at the site: a registration whose receiver class no supported JDK image declares cannot bind to an
ACC_NATIVEmethod, soregister()re-tags itSyntheticStubfrom the measured table innative-api/src/no_image_receiver.rs. If you are adding a native on a class the VM mints — an iterator stand-in, a functional combinator, acratonvm/…receiver — check that table before choosing a kind; it is probably already deciding for you. -
Use
NativeContext— thectxparameter provides:ctx.alloc_object(class_id)— allocate a new objectctx.get_field(obj, index)/ctx.set_field(obj, index, value)— field accessctx.get_string_value(obj)— extract a RustStringfrom a Java Stringctx.create_string(s)— create a Java String from a Rust&strctx.throw_exception(class, message)— throw a Java exception
-
Test it — add a
#[test]in the same file usingTestNativeContextfromnative-builtins/src/test_utils.rs.
Adding a Compatibility Stub
A compatibility stub is anything that stands in for the JDK's own code: a
NativeKind::SyntheticStub native, or a class fabricated without real class
bytes. They are permitted, but they are debt, and debt has to be booked. A PR
that adds one must carry all three of:
- An explicit non-strict classification. State in code that the thing is a
stub —
with_category(NativeKind::SyntheticStub, …)at the registration, or the correspondingClassOrigin::CompatibilityStub { reason }with a real reason string. Do not let a stub reach that classification by omission: an unclassified registration already defaults toSyntheticStub, so "it came out tagged correctly" is not evidence that anybody decided. - Tests. Cover the behaviour the stub stands in for, so the day it is deleted the replacement is checked against something.
- A tracking issue for its removal, linked from the code comment. A stub with no removal issue is a permanent divergence that nobody has agreed to.
Do not raise the ratchet baseline to go green. cargo test -p cratonvm-native-builtins --test stub_ratchet asserts that the SyntheticStub
count never exceeds the committed baseline, and that baseline is frozen with
zero slack precisely so a single new application-visible stub fails CI.
Editing the baseline constant to match your branch converts a signal into a
rubber stamp. If the count legitimately has to move, the baseline change is the
subject of the PR and needs its own justification — not a line in a diff that is
about something else. See
docs/contributing/stub-ratchet.md and the
no-synthetic-stubs policy.
Before writing a stub, check whether the JDK's own bytecode can run instead;
docs/jdk-only-native-review.md is the
checklist for that decision (and the gate every existing stub must pass to
survive into --jdk-only). Conversely, if you are implementing a genuine VM
boundary crossing or a proven intrinsic, tag it Bridge / Intrinsic — see
the ambient-category warning in step 3 above, because getting that wrong turns
a bridge into a stub silently.
See ROADMAP.md for the full list of planned work.
Reporting Issues
- Use GitHub Issues for bug reports and feature requests
- Include the Java source code and
.classfile (or steps to reproduce) for bugs - Include the full error output from CratonVM
Developer Certificate of Origin (DCO)
This project uses the Developer Certificate of Origin (DCO). By submitting a pull request, you certify that your contribution is your original work (or you have the right to submit it) and that you agree to license it under the project's Apache 2.0 license.
You can sign off your commits by adding -s to git commit:
git commit -s -m "reader: add support for ConstantDynamic"
This appends a Signed-off-by: Your Name <email@example.com> line to your commit message.
License
By contributing, you agree that your contributions will be licensed under the Apache License 2.0.