Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ jobs:
continue-on-error: true
run: cargo test

- name: Check JVM binding macros
id: jvm_macro_tests
continue-on-error: true
shell: bash
run: |
cargo fmt --manifest-path jvm/Cargo.toml --check
cargo test --manifest-path jvm/Cargo.toml

- name: Run self-tests (debug)
id: self_tests_debug
continue-on-error: true
Expand All @@ -90,10 +98,11 @@ jobs:
always() &&
(
steps.compiler_unit_tests.outcome != 'success' ||
steps.jvm_macro_tests.outcome != 'success' ||
steps.self_tests_debug.outcome != 'success' ||
steps.self_tests_release.outcome != 'success'
)
run: python -c "raise SystemExit('debug and release self-tests must both pass completely')"
run: python -c "raise SystemExit('compiler, macro, debug, and release tests must pass completely')"

coretests:
name: coretests (Ubuntu)
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/update-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ jobs:
if: steps.latest.outputs.changed == 'true'
run: cargo test

- name: Check JVM binding macros
if: steps.latest.outputs.changed == 'true'
shell: bash
run: |
cargo fmt --manifest-path jvm/Cargo.toml --check
cargo test --manifest-path jvm/Cargo.toml

- name: Run self-tests (debug)
if: steps.latest.outputs.changed == 'true'
run: python Tester.py
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Ignores relevant to Rust projects within this tree
**/target/*
/jvm/Cargo.lock

# Ignores relevant to Java projects within this tree
**/build/*
Expand Down
83 changes: 42 additions & 41 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,45 +242,39 @@ public class Main {
```

**Rust**
```rust
#![feature(extern_types, register_tool)]
#![register_tool(jvm)]

unsafe extern "C" {
#[link_name = "java/time/LocalDate"]
pub type JavaLocalDate;

#[link_name = "jvm:static:java/time/LocalDate:of"]
fn java_local_date_of(year: i32, month: i32, day: i32) -> *const JavaLocalDate;
Add `jvm = { package = "rcj", git = "https://github.com/IntegralPilot/rustc_codegen_jvm" }`
to `[dependencies]`.

#[link_name = "jvm:virtual:getYear"]
fn java_local_date_get_year(date: &JavaLocalDate) -> i32;
```rust
#![feature(extern_types, register_tool)]
#![register_tool(jvm_codegen)]

#[link_name = "Main$JavaCounter"]
pub type JavaCounter;
#[jvm::class("java.time.LocalDate", rename_all = "camelCase")]
impl JavaLocalDate {
#[jvm::static_method]
pub fn of(year: i32, month: i32, day: i32) -> *mut Self {}

#[link_name = "jvm:new:Main$JavaCounter"]
fn java_counter_new(value: i32) -> *mut JavaCounter;
#[jvm::method]
pub fn get_year(&self) -> i32 {}
}

// A return value makes this an instance-field getter.
#[link_name = "jvm:field:value"]
fn java_counter_value(counter: &JavaCounter) -> i32;
#[jvm::class("Main$JavaCounter", rename_all = "camelCase")]
impl JavaCounter {
#[jvm::constructor]
pub fn new(value: i32) -> *mut Self {}

// A value parameter and () return make this an instance-field setter.
#[link_name = "jvm:field:value"]
fn java_counter_set_value(counter: &mut JavaCounter, value: i32);
#[jvm::field]
pub fn value(&self) -> i32 {}

#[link_name = "jvm:static-field:Main:sharedCount"]
fn shared_count() -> i32;
#[jvm::field]
pub fn set_value(&mut self, value: i32) {}

#[link_name = "jvm:static-field:Main:sharedCount"]
fn set_shared_count(value: i32);
}
#[jvm::static_field(class = "Main")]
pub fn shared_count() -> i32 {}

impl JavaLocalDate {
pub fn year(&self) -> i32 {
unsafe { java_local_date_get_year(self) }
}
#[jvm::static_field(class = "Main")]
pub fn set_shared_count(value: i32) {}
}

pub struct NamedCounter {
Expand Down Expand Up @@ -318,7 +312,7 @@ pub enum NetworkEvent {

pub enum AppEvent {
// NetworkEvent extends AppEvent on the JVM; AppEvent$Network is omitted.
#[jvm::subtype]
#[jvm_codegen::subtype]
Network(NetworkEvent),
Calculation(Calculation),
}
Expand Down Expand Up @@ -354,19 +348,19 @@ pub fn run_accumulation(acc: &mut dyn Accumulator) -> i32 {
}

pub fn make_java_date(year: i32, month: i32, day: i32) -> *const JavaLocalDate {
unsafe { java_local_date_of(year, month, day) }
JavaLocalDate::of(year, month, day)
}

pub fn java_date_year(date: &JavaLocalDate) -> i32 {
date.year()
date.get_year()
}

pub fn update_java_counter() -> i32 {
unsafe {
let counter = java_counter_new(5);
java_counter_set_value(&mut *counter, java_counter_value(&*counter) + 1);
set_shared_count(shared_count() + 1);
java_counter_value(&*counter) + shared_count()
let counter = JavaCounter::new(5);
(&mut *counter).set_value((&*counter).value() + 1);
JavaCounter::set_shared_count(JavaCounter::shared_count() + 1);
(&*counter).value() + JavaCounter::shared_count()
}
}
```
Expand Down Expand Up @@ -431,6 +425,7 @@ The following example programs live in `tests/`, are compiled with the standard
| **[Rich Enums](tests/integration/inner_classes/Main.java)** | Constructing, inspecting, comparing, and dispatching through Rust enum interfaces and transparent subtypes. |
| **[Lambda Callbacks](tests/integration/lambda_callbacks/Main.java)** | Passing native Java lambdas directly into Rust functions expecting `Fn` closures. |
| **[Trait Implementors](tests/integration/trait_implementors/Main.java)** | Implementing a Rust trait on a Java class and passing it to Rust dynamic dispatch (`&dyn Trait`). |
| **[JVM Binding Macros](tests/integration/jvm_macros/src/lib.rs)** | Calling constructors, methods, and fields through ergonomic `#[jvm::...]` attributes. |
| **[JVM Link Names](tests/integration/jvm_link_names/src/lib.rs)** | Calling JVM constructors and accessing instance and static fields directly from Rust. |
| **[Kotlin Async](tests/kotlin/async_interop/Main.kt)** | Awaiting Rust `async` functions from Kotlin `suspend` code. |

Expand Down Expand Up @@ -532,25 +527,30 @@ Rust constructs map directly to JVM structures without requiring JNI wrapper cod
| `str` / `&str` | UTF-8-preserving `org.rustlang.runtime.Utf8View` |
| `*const T` / `*mut T` | Shared pointer wrapper (`org.rustlang.runtime.Pointer`) |

### Calling JVM APIs

The [`jvm` attribute crate](jvm) generates JVM method, constructor, and field
bindings without handwritten `link_name` strings. Raw link names remain supported.

### Enums

Rust enums become unsealed Java interfaces, with a final class and public
payload fields for each variant.

`#[jvm::subtype]` lets a one-field variant use its nested enum directly, without
a wrapper class:
`#[jvm_codegen::subtype]` lets a one-field variant use its nested enum directly,
without a wrapper class:

```rust
#![feature(register_tool)]
#![register_tool(jvm)]
#![register_tool(jvm_codegen)]

pub enum Leaf {
A(i32),
B,
}

pub enum Root {
#[jvm::subtype]
#[jvm_codegen::subtype]
Leaf(Leaf),
Other(i32),
}
Expand Down Expand Up @@ -740,6 +740,7 @@ build.
│ └── oomir.rs # OOMIR definitions
├── java-linker/ # JAR packaging and manifest utility
├── cargo-jvm/ # `cargo jvm` build, run, test and package command
├── jvm/ # Attribute macros for JVM bindings
├── runtime/ # Core Java runtime support library
├── std/ # Standard library JVM patch overlays
├── tests/ # Integration, binary, and multicrate tests
Expand Down
17 changes: 17 additions & 0 deletions jvm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "rcj"
version = "0.1.0"
edition = "2024"
description = "Ergonomic attribute macros for rustc_codegen_jvm interop (generates jvm: link_names)"
license = "MIT OR Apache-2.0"
repository = "https://github.com/IntegralPilot/rustc_codegen_jvm"
keywords = ["jvm", "java", "ffi", "macros"]
categories = ["development-tools::ffi", "compilers"]

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["full", "parsing", "printing", "proc-macro", "visit", "visit-mut"] }
quote = "1.0"
proc-macro2 = "1.0"
109 changes: 109 additions & 0 deletions jvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# `rcj`

Attribute macros for calling JVM classes from Rust compiled by
[`rustc_codegen_jvm`](https://github.com/IntegralPilot/rustc_codegen_jvm).
They generate the backend's `jvm:` link names and replace placeholder function
bodies with the corresponding JVM call.

```toml
[dependencies]
jvm = { package = "rcj", git = "https://github.com/IntegralPilot/rustc_codegen_jvm" }
```

```rust,no_run
#![feature(extern_types)]
# use rcj as jvm;

#[jvm::class("java.time.LocalDate", rename_all = "camelCase")]
impl LocalDate {
#[jvm::static_method]
pub fn of(year: i32, month: i32, day: i32) -> *mut Self {}

// Inferred as the JVM method getYear.
#[jvm::method]
pub fn get_year(&self) -> i32 {}
}

#[jvm::class("Main$Counter", rename_all = "camelCase")]
impl Counter {
#[jvm::constructor]
pub fn new(value: i32) -> *mut Self {}

#[jvm::field]
pub fn value(&self) -> i32 {}

// set_value is inferred as the field value.
#[jvm::field]
pub fn set_value(&mut self, value: i32) {}

// Named options keep cross-class bindings unambiguous.
#[jvm::static_field(class = "Main")]
pub fn shared_count() -> i32 {}
}
```

The package is named `rcj`; the `jvm` dependency alias gives the attributes their
natural `#[jvm::...]` spelling. Member attributes may also be imported directly,
such as `use jvm::{constructor, method};` followed by `#[constructor]` or
`#[method]`.

Fully dotted names and JVM slash names both work for ordinary classes. Nested
classes can use JVM `$` syntax (`java.util.Map$Entry`), or an explicit
package/class boundary followed by dots (`java/util/Map.Entry`). An all-dot name
is treated as a package path because package dots and nested-class dots are
otherwise indistinguishable.

A `#[jvm::class("...")]` impl reuses its class name and infers member names. A
single positional argument on a member attribute is the member name; use
`class = "..."` to target another class. Explicit JVM descriptors are available
as `descriptor = "..."` for ambiguous overloads.

For an additional impl block, use `#[jvm::bindings]` so the type is not declared
twice:

```rust,no_run
# #![feature(extern_types)]
# use rcj as jvm;
# #[jvm::class("java.lang.StringBuilder")]
# impl StringBuilder {}
#[jvm::bindings(rename_all = "camelCase")]
impl StringBuilder {
#[jvm::method]
pub fn append_code_point(&mut self, code_point: i32) -> *mut Self {}
}
```

The attributes also work directly in foreign blocks:

```rust,no_run
#![feature(extern_types)]
# use rcj as jvm;

unsafe extern "C" {
#[jvm::class("java.time.LocalDate")]
type LocalDate;

#[jvm::static_method("java.time.LocalDate", "of")]
fn date(year: i32, month: i32, day: i32) -> *mut LocalDate;

#[jvm::method("getYear")]
fn year(date: &LocalDate) -> i32;
}
```

For transparent nested enum variants, use the compiler marker separately so it
does not collide with this crate's `jvm` macro namespace:

```rust
#![feature(register_tool)]
#![register_tool(jvm_codegen)]

enum Leaf {
Value(i32),
}

enum Root {
#[jvm_codegen::subtype]
Leaf(Leaf),
}
```
Loading
Loading