diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0257cd6..f0a305e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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) diff --git a/.github/workflows/update-nightly.yml b/.github/workflows/update-nightly.yml index d1b699c..3c8cdfa 100644 --- a/.github/workflows/update-nightly.yml +++ b/.github/workflows/update-nightly.yml @@ -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 diff --git a/.gitignore b/.gitignore index 4b69161..a9e6ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/Readme.md b/Readme.md index 1046211..f79bd63 100644 --- a/Readme.md +++ b/Readme.md @@ -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 { @@ -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), } @@ -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() } } ``` @@ -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. | @@ -532,17 +527,22 @@ 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), @@ -550,7 +550,7 @@ pub enum Leaf { } pub enum Root { - #[jvm::subtype] + #[jvm_codegen::subtype] Leaf(Leaf), Other(i32), } @@ -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 diff --git a/jvm/Cargo.toml b/jvm/Cargo.toml new file mode 100644 index 0000000..af26d6d --- /dev/null +++ b/jvm/Cargo.toml @@ -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" diff --git a/jvm/README.md b/jvm/README.md new file mode 100644 index 0000000..2c80524 --- /dev/null +++ b/jvm/README.md @@ -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), +} +``` diff --git a/jvm/src/lib.rs b/jvm/src/lib.rs new file mode 100644 index 0000000..befdd4a --- /dev/null +++ b/jvm/src/lib.rs @@ -0,0 +1,1218 @@ +#![doc = include_str!("../README.md")] + +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::{ + Attribute, FnArg, Ident, ImplItem, ImplItemFn, ItemFn, LitStr, Meta, Pat, ReturnType, + Signature, Token, Type, + parse::{Parse, ParseStream, Parser}, + visit::Visit, + visit_mut::VisitMut, +}; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +enum RenameRule { + #[default] + None, + CamelCase, +} + +impl RenameRule { + fn apply(self, name: &str) -> String { + match self { + Self::None => name.to_string(), + Self::CamelCase => lower_camel_case(name), + } + } +} + +#[derive(Clone, Default)] +struct Args { + positional: Vec, + class: Option, + name: Option, + descriptor: Option, + rename_all: Option, +} + +impl Parse for Args { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut result = Self::default(); + while !input.is_empty() { + if input.peek(LitStr) { + result.positional.push(input.parse()?); + } else { + let key: Ident = input.parse()?; + input.parse::()?; + let value: LitStr = input.parse()?; + let slot = match key.to_string().as_str() { + "class" => &mut result.class, + "name" => &mut result.name, + "descriptor" => &mut result.descriptor, + "rename_all" => &mut result.rename_all, + _ => { + return Err(syn::Error::new_spanned( + key, + "unknown JVM option; expected `class`, `name`, `descriptor`, or `rename_all`", + )); + } + }; + if slot.replace(value).is_some() { + return Err(syn::Error::new_spanned(key, "duplicate JVM option")); + } + } + + if input.is_empty() { + break; + } + input.parse::()?; + } + Ok(result) + } +} + +impl Args { + fn parse_tokens(tokens: TokenStream2) -> syn::Result { + Self::parse.parse2(tokens) + } + + fn parse_macro(tokens: TokenStream) -> syn::Result { + Self::parse_tokens(tokens.into()) + } + + fn ensure_options( + &self, + class: bool, + name: bool, + descriptor: bool, + rename_all: bool, + ) -> syn::Result<()> { + for (allowed, option, spelling) in [ + (class, self.class.as_ref(), "class"), + (name, self.name.as_ref(), "name"), + (descriptor, self.descriptor.as_ref(), "descriptor"), + (rename_all, self.rename_all.as_ref(), "rename_all"), + ] { + if !allowed && let Some(value) = option { + return Err(syn::Error::new_spanned( + value, + format!("`{spelling}` is not supported by this JVM attribute"), + )); + } + } + if !self.positional.is_empty() + && (self.class.is_some() || self.name.is_some() || self.descriptor.is_some()) + { + return Err(syn::Error::new_spanned( + &self.positional[0], + "do not mix positional binding arguments with `class`, `name`, or `descriptor`", + )); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum BindingKind { + StaticMethod, + Method, + Constructor, + Field, + StaticField, +} + +impl BindingKind { + fn from_attribute(attribute: &Attribute) -> Option> { + let path = attribute.path(); + let (qualified, name) = match path.segments.len() { + 1 => (false, path.segments[0].ident.to_string()), + 2 if path.segments[0].ident == "jvm" || path.segments[0].ident == "rcj" => { + (true, path.segments[1].ident.to_string()) + } + _ => return None, + }; + Some(match name.as_str() { + "static_method" => Ok(Self::StaticMethod), + "method" => Ok(Self::Method), + "constructor" => Ok(Self::Constructor), + "field" => Ok(Self::Field), + "static_field" => Ok(Self::StaticField), + "static" if qualified => Err(syn::Error::new_spanned( + attribute, + "use `#[jvm::static_method]`; `static` is a Rust keyword", + )), + _ if qualified => Err(syn::Error::new_spanned( + attribute, + format!( + "unknown JVM binding attribute `{}`; expected `static_method`, `method`, `constructor`, `field`, or `static_field`", + quote!(#path), + ), + )), + _ => return None, + }) + } +} + +fn attribute_args(attribute: &Attribute) -> syn::Result { + match &attribute.meta { + Meta::Path(_) => Ok(Args::default()), + Meta::List(list) => Args::parse_tokens(list.tokens.clone()), + Meta::NameValue(value) => Err(syn::Error::new_spanned( + value, + "expected parentheses, for example `#[jvm::method(name = \"getValue\")]`", + )), + } +} + +fn normalize_class(raw: &str) -> String { + let raw = raw.strip_prefix("jvm:class:").unwrap_or(raw); + match raw.rsplit_once('/') { + Some((package, class)) => { + format!("{}/{}", package.replace('.', "/"), class.replace('.', "$")) + } + None => raw.replace('.', "/"), + } +} + +fn nonempty(value: &LitStr, what: &str) -> syn::Result { + let value_string = value.value(); + if value_string.is_empty() { + Err(syn::Error::new_spanned( + value, + format!("JVM {what} cannot be empty"), + )) + } else { + Ok(value_string) + } +} + +fn raw_ident_name(ident: &Ident) -> String { + let name = ident.to_string(); + name.strip_prefix("r#").unwrap_or(&name).to_string() +} + +fn lower_camel_case(name: &str) -> String { + let mut result = String::with_capacity(name.len()); + let mut uppercase_next = false; + for character in name.chars() { + if character == '_' { + uppercase_next = !result.is_empty(); + } else if uppercase_next { + result.extend(character.to_uppercase()); + uppercase_next = false; + } else { + result.push(character); + } + } + result +} + +fn returns_unit(signature: &Signature) -> bool { + match &signature.output { + ReturnType::Default => true, + ReturnType::Type(_, ty) => { + matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty()) + } + } +} + +fn inferred_name(signature: &Signature, kind: BindingKind, rename: RenameRule) -> String { + let rust_name = raw_ident_name(&signature.ident); + let base = match kind { + BindingKind::Field | BindingKind::StaticField if returns_unit(signature) => { + rust_name.strip_prefix("set_").unwrap_or(&rust_name) + } + BindingKind::Field | BindingKind::StaticField => { + rust_name.strip_prefix("get_").unwrap_or(&rust_name) + } + _ => &rust_name, + }; + rename.apply(base) +} + +fn parse_rename_rule(args: &Args) -> syn::Result { + let Some(value) = &args.rename_all else { + return Ok(RenameRule::None); + }; + match value.value().as_str() { + "camelCase" => Ok(RenameRule::CamelCase), + _ => Err(syn::Error::new_spanned( + value, + "unsupported rename rule; the available rule is `camelCase`", + )), + } +} + +fn class_config(args: &Args, required: bool) -> syn::Result<(Option, RenameRule)> { + args.ensure_options(true, false, false, true)?; + if args.positional.len() > 1 { + return Err(syn::Error::new_spanned( + &args.positional[1], + "expected at most one JVM class name", + )); + } + let class = args + .class + .as_ref() + .or_else(|| args.positional.first()) + .map(|value| nonempty(value, "class name")) + .transpose()? + .map(|value| normalize_class(&value)); + if required && class.is_none() { + return Err(syn::Error::new( + Span::call_site(), + "a JVM class name is required, for example `#[jvm::class(\"java.lang.String\")]`", + )); + } + Ok((class, parse_rename_rule(args)?)) +} + +fn named_value(value: Option<&LitStr>, what: &str) -> syn::Result> { + value.map(|value| nonempty(value, what)).transpose() +} + +fn positional_value(value: Option<&LitStr>, what: &str) -> syn::Result> { + value.map(|value| nonempty(value, what)).transpose() +} + +fn binding_link( + kind: BindingKind, + args: &Args, + signature: &Signature, + outer_class: Option<&str>, + rename: RenameRule, +) -> syn::Result { + match kind { + BindingKind::StaticMethod => { + args.ensure_options(true, true, true, false)?; + let inferred = inferred_name(signature, kind, rename); + let (class, name, descriptor) = if args.positional.is_empty() { + ( + named_value(args.class.as_ref(), "class name")? + .or_else(|| outer_class.map(str::to_string)), + named_value(args.name.as_ref(), "method name")?.unwrap_or(inferred), + named_value(args.descriptor.as_ref(), "method descriptor")?, + ) + } else { + let values = &args.positional; + match (outer_class, values.len()) { + (Some(class), 1) => ( + Some(class.to_string()), + nonempty(&values[0], "method name")?, + None, + ), + (Some(_), 2) | (None, 2) => ( + Some(nonempty(&values[0], "class name")?), + nonempty(&values[1], "method name")?, + None, + ), + (Some(_), 3) | (None, 3) => ( + Some(nonempty(&values[0], "class name")?), + nonempty(&values[1], "method name")?, + Some(nonempty(&values[2], "method descriptor")?), + ), + (None, 1) => (Some(nonempty(&values[0], "class name")?), inferred, None), + _ => { + return Err(syn::Error::new_spanned( + values.last().unwrap(), + "expected a class, optional method name, and optional descriptor", + )); + } + } + }; + let class = class.ok_or_else(|| { + syn::Error::new( + Span::call_site(), + "no JVM class is known; add `class = \"java.lang.Class\"` or put the method in a named `#[jvm::class]` impl", + ) + })?; + let class = normalize_class(&class); + Ok(match descriptor { + Some(descriptor) => format!("jvm:static:{class}:{name}:{descriptor}"), + None => format!("jvm:static:{class}:{name}"), + }) + } + BindingKind::Method => { + args.ensure_options(false, true, true, false)?; + let (name, descriptor) = if args.positional.is_empty() { + ( + named_value(args.name.as_ref(), "method name")? + .unwrap_or_else(|| inferred_name(signature, kind, rename)), + named_value(args.descriptor.as_ref(), "method descriptor")?, + ) + } else { + match args.positional.len() { + 1 => (nonempty(&args.positional[0], "method name")?, None), + 2 => ( + nonempty(&args.positional[0], "method name")?, + Some(nonempty(&args.positional[1], "method descriptor")?), + ), + _ => { + return Err(syn::Error::new_spanned( + args.positional.last().unwrap(), + "expected an optional method name and descriptor", + )); + } + } + }; + Ok(match descriptor { + Some(descriptor) => format!("jvm:virtual:{name}:{descriptor}"), + None => format!("jvm:virtual:{name}"), + }) + } + BindingKind::Constructor => { + args.ensure_options(true, false, false, false)?; + if args.positional.len() > 1 { + return Err(syn::Error::new_spanned( + &args.positional[1], + "expected at most one JVM class name", + )); + } + let class = named_value(args.class.as_ref(), "class name")? + .or(positional_value( + args.positional.first(), + "class name", + )?) + .or_else(|| outer_class.map(str::to_string)) + .ok_or_else(|| { + syn::Error::new( + Span::call_site(), + "no JVM class is known; add `class = \"java.lang.Class\"` or put the constructor in a named `#[jvm::class]` impl", + ) + })?; + Ok(format!("jvm:new:{}", normalize_class(&class))) + } + BindingKind::Field => { + args.ensure_options(false, true, false, false)?; + if args.positional.len() > 1 { + return Err(syn::Error::new_spanned( + &args.positional[1], + "expected at most one JVM field name", + )); + } + let name = named_value(args.name.as_ref(), "field name")? + .or(positional_value(args.positional.first(), "field name")?) + .unwrap_or_else(|| inferred_name(signature, kind, rename)); + Ok(format!("jvm:field:{name}")) + } + BindingKind::StaticField => { + args.ensure_options(true, true, false, false)?; + let inferred = inferred_name(signature, kind, rename); + let (class, name) = if args.positional.is_empty() { + ( + named_value(args.class.as_ref(), "class name")? + .or_else(|| outer_class.map(str::to_string)), + named_value(args.name.as_ref(), "field name")?.unwrap_or(inferred), + ) + } else { + match (outer_class, args.positional.len()) { + (Some(class), 1) => ( + Some(class.to_string()), + nonempty(&args.positional[0], "field name")?, + ), + (Some(_), 2) | (None, 2) => ( + Some(nonempty(&args.positional[0], "class name")?), + nonempty(&args.positional[1], "field name")?, + ), + (None, 1) => (Some(nonempty(&args.positional[0], "class name")?), inferred), + _ => { + return Err(syn::Error::new_spanned( + args.positional.last().unwrap(), + "expected a class and optional field name", + )); + } + } + }; + let class = class.ok_or_else(|| { + syn::Error::new( + Span::call_site(), + "no JVM class is known; add `class = \"java.lang.Class\"` or put the field in a named `#[jvm::class]` impl", + ) + })?; + Ok(format!( + "jvm:static-field:{}:{name}", + normalize_class(&class) + )) + } + } +} + +fn validate_receiver(receiver: &syn::Receiver) -> syn::Result<()> { + if receiver.reference.is_none() || receiver.colon_token.is_some() { + Err(syn::Error::new_spanned( + receiver, + "JVM instance wrappers require a shorthand `&self` or `&mut self` receiver", + )) + } else { + Ok(()) + } +} + +fn validate_wrapper_signature(signature: &Signature) -> syn::Result<()> { + if let Some(asyncness) = &signature.asyncness { + return Err(syn::Error::new_spanned( + asyncness, + "JVM wrapper functions cannot be `async`", + )); + } + if let Some(constness) = &signature.constness { + return Err(syn::Error::new_spanned( + constness, + "JVM wrapper functions cannot be `const`", + )); + } + if !signature.generics.params.is_empty() || signature.generics.where_clause.is_some() { + return Err(syn::Error::new_spanned( + &signature.generics, + "JVM wrapper functions cannot be generic", + )); + } + if let Some(variadic) = &signature.variadic { + return Err(syn::Error::new_spanned( + variadic, + "JVM wrapper functions cannot be variadic", + )); + } + for input in &signature.inputs { + match input { + FnArg::Receiver(receiver) => validate_receiver(receiver)?, + FnArg::Typed(typed) => match typed.pat.as_ref() { + Pat::Ident(ident) if ident.by_ref.is_none() && ident.subpat.is_none() => {} + _ => { + return Err(syn::Error::new_spanned( + &typed.pat, + "JVM wrapper parameters must be simple bindings such as `value: i32`", + )); + } + }, + } + } + Ok(()) +} + +fn validate_binding_shape( + kind: BindingKind, + signature: &Signature, + in_class_impl: bool, +) -> syn::Result<()> { + let receiver = signature.inputs.iter().find_map(|input| match input { + FnArg::Receiver(receiver) => Some(receiver), + FnArg::Typed(_) => None, + }); + let typed_count = signature + .inputs + .iter() + .filter(|input| matches!(input, FnArg::Typed(_))) + .count(); + + if in_class_impl { + match kind { + BindingKind::Method | BindingKind::Field if receiver.is_none() => { + return Err(syn::Error::new_spanned( + &signature.ident, + "an instance JVM binding in a `#[jvm::class]` impl requires `&self` or `&mut self`", + )); + } + BindingKind::StaticMethod | BindingKind::Constructor | BindingKind::StaticField + if receiver.is_some() => + { + return Err(syn::Error::new_spanned( + receiver.unwrap(), + "this JVM binding is static and cannot take `self`", + )); + } + _ => {} + } + } else if receiver.is_some() { + return Err(syn::Error::new_spanned( + receiver.unwrap(), + "methods using `self` must be inside a `#[jvm::class]` impl block", + )); + } + + match kind { + BindingKind::Method if !in_class_impl && typed_count == 0 => Err(syn::Error::new_spanned( + &signature.ident, + "an instance JVM method requires the JVM receiver as its first parameter", + )), + BindingKind::Constructor if returns_unit(signature) => Err(syn::Error::new_spanned( + &signature.output, + "a JVM constructor binding must return the constructed object", + )), + BindingKind::Field => { + let expected_getter = if in_class_impl { 0 } else { 1 }; + let expected_setter = expected_getter + 1; + if in_class_impl + && returns_unit(signature) + && receiver.is_some_and(|receiver| receiver.mutability.is_none()) + { + return Err(syn::Error::new_spanned( + receiver.unwrap(), + "an instance field setter requires `&mut self`", + )); + } + if (!returns_unit(signature) && typed_count != expected_getter) + || (returns_unit(signature) && typed_count != expected_setter) + { + Err(syn::Error::new_spanned( + &signature.inputs, + if in_class_impl { + "an instance field getter takes only `&self`; a setter takes `&mut self` and one value" + } else { + "an instance field getter takes one receiver; a setter takes a receiver and one value" + }, + )) + } else { + Ok(()) + } + } + BindingKind::StaticField => { + if (!returns_unit(signature) && typed_count != 0) + || (returns_unit(signature) && typed_count != 1) + { + Err(syn::Error::new_spanned( + &signature.inputs, + "a static field getter takes no parameters; a setter takes exactly one value", + )) + } else { + Ok(()) + } + } + _ => Ok(()), + } +} + +struct SelfReplacer { + concrete: Type, +} + +impl VisitMut for SelfReplacer { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let Type::Path(path) = ty + && path.qself.is_none() + && path.path.is_ident("Self") + { + *ty = self.concrete.clone(); + return; + } + syn::visit_mut::visit_type_mut(self, ty); + } + + fn visit_path_mut(&mut self, path: &mut syn::Path) { + if path.leading_colon.is_none() + && !path.segments.is_empty() + && path.segments[0].ident == "Self" + { + let Type::Path(concrete) = &self.concrete else { + unreachable!("concrete JVM impl types are paths") + }; + let tail = path.segments.iter().skip(1).cloned().collect::>(); + path.leading_colon = concrete.path.leading_colon; + path.segments = concrete.path.segments.clone(); + path.segments.extend(tail); + } + syn::visit_mut::visit_path_mut(self, path); + } +} + +struct FindsSelf(bool); + +impl<'ast> Visit<'ast> for FindsSelf { + fn visit_type_path(&mut self, node: &'ast syn::TypePath) { + if node.qself.is_none() && node.path.is_ident("Self") { + self.0 = true; + } + syn::visit::visit_type_path(self, node); + } + + fn visit_path(&mut self, node: &'ast syn::Path) { + if !node.segments.is_empty() && node.segments[0].ident == "Self" { + self.0 = true; + } + syn::visit::visit_path(self, node); + } +} + +fn wrapper_body( + signature: &Signature, + link: &str, + concrete: Option<&Type>, +) -> syn::Result { + validate_wrapper_signature(signature)?; + let hidden = format_ident!("__jvm_{}", signature.ident); + let mut replacer = concrete.map(|concrete| SelfReplacer { + concrete: concrete.clone(), + }); + let mut hidden_inputs = Vec::new(); + let mut call_args = Vec::new(); + + for input in &signature.inputs { + match input { + FnArg::Receiver(receiver) => { + let concrete = concrete.ok_or_else(|| { + syn::Error::new_spanned( + receiver, + "methods using `self` must be inside a `#[jvm::class]` impl block", + ) + })?; + let mutability = &receiver.mutability; + hidden_inputs.push(quote! { __this: &#mutability #concrete }); + call_args.push(quote! { self }); + } + FnArg::Typed(typed) => { + let Pat::Ident(pattern) = typed.pat.as_ref() else { + unreachable!("validated by validate_wrapper_signature") + }; + let ident = &pattern.ident; + let mut ty = (*typed.ty).clone(); + if let Some(replacer) = &mut replacer { + replacer.visit_type_mut(&mut ty); + } + hidden_inputs.push(quote! { #ident: #ty }); + call_args.push(quote! { #ident }); + } + } + } + + let mut output = signature.output.clone(); + if let Some(replacer) = &mut replacer { + replacer.visit_return_type_mut(&mut output); + } + syn::parse2(quote! {{ + unsafe extern "C" { + #[link_name = #link] + fn #hidden(#(#hidden_inputs),*) #output; + } + unsafe { #hidden(#(#call_args),*) } + }}) +} + +fn wrap_impl_function( + mut function: ImplItemFn, + link: &str, + concrete: Option<&Type>, +) -> syn::Result { + if concrete.is_none() { + let mut finder = FindsSelf(false); + finder.visit_signature(&function.sig); + if finder.0 { + return Err(syn::Error::new_spanned( + &function.sig, + "methods using `Self` must be inside a `#[jvm::class]` impl block", + )); + } + } + function.block = wrapper_body(&function.sig, link, concrete)?; + Ok(function) +} + +fn wrap_item_function(mut function: ItemFn, link: &str) -> syn::Result { + function.block = Box::new(wrapper_body(&function.sig, link, None)?); + Ok(function) +} + +fn concrete_impl_type(impl_block: &syn::ItemImpl) -> syn::Result { + if impl_block.trait_.is_some() { + return Err(syn::Error::new_spanned( + impl_block, + "`#[jvm::class]` cannot wrap a trait impl", + )); + } + if !impl_block.generics.params.is_empty() || impl_block.generics.where_clause.is_some() { + return Err(syn::Error::new_spanned( + &impl_block.generics, + "a `#[jvm::class]` impl cannot be generic", + )); + } + match impl_block.self_ty.as_ref() { + Type::Path(path) if path.qself.is_none() => { + if path + .path + .segments + .iter() + .any(|segment| !segment.arguments.is_none()) + { + Err(syn::Error::new_spanned( + &impl_block.self_ty, + "a `#[jvm::class]` impl must use a concrete non-generic type", + )) + } else { + Ok((*impl_block.self_ty).clone()) + } + } + _ => Err(syn::Error::new_spanned( + &impl_block.self_ty, + "a `#[jvm::class]` impl must use a concrete path type such as `impl JavaString`", + )), + } +} + +fn declared_type_ident(impl_block: &syn::ItemImpl) -> syn::Result { + match impl_block.self_ty.as_ref() { + Type::Path(path) + if path.qself.is_none() + && path.path.leading_colon.is_none() + && path.path.segments.len() == 1 + && path.path.segments[0].arguments.is_none() => + { + Ok(path.path.segments[0].ident.clone()) + } + _ => Err(syn::Error::new_spanned( + &impl_block.self_ty, + "a declaring `#[jvm::class]` impl must use a new unqualified type name such as `impl JavaString`; use `#[jvm::bindings]` for an existing or qualified type", + )), + } +} + +fn expand_class_impl( + mut impl_block: syn::ItemImpl, + outer_class: Option, + rename: RenameRule, +) -> syn::Result<(syn::ItemImpl, Vec)> { + let concrete = concrete_impl_type(&impl_block)?; + let mut expanded = Vec::with_capacity(impl_block.items.len()); + let mut direct_imports = Vec::new(); + + for item in impl_block.items { + let ImplItem::Fn(mut function) = item else { + expanded.push(item); + continue; + }; + let mut binding = None; + let mut kept_attributes = Vec::new(); + for attribute in function.attrs { + if let Some(kind) = BindingKind::from_attribute(&attribute) { + if binding.is_some() { + return Err(syn::Error::new_spanned( + attribute, + "only one `#[jvm::...]` binding attribute is allowed per method", + )); + } + if attribute.path().segments.len() == 1 { + let ident = attribute.path().segments[0].ident.clone(); + if !direct_imports.contains(&ident) { + direct_imports.push(ident); + } + } + binding = Some((kind?, attribute_args(&attribute)?)); + } else { + kept_attributes.push(attribute); + } + } + function.attrs = kept_attributes; + + let Some((kind, args)) = binding else { + if function.block.stmts.is_empty() { + return Err(syn::Error::new_spanned( + &function.sig, + "empty methods in a JVM binding impl need a `#[jvm::method]`, `#[jvm::static_method]`, `#[jvm::constructor]`, `#[jvm::field]`, or `#[jvm::static_field]` attribute", + )); + } + expanded.push(ImplItem::Fn(function)); + continue; + }; + validate_binding_shape(kind, &function.sig, true)?; + let link = binding_link(kind, &args, &function.sig, outer_class.as_deref(), rename)?; + expanded.push(ImplItem::Fn(wrap_impl_function( + function, + &link, + Some(&concrete), + )?)); + } + impl_block.items = expanded; + Ok((impl_block, direct_imports)) +} + +fn direct_import_uses(imports: &[Ident]) -> TokenStream2 { + quote! { + #( + #[allow(unused_imports)] + use #imports as _; + )* + } +} + +fn expand_binding(args: Args, item: TokenStream, kind: BindingKind) -> syn::Result { + let tokens = TokenStream2::from(item); + + if let Ok(mut function) = syn::parse2::(tokens.clone()) { + validate_binding_shape(kind, &function.sig, false)?; + let link = binding_link(kind, &args, &function.sig, None, RenameRule::None)?; + let link_attribute: Attribute = syn::parse_quote!(#[link_name = #link]); + function.attrs.push(link_attribute); + return Ok(quote! { #function }); + } + if let Ok(function) = syn::parse2::(tokens.clone()) { + validate_binding_shape(kind, &function.sig, false)?; + let link = binding_link(kind, &args, &function.sig, None, RenameRule::None)?; + let wrapped = wrap_item_function(function, &link)?; + return Ok(quote! { #wrapped }); + } + if let Ok(function) = syn::parse2::(tokens.clone()) { + validate_binding_shape(kind, &function.sig, false)?; + let link = binding_link(kind, &args, &function.sig, None, RenameRule::None)?; + let wrapped = wrap_impl_function(function, &link, None)?; + return Ok(quote! { #wrapped }); + } + + Err(syn::Error::new_spanned( + tokens, + "this JVM binding attribute can only be used on a function", + )) +} + +fn run_binding_macro(args: TokenStream, item: TokenStream, kind: BindingKind) -> TokenStream { + let result = Args::parse_macro(args).and_then(|args| expand_binding(args, item, kind)); + match result { + Ok(tokens) => tokens.into(), + Err(error) => error.to_compile_error().into(), + } +} + +/// Declares an opaque JVM class and its Rust binding methods in one `impl`. +/// +/// Class names may use dots or slashes. On an `impl`, `rename_all = "camelCase"` +/// converts inferred Rust member names while leaving explicit names unchanged. +#[proc_macro_attribute] +pub fn class(args: TokenStream, item: TokenStream) -> TokenStream { + let args = match Args::parse_macro(args) { + Ok(args) => args, + Err(error) => return error.to_compile_error().into(), + }; + let tokens = TokenStream2::from(item); + + if let Ok(impl_block) = syn::parse2::(tokens.clone()) { + let (class, rename) = match class_config(&args, true) { + Ok(config) => config, + Err(error) => return error.to_compile_error().into(), + }; + let ident = match declared_type_ident(&impl_block) { + Ok(ident) => ident, + Err(error) => return error.to_compile_error().into(), + }; + let class = class.unwrap(); + return match expand_class_impl(impl_block, Some(class.clone()), rename) { + Ok((expanded, imports)) => { + let imports = direct_import_uses(&imports); + quote! { + unsafe extern "C" { + #[link_name = #class] + pub type #ident; + } + + #imports + #expanded + } + .into() + } + Err(error) => error.to_compile_error().into(), + }; + } + + let (class, rename) = match class_config(&args, true) { + Ok(config) => config, + Err(error) => return error.to_compile_error().into(), + }; + if rename != RenameRule::None { + return syn::Error::new_spanned( + args.rename_all.unwrap(), + "`rename_all` belongs on the `#[jvm::class]` impl block", + ) + .to_compile_error() + .into(); + } + let class = class.unwrap(); + + if let Ok(mut foreign_type) = syn::parse2::(tokens.clone()) { + let link_attribute: Attribute = syn::parse_quote!(#[link_name = #class]); + foreign_type.attrs.push(link_attribute); + return quote! { #foreign_type }.into(); + } + if let Ok(struct_item) = syn::parse2::(tokens.clone()) { + if !matches!(struct_item.fields, syn::Fields::Unit) { + return syn::Error::new_spanned( + struct_item.fields, + "a JVM class declaration must be an opaque unit struct such as `pub struct JavaString;`", + ) + .to_compile_error() + .into(); + } + if !struct_item.generics.params.is_empty() || struct_item.generics.where_clause.is_some() { + return syn::Error::new_spanned( + struct_item.generics, + "a JVM class declaration cannot be generic", + ) + .to_compile_error() + .into(); + } + let attributes = &struct_item.attrs; + let visibility = &struct_item.vis; + let ident = &struct_item.ident; + return quote! { + unsafe extern "C" { + #(#attributes)* + #[link_name = #class] + #visibility type #ident; + } + } + .into(); + } + + syn::Error::new_spanned( + tokens, + "`#[jvm::class]` can only be used on an opaque unit struct, foreign type, or inherent impl", + ) + .to_compile_error() + .into() +} + +/// Adds JVM binding methods to a type that has already been declared. +/// +/// This is mainly useful for additional impl blocks. An optional class name is +/// required only by constructors and static members that do not name a class +/// themselves. +#[proc_macro_attribute] +pub fn bindings(args: TokenStream, item: TokenStream) -> TokenStream { + let args = match Args::parse_macro(args) { + Ok(args) => args, + Err(error) => return error.to_compile_error().into(), + }; + let (class, rename) = match class_config(&args, false) { + Ok(config) => config, + Err(error) => return error.to_compile_error().into(), + }; + let tokens = TokenStream2::from(item); + let impl_block = match syn::parse2::(tokens.clone()) { + Ok(impl_block) => impl_block, + Err(_) => { + return syn::Error::new_spanned( + tokens, + "`#[jvm::bindings]` can only be used on an inherent impl block", + ) + .to_compile_error() + .into(); + } + }; + match expand_class_impl(impl_block, class, rename) { + Ok((expanded, imports)) => { + let imports = direct_import_uses(&imports); + quote! { + #imports + #expanded + } + .into() + } + Err(error) => error.to_compile_error().into(), + } +} + +/// Binds a JVM static method. +/// +/// Use `class = "..."`, `name = "..."`, and `descriptor = "..."` when the +/// corresponding value cannot be inferred from the enclosing class or Rust name. +#[proc_macro_attribute] +pub fn static_method(args: TokenStream, item: TokenStream) -> TokenStream { + run_binding_macro(args, item, BindingKind::StaticMethod) +} + +/// Binds a JVM virtual/interface method. +#[proc_macro_attribute] +pub fn method(args: TokenStream, item: TokenStream) -> TokenStream { + run_binding_macro(args, item, BindingKind::Method) +} + +/// Binds a JVM constructor. +#[proc_macro_attribute] +pub fn constructor(args: TokenStream, item: TokenStream) -> TokenStream { + run_binding_macro(args, item, BindingKind::Constructor) +} + +/// Binds an instance-field getter or setter. +/// +/// The field name is inferred from `value`, `get_value`, or `set_value` when it +/// is omitted. A getter returns a value; a setter returns `()`. +#[proc_macro_attribute] +pub fn field(args: TokenStream, item: TokenStream) -> TokenStream { + run_binding_macro(args, item, BindingKind::Field) +} + +/// Binds a static-field getter or setter. +/// +/// The class must be supplied unless the function is in a named +/// `#[jvm::class]` impl. +#[proc_macro_attribute] +pub fn static_field(args: TokenStream, item: TokenStream) -> TokenStream { + run_binding_macro(args, item, BindingKind::StaticField) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn args(source: &str) -> Args { + syn::parse_str(source).unwrap() + } + + fn signature(source: &str) -> Signature { + syn::parse_str::(source).unwrap().sig + } + + #[test] + fn binding_attributes_accept_qualified_and_directly_imported_forms() { + for attribute in [ + syn::parse_quote!(#[jvm::method]), + syn::parse_quote!(#[rcj::method]), + syn::parse_quote!(#[method]), + ] { + assert_eq!( + BindingKind::from_attribute(&attribute).unwrap().unwrap(), + BindingKind::Method + ); + } + + let unrelated: Attribute = syn::parse_quote!(#[inline]); + assert!(BindingKind::from_attribute(&unrelated).is_none()); + } + + #[test] + fn class_names_support_explicit_nested_class_boundaries() { + assert_eq!( + normalize_class("java.time.LocalDate"), + "java/time/LocalDate" + ); + assert_eq!( + normalize_class("java.util.Map$Entry"), + "java/util/Map$Entry" + ); + assert_eq!( + normalize_class("java/util/Map.Entry"), + "java/util/Map$Entry" + ); + assert_eq!( + normalize_class("java.util/Map.Entry"), + "java/util/Map$Entry" + ); + assert_eq!( + normalize_class("java/util/Outer.Inner.Deep"), + "java/util/Outer$Inner$Deep" + ); + } + + #[test] + fn static_member_shorthand_depends_only_on_whether_a_class_is_supplied() { + let sig = signature("fn rust_name() {}"); + assert_eq!( + binding_link( + BindingKind::StaticMethod, + &args("\"javaName\""), + &sig, + Some("example/Owner"), + RenameRule::None, + ) + .unwrap(), + "jvm:static:example/Owner:javaName" + ); + assert_eq!( + binding_link( + BindingKind::StaticMethod, + &args("class = \"other.Owner\", name = \"javaName\""), + &sig, + Some("example/Owner"), + RenameRule::None, + ) + .unwrap(), + "jvm:static:other/Owner:javaName" + ); + } + + #[test] + fn field_accessors_and_camel_case_are_inferred() { + let getter = signature("fn get_shared_state(&self) -> i32 { 0 }"); + let setter = signature("fn set_shared_state(&mut self, value: i32) {}"); + assert_eq!( + inferred_name(&getter, BindingKind::Field, RenameRule::CamelCase), + "sharedState" + ); + assert_eq!( + inferred_name(&setter, BindingKind::Field, RenameRule::CamelCase), + "sharedState" + ); + } + + #[test] + fn named_and_positional_binding_arguments_do_not_mix() { + let sig = signature("fn value() -> i32 { 0 }"); + let error = binding_link( + BindingKind::StaticField, + &args("\"Owner\", name = \"value\""), + &sig, + None, + RenameRule::None, + ) + .unwrap_err(); + assert!(error.to_string().contains("do not mix")); + } + + #[test] + fn unsafe_wrapper_shapes_are_rejected_early() { + assert!( + validate_wrapper_signature(&signature("async fn call() {}")) + .unwrap_err() + .to_string() + .contains("cannot be `async`") + ); + assert!( + validate_wrapper_signature(&signature("const fn call() {}")) + .unwrap_err() + .to_string() + .contains("cannot be `const`") + ); + assert!( + validate_wrapper_signature(&signature("fn call(value: T) {}")) + .unwrap_err() + .to_string() + .contains("cannot be generic") + ); + assert!( + validate_binding_shape( + BindingKind::Field, + &signature("fn set_value(&self, value: i32) {}"), + true, + ) + .unwrap_err() + .to_string() + .contains("requires `&mut self`") + ); + } + + #[test] + fn trait_impls_are_not_treated_as_jvm_classes() { + let item: syn::ItemImpl = syn::parse_quote! { + impl Display for JavaString {} + }; + assert!( + concrete_impl_type(&item) + .err() + .unwrap() + .to_string() + .contains("trait impl") + ); + } + + #[test] + fn unannotated_empty_methods_are_not_silent_noops() { + let item: syn::ItemImpl = syn::parse_quote! { + impl JavaString { + fn accidentally_unbound(&self) {} + } + }; + assert!( + expand_class_impl(item, Some("java/lang/String".to_string()), RenameRule::None) + .err() + .unwrap() + .to_string() + .contains("need a `#[jvm::method]`") + ); + } +} diff --git a/jvm/tests/compile.rs b/jvm/tests/compile.rs new file mode 100644 index 0000000..62177e4 --- /dev/null +++ b/jvm/tests/compile.rs @@ -0,0 +1,85 @@ +#![feature(extern_types)] +#![allow(dead_code)] + +use jvm::{constructor, method}; +use rcj as jvm; + +unsafe extern "C" { + #[jvm::class("java.lang.String")] + type JString; + + #[jvm::class("java.time.LocalDate")] + type JavaLocalDate; + + #[jvm::static_method("java.time.LocalDate", "of")] + fn raw_date(year: i32, month: i32, day: i32) -> *const JavaLocalDate; + + #[jvm::method(name = "getYear")] + fn raw_year(date: &JavaLocalDate) -> i32; + + #[jvm::constructor(class = "java.lang.String")] + fn raw_string(bytes: *const u8) -> *mut JString; + + #[jvm::field] + fn raw_value(string: &JString) -> i32; + + #[jvm::static_field("example.Globals", "value")] + fn raw_global() -> i32; +} + +#[jvm::class("java.time.LocalDate", rename_all = "camelCase")] +impl Date { + #[jvm::static_method] + fn of(year: i32, month: i32, day: i32) -> *mut Self {} + + #[jvm::static_method("parse")] + fn parse_iso(value: &JString) -> *mut Self {} + + #[jvm::method] + fn get_year(&self) -> i32 {} + + #[jvm::field] + fn get_day_of_month(&self) -> i32 {} + + #[jvm::field] + fn set_day_of_month(&mut self, value: i32) {} + + #[jvm::static_field(class = "example.Globals")] + fn shared_value() -> i32 {} + + #[jvm::static_field(class = "example.Globals")] + fn set_shared_value(value: i32) {} +} + +#[jvm::static_method(class = "java.lang.Math", name = "max")] +fn max_i32(left: i32, right: i32) -> i32 {} + +struct Helpers; + +impl Helpers { + #[jvm::static_method(class = "java.lang.System", name = "nanoTime")] + fn nano_time() -> i64 {} +} + +mod qualified { + #[rcj::class("java.lang.StringBuilder")] + pub struct Builder; +} + +#[jvm::bindings] +impl qualified::Builder { + #[jvm::method("length")] + fn length(&self) -> i32 {} +} + +#[jvm::class("java.lang.StringBuilder")] +impl DirectlyImportedAttributes { + #[constructor] + fn new() -> *mut Self {} + + #[method("length")] + fn length(&self) -> i32 {} +} + +#[test] +fn macros_expand() {} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 053a522..f501a99 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] channel = "nightly-2026-09-02" profile = "minimal" -components = ["rustc-dev", "rust-src", "llvm-tools-preview"] +components = ["rustc-dev", "rust-src", "llvm-tools-preview", "rustfmt"] diff --git a/src/lower1/control_flow/rvalue.rs b/src/lower1/control_flow/rvalue.rs index 8376154..eb6da6e 100644 --- a/src/lower1/control_flow/rvalue.rs +++ b/src/lower1/control_flow/rvalue.rs @@ -6375,7 +6375,7 @@ pub(super) fn convert_rvalue_to_operand<'a>( constructor_args.into_iter().next().unwrap_or_else(|| { tcx.dcx().span_fatal( tcx.def_span(variant_def.def_id), - "`#[jvm::subtype]` payload has no JVM value", + "`#[jvm_codegen::subtype]` payload has no JVM value", ) }); instructions.push(oomir::Instruction::Cast { diff --git a/src/lower1/operand/const_eval.rs b/src/lower1/operand/const_eval.rs index 96d1850..dd7de0c 100644 --- a/src/lower1/operand/const_eval.rs +++ b/src/lower1/operand/const_eval.rs @@ -2821,10 +2821,9 @@ fn handle_constant_enum<'tcx>( generate_adt_jvm_class_name(&adt_def, substs, tcx, oomir_data_types, instance); force_define_named_adt(enum_ty, tcx, oomir_data_types, instance); if jvm_subtype_payload_ty(&adt_def, variant_def, substs, tcx).is_some() { - return params - .into_iter() - .next() - .ok_or_else(|| "`#[jvm::subtype]` constant payload has no JVM value".to_string()); + return params.into_iter().next().ok_or_else(|| { + "`#[jvm_codegen::subtype]` constant payload has no JVM value".to_string() + }); } let variant_class_name = format!( "{}${}", // Using '$' as inner class separator is common in JVM diff --git a/src/lower1/types.rs b/src/lower1/types.rs index e227fe3..b728f74 100644 --- a/src/lower1/types.rs +++ b/src/lower1/types.rs @@ -631,7 +631,11 @@ pub(crate) fn is_jvm_subtype_variant<'tcx>( #[allow(deprecated)] tcx.get_all_attrs(variant.def_id).iter().any(|attribute| { let path = attribute.path(); - path.len() == 2 && path[0].as_str() == "jvm" && path[1].as_str() == "subtype" + // `jvm` is retained for source compatibility. New code uses a separate + // tool namespace so the optional `jvm` proc-macro crate can coexist. + path.len() == 2 + && matches!(path[0].as_str(), "jvm_codegen" | "jvm") + && path[1].as_str() == "subtype" }) } @@ -648,30 +652,34 @@ pub(crate) fn jvm_subtype_payload_ty<'tcx>( if variant.fields.len() != 1 { tcx.dcx().span_fatal( span, - "`#[jvm::subtype]` requires an enum variant with exactly one field", + "`#[jvm_codegen::subtype]` requires an enum variant with exactly one field", ); } let payload_ty = variant.fields[FieldIdx::from_usize(0)] .ty(tcx, substs) .skip_norm_wip(); let TyKind::Adt(inner_adt, _) = payload_ty.kind() else { - tcx.dcx() - .span_fatal(span, "`#[jvm::subtype]` payload must be another Rust enum"); + tcx.dcx().span_fatal( + span, + "`#[jvm_codegen::subtype]` payload must be another Rust enum", + ); }; if !inner_adt.is_enum() { - tcx.dcx() - .span_fatal(span, "`#[jvm::subtype]` payload must be another Rust enum"); + tcx.dcx().span_fatal( + span, + "`#[jvm_codegen::subtype]` payload must be another Rust enum", + ); } if inner_adt.did() == outer_adt.did() { tcx.dcx().span_fatal( span, - "`#[jvm::subtype]` cannot transparently embed the enum itself", + "`#[jvm_codegen::subtype]` cannot transparently embed the enum itself", ); } if inner_adt.did().krate != outer_adt.did().krate { tcx.dcx().span_fatal( span, - "`#[jvm::subtype]` requires both enums to be defined in the same crate", + "`#[jvm_codegen::subtype]` requires both enums to be defined in the same crate", ); } Some(payload_ty) @@ -1528,7 +1536,7 @@ fn ensure_enum_data_types<'tcx>( Some(other) => tcx.dcx().span_fatal( tcx.def_span(variant.def_id), format!( - "`#[jvm::subtype]` payload has unsupported JVM representation {other:?}" + "`#[jvm_codegen::subtype]` payload has unsupported JVM representation {other:?}" ), ), None => format!("{base_enum_name}${variant_name}"), @@ -1546,7 +1554,7 @@ fn ensure_enum_data_types<'tcx>( data_types.get_mut(&shape.runtime_type) else { tcx.dcx().fatal(format!( - "`#[jvm::subtype]` payload {} was not lowered as an enum interface", + "`#[jvm_codegen::subtype]` payload {} was not lowered as an enum interface", shape.runtime_type )); }; diff --git a/tests/binary/unions/src/main.rs b/tests/binary/unions/src/main.rs index cc6a3cb..11b8aab 100644 --- a/tests/binary/unions/src/main.rs +++ b/tests/binary/unions/src/main.rs @@ -1,5 +1,5 @@ #![feature(register_tool)] -#![register_tool(jvm)] +#![register_tool(jvm_codegen)] #[derive(Copy, Clone)] #[repr(C)] @@ -183,7 +183,7 @@ enum FlatLeaf { #[derive(Copy, Clone)] #[repr(C, u8)] enum FlatOuter { - #[jvm::subtype] + #[jvm_codegen::subtype] Leaf(FlatLeaf) = 1, Other(u16) = 2, } diff --git a/tests/integration/inner_classes/src/lib.rs b/tests/integration/inner_classes/src/lib.rs index 006afae..c8e716f 100644 --- a/tests/integration/inner_classes/src/lib.rs +++ b/tests/integration/inner_classes/src/lib.rs @@ -1,5 +1,5 @@ #![feature(register_tool)] -#![register_tool(jvm)] +#![register_tool(jvm_codegen)] pub enum TrafficLight { Red, @@ -43,7 +43,7 @@ pub enum LeafEvent { } pub enum Event { - #[jvm::subtype] + #[jvm_codegen::subtype] Leaf(LeafEvent), Message(i32), } diff --git a/tests/integration/jvm_macros/Cargo.lock b/tests/integration/jvm_macros/Cargo.lock new file mode 100644 index 0000000..cc97df7 --- /dev/null +++ b/tests/integration/jvm_macros/Cargo.lock @@ -0,0 +1,54 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "jvm_macros" +version = "0.1.0" +dependencies = [ + "rcj", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rcj" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/tests/integration/jvm_macros/Cargo.toml b/tests/integration/jvm_macros/Cargo.toml new file mode 100644 index 0000000..5344103 --- /dev/null +++ b/tests/integration/jvm_macros/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "jvm_macros" +version = "0.1.0" +edition = "2024" + +[dependencies] +jvm = { package = "rcj", path = "../../../jvm" } diff --git a/tests/integration/jvm_macros/Main.java b/tests/integration/jvm_macros/Main.java new file mode 100644 index 0000000..0c13175 --- /dev/null +++ b/tests/integration/jvm_macros/Main.java @@ -0,0 +1,27 @@ +public class Main { + public static long shared = 21; + public static State sharedState; + + public static final class State { + public int value; + public long wide; + public State next; + + public State(int value, long wide) { + this.value = value; + this.wide = wide; + } + + public static int twice(int value) { + return value * 2; + } + } + + public static void main(String[] args) { + long result = jvm_macros.jvm_macros.exercise(); + if (result != 101 || shared != 34 || sharedState == null || sharedState.value != 13) { + throw new AssertionError( + "JVM macro interop failed: result=" + result); + } + } +} diff --git a/tests/integration/jvm_macros/src/lib.rs b/tests/integration/jvm_macros/src/lib.rs new file mode 100644 index 0000000..e839407 --- /dev/null +++ b/tests/integration/jvm_macros/src/lib.rs @@ -0,0 +1,110 @@ +#![feature(extern_types, register_tool)] +#![register_tool(jvm_codegen)] + +#[jvm::class("java.time.LocalDate", rename_all = "camelCase")] +impl JavaLocalDate { + #[jvm::static_method("of")] + pub fn of(year: i32, month: i32, day: i32) -> *const Self {} + + #[jvm::method] + pub fn get_year(&self) -> i32 {} +} + +#[jvm::class("Main$State", rename_all = "camelCase")] +impl JavaState { + #[jvm::constructor] + pub fn new(value: i32, wide: i64) -> *mut Self {} + + #[jvm::field] + pub fn get_value(&self) -> i32 {} + + #[jvm::field] + pub fn set_value(&mut self, value: i32) {} + + #[jvm::field] + pub fn get_wide(&self) -> i64 {} + + #[jvm::field] + pub fn get_next(&self) -> *mut Self {} + + #[jvm::field] + pub fn set_next(&mut self, next: *mut Self) {} + + #[jvm::static_field(class = "Main")] + pub fn shared() -> i64 {} + + #[jvm::static_field(class = "Main")] + pub fn set_shared(value: i64) {} + + #[jvm::static_field(class = "Main")] + pub fn shared_state() -> *mut Self {} + + #[jvm::static_field(class = "Main")] + pub fn set_shared_state(value: *mut Self) {} + + #[jvm::static_method("twice")] + pub fn twice(value: i32) -> i32 {} +} + +#[jvm::static_method(class = "java.lang.Math", name = "max")] +fn java_max(left: i32, right: i32) -> i32 {} + +unsafe extern "C" { + #[jvm::static_method("java.lang.Math", "min")] + fn java_min(left: i32, right: i32) -> i32; +} + +pub enum MacroLeaf { + Number(i32), + Empty, +} + +pub enum MacroRoot { + #[jvm_codegen::subtype] + Leaf(MacroLeaf), + Other(i32), +} + +pub fn exercise() -> i64 { + unsafe { + let leap = JavaLocalDate::of(2024, 2, 29); + assert_eq!((&*leap).get_year(), 2024); + + let first = JavaState::new(7, 4_000_000_000); + assert_eq!((&*first).get_value(), 7); + assert_eq!((&*first).get_wide(), 4_000_000_000); + + (&mut *first).set_value(13); + assert_eq!((&*first).get_value(), 13); + + let second = JavaState::new(11, 9); + (&mut *first).set_next(second); + assert_eq!((&*first).get_value(), 13); + assert_eq!((&*(&*first).get_next()).get_value(), 11); + + assert_eq!(JavaState::shared(), 21); + JavaState::set_shared(34); + assert_eq!(JavaState::shared(), 34); + + JavaState::set_shared_state(first); + assert_eq!((&*JavaState::shared_state()).get_value(), 13); + + assert_eq!(JavaState::twice(6), 12); + assert_eq!(java_max(17, 9), 17); + assert_eq!(java_min(17, 9), 9); + + let nested = MacroRoot::Leaf(MacroLeaf::Number(5)); + let nested_value = match nested { + MacroRoot::Leaf(MacroLeaf::Number(value)) => value, + _ => 0, + }; + + (&*first).get_value() as i64 + + (&*(&*first).get_next()).get_value() as i64 + + JavaState::shared() + + JavaState::twice(6) as i64 + + java_max(17, 9) as i64 + + java_min(17, 9) as i64 + + nested_value as i64 + } +}