├── book ├── .gitignore ├── src │ ├── chapter_1.md │ ├── traits │ │ ├── into_java.md │ │ ├── into_rust.md │ │ └── jvm_op.md │ ├── java_object.md │ ├── derive.md │ ├── java_rust_types.md │ ├── local_vs_global.md │ ├── traits.md │ ├── reference.md │ ├── logo.md │ ├── jvm_operations.md │ ├── tutorials.md │ ├── SUMMARY.md │ ├── java_signatures_in_rust.md │ ├── implementing_native_methods.md │ ├── intro.md │ ├── jvm.md │ ├── upcasts.md │ ├── safety.md │ ├── to_java.md │ ├── java_function.md │ └── setup.md └── book.toml ├── test-crates ├── duchess-java-tests │ ├── tests │ │ ├── rust-to-java │ │ │ ├── run_dummy.rs │ │ │ ├── interface_class_mismatch_specified.rs │ │ │ ├── interface_class_mismatch_reflected.rs │ │ │ ├── static_method_invocation.rs │ │ │ ├── duplicate_implements.rs │ │ │ ├── duplicate_extends.rs │ │ │ ├── examples │ │ │ │ ├── generics.stderr │ │ │ │ ├── memory_usage.rs │ │ │ │ ├── generics.rs │ │ │ │ ├── log_local_ref_and_two_calls.rs │ │ │ │ ├── log_one_big_call.rs │ │ │ │ ├── log.rs │ │ │ │ ├── log_global_ref_and_chained_calls.rs │ │ │ │ ├── log_global_ref_and_two_calls.rs │ │ │ │ └── greeting.rs │ │ │ ├── must_use_jvmop.rs │ │ │ ├── create_string_from_bytes.stderr │ │ │ ├── duplicate_implements.stderr │ │ │ ├── type_mismatch_return_type_in_Object_hashCode.stderr │ │ │ ├── interface_class_mismatch_specified.stderr │ │ │ ├── interface_class_mismatch_reflected.stderr │ │ │ ├── instance_method_fully_qualified.rs │ │ │ ├── type_mismatch_bad_argument_type_in_decl.stderr │ │ │ ├── type_mismatch_bad_argument_type_in_decl.rs │ │ │ ├── mismatched_flags_private_method_declared_as_public.rs │ │ │ ├── create_string_from_bytes.rs │ │ │ ├── mismatched_flags_private_method_declared_as_public.stderr │ │ │ ├── mismatched_flags_public_method_declared_as_private.rs │ │ │ ├── mismatched_flags_public_method_declared_as_private.stderr │ │ │ ├── duchess_can_use_jni_launched_jvm.rs │ │ │ ├── type_mismatch_return_type_in_Object_hashCode.rs │ │ │ ├── must_use_jvmop.stderr │ │ │ ├── derive_scalar_fields.rs │ │ │ ├── derive_lifetimes.rs │ │ │ ├── derive_options.rs │ │ │ ├── to_java_produce_local_java_ref.rs │ │ │ ├── exceptions.stderr │ │ │ ├── duplicate_extends.stderr │ │ │ ├── to_java_on_rust_string.rs │ │ │ ├── passing_null_values.rs │ │ │ ├── to_java_on_java_string.rs │ │ │ ├── instance_method_from_superclass.rs │ │ │ ├── type_mismatch_bad_argument_type.rs │ │ │ ├── type_mismatch_bad_argument_type_explicit.rs │ │ │ ├── native_fn_return_java_string.rs │ │ │ ├── native_fn_return_local_java_string.rs │ │ │ ├── native_fn_return_rust_string.rs │ │ │ ├── native_fn_return_fresh_java_string.rs │ │ │ ├── native_fn_return_local_java_string.stderr │ │ │ ├── native_fn_link_suite.rs │ │ │ ├── native_fn_return_fresh_java_string.stderr │ │ │ ├── to_java_java_refs.rs │ │ │ └── exceptions.rs │ │ └── java-to-rust │ │ │ ├── java │ │ │ ├── java_to_rust_greeting │ │ │ │ └── Java_Can_Call_Rust_Java_Function.java │ │ │ ├── java_passing_null │ │ │ │ └── JavaPassingNull.java │ │ │ ├── java_rust_java_exception │ │ │ │ ├── JavaRustJavaNPE.java │ │ │ │ └── JavaRustJavaException.java │ │ │ ├── java_to_rust_arrays │ │ │ │ └── JavaArrayTests.java │ │ │ ├── java_rust_scalars │ │ │ │ └── JavaRustScalars.java │ │ │ └── java_rust_initiated_exceptions │ │ │ │ └── JavaRustExceptions.java │ │ │ └── rust-libraries │ │ │ ├── java_rust_java_npe.rs │ │ │ ├── java_rust_java_exception.rs │ │ │ ├── native_fn_callable_from_java.rs │ │ │ ├── native_fn_passing_null.rs │ │ │ ├── native_fn_arrays.rs │ │ │ ├── java_rust_initiated_exceptions.rs │ │ │ └── native_fn_echos_int.rs │ ├── java │ │ ├── log │ │ │ ├── BaseEvent.java │ │ │ ├── BuildStep.java │ │ │ ├── NameStep.java │ │ │ ├── TimeStep.java │ │ │ ├── Event.java │ │ │ ├── Logger.java │ │ │ └── Builder.java │ │ ├── generics │ │ │ ├── MapKey.java │ │ │ ├── EventKey.java │ │ │ └── MapLike.java │ │ ├── derives │ │ │ └── OptionalFields.java │ │ ├── exceptions │ │ │ ├── DifferentException.java │ │ │ └── ThrowExceptions.java │ │ ├── type_mismatch │ │ │ └── TakesInt.java │ │ ├── auth │ │ │ ├── AuthenticateRequest.java │ │ │ ├── AuthorizeRequest.java │ │ │ ├── AuthenticationExceptionInvalidSignature.java │ │ │ ├── AuthenticationExceptionInvalidSecurityToken.java │ │ │ ├── AuthorizationExceptionDenied.java │ │ │ ├── Authenticated.java │ │ │ ├── HttpRequest.java │ │ │ ├── AuthenticationExceptionUnauthenticated.java │ │ │ ├── AuthorizationException.java │ │ │ ├── HttpAuth.java │ │ │ └── AuthenticationException.java │ │ ├── native_greeting │ │ │ └── Native.java │ │ ├── take_null │ │ │ └── TakeNull.java │ │ └── flags │ │ │ └── Flags.java │ ├── src │ │ ├── main.rs │ │ └── java_wrapper.rs │ ├── Cargo.toml │ └── build.rs ├── Cargo.toml ├── viper │ ├── Cargo.toml │ ├── build.rs │ ├── tests │ │ └── test.rs │ └── src │ │ └── lib.rs └── README.md ├── .gitignore ├── macro ├── README.md ├── Cargo.toml └── src │ └── lib.rs ├── duchess-build-rs ├── src │ ├── main.rs │ ├── log.rs │ ├── code_writer.rs │ ├── re.rs │ ├── derive_java.rs │ ├── impl_java_trait.rs │ ├── files.rs │ └── shim_writer.rs ├── Cargo.toml └── test-files │ └── java_package_1.rs ├── src ├── null.rs ├── jvm │ └── test.rs ├── from_ref.rs ├── not_null.rs ├── try_catch.rs ├── link.rs ├── error.rs ├── libjvm.rs ├── lib.rs ├── refs.rs └── cast.rs ├── macro-rules ├── Cargo.toml └── src │ ├── lib.rs │ ├── java_types │ ├── view_of_obj.rs │ ├── view_of_op.rs │ ├── field_output_trait.rs │ ├── output_type.rs │ ├── argument_impl_trait.rs │ ├── output_trait.rs │ ├── jni_call_fn.rs │ ├── jni_static_field_get_fn.rs │ ├── jni_static_call_fn.rs │ ├── rust_ty.rs │ └── prepare_input.rs │ ├── setup_obj_method.rs │ ├── setup_op_method.rs │ ├── java_types.rs │ ├── mro.rs │ ├── macro_if.rs │ └── setup_static_field_getter.rs ├── tests ├── bad_jvm_construction_2.rs ├── good_jvm_construction_1.rs ├── good_jvm_construction_2.rs ├── bad_jvm_construction_1.rs ├── racing_jvm_construction.rs ├── test_hashmaps.rs ├── arrays.rs └── string.rs ├── cargo-duchess ├── Cargo.toml └── src │ ├── main.rs │ └── init.rs ├── duchess-reflect ├── build.rs ├── Cargo.toml └── src │ ├── lib.rs │ ├── substitution.rs │ ├── class_info │ ├── from_syn.rs │ └── javap.rs │ └── reflect │ └── javap.rs ├── LICENSE-MIT ├── Cargo.toml ├── .devcontainer └── devcontainer.json ├── CONTRIBUTING.md ├── .github └── workflows │ ├── autobless.yml │ ├── rust.yml │ └── mdbook.yml ├── CHANGELOG.md └── README.md /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/src/chapter_1.md: -------------------------------------------------------------------------------- 1 | # Chapter 1 2 | -------------------------------------------------------------------------------- /book/src/traits/into_java.md: -------------------------------------------------------------------------------- 1 | # IntoJava 2 | -------------------------------------------------------------------------------- /book/src/traits/into_rust.md: -------------------------------------------------------------------------------- 1 | # IntoRust 2 | -------------------------------------------------------------------------------- /book/src/java_object.md: -------------------------------------------------------------------------------- 1 | # The `JavaObject` trait 2 | -------------------------------------------------------------------------------- /book/src/derive.md: -------------------------------------------------------------------------------- 1 | # Deriving Java/Rust conversions 2 | -------------------------------------------------------------------------------- /book/src/java_rust_types.md: -------------------------------------------------------------------------------- 1 | # Java/Rust type conversions 2 | -------------------------------------------------------------------------------- /book/src/traits/jvm_op.md: -------------------------------------------------------------------------------- 1 | # JvmOp 2 | 3 | The `JvmOp` trait -------------------------------------------------------------------------------- /book/src/local_vs_global.md: -------------------------------------------------------------------------------- 1 | # Local vs global object references 2 | -------------------------------------------------------------------------------- /book/src/traits.md: -------------------------------------------------------------------------------- 1 | # Traits 2 | 3 | Documents core traits used in Duchess. 4 | 5 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/run_dummy.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /test-crates/target 4 | /test-crates/Cargo.lock 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /macro/README.md: -------------------------------------------------------------------------------- 1 | Internal helper crate for [duchess](https://crates.io/crates/duchess). Do not rely on this API. -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/log/BaseEvent.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | public class BaseEvent { 4 | } 5 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/generics/MapKey.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | public interface MapKey { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /duchess-build-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> anyhow::Result<()> { 2 | duchess_build_rs::DuchessBuildRs::new().execute() 3 | } 4 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/log/BuildStep.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | public interface BuildStep { 4 | Event build(); 5 | } -------------------------------------------------------------------------------- /test-crates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | resolver = "2" 4 | 5 | members = [ 6 | "duchess-java-tests", 7 | "viper", 8 | ] 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/derives/OptionalFields.java: -------------------------------------------------------------------------------- 1 | package derives; 2 | public record OptionalFields(String a, String b) {} -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Tests will fail to compile if this file doesn't exist"); 3 | } 4 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/log/NameStep.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | public interface NameStep { 4 | BuildStep withName(String name); 5 | } -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/exceptions/DifferentException.java: -------------------------------------------------------------------------------- 1 | package exceptions; 2 | 3 | public class DifferentException extends Exception { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/type_mismatch/TakesInt.java: -------------------------------------------------------------------------------- 1 | package type_mismatch; 2 | 3 | public class TakesInt { 4 | public void take(int i) { 5 | } 6 | } -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/log/TimeStep.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | import java.util.Date; 4 | 5 | public interface TimeStep { 6 | S withTime(Date eventTime); 7 | } 8 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthenticateRequest.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | public record AuthenticateRequest( 4 | String requestId, 5 | HttpRequest request, 6 | String action 7 | ) { } 8 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/interface_class_mismatch_specified.rs: -------------------------------------------------------------------------------- 1 | duchess::java_package! { 2 | package exceptions; 3 | 4 | public interface ThrowExceptions { } //~ ERROR: error in class 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/interface_class_mismatch_reflected.rs: -------------------------------------------------------------------------------- 1 | duchess::java_package! { 2 | package exceptions; 3 | 4 | public interface DifferentException { * } //~ ERROR: error in class 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthorizeRequest.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | import java.util.Map; 4 | 5 | public record AuthorizeRequest( 6 | String resource, 7 | String action, 8 | Map context 9 | ) { } 10 | -------------------------------------------------------------------------------- /src/null.rs: -------------------------------------------------------------------------------- 1 | use crate::{AsJRef, JavaObject, NullJRef}; 2 | 3 | #[derive(Copy, Clone)] 4 | pub struct Null; 5 | 6 | impl AsJRef for Null { 7 | fn as_jref(&self) -> crate::Nullable<&J> { 8 | Err(NullJRef) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /macro-rules/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duchess-macro-rules" 3 | edition = "2021" 4 | version.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | homepage.workspace = true 8 | documentation.workspace = true 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/static_method_invocation.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::prelude::*; 3 | 4 | pub fn main() -> duchess::Result<()> { 5 | let l: i64 = java::util::Date::parse("Feb 1, 2022").execute()?; 6 | println!("{l}"); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /tests/bad_jvm_construction_2.rs: -------------------------------------------------------------------------------- 1 | use duchess::Jvm; 2 | 3 | #[test] 4 | fn test_jvm_construction_error() { 5 | Jvm::builder().try_launch().unwrap(); 6 | let res = Jvm::builder().try_launch(); 7 | assert!(matches!(res, Err(duchess::Error::JvmAlreadyExists))); 8 | } 9 | -------------------------------------------------------------------------------- /tests/good_jvm_construction_1.rs: -------------------------------------------------------------------------------- 1 | use duchess::prelude::*; 2 | 3 | #[test] 4 | fn test_jvm_construction() { 5 | java::lang::Object::new().execute().unwrap(); 6 | java::lang::Object::new().execute().unwrap(); 7 | java::lang::Object::new().execute().unwrap(); 8 | } 9 | -------------------------------------------------------------------------------- /tests/good_jvm_construction_2.rs: -------------------------------------------------------------------------------- 1 | use duchess::prelude::*; 2 | 3 | #[test] 4 | fn test_jvm_construction() { 5 | duchess::Jvm::builder().try_launch().unwrap(); 6 | java::lang::Object::new().execute().unwrap(); 7 | java::lang::Object::new().execute().unwrap(); 8 | } 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthenticationExceptionInvalidSignature.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | // XX: Mangled name only needed until we can parse innner classes 4 | public class AuthenticationExceptionInvalidSignature extends AuthenticationException { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthenticationExceptionInvalidSecurityToken.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | // XX: Mangled name only needed until we can parse innner classes 4 | public class AuthenticationExceptionInvalidSecurityToken extends AuthenticationException { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthorizationExceptionDenied.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | public final class AuthorizationExceptionDenied extends AuthorizationException { 4 | public String userMessage() { 5 | return "User is not allowed to do that"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/log/Event.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | import java.util.Date; 4 | 5 | public record Event( 6 | String name, 7 | Date eventTime) { 8 | 9 | public static TimeStep builder() { 10 | return new Builder(); 11 | } 12 | } -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/native_greeting/Native.java: -------------------------------------------------------------------------------- 1 | package native_greeting; 2 | 3 | public class Native { 4 | public String greet(String name) { 5 | return baseGreeting(name) + ", from Java"; 6 | } 7 | 8 | native String baseGreeting(String name); 9 | } 10 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/generics/EventKey.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | public enum EventKey implements MapKey { 4 | A("abc"), 5 | B("cde"); 6 | 7 | private final String name; 8 | 9 | private EventKey(String name) { 10 | this.name = name; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /macro-rules/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod java_types; 2 | mod macro_if; 3 | mod mro; 4 | mod setup_class; 5 | mod setup_constructor; 6 | mod setup_inherent_object_method; 7 | mod setup_java_function; 8 | mod setup_obj_method; 9 | mod setup_op_method; 10 | mod setup_static_field_getter; 11 | mod setup_static_method; 12 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/Authenticated.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | public final class Authenticated { 4 | 5 | public String accountId() { 6 | return "some-account-id"; 7 | } 8 | 9 | public String user() { 10 | return "some-user"; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/take_null/TakeNull.java: -------------------------------------------------------------------------------- 1 | package take_null; 2 | 3 | public class TakeNull { 4 | public boolean take_null_object(Object o) { 5 | return o == null; 6 | } 7 | 8 | public boolean take_null_string(String o) { 9 | return o == null; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/duplicate_implements.rs: -------------------------------------------------------------------------------- 1 | duchess::java_package! { 2 | package log; 3 | 4 | public interface log.BuildStep { * } 5 | 6 | public class log.Builder implements log.BuildStep, log.BuildStep {} //~ ERROR: duplicate reference 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /cargo-duchess/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-duchess" 3 | edition = "2021" 4 | version.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | homepage.workspace = true 8 | documentation.workspace = true 9 | 10 | [dependencies] 11 | anyhow = "1.0.89" 12 | structopt = "0.3.26" 13 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/flags/Flags.java: -------------------------------------------------------------------------------- 1 | package flags; 2 | 3 | public class Flags { 4 | private int privateField; 5 | 6 | private int privateMethod() { 7 | return privateField; 8 | } 9 | 10 | public int publicMethod() { 11 | return privateMethod(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/bad_jvm_construction_1.rs: -------------------------------------------------------------------------------- 1 | use duchess::prelude::*; 2 | use duchess::Jvm; 3 | 4 | #[test] 5 | fn test_jvm_construction_error() { 6 | java::lang::Object::new().execute().unwrap(); 7 | let res = Jvm::builder().try_launch(); 8 | assert!(matches!(res, Err(duchess::Error::JvmAlreadyExists))); 9 | } 10 | -------------------------------------------------------------------------------- /src/jvm/test.rs: -------------------------------------------------------------------------------- 1 | use crate::Jvm; 2 | 3 | #[test] 4 | fn nested_jvm_with() { 5 | Jvm::with(|_jvm| { 6 | let err = Jvm::with(|_jvm| Ok(())).expect_err("nested JVMs are illegal"); 7 | assert!(matches!(err, crate::Error::NestedUsage)); 8 | Ok(()) 9 | }) 10 | .expect("returns Ok") 11 | } 12 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/log/Logger.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | public class Logger { 4 | public Logger() { 5 | System.out.println("new Logger"); 6 | } 7 | 8 | public void addEvent(Event e) { 9 | System.out.println("LOG: " + e.name() + " at " + e.eventTime()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/duplicate_extends.rs: -------------------------------------------------------------------------------- 1 | duchess::java_package! { 2 | package log; 3 | 4 | public class log.Builder extends java.lang.Object, java.lang.Object {} //~ ERROR: declared interface 5 | //~^ ERROR: duplicate reference 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/generics.stderr: -------------------------------------------------------------------------------- 1 | warning: unused import: `java` 2 | --> tests/ui/examples/generics.rs:2:15 3 | | 4 | 2 | use duchess::{java, prelude::*}; 5 | | ^^^^ 6 | | 7 | = note: `#[warn(unused_imports)]` on by default 8 | 9 | warning: 1 warning emitted 10 | 11 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/must_use_jvmop.rs: -------------------------------------------------------------------------------- 1 | use duchess::java; 2 | 3 | #[deny(unused_must_use)] 4 | fn main() -> duchess::Result<()> { 5 | let timestamp = java::util::Date::new(); 6 | timestamp.set_time(0i64); //~ ERROR: unused implementer of `JvmOp` that must be used 7 | 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/HttpRequest.java: -------------------------------------------------------------------------------- 1 | 2 | package auth; 3 | 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | public record HttpRequest( 8 | String verb, 9 | String path, 10 | byte[] hashedPayload, 11 | Map> parameters, 12 | Map> headers 13 | ) { } 14 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Niko Matsakis"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Duchess" 7 | 8 | [rust] 9 | edition = "2021" 10 | 11 | [output.html] 12 | git-repository-url = "https://github.com/duchess-rs/duchess" 13 | edit-url-template = "https://github.com/duchess-rs/duchess/edit/master/book/{path}" -------------------------------------------------------------------------------- /test-crates/viper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "viper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | duchess = { path = "../.." } 8 | 9 | [build-dependencies] 10 | ureq = "2.1" 11 | duchess-build-rs = { path = "../../duchess-build-rs" } 12 | 13 | [features] 14 | jni_1_6 = ["duchess/jni_1_6"] 15 | jni_1_8 = ["duchess/jni_1_8"] 16 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/create_string_from_bytes.stderr: -------------------------------------------------------------------------------- 1 | warning: unused import: `IntoJava` 2 | --> tests/ui/create_string_from_bytes.rs:3:27 3 | | 4 | 3 | use duchess::{prelude::*, IntoJava}; 5 | | ^^^^^^^^ 6 | | 7 | = note: `#[warn(unused_imports)]` on by default 8 | 9 | warning: 1 warning emitted 10 | 11 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/duplicate_implements.stderr: -------------------------------------------------------------------------------- 1 | error: error in class `log.Builder`: duplicate reference in 'implements' to 'log.BuildStep' 2 | --> tests/ui/duplicate_implements.rs:4:5 3 | | 4 | 4 | public class log.Builder implements log.BuildStep, log.BuildStep {} 5 | | ^^^^^^ 6 | 7 | error: aborting due to 1 previous error 8 | 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthenticationExceptionUnauthenticated.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | // XX: Mangled name only needed until we can parse innner classes 4 | public class AuthenticationExceptionUnauthenticated extends AuthenticationException { 5 | 6 | public String userMessage() { 7 | return "not authenticated"; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/type_mismatch_return_type_in_Object_hashCode.stderr: -------------------------------------------------------------------------------- 1 | error: generic type parameter `Id { data: "bool" }` not among in-scope parameters: [] 2 | --> tests/ui/type_mismatch_return_type_in_Object_hashCode.rs:6:5 3 | | 4 | 6 | public class java.lang.Object { 5 | | ^^^^^^ 6 | 7 | error: aborting due to 1 previous error 8 | 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/interface_class_mismatch_specified.stderr: -------------------------------------------------------------------------------- 1 | error: error in class `exceptions.ThrowExceptions`: class `exceptions.ThrowExceptions` should be type `class` 2 | --> tests/ui/interface_class_mismatch_specified.rs:4:5 3 | | 4 | 4 | public interface ThrowExceptions { } 5 | | ^^^^^^ 6 | 7 | error: aborting due to 1 previous error 8 | 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/interface_class_mismatch_reflected.stderr: -------------------------------------------------------------------------------- 1 | error: error in class `exceptions.DifferentException`: class `exceptions.DifferentException` should be type `class` 2 | --> tests/ui/interface_class_mismatch_reflected.rs:4:5 3 | | 4 | 4 | public interface DifferentException { * } 5 | | ^^^^^^ 6 | 7 | error: aborting due to 1 previous error 8 | 9 | -------------------------------------------------------------------------------- /book/src/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ## Features 4 | 5 | ### `dylibjvm` 6 | 7 | `libjvm` can be either statically or dynamically linked. If the `dylibjvm` feature is enabled, `duchess` will dynamically load `libjvm` when trying to create or find a JVM. Unless the lib path is specified in `JvmBuilder::load_libjvm_at()`, it uses the `java-locator` crate to find the likely location of `libjvm` on the platform. -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthorizationException.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | public abstract class AuthorizationException extends RuntimeException { 4 | 5 | // XX: inner classes are unsupported right now 6 | // public static final class Denied extends AuthorizationException { } 7 | // public static final class InternalFailure extends AuthorizationException { } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/HttpAuth.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | public final class HttpAuth { 4 | 5 | public Authenticated authenticate(HttpRequest request) throws AuthenticationException { 6 | return new Authenticated(); 7 | } 8 | 9 | public void authorize(Authenticated authenticated, AuthorizeRequest request) throws AuthorizationException { 10 | 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/instance_method_fully_qualified.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::prelude::*; 3 | 4 | pub fn main() -> duchess::Result<()> { 5 | let date = java::util::Date::new().execute()?; 6 | let s: String = java::lang::Object::to_string(&date) 7 | .assert_not_null() 8 | .execute()?; 9 | println!("Today's date is {s}"); 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/type_mismatch_bad_argument_type_in_decl.stderr: -------------------------------------------------------------------------------- 1 | error: error in class `type_mismatch.TakesInt`: method `take(long)` does not match any of the methods in the reflected class: take(int) 2 | --> tests/ui/type_mismatch_bad_argument_type_in_decl.rs:6:5 3 | | 4 | 6 | public class TakesInt { 5 | | ^^^^^^ 6 | 7 | error: aborting due to 1 previous error 8 | 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/type_mismatch_bad_argument_type_in_decl.rs: -------------------------------------------------------------------------------- 1 | //@compile-flags: --crate-type lib 2 | 3 | duchess::java_package! { 4 | package type_mismatch; 5 | 6 | public class TakesInt { 7 | //~^ ERROR: method `take(long)` does not match any of the methods in the reflected class 8 | public void take(short); 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /src/from_ref.rs: -------------------------------------------------------------------------------- 1 | /// A trait that permits converting from a `&J` to an `&Self`. 2 | /// This is part of the internal "plumbing" for duchess. 3 | /// See the [user manual](https://duchess-rs.github.io/duchess/internals.html) for more information. 4 | pub trait FromRef { 5 | fn from_ref(j: &J) -> &Self; 6 | } 7 | 8 | impl FromRef for () { 9 | fn from_ref(_: &J) -> &Self { 10 | &() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/mismatched_flags_private_method_declared_as_public.rs: -------------------------------------------------------------------------------- 1 | duchess::java_package! { 2 | package flags; 3 | 4 | public class flags.Flags { //~ ERROR: member declared as `public` 5 | private int privateField; 6 | public flags.Flags(); 7 | public int privateMethod(); 8 | public int publicMethod(); 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/create_string_from_bytes.rs: -------------------------------------------------------------------------------- 1 | //@ run 2 | 3 | use duchess::{prelude::*, IntoJava}; 4 | 5 | fn main() -> duchess::Result<()> { 6 | let v = vec!['H' as u8, 'i' as u8]; 7 | 8 | let n: Java = java::lang::String::new(v.to_java()).execute()?; 9 | 10 | let n: String = n.execute()?; 11 | 12 | assert_eq!(&n[..], "Hi"); 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/mismatched_flags_private_method_declared_as_public.stderr: -------------------------------------------------------------------------------- 1 | error: error in class `flags.Flags`: member declared as `public` but it is `private` in Java, which appears in method `privateMethod()` 2 | --> tests/ui/mismatched_flags_private_method_declared_as_public.rs:4:5 3 | | 4 | 4 | public class flags.Flags { 5 | | ^^^^^^ 6 | 7 | error: aborting due to 1 previous error 8 | 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/mismatched_flags_public_method_declared_as_private.rs: -------------------------------------------------------------------------------- 1 | duchess::java_package! { 2 | package flags; 3 | 4 | public class flags.Flags { //~ ERROR: member declared as `private` 5 | private int privateField; 6 | public flags.Flags(); 7 | private int privateMethod(); 8 | private int publicMethod(); 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/mismatched_flags_public_method_declared_as_private.stderr: -------------------------------------------------------------------------------- 1 | error: error in class `flags.Flags`: member declared as `private` but it is `public` in Java, which appears in method `publicMethod()` 2 | --> tests/ui/mismatched_flags_public_method_declared_as_private.rs:4:5 3 | | 4 | 4 | public class flags.Flags { 5 | | ^^^^^^ 6 | 7 | error: aborting due to 1 previous error 8 | 9 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/duchess_can_use_jni_launched_jvm.rs: -------------------------------------------------------------------------------- 1 | //@ run 2 | use jni::JavaVM; 3 | 4 | fn main() -> duchess::Result<()> { 5 | let args = jni::InitArgsBuilder::new() 6 | .build() 7 | .expect("Failed to build JVM InitArgs"); 8 | let jvm = JavaVM::new(args).expect("Failed to build JVM"); 9 | duchess::Jvm::builder().launch_or_use_existing().unwrap(); 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /book/src/logo.md: -------------------------------------------------------------------------------- 1 | # Logo 2 | 3 | The duchess logo: 4 | 5 | 6 | 7 | combines Java's [Duke] (specifically the [Surfing] version) and [Ferris], both of which are released under open source licenses that permit (and encourage) duplication and modification. 8 | 9 | [Duke]: https://openjdk.org/projects/duke/index.html 10 | [Surfing]: https://github.com/openjdk/duke/blob/master/vector/Surfing.svg 11 | [Ferris]: https://rustacean-principles.netlify.app/ -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/type_mismatch_return_type_in_Object_hashCode.rs: -------------------------------------------------------------------------------- 1 | //@compile-flags: --crate-type lib 2 | 3 | duchess::java_package! { 4 | package java.lang; 5 | 6 | public class java.lang.Object { //~ ERROR: generic type parameter `Id { data: "bool" }` not among in-scope parameters: [] 7 | public java.lang.Object(); 8 | public native bool hashCode(); // <-- this is the wrong return type! 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /duchess-build-rs/src/log.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! log { 3 | ($($tokens: tt)*) => { 4 | if std::env::var("DUCHESS_DEBUG").is_ok() { 5 | // print logging at warn level so it's easy to see (without intentionally failing the build) 6 | println!("cargo:warning={}", format!($($tokens)*)) 7 | } else { 8 | // otherwise `eprintln` (this will be visible if the build script panics) 9 | eprintln!("{}", format!($($tokens)*)) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/must_use_jvmop.stderr: -------------------------------------------------------------------------------- 1 | error: unused implementer of `JvmOp` that must be used 2 | --> tests/ui/must_use_jvmop.rs:6:5 3 | | 4 | 6 | timestamp.set_time(0i64); 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^ 6 | | 7 | = note: JvmOps do nothing unless you call `.execute() 8 | note: the lint level is defined here 9 | --> tests/ui/must_use_jvmop.rs:3:8 10 | | 11 | 3 | #[deny(unused_must_use)] 12 | | ^^^^^^^^^^^^^^^ 13 | 14 | error: aborting due to 1 previous error 15 | 16 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/java/java_to_rust_greeting/Java_Can_Call_Rust_Java_Function.java: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | package java_to_rust_greeting; 3 | 4 | public class Java_Can_Call_Rust_Java_Function { 5 | native String base_greeting(String name); 6 | 7 | public static void main(String[] args) { 8 | System.loadLibrary("native_fn_callable_from_java"); 9 | Java_Can_Call_Rust_Java_Function sut = new Java_Can_Call_Rust_Java_Function(); 10 | sut.base_greeting("duchess"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/racing_jvm_construction.rs: -------------------------------------------------------------------------------- 1 | use duchess::prelude::*; 2 | use std::{sync, thread}; 3 | 4 | #[test] 5 | fn race_multiple_threads_to_launch_jvm() { 6 | let n = 10; 7 | let barrier = sync::Arc::new(sync::Barrier::new(10)); 8 | thread::scope(|scope| { 9 | for _ in 0..n { 10 | let barrier = sync::Arc::clone(&barrier); 11 | scope.spawn(move || { 12 | barrier.wait(); 13 | java::lang::Object::new().execute().unwrap(); 14 | }); 15 | } 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/derive_scalar_fields.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | #[derive(duchess::ToJava)] 5 | #[java(java.time.Instant::ofEpochMilli)] 6 | struct RustInstant { 7 | epoch_millis: i64, 8 | } 9 | 10 | pub fn main() -> duchess::Result<()> { 11 | let rust = RustInstant { epoch_millis: 42 }; 12 | let java = rust.to_java().assert_not_null().execute()?; 13 | let and_back = java.to_epoch_milli().execute()?; 14 | assert_eq!(rust.epoch_millis, and_back); 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /book/src/jvm_operations.md: -------------------------------------------------------------------------------- 1 | # JVM Operations 2 | 3 | *JVM operations* correspond to code that will execute on the JVM. Like futures and iterators, JVM operations are lazy. This means that you compose them together using a series of method calls and, once you've built up the entire thing that you want to do, you invoke the `execute` method, giving it a [`&mut Jvm`](./jvm.md) to execute on. This lazy style is convenient to use, because you only have to supply the `jvm` argument once, but it also gives duchess a chance to optimize for fewer JNI invocations, making your code run faster. 4 | 5 | -------------------------------------------------------------------------------- /duchess-reflect/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | lalrpop::process_root().unwrap(); 3 | 4 | // Recompile the crate when the lalrpop grammar changes 5 | println!("cargo:rerun-if-changed=src"); 6 | 7 | // Procedural macros currently do not automatically rerun when the environment variables on 8 | // which they depend change. So, as a workaround we force a recompilation. 9 | // See issue: https://github.com/duchess-rs/duchess/issues/7 10 | println!("cargo:rerun-if-env-changed=CLASSPATH"); 11 | println!("cargo:rerun-if-env-changed=DUCHESS_DEBUG"); 12 | } 13 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries/java_rust_java_npe.rs: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | use duchess::prelude::*; 4 | 5 | duchess::java_package! { 6 | package java_rust_java_exception; 7 | 8 | public class java_rust_java_exception.JavaRustJavaNPE { * } 9 | } 10 | 11 | #[duchess::java_function(java_rust_java_exception.JavaRustJavaNPE::rustFunction)] 12 | fn rust_function( 13 | this: &java_rust_java_exception::JavaRustJavaNPE, 14 | ) -> duchess::Result> { 15 | Ok(this.java_function().assert_not_null().execute()?) 16 | } 17 | -------------------------------------------------------------------------------- /duchess-build-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duchess-build-rs" 3 | edition = "2021" 4 | version.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | homepage.workspace = true 8 | documentation.workspace = true 9 | 10 | [dependencies] 11 | anyhow = "1.0.86" 12 | duchess-reflect = { version = "0.3.0", path = "../duchess-reflect", features = ["javap-reflection"] } 13 | lazy_static = "1.5.0" 14 | proc-macro2 = "1.0.86" 15 | quote = "1.0.36" 16 | regex = "1.10.5" 17 | syn = { version = "2.0.71", features = ["full"] } 18 | tempfile = "3.10.1" 19 | walkdir = "2.5.0" 20 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/derive_lifetimes.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | #[derive(duchess::ToJava)] 5 | #[java(java.lang.Long::decode)] 6 | struct LongWrapper<'a> { 7 | value: &'a str, 8 | } 9 | 10 | pub fn main() -> duchess::Result<()> { 11 | let my_string = String::from("1234"); 12 | let rust = LongWrapper { value: &my_string }; 13 | let java = rust.to_java().assert_not_null().execute()?; 14 | let and_back: String = java.to_string().execute().unwrap().unwrap(); 15 | assert_eq!(rust.value, and_back); 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/derive_options.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | duchess::java_package! { 5 | package derives; 6 | class derives.OptionalFields { * } 7 | } 8 | 9 | #[derive(duchess::ToJava)] 10 | #[java(derives.OptionalFields)] 11 | struct OptionalFields { 12 | a: Option, 13 | b: String, 14 | } 15 | 16 | pub fn main() -> duchess::Result<()> { 17 | let rust = OptionalFields { 18 | a: None, 19 | b: "hello".to_string(), 20 | }; 21 | let java = rust.to_java().execute()?; 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/view_of_obj.rs: -------------------------------------------------------------------------------- 1 | /// Creates the "object type" for `$r`, which should be a reference type. 2 | /// See the [method resolution order][mro] docs for background on the "object type". 3 | /// 4 | /// # Examples 5 | /// 6 | /// * `(class[java::lang::Object])` expands to `::OfObj` 7 | /// 8 | /// [mro]: https://duchess-rs.github.io/duchess/methods.html 9 | #[macro_export] 10 | macro_rules! view_of_obj { 11 | ($r:tt) => { 12 | ::OfObj 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/view_of_op.rs: -------------------------------------------------------------------------------- 1 | /// Creates the "operation type" for `$r`, which should be a reference type. 2 | /// See the [method resolution order][mro] docs for background on the "operation type". 3 | /// 4 | /// # Examples 5 | /// 6 | /// * `(class[java::lang::Object])` expands to `::OfOp` 7 | /// 8 | /// [mro]: https://duchess-rs.github.io/duchess/methods.html 9 | #[macro_export] 10 | macro_rules! view_of_op { 11 | ($r:tt) => { 12 | ::OfOp 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries/java_rust_java_exception.rs: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | use duchess::prelude::*; 4 | 5 | duchess::java_package! { 6 | package java_rust_java_exception; 7 | 8 | public class java_rust_java_exception.JavaRustJavaException { * } 9 | } 10 | 11 | #[duchess::java_function(java_rust_java_exception.JavaRustJavaException::rustFunction)] 12 | fn rust_function( 13 | this: &java_rust_java_exception::JavaRustJavaException, 14 | ) -> duchess::Result> { 15 | Ok(this.java_function().assert_not_null().execute()?) 16 | } 17 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/to_java_produce_local_java_ref.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | 3 | #![allow(dead_code)] 4 | 5 | use duchess::prelude::*; 6 | use duchess::Local; 7 | use java::lang::String as JavaString; 8 | use java::util::ArrayList as JavaList; 9 | 10 | // Test that `to_java` can accomodate a Rust vector of (local) Java objects 11 | // and produce a Java list of Java objects. 12 | fn produce_from_local_rust_vec(r: &Vec>) { 13 | let _data: Option>> = 14 | r.to_java::>().execute().unwrap(); 15 | } 16 | 17 | fn main() {} 18 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/field_output_trait.rs: -------------------------------------------------------------------------------- 1 | /// Returns an appropriate trait for a method that 2 | /// returns `ty`. Assumes objects are nullable. 3 | /// 4 | /// # Examples 5 | /// 6 | /// * `int` expands to `impl ScalarField` 7 | /// * `(class[java::lang::Object])` expands to `impl JavaField` 8 | #[macro_export] 9 | macro_rules! field_output_trait { 10 | ($scalar:ident) => { 11 | impl duchess::ScalarField< duchess::semver_unstable::rust_ty!($scalar) > 12 | }; 13 | 14 | ($r:tt) => { 15 | impl duchess::JavaField< duchess::semver_unstable::rust_ty!($r) > 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/exceptions.stderr: -------------------------------------------------------------------------------- 1 | warning: unused import: `duchess::Jvm` 2 | --> tests/ui/exceptions.rs:3:5 3 | | 4 | 3 | use duchess::Jvm; 5 | | ^^^^^^^^^^^^ 6 | | 7 | = note: `#[warn(unused_imports)]` on by default 8 | 9 | warning: unused variable: `caught_exception` 10 | --> tests/ui/exceptions.rs:43:9 11 | | 12 | 43 | let caught_exception = thrower 13 | | ^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_caught_exception` 14 | | 15 | = note: `#[warn(unused_variables)]` on by default 16 | 17 | warning: 2 warnings emitted 18 | 19 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/duplicate_extends.stderr: -------------------------------------------------------------------------------- 1 | error: error in class `log.Builder`: declared interface `java.lang.Object` not found in the reflected superclasses () 2 | --> tests/ui/duplicate_extends.rs:4:5 3 | | 4 | 4 | public class log.Builder extends java.lang.Object, java.lang.Object {} 5 | | ^^^^^^ 6 | 7 | error: error in class `log.Builder`: duplicate reference in 'extends' to 'java.lang.Object' 8 | --> tests/ui/duplicate_extends.rs:4:5 9 | | 10 | 4 | public class log.Builder extends java.lang.Object, java.lang.Object {} 11 | | ^^^^^^ 12 | 13 | error: aborting due to 2 previous errors 14 | 15 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/memory_usage.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | 3 | fn main() { 4 | use duchess::prelude::*; 5 | use java::lang::management::ManagementFactory; 6 | let memory_bean = ManagementFactory::get_memory_mx_bean(); 7 | let heap_usage = memory_bean 8 | .get_heap_memory_usage() 9 | .get_used() 10 | .execute() 11 | .expect("failed to load usage"); 12 | let other_usage = memory_bean 13 | .get_non_heap_memory_usage() 14 | .get_used() 15 | .execute() 16 | .expect("failed to load usage"); 17 | println!("total memory usage: {}", heap_usage + other_usage); 18 | } 19 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duchess-java-tests" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | duchess = { path = "../.." } 8 | thiserror = "1.0.40" 9 | jni = { version = "0.21", features = ["invocation"] } 10 | 11 | [dev-dependencies] 12 | ui_test = "0.23" 13 | 14 | [build-dependencies] 15 | anyhow = "1.0.90" 16 | duchess-build-rs = { version = "0.3.0", path = "../../duchess-build-rs" } 17 | walkdir = "2.3" 18 | 19 | [[bin]] 20 | name = "java_wrapper" 21 | path = "src/java_wrapper.rs" 22 | 23 | [[test]] 24 | name = "ui" 25 | harness = false 26 | 27 | [features] 28 | jni_1_6 = ["duchess/jni_1_6"] 29 | jni_1_8 = ["duchess/jni_1_8"] 30 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/to_java_on_rust_string.rs: -------------------------------------------------------------------------------- 1 | //@ run 2 | 3 | #![allow(dead_code)] 4 | 5 | use duchess::prelude::*; 6 | 7 | fn main() -> duchess::Result<()> { 8 | let data = format!("Hello, Duchess!"); 9 | let hash_code: i32 = data 10 | .to_java::() // Returns a `JvmOp` producing a `java::lang::String` 11 | .hash_code() // Returns a `JvmOp` invoking `hashCode` on this string 12 | .execute()?; // Execute the jvmop 13 | 14 | // NB: [hashCode for string is documented](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#hashCode--) 15 | assert_eq!(hash_code, -928531272); 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/log/Builder.java: -------------------------------------------------------------------------------- 1 | package log; 2 | 3 | import java.util.Date; 4 | 5 | public class Builder 6 | implements TimeStep, NameStep, BuildStep { 7 | 8 | String n; 9 | Date d; 10 | 11 | // FIXME: support static methods 12 | Builder() { 13 | } 14 | 15 | @Override 16 | public Event build() { 17 | return new Event(n, d); 18 | } 19 | 20 | @Override 21 | public BuildStep withName(String name) { 22 | n = name; 23 | return this; 24 | } 25 | 26 | @Override 27 | public NameStep withTime(Date eventTime) { 28 | d = eventTime; 29 | return this; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/java/java_passing_null/JavaPassingNull.java: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | package java_passing_null; 4 | 5 | public class JavaPassingNull { 6 | native String identity(String name); 7 | 8 | public static void main(String[] args) { 9 | System.loadLibrary("native_fn_passing_null"); 10 | JavaPassingNull p = new JavaPassingNull(); 11 | 12 | String in = "duchess"; 13 | String out = p.identity("duchess"); 14 | if (in != out) 15 | throw new RuntimeException("Did not get expected string"); 16 | 17 | if (p.identity(null) != null) 18 | throw new RuntimeException("Did not get null"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/output_type.rs: -------------------------------------------------------------------------------- 1 | /// Returns an appropriate trait for a method that 2 | /// returns `ty`. Assumes objects are nullable. 3 | /// 4 | /// # Examples 5 | /// 6 | /// * `'a, void` expands to `()` 7 | /// * `'a, int` expands to `i32` 8 | /// * `'a, (object[java::lang::Object])` expands to `Option>` 9 | #[macro_export] 10 | macro_rules! output_type { 11 | ($lt:lifetime, void) => { 12 | () 13 | }; 14 | 15 | ($lt:lifetime, $scalar:ident) => { 16 | duchess::semver_unstable::rust_ty!($scalar) 17 | }; 18 | 19 | ($lt:lifetime, $r:tt) => { 20 | Option> 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries/native_fn_callable_from_java.rs: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | use duchess::prelude::*; 4 | 5 | duchess::java_package! { 6 | package java_to_rust_greeting; 7 | 8 | public class Java_Can_Call_Rust_Java_Function { 9 | native java.lang.String base_greeting(java.lang.String); 10 | } 11 | } 12 | 13 | #[duchess::java_function(java_to_rust_greeting.Java_Can_Call_Rust_Java_Function::base_greeting)] 14 | fn base_greeting( 15 | _this: &java_to_rust_greeting::Java_Can_Call_Rust_Java_Function, 16 | name: Option<&java::lang::String>, 17 | ) -> duchess::Result> { 18 | Ok(name.assert_not_null().execute().unwrap()) 19 | } 20 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/passing_null_values.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::prelude::*; 3 | 4 | duchess::java_package! { 5 | package take_null; 6 | 7 | public class TakeNull { * } 8 | } 9 | 10 | pub fn main() -> duchess::Result<()> { 11 | let take_null = take_null::TakeNull::new().execute()?; 12 | 13 | let is_null = take_null.take_null_object(duchess::Null).execute()?; 14 | assert!(is_null); 15 | 16 | let is_null = take_null 17 | .take_null_object(&None::>) 18 | .execute()?; 19 | assert!(is_null); 20 | 21 | let is_null = take_null.take_null_string(duchess::Null).execute()?; 22 | assert!(is_null); 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_hashmaps.rs: -------------------------------------------------------------------------------- 1 | use duchess::{java, Java, JvmOp, ToJava}; 2 | use std::collections::HashMap; 3 | 4 | #[test] 5 | fn test_hashmap_roundtrip() { 6 | let mut test_map = HashMap::new(); 7 | test_map.insert("a".to_string(), "abc".to_string()); 8 | test_map.insert("b".to_string(), "cde".to_string()); 9 | 10 | let java: Java> = test_map 11 | .to_java::>() 12 | .execute() 13 | .unwrap() 14 | .unwrap(); 15 | assert_eq!(java.get("a").execute().unwrap(), Some("abc".to_string())); 16 | assert_eq!(java.get("b").execute().unwrap(), Some("cde".to_string())); 17 | } 18 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/to_java_on_java_string.rs: -------------------------------------------------------------------------------- 1 | //@ run 2 | 3 | #![allow(dead_code)] 4 | 5 | use duchess::prelude::*; 6 | 7 | fn main() -> duchess::Result<()> { 8 | let data: Java = format!("Hello, Duchess!").execute()?; 9 | 10 | let hash_code: i32 = data 11 | .to_java::() // Returns a `JvmOp` producing a `java::lang::String` 12 | .hash_code() // Returns a `JvmOp` invoking `hashCode` on this string 13 | .execute()?; // Execute the jvmop 14 | 15 | // NB: [hashCode for string is documented](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#hashCode--) 16 | assert_eq!(hash_code, -928531272); 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /cargo-duchess/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[derive(StructOpt, Debug)] 4 | #[structopt(name = "cargo-duchess")] 5 | enum Opt { 6 | /// Initialize something 7 | Init { 8 | #[structopt(flatten)] 9 | options: init::InitOptions, 10 | }, 11 | /// Package something 12 | Package { 13 | /// Path to the package 14 | #[structopt(short, long)] 15 | path: String, 16 | }, 17 | } 18 | 19 | mod init; 20 | 21 | fn main() -> anyhow::Result<()> { 22 | let opt = Opt::from_args(); 23 | match opt { 24 | Opt::Init { options } => { 25 | init::init(options)?; 26 | } 27 | Opt::Package { path: _ } => todo!(), 28 | } 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries/native_fn_passing_null.rs: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | use duchess::prelude::*; 4 | 5 | duchess::java_package! { 6 | package java_passing_null; 7 | 8 | public class JavaPassingNull { 9 | native java.lang.String identity(java.lang.String); 10 | } 11 | } 12 | 13 | #[duchess::java_function(java_passing_null.JavaPassingNull::identity)] 14 | fn the_identity_fn( 15 | _this: &java_passing_null::JavaPassingNull, 16 | name: Option<&java::lang::String>, 17 | ) -> duchess::Result>> { 18 | // this should be easier :) 19 | Ok(match name { 20 | Some(n) => Some(n.execute()?), 21 | None => None, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/java/java_rust_java_exception/JavaRustJavaNPE.java: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | package java_rust_java_exception; 4 | 5 | public class JavaRustJavaNPE { 6 | native String rustFunction(); 7 | 8 | public String javaFunction() { 9 | throw new NullPointerException(); 10 | } 11 | 12 | public static void main(String[] args) { 13 | System.loadLibrary("java_rust_java_npe"); 14 | JavaRustJavaNPE test = new JavaRustJavaNPE(); 15 | try { 16 | test.rustFunction(); 17 | } catch (NullPointerException e) { 18 | return; 19 | } 20 | 21 | throw new RuntimeException("Did not catch NullPointerException"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duchess-macro" 3 | version.workspace = true 4 | edition = "2021" 5 | license.workspace = true 6 | repository.workspace = true 7 | readme = "README.md" 8 | description = "Internal component of duchess crate" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | anyhow = "1.0.70" 15 | lalrpop-util = { version = "0.19.9", features = ["lexer"] } 16 | litrs = "0.4.0" 17 | lazy_static = "1.4.0" 18 | proc-macro2 = "1.0.56" 19 | quote = "1.0.26" 20 | regex = "1" 21 | str_inflector = "0.12.0" 22 | once_cell = "1.17.1" 23 | synstructure = "0.13.0" 24 | syn = "2.0.15" 25 | derive-where = "1.2.1" 26 | duchess-reflect = { version = "0.3.0", path = "../duchess-reflect" } 27 | 28 | [build-dependencies] 29 | lalrpop = "0.19.9" 30 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/auth/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | package auth; 2 | 3 | public abstract class AuthenticationException extends RuntimeException { 4 | 5 | // XX: inner classes are unsupported right now 6 | // public static final class Unauthenticated extends AuthenticationException { } 7 | // public static final class InvalidSecurityToken extends AuthenticationException { } 8 | // public static final class MissingSignature extends AuthenticationException { } 9 | // public static final class IncompleteSignature extends AuthenticationException { } 10 | // public static final class InvalidSignature extends AuthenticationException { } 11 | // public static final class InternalFailure extends AuthenticationException { } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /book/src/tutorials.md: -------------------------------------------------------------------------------- 1 | # Tutorials and examples 2 | 3 | ## Examples 4 | 5 | * The [examples directory](https://github.com/duchess-rs/duchess/tree/main/test-crates/duchess-java-tests/tests/ui/examples) on github contains some self-contained examples; the corresponding Java code is in the [java directory](https://github.com/duchess-rs/duchess/tree/main/java). 6 | * The [test-crates directory]() contains some other standalone tests. 7 | * Duchess itself uses duchess to [mirror the classes from the JVM](https://github.com/duchess-rs/duchess/blob/main/src/java.rs). 8 | 9 | ## Tutorials 10 | 11 | Be sure to follow the [setup instructions](./setup.md). 12 | 13 | * [Calling Java from Rust](./call_java_from_rust.md) 14 | * [Implementing native methods](./implementing_native_methods.md) 15 | -------------------------------------------------------------------------------- /duchess-build-rs/test-files/java_package_1.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::prelude::*; 3 | 4 | // Generate our own version of java.util.Date that 5 | // explicitly does NOT include the `toString` method, 6 | // so that we have to get it by upcasting to Object. 7 | mod our_java { 8 | duchess::java_package! { 9 | package java.util; 10 | 11 | public class java.util.Date { 12 | public java.util.Date(); 13 | } 14 | } 15 | pub use java::*; 16 | } 17 | 18 | pub fn main() -> duchess::Result<()> { 19 | let date = our_java::util::Date::new().execute()?; 20 | let s: String = date.to_string().assert_not_null().execute()?; 21 | // ^^^^^^^^^^^ this is defined on `java.lang.Object` 22 | println!("Today's date is {s}"); 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /tests/arrays.rs: -------------------------------------------------------------------------------- 1 | use duchess::{java, Java, JvmOp, ToJava}; 2 | 3 | macro_rules! test_array { 4 | ($type: ty, $item: expr) => { 5 | for test_array in [vec![$item], vec![], vec![$item, $item, $item]] { 6 | let java: Java> = 7 | test_array.to_java().assert_not_null().execute().unwrap(); 8 | let and_back: Vec<_> = (&*java).execute().unwrap(); 9 | assert_eq!(test_array, and_back); 10 | } 11 | }; 12 | } 13 | 14 | #[test] 15 | fn test_array_types() { 16 | test_array!(bool, true); 17 | test_array!(i8, 5_i8); 18 | test_array!(u16, 5_u16); 19 | test_array!(i16, 5_i16); 20 | test_array!(i32, 5_i32); 21 | test_array!(i64, 5_i64); 22 | test_array!(f32, 5_f32); 23 | test_array!(f64, 5_f64); 24 | } 25 | -------------------------------------------------------------------------------- /duchess-reflect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duchess-reflect" 3 | edition = "2021" 4 | version.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | homepage.workspace = true 8 | documentation.workspace = true 9 | description = "Internal component of duchess crate" 10 | 11 | [features] 12 | javap-reflection = [] 13 | 14 | [dependencies] 15 | anyhow = "1.0.70" 16 | lalrpop-util = { version = "0.19.9", features = ["lexer"] } 17 | lazy_static = "1.4.0" 18 | proc-macro2 = "1.0.56" 19 | quote = "1.0.26" 20 | rust-format = { version = "0.3.4", features = ["token_stream"] } 21 | serde = { version = "1.0.214", features = ["derive", "rc"] } 22 | serde_json = "1.0.132" 23 | str_inflector = "0.12.0" 24 | syn = "2.0.15" 25 | tempfile = "3.8.1" 26 | 27 | [build-dependencies] 28 | lalrpop = "0.19.9" 29 | -------------------------------------------------------------------------------- /test-crates/viper/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, io, path}; 2 | 3 | use duchess_build_rs::Configuration; 4 | 5 | static JAR_URL: &str = 6 | "https://github.com/viperproject/viperserver/releases/download/v.23.01-release/viperserver.jar"; 7 | 8 | fn main() { 9 | let out_dir_string = env::var("OUT_DIR").unwrap(); 10 | let out_dir = path::Path::new(&out_dir_string); 11 | let jar_path = out_dir.join("viper.jar"); 12 | 13 | let jar_data = ureq::get(JAR_URL).call().unwrap(); 14 | let mut jar_file = fs::File::create(jar_path.clone()).unwrap(); 15 | io::copy(&mut jar_data.into_reader(), &mut jar_file).unwrap(); 16 | 17 | duchess_build_rs::DuchessBuildRs::new() 18 | .with_configuration(Configuration::new().push_classpath(jar_path.display())) 19 | .execute() 20 | .unwrap(); 21 | } 22 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/instance_method_from_superclass.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::prelude::*; 3 | 4 | // Generate our own version of java.util.Date that 5 | // explicitly does NOT include the `toString` method, 6 | // so that we have to get it by upcasting to Object. 7 | mod our_java { 8 | duchess::java_package! { 9 | package java.util; 10 | 11 | public class java.util.Date { 12 | public java.util.Date(); 13 | } 14 | } 15 | pub use java::*; 16 | } 17 | 18 | pub fn main() -> duchess::Result<()> { 19 | let date = our_java::util::Date::new().execute()?; 20 | let s: String = date.to_string().assert_not_null().execute()?; 21 | // ^^^^^^^^^^^ this is defined on `java.lang.Object` 22 | println!("Today's date is {s}"); 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/argument_impl_trait.rs: -------------------------------------------------------------------------------- 1 | /// Generates an `impl Trait` expression that is used as the type of a method argument 2 | /// in a Rust function reflecting a Java method. 3 | /// 4 | /// # Examples 5 | /// 6 | /// * `int` expands to `impl IntoScalar` 7 | /// * `int + 'a` expands to `impl IntoScalar + 'a` 8 | /// * `(class[java::lang::Object])` expands to `impl IntoJava` 9 | /// * `(class[java::lang::Object]) + 'a` expands to `impl IntoJava + 'a` 10 | #[macro_export] 11 | macro_rules! argument_impl_trait { 12 | ($scalar:ident $(+ $lt:lifetime)?) => { 13 | impl duchess::IntoScalar< duchess::semver_unstable::rust_ty!($scalar) > $(+ $lt)? 14 | }; 15 | 16 | ($r:tt $(+ $lt:lifetime)?) => { 17 | impl duchess::IntoJava< duchess::semver_unstable::rust_ty!($r) > $(+ $lt)? 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /macro-rules/src/setup_obj_method.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! setup_obj_method { 3 | ( 4 | struct_name: [$S:ident], 5 | java_class_generics: [$($G:ident,)*], 6 | rust_method_name: [$M:ident], 7 | rust_method_generics: [$($MG:ident,)*], 8 | input_names: [$($I:tt,)*], 9 | input_ty_tts: [$($I_ty:tt,)*], 10 | output_ty_tt: [$O_ty:tt], 11 | sig_where_clauses: [$($SIG:tt)*], 12 | ) => { 13 | pub fn $M<'a, $($MG,)*>( 14 | &'a self, 15 | $($I: duchess::semver_unstable::argument_impl_trait!($I_ty + 'a),)* 16 | ) -> duchess::semver_unstable::output_trait!($O_ty + 'a) 17 | where 18 | $($SIG)* 19 | { 20 | <$S<$($G,)*>>::$M( 21 | &self.this, 22 | $($I,)* 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /macro-rules/src/setup_op_method.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! setup_op_method { 3 | ( 4 | struct_name: [$S:ident], 5 | java_class_generics: [$($G:ident,)*], 6 | rust_method_name: [$M:ident], 7 | rust_method_generics: [$($MG:ident,)*], 8 | input_names: [$($I:ident,)*], 9 | input_ty_tts: [$($I_ty:tt,)*], 10 | output_ty_tt: [$O_ty:tt], 11 | sig_where_clauses: [$($SIG:tt)*], 12 | ) => { 13 | pub fn $M<$($MG,)*>( 14 | &self, 15 | $($I: duchess::semver_unstable::argument_impl_trait!($I_ty),)* 16 | ) -> duchess::semver_unstable::output_trait!($O_ty) 17 | where 18 | $($SIG)* 19 | { 20 | <$S<$($G,)*>>::$M( 21 | Clone::clone(&self.this), 22 | $($I,)* 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/not_null.rs: -------------------------------------------------------------------------------- 1 | use crate::{JavaObject, JvmOp, Local, TryJDeref}; 2 | 3 | #[derive_where::derive_where(Clone)] 4 | #[derive_where(Copy; J: Copy)] 5 | pub struct NotNull { 6 | j: J, 7 | } 8 | 9 | impl NotNull 10 | where 11 | for<'jvm> J: JvmOp: TryJDeref>, 12 | T: JavaObject, 13 | { 14 | pub(crate) fn new(j: J) -> NotNull { 15 | NotNull { j } 16 | } 17 | } 18 | 19 | impl JvmOp for NotNull 20 | where 21 | for<'jvm> J: JvmOp: TryJDeref>, 22 | T: JavaObject, 23 | { 24 | type Output<'jvm> = Local<'jvm, T>; 25 | 26 | fn do_jni<'jvm>( 27 | self, 28 | jvm: &mut crate::Jvm<'jvm>, 29 | ) -> crate::LocalResult<'jvm, Self::Output<'jvm>> { 30 | let deref = self.j.do_jni(jvm)?; 31 | let j = deref.try_jderef()?; 32 | Ok(jvm.local(j)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/type_mismatch_bad_argument_type.rs: -------------------------------------------------------------------------------- 1 | //@compile-flags: --crate-type lib 2 | use duchess::prelude::*; 3 | 4 | duchess::java_package! { 5 | package type_mismatch; 6 | 7 | public class TakesInt {*} 8 | } 9 | 10 | fn call_with_u64(i: u64) { 11 | type_mismatch::TakesInt::new().take(i).execute(); 12 | //~^ERROR: the trait bound `u64: duchess::IntoScalar` is not satisfied 13 | //~|ERROR: the trait bound `u64: duchess::JvmOp` is not satisfied 14 | } 15 | 16 | fn call_with_u32(i: u32) { 17 | type_mismatch::TakesInt::new().take(i).execute(); 18 | //~^ ERROR: the trait bound `u32: duchess::JvmOp` is not satisfied 19 | //~| ERROR: the trait bound `u32: duchess::IntoScalar` is not satisfied 20 | } 21 | 22 | fn call_with_i32(i: i32) { 23 | type_mismatch::TakesInt::new().take(i).execute(); 24 | // OK 25 | } 26 | 27 | fn main() {} 28 | -------------------------------------------------------------------------------- /macro-rules/src/java_types.rs: -------------------------------------------------------------------------------- 1 | //! These macros take in a "java type description" and generate various Rust types to reflect it. 2 | //! These java type descriptions are each a token tree and they have the following format: 3 | //! 4 | //! Java scalar types are a single identifier 5 | //! * `int` 6 | //! * `short` 7 | //! * etc 8 | //! 9 | //! Java reference types are a `()`-token tree like: 10 | //! * `(class[$path] $javaty*)`, e.g., `(class[java::util::Vector] (class[java::lang::String))` for `Vector` 11 | //! * `(array $javaty)`, e.g., `(array[(class[java::lang::String])])` for `String[]` 12 | //! * `(generic $name)` to reference a generic (possible captured) type, e.g., `(generic[T])` 13 | 14 | mod argument_impl_trait; 15 | mod field_output_trait; 16 | mod jni_call_fn; 17 | mod jni_static_call_fn; 18 | mod jni_static_field_get_fn; 19 | mod output_trait; 20 | mod output_type; 21 | mod prepare_input; 22 | mod rust_ty; 23 | mod view_of_obj; 24 | mod view_of_op; 25 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/java/java_rust_java_exception/JavaRustJavaException.java: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | package java_rust_java_exception; 4 | 5 | public class JavaRustJavaException { 6 | native String rustFunction(); 7 | 8 | public String javaFunction() { 9 | throw new RuntimeException("Exception from `javaFunction`"); 10 | } 11 | 12 | public static void main(String[] args) { 13 | System.loadLibrary("java_rust_java_exception"); 14 | JavaRustJavaException test = new JavaRustJavaException(); 15 | String message = "no exception thrown"; 16 | try { 17 | test.rustFunction(); 18 | } catch (RuntimeException e) { 19 | message = e.getMessage(); 20 | } 21 | 22 | if (message != "Exception from `javaFunction`") { 23 | throw new RuntimeException("Caught no exception or the wrong exception: " + message); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/output_trait.rs: -------------------------------------------------------------------------------- 1 | /// Returns an appropriate trait for a method that 2 | /// returns `ty`. Assumes objects are nullable. 3 | /// 4 | /// # Examples 5 | /// 6 | /// * `void` expands to `impl VoidMethod` 7 | /// * `void + 'a` expands to `impl VoidMethod + 'a` 8 | /// * `int` expands to `impl ScalarMethod` 9 | /// * `(object[java::lang::Object])` expands to `impl JavaMethod` 10 | /// * `(object[java::lang::Object]) + 'a` expands to `impl JavaMethod + 'a` 11 | #[macro_export] 12 | macro_rules! output_trait { 13 | (void $(+ $lt:lifetime)?) => { 14 | impl duchess::VoidMethod $(+ $lt)? 15 | }; 16 | 17 | ($scalar:ident $(+ $lt:lifetime)?) => { 18 | impl duchess::ScalarMethod< duchess::semver_unstable::rust_ty!($scalar) > $(+ $lt)? 19 | }; 20 | 21 | ($r:tt $(+ $lt:lifetime)?) => { 22 | impl duchess::JavaMethod< duchess::semver_unstable::rust_ty!($r) > $(+ $lt)? 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/java/java_to_rust_arrays/JavaArrayTests.java: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | package java_to_rust_arrays; 3 | 4 | import java.util.Arrays; 5 | 6 | public class JavaArrayTests { 7 | public static native long combine_bytes(byte[] bytes); 8 | public static native byte[] break_bytes(long num); 9 | 10 | public static void main(String[] args) { 11 | System.loadLibrary("native_fn_arrays"); 12 | byte[] bytes = {1, 1, 1, 1, 1, 1, 1, 1}; 13 | long combo = JavaArrayTests.combine_bytes(bytes); 14 | if (combo != 72340172838076673L) { 15 | throw new RuntimeException("expected: 72340172838076673 got: " + combo); 16 | } 17 | byte[] broken_combo = JavaArrayTests.break_bytes(combo); 18 | if (!Arrays.equals(broken_combo, bytes)) { 19 | throw new RuntimeException("expected: " + Arrays.toString(bytes) + " got: " + Arrays.toString(broken_combo)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/type_mismatch_bad_argument_type_explicit.rs: -------------------------------------------------------------------------------- 1 | //@compile-flags: --crate-type lib 2 | use duchess::prelude::*; 3 | 4 | duchess::java_package! { 5 | package type_mismatch; 6 | 7 | public class TakesInt { 8 | public type_mismatch.TakesInt(); 9 | public void take(int); 10 | } 11 | } 12 | 13 | fn call_with_u64(i: u64) { 14 | type_mismatch::TakesInt::new().take(i).execute(); 15 | //~^ ERROR: `u64: duchess::JvmOp` is not satisfied 16 | //~| ERROR: the trait bound `u64: duchess::IntoScalar` is not satisfied 17 | } 18 | 19 | fn call_with_u32(i: u32) { 20 | type_mismatch::TakesInt::new().take(i).execute(); 21 | //~^ ERROR: `u32: duchess::JvmOp` is not satisfied 22 | //~| ERROR: the trait bound `u32: duchess::IntoScalar` is not satisfied 23 | } 24 | 25 | fn call_with_i32(i: i32) { 26 | type_mismatch::TakesInt::new().take(i).execute(); 27 | // OK 28 | } 29 | 30 | fn main() {} 31 | -------------------------------------------------------------------------------- /test-crates/README.md: -------------------------------------------------------------------------------- 1 | # Test crates 2 | 3 | This folder contains crates that test the end-to-end behavior of `duchess`. This setup makes it easy to test different ways of setting the CLASSPATH, or usages of real-world libraries whose JAR is too big to be included in the repository. 4 | 5 | The ui tests are organized into 2 directories, ui and java_ui 6 | * ui - Rust tests that test the ability of Rust initiated applications to interop with Java 7 | * java_ui - Java tests that test the ability of Java initiated applications to interop with Rust 8 | 9 | java_ui tests are run after the completion of the Rust ui tests. This allows rust tests to be add in the ui directory which are compiled into *.so files that the java tests consume. For an example of this interaction see [native_fn_callable_from_java.rs](duchess-java-tests/tests/ui/native_fn_callable_from_java.rs) and [JavaCanCallRustJavaFunction.java](duchess-java-tests/tests/java_ui/java_to_rust_greeting/JavaCanCallRustJavaFunction.java) 10 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/exceptions/ThrowExceptions.java: -------------------------------------------------------------------------------- 1 | package exceptions; 2 | 3 | public class ThrowExceptions { 4 | public static String staticStringNotNull = "notnull"; 5 | public static String nullString; 6 | 7 | public void throwRuntime() { 8 | throw new RuntimeException("something has gone horribly wrong"); 9 | } 10 | 11 | public Object nullObject() { 12 | Object a = null; 13 | return a; 14 | } 15 | 16 | public void throwExceptionWithCrashingMessage() throws MessageRetrievalException { 17 | throw new MessageRetrievalException(); 18 | } 19 | 20 | public class MessageRetrievalException extends Exception { 21 | 22 | public MessageRetrievalException() { 23 | } 24 | 25 | public String getMessage() { 26 | // Throw an exception when attempting to retrieve the message 27 | throw new RuntimeException("My exception threw an exception"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/jni_call_fn.rs: -------------------------------------------------------------------------------- 1 | /// Closure that selects the appropriate JNI method to call based on its return type. 2 | /// 3 | /// # Examples 4 | /// 5 | /// * `byte` expands to `|env| env.CallByteMethodA` 6 | #[macro_export] 7 | macro_rules! jni_call_fn { 8 | (byte) => { 9 | |env| env.CallByteMethodA 10 | }; 11 | (short) => { 12 | |env| env.CallShortMethodA 13 | }; 14 | (int) => { 15 | |env| env.CallIntMethodA 16 | }; 17 | (long) => { 18 | |env| env.CallLongMethodA 19 | }; 20 | (float) => { 21 | |env| env.CallFloatMethodA 22 | }; 23 | (double) => { 24 | |env| env.CallDoubleMethodA 25 | }; 26 | (char) => { 27 | |env| env.CallCharMethodA 28 | }; 29 | (boolean) => { 30 | |env| env.CallBooleanMethodA 31 | }; 32 | (void) => { 33 | |env| env.CallVoidMethodA 34 | }; 35 | 36 | // Reference types 37 | ($r:tt) => { 38 | |env| env.CallObjectMethodA 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /duchess-build-rs/src/code_writer.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::io::Write; 3 | 4 | pub struct CodeWriter<'w> { 5 | writer: &'w mut dyn Write, 6 | indent: usize, 7 | } 8 | 9 | impl<'w> CodeWriter<'w> { 10 | pub fn new(writer: &'w mut dyn Write) -> Self { 11 | CodeWriter { writer, indent: 0 } 12 | } 13 | 14 | pub fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> anyhow::Result<()> { 15 | let mut string = String::new(); 16 | fmt::write(&mut string, fmt).unwrap(); 17 | 18 | if string.starts_with("}") || string.starts_with(")") || string.starts_with("]") { 19 | self.indent -= 1; 20 | } 21 | 22 | write!( 23 | self.writer, 24 | "{:indent$}{}\n", 25 | "", 26 | string, 27 | indent = self.indent * 4 28 | )?; 29 | 30 | if string.ends_with("{") || string.ends_with("(") || string.ends_with("[") { 31 | self.indent += 1; 32 | } 33 | 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/java/generics/MapLike.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | import java.util.HashMap; 4 | import java.util.TreeSet; 5 | import java.util.Comparator; 6 | 7 | public class MapLike { 8 | public MapLike() { 9 | this.storage = new HashMap<>(); 10 | } 11 | 12 | private HashMap storage; 13 | 14 | public void add(TT key, Object value) { 15 | storage.put(key, value); 16 | } 17 | 18 | // Note: although Java supports shadowing generics, Duchess currently does not. 19 | public void methodGeneric(T key, Object value) { 20 | storage.put((TT) key, value); 21 | } 22 | 23 | public String toString() { 24 | StringBuilder sb = new StringBuilder(); 25 | TreeSet sortedKeys = new TreeSet(); 26 | sortedKeys.addAll(storage.keySet()); 27 | 28 | for (TT key : sortedKeys) { 29 | sb.append(key + "=" + storage.get(key) + "\n"); 30 | } 31 | return sb.toString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/jni_static_field_get_fn.rs: -------------------------------------------------------------------------------- 1 | /// Generates a closure that selects the appropriate JNI method 2 | /// to call to get a static field based on the field type. 3 | /// 4 | /// # Examples 5 | /// 6 | /// * `byte` expands to `|env| env.GetStaticByteField` 7 | #[macro_export] 8 | macro_rules! jni_static_field_get_fn { 9 | (byte) => { 10 | |env| env.GetStaticByteField 11 | }; 12 | (short) => { 13 | |env| env.GetStaticShortField 14 | }; 15 | (int) => { 16 | |env| env.GetStaticIntField 17 | }; 18 | (long) => { 19 | |env| env.GetStaticLongField 20 | }; 21 | (float) => { 22 | |env| env.GetStaticFloatField 23 | }; 24 | (double) => { 25 | |env| env.GetStaticDoubleField 26 | }; 27 | (char) => { 28 | |env| env.GetStaticCharField 29 | }; 30 | (boolean) => { 31 | |env| env.GetStaticBooleanField 32 | }; 33 | 34 | // Reference types 35 | ($r:tt) => { 36 | |env| env.GetStaticObjectField 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/jni_static_call_fn.rs: -------------------------------------------------------------------------------- 1 | /// Closure that selects the appropriate JNI method to call based on its return type. 2 | /// 3 | /// # Examples 4 | /// 5 | /// * `byte` expands to `|env| env.CallStaticByteMethodA` 6 | #[macro_export] 7 | macro_rules! jni_static_call_fn { 8 | (byte) => { 9 | |env| env.CallStaticByteMethodA 10 | }; 11 | (short) => { 12 | |env| env.CallStaticShortMethodA 13 | }; 14 | (int) => { 15 | |env| env.CallStaticIntMethodA 16 | }; 17 | (long) => { 18 | |env| env.CallStaticLongMethodA 19 | }; 20 | (float) => { 21 | |env| env.CallStaticFloatMethodA 22 | }; 23 | (double) => { 24 | |env| env.CallStaticDoubleMethodA 25 | }; 26 | (char) => { 27 | |env| env.CallStaticCharMethodA 28 | }; 29 | (boolean) => { 30 | |env| env.CallStaticBooleanMethodA 31 | }; 32 | (void) => { 33 | |env| env.CallStaticVoidMethodA 34 | }; 35 | 36 | // Reference types 37 | ($r:tt) => { 38 | |env| env.CallStaticObjectMethodA 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/native_fn_return_java_string.rs: -------------------------------------------------------------------------------- 1 | //@ run 2 | 3 | use duchess::prelude::*; 4 | 5 | duchess::java_package! { 6 | package native_greeting; 7 | 8 | public class native_greeting.Native { 9 | public native_greeting.Native(); 10 | public java.lang.String greet(java.lang.String); 11 | native java.lang.String baseGreeting(java.lang.String); 12 | } 13 | } 14 | 15 | #[duchess::java_function(native_greeting.Native::baseGreeting)] 16 | fn base_greeting( 17 | _this: &native_greeting::Native, 18 | name: Option<&java::lang::String>, 19 | ) -> duchess::Result> { 20 | name.assert_not_null().execute() 21 | } 22 | 23 | fn main() -> duchess::Result<()> { 24 | duchess::Jvm::builder() 25 | .link(base_greeting::java_fn()) 26 | .try_launch()?; 27 | 28 | let n: String = native_greeting::Native::new() 29 | .greet("Ferris") 30 | .assert_not_null() 31 | .execute() 32 | .unwrap(); 33 | 34 | assert_eq!(n, "Ferris, from Java"); 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/native_fn_return_local_java_string.rs: -------------------------------------------------------------------------------- 1 | use duchess::{java, prelude::*}; 2 | 3 | duchess::java_package! { 4 | package native_greeting; 5 | 6 | public class native_greeting.Native { 7 | public native_greeting.Native(); 8 | public java.lang.String greet(java.lang.String); 9 | native java.lang.String baseGreeting(java.lang.String); 10 | } 11 | } 12 | 13 | #[duchess::java_function(native_greeting.Native::baseGreeting)] 14 | fn base_greeting<'n>( 15 | _this: &native_greeting::Native, 16 | name: Option<&'n java::lang::String>, 17 | ) -> duchess::Result> { 18 | name.assert_not_null().execute() //~ ERROR: trait bound 19 | } 20 | 21 | fn main() -> duchess::Result<()> { 22 | duchess::Jvm::builder() 23 | .link(base_greeting::java_fn()) 24 | .try_launch()?; 25 | 26 | let n: String = native_greeting::Native::new() 27 | .greet("Ferris") 28 | .assert_not_null() 29 | .execute() 30 | .unwrap(); 31 | 32 | assert_eq!(n, "Ferris, from Java"); 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Intro](./intro.md) 4 | - [Tutorials](./tutorials.md) 5 | - [Setup](./setup.md) 6 | - [Calling Java from Rust](./call_java_from_rust.md) 7 | - [Implementing native methods](./implementing_native_methods.md) 8 | - [Memory safety requirements](./safety.md) 9 | - [Threat model](./threat_model.md) 10 | - [Reference](./reference.md) 11 | - [The `java_package` macro](./java_package.md) 12 | - [Translating Java method signatures to Rust](./java_signatures_in_rust.md) 13 | - [The `java_function` macro](./java_function.md) 14 | - [Linking native functions into the JVM](./linking_native_functions.md) 15 | - [Deriving Java/Rust conversions](./derive.md) 16 | - [JVM Operations](./jvm_operations.md) 17 | - [The `ToJava` trait](./to_java.md) 18 | - [Java/Rust type conversions](./java_rust_types.md) 19 | - [The `Jvm` type](./jvm.md) 20 | - [Local vs global object references](./local_vs_global.md) 21 | - [Internals](./internals.md) 22 | - [The `JavaObject` trait](./java_object.md) 23 | - [Upcasts](./upcasts.md) 24 | - [Methods](./methods.md) 25 | - [Logo](./logo.md) -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/native_fn_return_rust_string.rs: -------------------------------------------------------------------------------- 1 | //@ run 2 | 3 | use duchess::{java, prelude::*}; 4 | 5 | duchess::java_package! { 6 | package native_greeting; 7 | 8 | public class native_greeting.Native { 9 | public native_greeting.Native(); 10 | public java.lang.String greet(java.lang.String); 11 | native java.lang.String baseGreeting(java.lang.String); 12 | } 13 | } 14 | 15 | #[duchess::java_function(native_greeting.Native::baseGreeting)] 16 | fn base_greeting( 17 | _this: &native_greeting::Native, 18 | name: Option<&java::lang::String>, 19 | ) -> duchess::Result { 20 | let name: String = name.assert_not_null().execute()?; 21 | Ok(format!("Hello, {name}")) 22 | } 23 | 24 | fn main() -> duchess::Result<()> { 25 | duchess::Jvm::builder() 26 | .link(base_greeting::java_fn()) 27 | .try_launch()?; 28 | 29 | let n: String = native_greeting::Native::new() 30 | .greet("Ferris") 31 | .assert_not_null() 32 | .execute() 33 | .unwrap(); 34 | 35 | assert_eq!(n, "Hello, Ferris, from Java"); 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/generics.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | duchess::java_package! { 5 | package generics; 6 | 7 | public final class generics.EventKey implements generics.MapKey { 8 | public static final generics.EventKey A; 9 | public static final generics.EventKey B; 10 | } 11 | 12 | interface generics.MapKey { * } 13 | 14 | 15 | public class generics.MapLike { 16 | public generics.MapLike(); 17 | public void add(TT, java.lang.Object); 18 | public void methodGeneric(T, java.lang.Object); 19 | } 20 | } 21 | 22 | fn main() { 23 | use generics::*; 24 | let base = EventKey::get_a(); 25 | let event = MapLike::::new().execute().unwrap(); 26 | event.add(base, "value-a").execute().unwrap(); 27 | event 28 | .method_generic::(EventKey::get_b(), "value-b") 29 | .execute() 30 | .unwrap(); 31 | assert_eq!( 32 | event.to_string().execute().unwrap(), 33 | Some("A=value-a\nB=value-b\n".to_string()) 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 The LALRPOP Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /macro-rules/src/mro.rs: -------------------------------------------------------------------------------- 1 | /// Generates a reference to the op or obj struct for a given type. 2 | /// This type is named by using the associated types on the `duchess::semver_unstable::JavaView` trait. 3 | /// It expands to a recursive type that encodes the [method resolution order][mro] from the 4 | /// original source type. 5 | /// 6 | /// [mro]: https://duchess-rs.github.io/duchess/methods.html 7 | #[macro_export] 8 | macro_rules! mro { 9 | ($J:ident,$assoc_name:ident,[]) => { 10 | // The sequence terminates on `()` 11 | () 12 | }; 13 | 14 | ($J:ident,$assoc_name:ident,[$mro_ty_head:ty, $($mro_ty_tail:ty,)*]) => { 15 | // The head type is the type we are viewing our original object as 16 | // (some superclass/interface of the original type). 17 | <$mro_ty_head as duchess::semver_unstable::JavaView>::$assoc_name< 18 | // J here represents the type we are coming *from* 19 | $J, 20 | 21 | // N here represents the next type in the MRO sequence, 22 | // which is generated recursively 23 | duchess::semver_unstable::mro!($J, $assoc_name, [$($mro_ty_tail,)*]), 24 | > 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/native_fn_return_fresh_java_string.rs: -------------------------------------------------------------------------------- 1 | use duchess::{java, prelude::*}; 2 | 3 | duchess::java_package! { 4 | package native_greeting; 5 | 6 | public class native_greeting.Native { 7 | public native_greeting.Native(); 8 | public java.lang.String greet(java.lang.String); 9 | native java.lang.String baseGreeting(java.lang.String); 10 | } 11 | } 12 | 13 | #[duchess::java_function(native_greeting.Native::baseGreeting)] 14 | fn base_greeting<'n>( 15 | _this: &'n native_greeting::Native, 16 | _name: Option<&'n java::lang::String>, 17 | ) -> duchess::Result> { 18 | let v = vec!['H' as u8, 'i' as u8]; 19 | java::lang::String::new(v.to_java()).execute() //~ ERROR: trait bound 20 | } 21 | 22 | fn main() -> duchess::Result<()> { 23 | duchess::Jvm::builder() 24 | .link(base_greeting::java_fn()) 25 | .try_launch()?; 26 | 27 | let n: String = native_greeting::Native::new() 28 | .greet("Ferris") 29 | .assert_not_null() 30 | .execute() 31 | .unwrap(); 32 | 33 | assert_eq!(n, "Hi, from Java"); 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/native_fn_return_local_java_string.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `&duchess::java::lang::String: IntoRust>` is not satisfied 2 | --> tests/ui/native_fn_return_local_java_string.rs:18:10 3 | | 4 | 18 | name.execute() 5 | | ^^^^^^^ the trait `IntoRust>` is not implemented for `&duchess::java::lang::String` 6 | | 7 | = help: the trait `IntoRust` is implemented for `&duchess::java::lang::String` 8 | = help: for that trait implementation, expected `std::string::String`, found `Local<'_, duchess::java::lang::String>` 9 | note: required by a bound in `execute` 10 | --> /home/ubuntu/crates/duchess/src/jvm.rs:103:39 11 | | 12 | 101 | fn execute(self) -> crate::Result 13 | | ------- required by a bound in this associated function 14 | 102 | where 15 | 103 | for<'jvm> Self::Output<'jvm>: IntoRust, 16 | | ^^^^^^^^^^^ required by this bound in `JvmOp::execute` 17 | 18 | error: aborting due to 1 previous error 19 | 20 | For more information about this error, try `rustc --explain E0277`. 21 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/rust_ty.rs: -------------------------------------------------------------------------------- 1 | /// Convert a Java type to its corresponding Rust type. 2 | /// 3 | /// # Examples 4 | /// 5 | /// * `byte` expands to `i8` 6 | /// * `(class[java::lang::Object])` expands to `java::lang::Object` 7 | /// * `(class[java::util::List] (class[java::lang::Object]))` expands to `java::util::List` 8 | #[macro_export] 9 | macro_rules! rust_ty { 10 | // Scalar types 11 | 12 | (byte) => { 13 | i8 14 | }; 15 | (short) => { 16 | i16 17 | }; 18 | (int) => { 19 | i32 20 | }; 21 | (long) => { 22 | i64 23 | }; 24 | (float) => { 25 | f32 26 | }; 27 | (double) => { 28 | f64 29 | }; 30 | (char) => { 31 | u16 32 | }; 33 | (boolean) => { 34 | bool 35 | }; 36 | 37 | // Reference types 38 | 39 | ((class[$($path:tt)*])) => { 40 | $($path)* 41 | }; 42 | ((class[$($path:tt)*] $($args:tt)*)) => { 43 | ($($path)* < $(duchess::semver_unstable::rust_ty!($args),)* >) 44 | }; 45 | ((array $elem:tt)) => { 46 | java::Array 47 | }; 48 | ((generic $name:ident)) => { 49 | $name 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/native_fn_link_suite.rs: -------------------------------------------------------------------------------- 1 | //@ run 2 | 3 | use duchess::{java, prelude::*}; 4 | 5 | duchess::java_package! { 6 | package native_greeting; 7 | 8 | public class native_greeting.Native { 9 | public native_greeting.Native(); 10 | public java.lang.String greet(java.lang.String); 11 | native java.lang.String baseGreeting(java.lang.String); 12 | } 13 | } 14 | 15 | #[duchess::java_function(native_greeting.Native::baseGreeting)] 16 | fn base_greeting( 17 | _this: &native_greeting::Native, 18 | name: Option<&java::lang::String>, 19 | ) -> duchess::Result { 20 | let name: String = name.assert_not_null().execute()?; 21 | Ok(format!("Hello, {name}")) 22 | } 23 | 24 | fn native_functions() -> Vec { 25 | vec![base_greeting::java_fn()] 26 | } 27 | 28 | fn main() -> duchess::Result<()> { 29 | duchess::Jvm::builder() 30 | .link(native_functions()) 31 | .try_launch()?; 32 | 33 | let n: String = native_greeting::Native::new() 34 | .greet("Ferris") 35 | .assert_not_null() 36 | .execute() 37 | .unwrap(); 38 | 39 | assert_eq!(n, "Hello, Ferris, from Java"); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries/native_fn_arrays.rs: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | use duchess::prelude::*; 4 | use duchess::{java, Java, JvmOp, ToJava}; 5 | 6 | duchess::java_package! { 7 | package java_to_rust_arrays; 8 | 9 | public class JavaArrayTests { 10 | public static native long combine_bytes(byte[]); 11 | public static native byte[] break_bytes(long); 12 | } 13 | } 14 | 15 | #[duchess::java_function(java_to_rust_arrays.JavaArrayTests::combine_bytes)] 16 | fn combine_bytes(bytes: Option<&duchess::java::Array>) -> i64 { 17 | let signed_bytes: &[i8] = &*bytes.assert_not_null().execute::>().unwrap(); 18 | let unsigned_bytes = signed_bytes.iter().map(|x| *x as u8).collect::>(); 19 | return i64::from_le_bytes(unsigned_bytes.try_into().unwrap()); 20 | } 21 | 22 | #[duchess::java_function(java_to_rust_arrays.JavaArrayTests::break_bytes)] 23 | fn break_bytes(num: i64) -> duchess::Result>> { 24 | let unsigned_bytes = num.to_le_bytes(); 25 | let signed_bytes = unsigned_bytes.iter().map(|x| *x as i8).collect::>(); 26 | let java_array: Java> = signed_bytes.to_java().execute()?.unwrap(); 27 | 28 | return Ok(java_array); 29 | } 30 | -------------------------------------------------------------------------------- /tests/string.rs: -------------------------------------------------------------------------------- 1 | use duchess::prelude::*; 2 | 3 | #[test] 4 | fn to_java_and_back() { 5 | for example in [ 6 | //basic cases 7 | "", 8 | "abc\tdef", 9 | "hello from 🦀!", 10 | // various lengths 11 | "a".repeat(1).as_str(), 12 | "a".repeat(2).as_str(), 13 | "a".repeat(3).as_str(), 14 | "a".repeat(4).as_str(), 15 | "a".repeat(63).as_str(), 16 | "a".repeat(64).as_str(), 17 | "a".repeat(65).as_str(), 18 | "a".repeat(127).as_str(), 19 | "a".repeat(128).as_str(), 20 | "a".repeat(129).as_str(), 21 | "a".repeat(1024 * 1024 - 1).as_str(), 22 | "a".repeat(1024 * 1024).as_str(), 23 | "a".repeat(1024 * 1024 + 1).as_str(), 24 | // unicode code points of various UTF-8 lengths, some requiring surrogate pairs in Java 25 | "$", // 1 26 | "£", // 2 27 | "€", // 3 28 | "𐍈", // 4 29 | // combinations of various codepoint lengths 30 | "$£€𐍈€£$𐍈", 31 | // nul byte 32 | "\u{0000}", 33 | ] { 34 | let java: Java = example.to_java().assert_not_null().execute().unwrap(); 35 | let and_back: String = (&*java).execute().unwrap(); 36 | assert_eq!(example, and_back); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /macro-rules/src/java_types/prepare_input.rs: -------------------------------------------------------------------------------- 1 | /// Prepares an input from a JVM method call to be passed to JNI. 2 | /// 3 | /// Expected to be used in the `JvmOp` impl for some struct that was 4 | /// created to represent the method call; each method input is expected 5 | /// to be stored in a field whose type implements either `JvmRefOp` 6 | /// or `JvmScalarOp`, appropriately. 7 | /// 8 | /// Used like `prepare_input!(let $O = ($self.$I: $I_ty) in $jvm)`, the parameters are 9 | /// 10 | /// * `$O`: name of the local variable we will define (usually same as `$I`) 11 | /// * `$self`: the struct representing the method call 12 | /// * `$I`: the field in the struct that holds the input 13 | /// * `$I_ty`: the (Java) type of the input 14 | /// * `$jvm`: the `Jvm` instance that will be used to prepare the input 15 | #[macro_export] 16 | macro_rules! prepare_input { 17 | (let $O:ident = ($self:ident.$I:ident: $I_scalar_ty:ident) in $jvm:ident) => { 18 | let $O = $self.$I.do_jni($jvm)?; 19 | }; 20 | 21 | (let $O:ident = ($self:ident.$I:ident: $I_ref_ty:tt) in $jvm:ident) => { 22 | let $O = $self.$I.into_as_jref($jvm)?; 23 | let $O = match duchess::prelude::AsJRef::as_jref(&$I) { 24 | Ok(v) => Some(v), 25 | Err(duchess::NullJRef) => None, 26 | }; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "cargo-duchess","duchess-build-rs", "duchess-reflect", "macro-rules"] 3 | 4 | [workspace.package] 5 | version = "0.3.0" 6 | license = "Apache-2.0 OR MIT" 7 | repository = "https://github.com/duchess-rs/duchess" 8 | homepage = "https://duchess-rs.github.io/duchess/" 9 | documentation = "https://duchess-rs.github.io/duchess/rustdoc/doc/duchess/index.html" 10 | 11 | [package] 12 | name = "duchess" 13 | version.workspace = true 14 | edition = "2021" 15 | license.workspace = true 16 | repository.workspace = true 17 | description = "Silky smooth Java-Rust interop" 18 | readme = "README.md" 19 | 20 | [dependencies] 21 | derive_more = "0.99.17" 22 | duchess-macro = { path = "macro", version = "0.3.0" } 23 | duchess-macro-rules = { version = "0.3.0", path = "macro-rules" } 24 | jni-sys = "0.3.0" 25 | cesu8 = "1.1.0" 26 | once_cell = "1.17.1" 27 | thiserror = "1.0.40" 28 | tracing = "0.1.37" 29 | java-locator = { version = "0.1.3", optional = true } 30 | libloading = { version = "0.8.0", optional = true } 31 | derive-where = "1.2.1" 32 | serde = { version = "1.0.214", features = ["derive"] } 33 | 34 | [build-dependencies] 35 | duchess-build-rs = { path = "duchess-build-rs" } 36 | 37 | 38 | [features] 39 | default = ["dylibjvm"] 40 | dylibjvm = ["java-locator", "libloading"] 41 | jni_1_6 = [] 42 | jni_1_8 = [] 43 | -------------------------------------------------------------------------------- /macro-rules/src/macro_if.rs: -------------------------------------------------------------------------------- 1 | /// Used to conditionally include code. 2 | /// 3 | /// # Examples 4 | /// 5 | /// * `if [] { ... }` expands to nothing, because `[]` is empty. 6 | /// * `if [ ...1 ] { ...2 }` expands to `...2`, presuming `...1` is non-empty. 7 | /// * `if false { ... }` expands to nothing, because `[]` is empty. 8 | /// * `if true { ...2 }` expands to `...2`. 9 | /// * `if is_ref_ty(int) { ...2 }` expands to nothing. 10 | /// * `if is_ref_ty((class[java::lang::Object])) { ...2 }` expands to `...2`. 11 | #[macro_export] 12 | macro_rules! macro_if { 13 | // With `[]`, test if we have an empty input. 14 | (if [] { $($then:tt)* }) => {}; 15 | (if [$($input:tt)+] { $($then:tt)* }) => { 16 | $($then)* 17 | }; 18 | 19 | // With `false` or `true`, test a statically known boolean 20 | (if false { $($then:tt)* }) => {}; 21 | (if true { $($then:tt)* }) => { 22 | $($then)* 23 | }; 24 | 25 | // Testing what kind of type we have (scalar vs ref). 26 | // As described in [`crate::java_types`][], 27 | // scalar java types like `int` are an identifier; 28 | // reference types like `Object` are represented with a parenthesied `(...)` token tree. 29 | (if is_ref_ty($t:ident) { $($then:tt)* }) => {}; 30 | (if is_ref_ty(($($t:tt)*)) { $($then:tt)* }) => { 31 | $($then)* 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", 7 | "features": { 8 | "ghcr.io/devcontainers/features/java:1": {}, 9 | "ghcr.io/mikaello/devcontainer-features/kotlinc:1": {}, 10 | "ghcr.io/guiyomh/features/just:0": {} 11 | } 12 | 13 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 14 | // "mounts": [ 15 | // { 16 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 17 | // "target": "/usr/local/cargo", 18 | // "type": "volume" 19 | // } 20 | // ] 21 | 22 | // Features to add to the dev container. More info: https://containers.dev/features. 23 | // "features": {}, 24 | 25 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 26 | // "forwardPorts": [], 27 | 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "rustc --version", 30 | 31 | // Configure tool-specific properties. 32 | // "customizations": {}, 33 | 34 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 35 | // "remoteUser": "root" 36 | } 37 | -------------------------------------------------------------------------------- /book/src/java_signatures_in_rust.md: -------------------------------------------------------------------------------- 1 | # Translating Java method signatures to Rust 2 | 3 | The [`java_package`](./java_package.md) macro translates Java methods into Rust methods. 4 | The method argument types are translated as follows: 5 | 6 | | Java argument type | Rust argument type | 7 | | --------- | --------- | 8 | | `byte` | `impl duchess::IntoScalar` | 9 | | `short` | `impl duchess::IntoScalar` | 10 | | `int` | `impl duchess::IntoScalar` | 11 | | `long` | `impl duchess::IntoScalar` | 12 | | Java object type J | `impl duchess::IntoJava` | 13 | | e.g., `java.lang.String` | `impl duchess::IntoJava` | 14 | 15 | The Rust version of the Java method will return one of the following traits. 16 | These are not the actual Rust value, but rather the [JVM operation](./jvm_operations.md) 17 | that will yield the value when executed: 18 | 19 | | Java return type | Rust return type | 20 | | --- | --- | 21 | | `void` | `impl duchess::VoidMethod` | 22 | | `byte` | `impl duchess::ScalarMethod` | 23 | | ... | `impl duchess::ScalarMethod<...>` | 24 | | `long` | `impl duchess::ScalarMethod` | 25 | | Java object type J | `impl duchess::JavaMethod` | 26 | | e.g., `java.lang.String` | `impl duchess::JavaMethod` | 27 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/log_local_ref_and_two_calls.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | duchess::java_package! { 5 | package log; 6 | 7 | class BaseEvent { * } 8 | 9 | public class log.Builder implements log.TimeStep, log.NameStep, log.BuildStep { 10 | java.lang.String n; 11 | java.util.Date d; 12 | public log.Builder(); 13 | public log.Event build(); 14 | public log.BuildStep withName(java.lang.String); 15 | public log.NameStep withTime(java.util.Date); 16 | 17 | // FIXME: java generates two `withTime` methods, so we have to specify this 18 | // fully and comment out one of them. Not sure if this is avoidable 19 | // given the way Java does things right now. Note the distinct return types! 20 | // 21 | // public java.lang.Object withTime(java.util.Date); 22 | } 23 | 24 | class Event { * } 25 | class Logger { * } 26 | interface NameStep { * } 27 | interface TimeStep { * } 28 | interface BuildStep { * } 29 | } 30 | 31 | fn main() -> duchess::Result<()> { 32 | let logger = log::Logger::new().execute()?; 33 | let event = log::Event::builder() 34 | .with_time(java::util::Date::new()) 35 | .with_name("foo") 36 | .build() 37 | .execute()?; 38 | logger.add_event(&event).execute()?; 39 | logger.add_event(&event).execute()?; 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/try_catch.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{cast::Upcast, java::lang::Throwable, Jvm, JvmOp, Local}; 4 | 5 | #[derive_where::derive_where(Clone)] 6 | #[derive_where(Copy; This: Copy)] 7 | pub struct TryCatch 8 | where 9 | This: JvmOp, 10 | J: Upcast, 11 | { 12 | this: This, 13 | phantom: PhantomData, 14 | } 15 | 16 | impl TryCatch 17 | where 18 | This: JvmOp, 19 | J: Upcast, 20 | { 21 | pub(crate) fn new(this: This) -> Self { 22 | Self { 23 | this, 24 | phantom: PhantomData, 25 | } 26 | } 27 | } 28 | 29 | impl JvmOp for TryCatch 30 | where 31 | This: JvmOp, 32 | J: Upcast, 33 | { 34 | type Output<'jvm> = Result, Local<'jvm, J>>; 35 | 36 | fn do_jni<'jvm>(self, jvm: &mut Jvm<'jvm>) -> crate::LocalResult<'jvm, Self::Output<'jvm>> { 37 | match self.this.do_jni(jvm) { 38 | Ok(v) => Ok(Ok(v)), 39 | Err(e) => match e { 40 | crate::Error::Thrown(exception) => { 41 | if let Ok(exception) = exception.try_downcast::().do_jni(jvm)? { 42 | Ok(Err(exception)) 43 | } else { 44 | Err(crate::Error::Thrown(exception)) 45 | } 46 | } 47 | _ => Err(e), 48 | }, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries/java_rust_initiated_exceptions.rs: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | use duchess::prelude::*; 4 | 5 | duchess::java_package! { 6 | package java_rust_initiated_exceptions; 7 | 8 | public class java_rust_initiated_exceptions.JavaRustExceptions { * } 9 | } 10 | 11 | #[duchess::java_function(java_rust_initiated_exceptions.JavaRustExceptions::raiseNPE)] 12 | fn raiseNPE( 13 | this: &java_rust_initiated_exceptions::JavaRustExceptions, 14 | ) -> duchess::Result> { 15 | Err(duchess::Error::NullDeref) 16 | } 17 | 18 | #[duchess::java_function(java_rust_initiated_exceptions.JavaRustExceptions::raiseSliceTooLong)] 19 | fn raiseSliceTooLong( 20 | this: &java_rust_initiated_exceptions::JavaRustExceptions, 21 | ) -> duchess::Result> { 22 | Err(duchess::Error::SliceTooLong(5)) 23 | } 24 | 25 | #[duchess::java_function(java_rust_initiated_exceptions.JavaRustExceptions::raiseJvmInternal)] 26 | fn raiseJvmInternal( 27 | this: &java_rust_initiated_exceptions::JavaRustExceptions, 28 | ) -> duchess::Result> { 29 | Err(duchess::Error::JvmInternal("JvmInternal".to_string())) 30 | } 31 | 32 | #[duchess::java_function(java_rust_initiated_exceptions.JavaRustExceptions::panic)] 33 | fn panic( 34 | this: &java_rust_initiated_exceptions::JavaRustExceptions, 35 | ) -> duchess::Result> { 36 | panic!("RUST PANIC!"); 37 | } 38 | -------------------------------------------------------------------------------- /book/src/implementing_native_methods.md: -------------------------------------------------------------------------------- 1 | # Tutorial: implementing native methods 2 | 3 | **WARNING:** This support is not yet implemented. 4 | 5 | Duchess also supports implementing Java native methods, making it easy to call Rust code from Java. 6 | 7 | ## Setup 8 | 9 | Be sure to follow the [setup instructions](./setup.md). 10 | 11 | ## Example 12 | 13 | Given a Java class 14 | 15 | ```java 16 | package me.ferris; 17 | 18 | public class ClassWithNativeMethod { 19 | int data() { return 22; } 20 | native String compute(Object o); 21 | } 22 | ``` 23 | 24 | you can provide an implementation for `compute` like so: 25 | 26 | ```rust,ignore 27 | // First, reflect the class, as described in the "calling Java from Rust" tutorial: 28 | duchess::java_package! { 29 | package me.ferris; 30 | class ClassWithNativeMethod { * } 31 | } 32 | 33 | use duchess::{java, IntoJava}; 34 | use me::ferris::ClassWithNativeMethod; 35 | 36 | // Next, provide a decorated Rust function. 37 | // The arguments are translated from Java, including the `this`. 38 | // The return type is either a scalar or `impl IntoJava` 39 | // where `J` is the Java type. 40 | #[duchess::native(me.ferris.ClassWithNativeMethod::compute)] 41 | fn compute( 42 | this: &ClassWithNativeMethod, 43 | object: &java::lang::Object, 44 | ) -> impl IntoJava { 45 | // in here you can call back to JVM too 46 | let data = this.data().execute(); 47 | format!("Hello from Rust {data}") 48 | } 49 | ``` 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/log_one_big_call.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | duchess::java_package! { 5 | package log; 6 | 7 | class BaseEvent { * } 8 | 9 | public class log.Builder implements log.TimeStep, log.NameStep, log.BuildStep { 10 | java.lang.String n; 11 | java.util.Date d; 12 | public log.Builder(); 13 | public log.Event build(); 14 | public log.BuildStep withName(java.lang.String); 15 | public log.NameStep withTime(java.util.Date); 16 | 17 | // FIXME: java generates two `withTime` methods, so we have to specify this 18 | // fully and comment out one of them. Not sure if this is avoidable 19 | // given the way Java does things right now. Note the distinct return types! 20 | // 21 | // public java.lang.Object withTime(java.util.Date); 22 | } 23 | 24 | class Event { * } 25 | class Logger { * } 26 | interface NameStep { * } 27 | interface TimeStep { * } 28 | interface BuildStep { * } 29 | } 30 | 31 | fn main() -> duchess::Result<()> { 32 | // FIXME: conflict between interface trait (LoggerExt) and class trait (BuilderExt) 33 | 34 | log::Logger::new() 35 | .add_event( 36 | log::Event::builder() 37 | .with_time(java::util::Date::new()) 38 | .with_name("foo") 39 | .build(), 40 | ) 41 | .execute()?; 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/log.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | duchess::java_package! { 5 | package log; 6 | 7 | class BaseEvent { * } 8 | 9 | public class log.Builder implements log.TimeStep, log.NameStep, log.BuildStep { 10 | java.lang.String n; 11 | java.util.Date d; 12 | public log.Builder(); 13 | public log.Event build(); 14 | public log.BuildStep withName(java.lang.String); 15 | public log.NameStep withTime(java.util.Date); 16 | 17 | // FIXME: java generates two `withTime` methods, so we have to specify this 18 | // fully and comment out one of them. Not sure if this is avoidable 19 | // given the way Java does things right now. Note the distinct return types! 20 | // 21 | // public java.lang.Object withTime(java.util.Date); 22 | } 23 | 24 | class Event { * } 25 | class Logger { * } 26 | interface NameStep { * } 27 | interface TimeStep { * } 28 | interface BuildStep { * } 29 | } 30 | 31 | fn main() -> duchess::Result<()> { 32 | let logger = log::Logger::new().execute()?; 33 | 34 | let timestamp = java::util::Date::new(); 35 | timestamp.set_time(0i64).execute().unwrap(); 36 | 37 | logger 38 | .add_event( 39 | log::Event::builder() 40 | .with_time(timestamp) 41 | .with_name("foo") 42 | .build(), 43 | ) 44 | .execute()?; 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/native_fn_return_fresh_java_string.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `for<'a> &'a duchess::java::lang::String: IntoRust>` is not satisfied 2 | --> tests/ui/native_fn_return_fresh_java_string.rs:19:42 3 | | 4 | 19 | java::lang::String::new(v.to_java()).execute() 5 | | ^^^^^^^ the trait `for<'a> IntoRust>` is not implemented for `&'a duchess::java::lang::String`, which is required by `for<'jvm> Local<'jvm, duchess::java::lang::String>: IntoRust<_>` 6 | | 7 | = help: the trait `IntoRust` is implemented for `&duchess::java::lang::String` 8 | = help: for that trait implementation, expected `std::string::String`, found `Local<'_, duchess::java::lang::String>` 9 | = note: required for `Local<'jvm, duchess::java::lang::String>` to implement `for<'jvm> IntoRust>` 10 | note: required by a bound in `execute` 11 | --> /home/ubuntu/crates/duchess/src/jvm.rs:103:39 12 | | 13 | 101 | fn execute(self) -> crate::Result 14 | | ------- required by a bound in this associated function 15 | 102 | where 16 | 103 | for<'jvm> Self::Output<'jvm>: IntoRust, 17 | | ^^^^^^^^^^^ required by this bound in `JvmOp::execute` 18 | 19 | error: aborting due to 1 previous error 20 | 21 | For more information about this error, try `rustc --explain E0277`. 22 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/log_global_ref_and_chained_calls.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | duchess::java_package! { 5 | package log; 6 | 7 | class BaseEvent { * } 8 | 9 | public class log.Builder implements log.TimeStep, log.NameStep, log.BuildStep { 10 | java.lang.String n; 11 | java.util.Date d; 12 | public log.Builder(); 13 | public log.Event build(); 14 | public log.BuildStep withName(java.lang.String); 15 | public log.NameStep withTime(java.util.Date); 16 | 17 | // FIXME: java generates two `withTime` methods, so we have to specify this 18 | // fully and comment out one of them. Not sure if this is avoidable 19 | // given the way Java does things right now. Note the distinct return types! 20 | // 21 | // public java.lang.Object withTime(java.util.Date); 22 | } 23 | 24 | class Event { * } 25 | class Logger { * } 26 | interface NameStep { * } 27 | interface TimeStep { * } 28 | interface BuildStep { * } 29 | } 30 | 31 | fn main() -> duchess::Result<()> { 32 | // FIXME: conflict between interface trait (LoggerExt) and class trait (BuilderExt) 33 | 34 | let logger = log::Logger::new().execute()?; 35 | 36 | logger 37 | .add_event( 38 | log::Event::builder() 39 | .with_time(java::util::Date::new()) 40 | .with_name("foo") 41 | .build(), 42 | ) 43 | .execute()?; 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/log_global_ref_and_two_calls.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | duchess::java_package! { 5 | package log; 6 | 7 | class BaseEvent { * } 8 | 9 | public class log.Builder implements log.TimeStep, log.NameStep, log.BuildStep { 10 | java.lang.String n; 11 | java.util.Date d; 12 | public log.Builder(); 13 | public log.Event build(); 14 | public log.BuildStep withName(java.lang.String); 15 | public log.NameStep withTime(java.util.Date); 16 | 17 | // FIXME: java generates two `withTime` methods, so we have to specify this 18 | // fully and comment out one of them. Not sure if this is avoidable 19 | // given the way Java does things right now. Note the distinct return types! 20 | // 21 | // public java.lang.Object withTime(java.util.Date); 22 | } 23 | 24 | class Event { * } 25 | class Logger { * } 26 | interface NameStep { * } 27 | interface TimeStep { * } 28 | interface BuildStep { * } 29 | } 30 | 31 | fn main() -> duchess::Result<()> { 32 | // FIXME: conflict between interface trait (LoggerExt) and class trait (BuilderExt) 33 | 34 | let logger = log::Logger::new().execute()?; 35 | 36 | let event = log::Event::builder() 37 | .with_time(java::util::Date::new()) 38 | .with_name("foo") 39 | .build() 40 | .execute()?; 41 | logger.add_event(&event).execute()?; 42 | logger.add_event(&event).execute()?; 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/src/java_wrapper.rs: -------------------------------------------------------------------------------- 1 | // When ui_test runs the given binary, it passes the the binary the path to the test case 2 | // java expects to be given the path to the class file in Java package format 3 | // 4 | // For example, ui_test will call the java application as: 5 | // `java test/java_ui/java_test/JavaTestClass.java` 6 | // 7 | // but java expects to be invoked as 8 | // `java test.java_ui.java_test.JavaTestClass` 9 | // 10 | // This wrapper script converts the given path into the expected java format 11 | use std::env; 12 | use std::ffi::OsStr; 13 | use std::path::Path; 14 | use std::process::Command; 15 | 16 | fn main() { 17 | let mut args: Vec = env::args().collect(); 18 | args.remove(0); 19 | 20 | let path_to_java_file = args.pop().unwrap(); 21 | 22 | let path_converted_to_package_fmt = Path::new(&path_to_java_file) 23 | .strip_prefix("tests/java-to-rust/java") 24 | .unwrap() 25 | .with_extension("") 26 | .iter() 27 | .map(OsStr::to_str) 28 | .map(Option::unwrap) 29 | .collect::>() 30 | .join("."); 31 | 32 | args.push("-Xcheck:jni".to_string()); 33 | args.push(path_converted_to_package_fmt); 34 | 35 | let output = Command::new("java").args(&args).output().unwrap(); 36 | 37 | if !output.status.success() { 38 | let mut stdout: String = 39 | String::from_utf8(output.stdout).expect("Unable to parse java output"); 40 | stdout.push_str(String::from_utf8(output.stderr).unwrap().as_str()); 41 | 42 | panic!("Failed to run java {:?}: {}", args, stdout); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Running Tests 4 | Tests are split into two parts: 5 | 1. Unit tests, run by running `cargo test` in the root directory. 6 | 2. Integration tests (containing usages of the proc macro and ui-tests): `(cd test-crates && cargo test)` 7 | 8 | You can run both of these with `just test` (see [just](https://github.com/casey/just)) for more information. Just is not required to contribute but may save you a small amount of time. 9 | 10 | The [UI tests](test-crates) are currently tested against Rust 1.79.0 11 | 12 | To run a single test set the TESTNAME environment variable. For example: 13 | 14 | ``` 15 | TESTNAME=log_global_ref_and_chained_calls just test 16 | ``` 17 | 18 | ### Debugging 19 | Duchess looks for the `DUCHESS_DEBUG` environment variable during proc-macro expansion and `build.rs` execution. When this variable is set, if it is `true` or `1`, **all** generated code will be formatted and dumped to a directory. Additionally, this emits output from the buildscript as `warning` lines for cargo so they become visible. 20 | 21 | For proc macro expansion, clickable links are printed to stderr: 22 | 23 | For example: 24 | ``` 25 | file:////var/folders/20/gm3mpm1n6lj3r3tb6q2hx2_80000gr/T/.tmpjJadBD/auth_Authenticated.rs 26 | file:////var/folders/20/gm3mpm1n6lj3r3tb6q2hx2_80000gr/T/.tmpjJadBD/auth_AuthorizeRequest.rs 27 | file:////var/folders/20/gm3mpm1n6lj3r3tb6q2hx2_80000gr/T/.tmpjJadBD/auth_HttpAuth.rs 28 | ``` 29 | 30 | If you want to filter specific Java class paths, you can pass a string like `auth` or `java.lang`: 31 | ``` 32 | DUCHESS_DEBUG=java.lang 33 | ``` 34 | 35 | This will only dump debug information for these specific classes. 36 | -------------------------------------------------------------------------------- /duchess-build-rs/src/re.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::sync::OnceLock; 3 | 4 | macro_rules! declare_regex { 5 | ($name:ident() = $regex:expr) => { 6 | pub(crate) fn $name() -> &'static Regex { 7 | static STATIC: OnceLock = OnceLock::new(); 8 | STATIC.get_or_init(|| Regex::new($regex).unwrap()) 9 | } 10 | }; 11 | } 12 | 13 | declare_regex!(impl_java_interface() = r"#\[duchess::impl_java_interface\]"); 14 | 15 | declare_regex!(java_package() = r"(?m)^\s*(duchess|duchess_macro)::java_package! *\{"); 16 | 17 | declare_regex!(java_derive() = r"#\[java\(([\w.]+)(?:::\w+)?\)\]"); 18 | 19 | #[cfg(test)] 20 | mod test { 21 | #[test] 22 | fn test_java_package_regex() { 23 | assert!(super::java_package().is_match(r#" duchess_macro::java_package! { "#)); 24 | let java_file = r#" 25 | NB. in doctests, the current crate is already available as duchess. 26 | 27 | duchess_macro::java_package! { 28 | package java.lang; 29 | 30 | public class java.lang.Object { 31 | public java.lang.Object(); 32 | public native int hashCode(); 33 | public boolean equals(java.lang.Object); 34 | public java.lang.String toString(); 35 | public final native void notify(); 36 | public final native void notifyAll();"#; 37 | assert!(super::java_package().is_match(java_file)); 38 | } 39 | 40 | #[test] 41 | fn test_java_derive() { 42 | assert!(super::java_derive().is_match("#[java(java.lang.Long::decode)]")); 43 | assert!(super::java_derive().is_match("#[java(java.lang.Throwable)]")); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/examples/greeting.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::{java, prelude::*}; 3 | 4 | // Declare the java class that includes a native method 5 | duchess::java_package! { 6 | package native_greeting; 7 | 8 | public class native_greeting.Native { 9 | public native_greeting.Native(); 10 | public java.lang.String greet(java.lang.String); 11 | native java.lang.String baseGreeting(java.lang.String); 12 | } 13 | } 14 | 15 | // Implement the native method with a Rust function 16 | #[duchess::java_function(native_greeting.Native::baseGreeting)] 17 | fn base_greeting( 18 | _this: &native_greeting::Native, 19 | name: Option<&java::lang::String>, 20 | ) -> duchess::Result { 21 | let name: String = name.assert_not_null().execute()?; 22 | Ok(format!("Hello, {name}")) 23 | } 24 | 25 | fn main() -> duchess::Result<()> { 26 | // When creating the JVM, link the native method 27 | // by calling `link`. 28 | duchess::Jvm::builder() 29 | .link(base_greeting::java_fn()) 30 | .try_launch()?; 31 | 32 | // Call the `greet` method in Java; this will invoke 33 | // the native `baseGreeting` method, which will call into 34 | // the Rust function above. 35 | let n: String = native_greeting::Native::new() 36 | .greet("Ferris") 37 | .assert_not_null() 38 | .execute() 39 | .unwrap(); 40 | 41 | // Final result: 42 | // 43 | // * Java function calls Rust with "Ferris" as argument 44 | // * Rust function returns "Hello, Ferris" 45 | // * Java function appends ", from Java" 46 | assert_eq!(n, "Hello, Ferris, from Java"); 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /book/src/intro.md: -------------------------------------------------------------------------------- 1 | # Duchess: silky smooth Java integration 2 | 3 | [][Zulip] 4 | 5 | Duchess is a Rust crate that makes it [safe](./safety.md), ergonomic, and efficient to interoperate with Java code. 6 | 7 | 8 | 9 | Coverage Report 10 | 11 | 12 | ## TL;DR 13 | 14 | Duchess permits you to reflect Java classes into Rust and easily invoke methods on Java objects. For example the following Java code... 15 | 16 | ```rust,ignore 17 | Logger logger = new log.Logger(); 18 | logger.addEvent( 19 | Event.builder() 20 | .withTime(new Date()) 21 | .withName("foo") 22 | .build() 23 | ); 24 | ``` 25 | 26 | ...could be executed in Rust as follows: 27 | 28 | ```rust,ignore 29 | let logger: Java = log::Logger::new().execute()?; 30 | logger 31 | .add_event( 32 | log::Event::builder() 33 | .with_time(java::util::Date::new()) 34 | .with_name("foo") 35 | .build(), 36 | ) 37 | .execute()?; 38 | ``` 39 | 40 | ## Curious to learn more? 41 | 42 | Check out the... 43 | 44 | * [Rustdoc](./rustdoc/doc/duchess/index.html) 45 | * The [examples](https://github.com/duchess-rs/duchess/tree/main/test-crates/duchess-java-tests/tests/ui/examples) directory 46 | * The [tutorials](./tutorials.md) chapter 47 | 48 | ## Curious to get involved? 49 | 50 | Look for [issues tagged with good first issue][] and join the [Zulip][]. 51 | 52 | [issues tagged with good first issue]: https://github.com/duchess-rs/duchess/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 53 | [Zulip]: https://duchess.zulipchat.com/ 54 | -------------------------------------------------------------------------------- /book/src/jvm.md: -------------------------------------------------------------------------------- 1 | # The Jvm type 2 | 3 | The `Jvm` type represents a running Java Virtual Machine (JVM). It is mostly used to `execute` [JVM operations], but it also has some methods for interacting with the JVM that you may find useful. The way you get access to a `Jvm` instance depends on the language of the primary application: 4 | 5 | * If your main process is **Rust**, then use `Jvm::with` to start the global JVM instance. 6 | * If your main process is **Java**, then when your Rust code is invoked via JNI, you will be given a `Jvm` instance. 7 | 8 | [JVM operations]: ./jvm_operations.md 9 | 10 | ## Starting multiple JVMs 11 | 12 | As long as a thread has access to a `Jvm`, either by invoking `Jvm::with` or by getting called via JNI, you cannot get access to another one. Invoking `Jvm::with` on a thread that already has access to a Jvm is an error. This is required to ensure safety, because it allows us to be sure that mutably borrowing a `Jvm` instance blocks the thread from performing other `Jvm` operations until that borrow is complete. Sequential invocations of `Jvm::with` are allowed and will all be attached to that same underlying JVM instance. 13 | 14 | Multiple threads can invoke `Jvm::with`, but only one underlying JVM can ever be active at a time. If multiple threads invoke `Jvm::with`, one of them will succeed in starting the JVM, and the others will be attached to that same underlying JVM instance as additional active threads. 15 | 16 | ## Starting the JVM: setting options 17 | 18 | When you start the JVM from your Rust code, you can set various options by using the jvm builder: 19 | 20 | ```rust,ignore 21 | Jvm::builder() 22 | .add_classpath("foo") 23 | .add_classpath("bar") 24 | .memory() 25 | .custom("-X foobar") 26 | .launch_or_use_existing() 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/autobless.yml: -------------------------------------------------------------------------------- 1 | name: autobless 2 | 3 | on: 4 | schedule: 5 | - cron: '44 4 * * *' # At 4:44 UTC every day. 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | java-version: ['21'] 17 | fail-fast: false 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-java@v3 23 | with: 24 | distribution: 'corretto' 25 | java-version: ${{ matrix.java-version }} 26 | - name: Build 27 | run: cargo build --verbose 28 | - name: Test crates, JNI 1.8 29 | run: cargo test --all-targets --verbose --features jni_1_8 30 | - name: Test crates, JNI 1.6 31 | run: cargo test --all-targets --verbose --features jni_1_6 32 | - name: setup bot git name and email 33 | run: | 34 | git config --global user.name 'The Duchess Cronjob Bot' 35 | git config --global user.email 'duchess@cron.bot' 36 | - name: Push changes to a branch 37 | run: | 38 | BRANCH="bless-$(date -u +%Y-%m-%d)" 39 | git switch -c $BRANCH 40 | git push -u origin $BRANCH 41 | - name: Create Pull Request 42 | run: | 43 | BRANCH="bless-$(date -u +%Y-%m-%d)" 44 | MAIN_SHA=$(git rev-parse main) 45 | BRANCH_SHA=$(git rev-parse $BRANCH) 46 | 47 | # Check if the commit hashes are different 48 | if [ "$MAIN_SHA" != "$BRANCH_SHA" ]; then 49 | gh pr create -B main --title 'Automatic bless' --body '' 50 | else 51 | echo "No changes between main and $BRANCH, skipping pull request creation." 52 | fi 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | DUCHESS_BLESS: 1 56 | -------------------------------------------------------------------------------- /book/src/upcasts.md: -------------------------------------------------------------------------------- 1 | # Upcasts 2 | 3 | The `Upcast` trait encodes extends / implements relations between classes and interfaces. 4 | It is implemented both for direct supertypes as well as indirect (transitive) ones. 5 | For example, if you have this Java class: 6 | 7 | ```java 8 | class Foo extends Bar { } 9 | class Bar implements Serializable { } 10 | ``` 11 | 12 | then the `Foo` type in Rust would have several `Upcast` impls: 13 | 14 | * `Foo: Upcast` -- because `Foo` extends `Bar` 15 | * `Foo: Upcast` -- because `Bar` implements `Serializable` 16 | * `Foo: Upcast` -- because `Bar` extends `Object` 17 | 18 | There is however one caveat. 19 | We can only inspect the tokens presented to us. 20 | And, while we could reflect on the Java classes directly, 21 | we don't know what subset of the supertypes the user has chosen to reflect into Rust. 22 | Therefore, we stop our transitive upcasts at the "water's edge" -- 23 | i.e., at the point where we encounter classes that are outside our package. 24 | 25 | ## Computing transitive upcasts 26 | 27 | Transitive upcasts are computed in `upcasts.rs`. 28 | The process is very simple. 29 | A map is seeded with each type `C` that we know about along its direct upcasts. 30 | So, for the example above, this map would initially contain: 31 | 32 | * `Foo => {Bar, Object}` 33 | * `Bar => {Serializable, Object}` 34 | 35 | we then iterate over the map and grow the entry for each class `C` with the supertypes of each class `D` that is extended by `C`. 36 | So, for the example above, we would iterate to `Foo`, fetch the superclasses of `Bar`, and then union the into the set for `Foo`. 37 | 38 | ## Substitution 39 | 40 | One caveat on the above is that we have to account for substitution. 41 | If `Foo` extends `Baz`, then we substitute `X` for the generic parameter of `Baz`. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 (July 22nd, 2024) 2 | This release contains many improvements for calling Rust code from Java: 3 | 1. Add support for returning scalars (#181) 4 | 2. Allow specifying a minimum JNI version (#180) 5 | 6 | **Breaking changes**: 7 | 1. `class` or `interface` when specified in `java_package` must actually match (#168). If you get an error after upgrading, change the keyword in your `java_package` macro to match the actual type in Java. 8 | 2. A `duchess-reflect` crate has also been split out from the macro package. 9 | 10 | **Bug fixes**: 11 | * Fix bug where passing `None` for `Option` resulted in a spurious error from Duchess (#182). 12 | 13 | # 0.2.1 (June 4th, 2024) 14 | * Add `JMX` APIs to Java prelude. These allow querying the current memory usage of the JVM. 15 | 16 | # 0.2 (May 17th, 2024) 17 | This release contains several breaking changes to be aware of: 18 | 1. The public API has been simplfied: Duchess references are now "global" references by default. The `to_rust`, `global`, and `execute` combinators have all been merged. You now invoke `execute` and then the result depends on the return value: returning a `Java` will create a global reference (matching the previous behavior of `global`), and returning a Rust value like `String` will invoke the "to rust" conversion (like `to_rust` used to do). For context and examples of upgrading see https://github.com/duchess-rs/duchess/pull/147. 19 | 20 | 2. `Jvm::with` has been removed. You can no longer obtain explicit handles to the JVM, preventing panics due to nested `Jvm::with` invocations. For context and examples see https://github.com/duchess-rs/duchess/pull/147. 21 | 22 | 3. `JvmOp`, the type returned by most Duchess operations-in-progress is now `#[must_use]`. If you encounter this error in your code, note that the code as written had no effect. `JvmOp` does nothing unless `.execute()` is called. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Duchess: silky smooth Java integration 2 | 3 | [][Zulip] 4 | [][Coverage] 5 | 6 | Duchess is a Rust crate that makes it easy, ergonomic, and efficient to interoperate with Java code. 7 | 8 | 9 | 10 | 11 | ## TL;DR 12 | 13 | Duchess permits you to reflect Java classes into Rust and easily invoke methods on Java objects. For example the following Java code... 14 | 15 | ```rust 16 | Logger logger = new log.Logger(); 17 | logger.addEvent( 18 | Event.builder() 19 | .withTime(new Date()) 20 | .withName("foo") 21 | .build() 22 | ); 23 | ``` 24 | 25 | ...could be executed in Rust as follows: 26 | 27 | ```rust 28 | let logger = log::Logger::new().execute()?; 29 | logger 30 | .add_event( 31 | log::Event::builder() 32 | .with_time(java::util::Date::new()) 33 | .with_name("foo") 34 | .build(), 35 | ) 36 | .execute()?; 37 | ``` 38 | 39 | ## Curious to learn more? 40 | 41 | Check out the... 42 | 43 | * The [examples](https://github.com/duchess-rs/duchess/tree/main/test-crates/duchess-java-tests/tests) 44 | * The [tutorials](https://duchess-rs.github.io/duchess/tutorials.html) chapter 45 | 46 | ## Curious to get involved? 47 | 48 | Look for [issues tagged with good first issue][] and join the [Zulip][]. For more information on how to develop duchess, 49 | see [Contributing][]. You may also be able to improve test [coverage]. 50 | 51 | [issues tagged with good first issue]: https://github.com/duchess-rs/duchess/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 52 | [Zulip]: https://duchess.zulipchat.com/ 53 | [Contributing]: CONTRIBUTING.md 54 | [Coverage]: https://duchess-rs.github.io/duchess/coverage 55 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/to_java_java_refs.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | 3 | #![allow(dead_code)] 4 | 5 | use duchess::prelude::*; 6 | use duchess::Local; 7 | use java::lang::String as JavaString; 8 | use java::util::ArrayList as JavaList; 9 | 10 | // Test that `to_java` can accomodate a Rust vector of (local) Java objects 11 | // and produce a Java list of Java objects. 12 | fn produce_from_local_rust_vec(r: &Vec>) { 13 | let _data: Option>> = 14 | r.to_java::>().execute().unwrap(); 15 | } 16 | 17 | // Test that `to_java` can accomodate a Rust vector of (global) Java objects 18 | // and produce a Java list of Java objects. 19 | fn produce_from_global_rust_vec(r: &Vec>) { 20 | let _data: Option>> = 21 | r.to_java::>().execute().unwrap(); 22 | } 23 | 24 | // Test that `to_java` can accomodate a global Java object. 25 | fn produce_from_global_object(r: Java) { 26 | let _data: Option> = r.to_java::().execute().unwrap(); 27 | } 28 | 29 | // Test that `to_java` can accomodate a local Java object. 30 | fn produce_from_local_object(r: Local<'_, JavaString>) { 31 | let _data: Option> = r.to_java::().execute().unwrap(); 32 | } 33 | 34 | // Test that `to_java` can accomodate an optional local Java object. 35 | fn produce_from_optlocal_object(r: Option>) { 36 | let _data: Option> = r.to_java::().execute().unwrap(); 37 | } 38 | 39 | // Test that `to_java` can accomodate a ref to an optional local Java object. 40 | fn produce_from_optlocal_object_ref(r: &Option>) { 41 | let _data: Option> = r.to_java::().execute().unwrap(); 42 | } 43 | 44 | fn main() {} 45 | -------------------------------------------------------------------------------- /book/src/safety.md: -------------------------------------------------------------------------------- 1 | # Memory safety requirements 2 | 3 | Duchess provides a **safe abstraction** atop the [Java Native Interface (JNI)][jni]. 4 | This means that, as long as you are using Duchess to interact with the JVM, 5 | you cannot cause memory unsafety. 6 | However, there are edge cases that can "void" this guarantee and which Duchess cannot control. 7 | 8 | [jni]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html 9 | 10 | ## Memory safety requirements 11 | 12 | Duchess will guarantee memory safety within your crate, but there are two conditions that it cannot by itself guarantee: 13 | * Duchess does not "sandbox" or "make safe" the Java code it executes. You MUST ensure that Java code being invoked is safe and trusted. You MUST NOT invoke untrusted Java code with Duchess. 14 | * **You SHOULD with the same Java class files that you will use when you deploy:** 15 | * We believe that no loss of memory-safety is possible if incorrect `.class` files are loaded, however, if interfaces change you will experience failures at runtime. 16 | * **You must be careful when mixing Duchess with other Rust JNI libraries:** (e.g., the [jni crate](https://crates.io/crates/jni) or [robusta_jni](https://crates.io/crates/robusta_jni)) 17 | * For the most part, interop between Duchess and other JNI crates should be no problem. But there are some particular things that can cause issues: 18 | * The JVM cannot be safely started from multiple threads at once. 19 | Duchess uses a lock to avoid contending with itself but we cannot protect from other libraries starting the JVM in parallel with us. 20 | It is generally best to start the JVM yourself (via any means) in the `main` function or some other central place so that you are guaranteed it happens once and exactly once. 21 | Duchess should work just fine if the JVM has been started by another crate. 22 | -------------------------------------------------------------------------------- /test-crates/viper/tests/test.rs: -------------------------------------------------------------------------------- 1 | use duchess::prelude::*; 2 | use java::lang::{Object, String}; 3 | use viper::scala::collection::immutable::Seq; 4 | use viper::scala::collection::mutable::ArrayBuffer; 5 | use viper::scala::Tuple2; 6 | use viper::viper::carbon::CarbonVerifier; 7 | use viper::viper::silicon::Silicon; 8 | use viper::viper::silver::ast; 9 | use viper::viper::silver::reporter::NoopReporter__; 10 | 11 | /// Builds an empty Scala Seq of the specified type 12 | fn empty_scala_seq() -> impl IntoJava> { 13 | ArrayBuffer::new().to_seq().assert_not_null() 14 | } 15 | 16 | #[test] 17 | fn test_program_construction() -> duchess::Result<()> { 18 | let domains = empty_scala_seq::(); 19 | let fields = empty_scala_seq::(); 20 | let functions = empty_scala_seq::(); 21 | let predicates = empty_scala_seq::(); 22 | let methods = empty_scala_seq::(); 23 | let extension_member = empty_scala_seq::(); 24 | let position = ast::NoPosition__::get_module(); 25 | let info = ast::NoInfo__::get_module(); 26 | let error_trafo = ast::NoTrafos__::get_module(); 27 | let _program = ast::Program::new( 28 | domains, 29 | fields, 30 | functions, 31 | predicates, 32 | methods, 33 | extension_member, 34 | position, 35 | info, 36 | error_trafo, 37 | ) 38 | .execute()?; 39 | Ok(()) 40 | } 41 | 42 | #[test] 43 | fn test_carbon_construction() -> duchess::Result<()> { 44 | let reporter = NoopReporter__::get_module(); 45 | let debug_info = empty_scala_seq::>(); 46 | let _carbon = CarbonVerifier::new(reporter, debug_info).execute()?; 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn test_silicon_construction() -> duchess::Result<()> { 52 | let _silicon = Silicon::new().execute()?; 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use argument::{DuchessDeclaration, MethodSelector}; 2 | use config::Configuration; 3 | use parse::Parser; 4 | use proc_macro::TokenStream; 5 | 6 | use duchess_reflect::*; 7 | 8 | mod derive; 9 | mod java_function; 10 | 11 | /// The main duchess macro, used like so 12 | /// 13 | /// ```rust,ignore 14 | /// duchess::java_package! { 15 | /// package some.pkg.name; 16 | /// class SomeDotId { * } 17 | /// } 18 | /// ``` 19 | /// 20 | /// see the tutorial in the [duchess book] for more info. 21 | /// 22 | /// [duchess book]: https://nikomatsakis.github.io/duchess/ 23 | #[proc_macro] 24 | pub fn java_package(input: TokenStream) -> TokenStream { 25 | let input: proc_macro2::TokenStream = input.into(); 26 | let decl = match Parser::from(input).parse::() { 27 | Ok(decl) => decl, 28 | Err(err) => return err.to_compile_error().into(), 29 | }; 30 | 31 | match decl.to_tokens(&Configuration::default()) { 32 | Ok(t) => return t.into(), 33 | Err(e) => return e.into_compile_error().into(), 34 | } 35 | } 36 | 37 | #[proc_macro_attribute] 38 | pub fn java_function(args: TokenStream, input: TokenStream) -> TokenStream { 39 | let args: proc_macro2::TokenStream = args.into(); 40 | let args = match Parser::from(args).parse::() { 41 | Ok(decl) => decl, 42 | Err(err) => return err.to_compile_error().into(), 43 | }; 44 | 45 | let item_fn = match syn::parse::(input) { 46 | Ok(item_fn) => item_fn, 47 | Err(err) => return err.into_compile_error().into(), 48 | }; 49 | 50 | match java_function::java_function(args, item_fn) { 51 | Ok(t) => t.into(), 52 | Err(err) => err.into_compile_error().into(), 53 | } 54 | } 55 | 56 | synstructure::decl_derive!([ToRust, attributes(java)] => derive::derive_to_rust); 57 | 58 | synstructure::decl_derive!([ToJava, attributes(java)] => derive::derive_to_java); 59 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries/native_fn_echos_int.rs: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | use duchess::prelude::*; 3 | 4 | duchess::java_package! { 5 | package java_rust_scalars; 6 | 7 | public class JavaRustScalars { 8 | native int echoInt(int); 9 | native long echoLong(long); 10 | native double echoDouble(double); 11 | native byte echoByte(byte); 12 | native short echoShort(short); 13 | native float echoFloat(float); 14 | native char echoChar(char); 15 | } 16 | } 17 | 18 | #[duchess::java_function(java_rust_scalars.JavaRustScalars::echoInt)] 19 | fn echo_int(_this: &java_rust_scalars::JavaRustScalars, input: i32) -> duchess::Result { 20 | Ok(input) 21 | } 22 | 23 | #[duchess::java_function(java_rust_scalars.JavaRustScalars::echoLong)] 24 | fn echo_long(_this: &java_rust_scalars::JavaRustScalars, input: i64) -> duchess::Result { 25 | Ok(input) 26 | } 27 | 28 | #[duchess::java_function(java_rust_scalars.JavaRustScalars::echoDouble)] 29 | fn echo_double(_this: &java_rust_scalars::JavaRustScalars, input: f64) -> duchess::Result { 30 | Ok(input) 31 | } 32 | 33 | #[duchess::java_function(java_rust_scalars.JavaRustScalars::echoByte)] 34 | fn echo_byte(_this: &java_rust_scalars::JavaRustScalars, input: i8) -> duchess::Result { 35 | Ok(input) 36 | } 37 | 38 | #[duchess::java_function(java_rust_scalars.JavaRustScalars::echoShort)] 39 | fn echo_short(_this: &java_rust_scalars::JavaRustScalars, input: i16) -> duchess::Result { 40 | Ok(input) 41 | } 42 | 43 | #[duchess::java_function(java_rust_scalars.JavaRustScalars::echoFloat)] 44 | fn echo_float(_this: &java_rust_scalars::JavaRustScalars, input: f32) -> duchess::Result { 45 | Ok(input) 46 | } 47 | 48 | #[duchess::java_function(java_rust_scalars.JavaRustScalars::echoChar)] 49 | fn echo_char(_this: &java_rust_scalars::JavaRustScalars, input: u16) -> duchess::Result { 50 | Ok(input) 51 | } 52 | -------------------------------------------------------------------------------- /duchess-reflect/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use rust_format::Formatter as _; 4 | 5 | pub mod argument; 6 | pub mod check; 7 | pub mod class_info; 8 | pub mod codegen; 9 | pub mod config; 10 | pub mod parse; 11 | pub mod reflect; 12 | pub mod signature; 13 | pub mod substitution; 14 | pub mod upcasts; 15 | 16 | lazy_static::lazy_static! { 17 | pub static ref DEBUG_DIR: PathBuf = { 18 | let tmp_dir = tempfile::TempDir::new().expect("failed to create temp directory"); 19 | tmp_dir.into_path() 20 | }; 21 | } 22 | 23 | pub fn debug_tokens(name: impl std::fmt::Display, token_stream: &proc_macro2::TokenStream) { 24 | let Ok(debug_filter) = std::env::var("DUCHESS_DEBUG") else { 25 | return; 26 | }; 27 | let name = name.to_string(); 28 | let debug_enabled = match debug_filter { 29 | f if f.eq_ignore_ascii_case("true") || f.eq_ignore_ascii_case("1") => true, 30 | filter => name.starts_with(&filter), 31 | }; 32 | if debug_enabled { 33 | let path: PathBuf = DEBUG_DIR.join(name.replace('.', "_")).with_extension("rs"); 34 | match rust_format::RustFmt::default().format_tokens(token_stream.clone()) { 35 | Ok(formatted_tokens) => { 36 | std::fs::write(&path, formatted_tokens).expect("failed to write to debug file"); 37 | } 38 | Err(_) => { 39 | std::fs::write(&path, format!("{token_stream:?}")) 40 | .expect("failed to write to debug file"); 41 | } 42 | } 43 | // in JetBrains terminal, links are only clickable with a `file:///` prefix. But in VsCode 44 | // iTerm, and most other terminals, they are only clickable if they are absolute paths. 45 | if running_in_jetbrains() { 46 | eprintln!("file:///{}", path.display()) 47 | } else { 48 | eprintln!("{}", path.display()) 49 | } 50 | } 51 | } 52 | 53 | fn running_in_jetbrains() -> bool { 54 | std::env::var("TERMINAL_EMULATOR") 55 | .map(|var| var.contains("JetBrains")) 56 | .unwrap_or_default() 57 | } 58 | -------------------------------------------------------------------------------- /cargo-duchess/src/init.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process::Command}; 2 | 3 | use structopt::StructOpt; 4 | 5 | #[derive(StructOpt, Debug)] 6 | pub struct InitOptions { 7 | /// Directory of the project 8 | #[structopt(short, long, default_value = ".")] 9 | dir: PathBuf, 10 | } 11 | 12 | const DEFAULT_BUILD_RS: &str = " 13 | // This file is automatically generated by `cargo duchess init`. 14 | use duchess_build_rs::DuchessBuildRs; 15 | 16 | fn main() -> anyhow::Result<()> { 17 | DuchessBuildRs::new().execute() 18 | } 19 | "; 20 | 21 | const ONE_LINE: &str = "duchess_build_rs::DuchessBuildRs::new().execute().unwrap()"; 22 | 23 | pub fn init(options: InitOptions) -> anyhow::Result<()> { 24 | let InitOptions { dir } = options; 25 | 26 | if !dir.exists() { 27 | anyhow::bail!("directory `{}` not found", dir.display()); 28 | } 29 | 30 | if !dir.is_dir() { 31 | anyhow::bail!("`{}` is not a directory", dir.display()); 32 | } 33 | 34 | let cargo_toml_path = dir.join("Cargo.toml"); 35 | if !cargo_toml_path.exists() { 36 | anyhow::bail!( 37 | "directory `{}` does not contain a `Cargo.toml`", 38 | dir.display() 39 | ); 40 | } 41 | 42 | Command::new("cargo") 43 | .arg("add") 44 | .arg("duchess") 45 | .current_dir(&dir) 46 | .spawn()? 47 | .wait()?; 48 | 49 | Command::new("cargo") 50 | .arg("add") 51 | .arg("--build") 52 | .arg("duchess-build-rs") 53 | .current_dir(&dir) 54 | .spawn()? 55 | .wait()?; 56 | 57 | // If `build.rs` does not exist, initialize it with default. 58 | let build_rs_path = dir.join("build.rs"); 59 | if !build_rs_path.exists() { 60 | std::fs::write(&build_rs_path, DEFAULT_BUILD_RS)?; 61 | } else { 62 | // Tell user to add it themselves 63 | // (FIXME: use syn to insert it) 64 | eprintln!("Warning: `build.rs` already exists. Please add the following line to `main`:"); 65 | eprintln!(); 66 | eprintln!("```rust"); 67 | eprintln!("{}", ONE_LINE); 68 | eprintln!("```"); 69 | } 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src/link.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::CString, ptr::NonNull}; 2 | 3 | use crate::{java::lang::Class, Jvm, Local}; 4 | 5 | pub struct JavaFunction { 6 | pub(crate) name: CString, 7 | pub(crate) signature: CString, 8 | pub(crate) pointer: NonNull<()>, 9 | pub(crate) class_fn: ClassFn, 10 | } 11 | 12 | pub type ClassFn = 13 | for<'jvm> fn(jvm: &mut Jvm<'jvm>) -> crate::LocalResult<'jvm, Local<'jvm, Class>>; 14 | 15 | impl JavaFunction { 16 | /// Create a new `JavaFunction` value with an appropriate name, signature, and function pointer. 17 | /// Don't call this directly. Instead, use [the `#[java_function]` decorator][java_fn]. 18 | /// 19 | /// [java_fn]: https://duchess-rs.github.io/duchess/java_function.html 20 | /// 21 | /// # Panic 22 | /// 23 | /// Panics if `name` or `signature` contain nul values or cannot be converted into C strings. 24 | /// 25 | /// # Unsafe 26 | /// 27 | /// This function is unsafe because these values will be supplied to the JVM's 28 | /// [`RegisterNatives`](https://docs.oracle.com/en/java/javase/12/docs/specs/jni/functions.html#registernatives) 29 | /// function. If they are incorrect, undefined behavior will occur. 30 | pub unsafe fn new( 31 | name: &str, 32 | signature: &str, 33 | pointer: NonNull<()>, 34 | class_fn: ClassFn, 35 | ) -> Self { 36 | Self { 37 | name: CString::new(name).unwrap(), 38 | signature: CString::new(signature).unwrap(), 39 | pointer, 40 | class_fn, 41 | } 42 | } 43 | } 44 | 45 | /// Create a `JavaFunction` that can be linked into the JVM. 46 | /// Implemented by [the `#[java_function]` decorator][java_fn]. 47 | /// 48 | /// [java_fn]: https://duchess-rs.github.io/duchess/java_function.html 49 | pub trait JavaFn { 50 | fn java_fn() -> JavaFunction; 51 | } 52 | 53 | pub trait IntoJavaFns { 54 | fn into_java_fns(self) -> Vec; 55 | } 56 | 57 | impl IntoJavaFns for JavaFunction { 58 | fn into_java_fns(self) -> Vec { 59 | vec![self] 60 | } 61 | } 62 | 63 | impl IntoJavaFns for Vec { 64 | fn into_java_fns(self) -> Vec { 65 | self 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/java/java_rust_scalars/JavaRustScalars.java: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | package java_rust_scalars; 3 | 4 | public class JavaRustScalars { 5 | native int echoInt(int i); 6 | native long echoLong(long l); 7 | native double echoDouble(double d); 8 | native byte echoByte(byte b); 9 | native short echoShort(short s); 10 | native float echoFloat(float f); 11 | native char echoChar(char c); 12 | 13 | public static void main(String[] args) { 14 | System.loadLibrary("native_fn_echos_int"); 15 | JavaRustScalars sut = new JavaRustScalars(); 16 | int expectedInt = -123456; 17 | int i = sut.echoInt(expectedInt); 18 | if (i != expectedInt) { 19 | throw new RuntimeException("expected: " + expectedInt + " got: " + i); 20 | } 21 | 22 | long expectedLong = 99L; 23 | long l = sut.echoLong(expectedLong); 24 | if (l != expectedLong) { 25 | throw new RuntimeException("expected: " + expectedLong + " got: " + l); 26 | } 27 | 28 | double expectedDouble = 123.4; 29 | double d = sut.echoDouble(expectedDouble); 30 | if (d != expectedDouble) { 31 | throw new RuntimeException("expected: " + expectedDouble + " got: " + d); 32 | } 33 | 34 | byte expectedByte = 5; 35 | byte b = sut.echoByte(expectedByte); 36 | if (b != expectedByte) { 37 | throw new RuntimeException("expected: " + expectedByte + " got: " + b); 38 | } 39 | 40 | short expectedShort = 5; 41 | short s = sut.echoShort(expectedShort); 42 | if (s != expectedShort) { 43 | throw new RuntimeException("expected: " + expectedShort + " got: " + s); 44 | } 45 | 46 | float expectedFloat = 5.5f; 47 | float f = sut.echoFloat(expectedFloat); 48 | if (f != expectedFloat) { 49 | throw new RuntimeException("expected: " + expectedFloat + " got: " + f); 50 | } 51 | 52 | char expectedChar = 'a'; 53 | char c = sut.echoChar(expectedChar); 54 | if (c != expectedChar) { 55 | throw new RuntimeException("expected: " + expectedChar + " got: " + c); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /book/src/to_java.md: -------------------------------------------------------------------------------- 1 | # The `ToJava` trait 2 | 3 | The `ToJava` trait is part of the Duchess prelude. 4 | It defines an `&self` method `to_java` that can be used to convert Rust values into Java objects; 5 | if those Rust types are references to a Java object, then the result is just an identity operation. 6 | The result of `to_java` is not the Java itself but rather a [`JvmOp`] that produces the Java object. 7 | 8 | [`JvmOp`]: ./jvm_op.md 9 | 10 | In some cases, the same Rust type can be converted into multiple Java types. 11 | For example, a Rust Vec can be converted into a Java `ArrayList` but also a Java `List` or `Vector`. 12 | The `to_java` method takes a type parameter for these cases that can be specified with turbofish, 13 | e.g., `vec.to_java::>()`. 14 | 15 | ## Examples 16 | 17 | ### `String` 18 | 19 | The Rust `String` type converts to the Java string type. 20 | One could compute the Java `hashCode` for a string as follows: 21 | 22 | ```rust,ignore 23 | use duchess::prelude::*; 24 | use duchess::java; 25 | 26 | let data = format!("Hello, Duchess!"); 27 | let hash_code: i32 = 28 | data.to_java::() // Returns a `JvmOp` producing a `java::lang::String` 29 | .hash_code() // Returns a `JvmOp` invoking `hashCode` on this string 30 | .execute()?; // Execute the jvmop 31 | ``` 32 | 33 | ### `Java` 34 | 35 | Converting a Rust reference to a Java object, such as a `Global` reference, is an identity operation. 36 | 37 | ```rust,ignore 38 | use duchess::prelude::*; 39 | use duchess::java; 40 | 41 | // Produce a Global reference from a Rust string 42 | let data: Java = 43 | format!("Hello, Duchess!").execute()?; 44 | 45 | // Invoke `to_java` on the `Global` reference 46 | let hashCode: i32 = 47 | data.to_java::() // Returns a `JvmOp` producing a `java::lang::String` 48 | .hashCode() // Returns a `JvmOp` invoking `hashCode` on this string 49 | .execute()?; // Execute the jvmop 50 | ``` 51 | 52 | ## Deriving `ToJava` for your own types 53 | 54 | Duchess provides a derive for `ToJava` that you can apply to structs or enums. 55 | Details can be found in the [dedicated book section covering derive](./derive.md). -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main", "release-*" ] 6 | pull_request: 7 | branches: [ "main", "release-*" ] 8 | 9 | # Allow one instance of this workflow per pull request, and cancel older runs when new changes are pushed 10 | concurrency: 11 | group: ci-yaml-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | 17 | jobs: 18 | build: 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04-arm] 22 | java-version: ['17', '21'] 23 | jni-version: ['1_6', '1_8'] 24 | fail-fast: false 25 | runs-on: ${{ matrix.os }} 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-java@v3 30 | with: 31 | distribution: 'corretto' 32 | java-version: ${{ matrix.java-version }} 33 | - name: Install mdBook 34 | if: matrix.os == 'ubuntu-latest' 35 | run: curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.28/mdbook-v0.4.28-x86_64-unknown-linux-gnu.tar.gz | tar -xz 36 | - name: Build 37 | run: cargo build --verbose 38 | - name: Test crates, jni_${{ matrix.jni-version }} 39 | run: cargo test --all-targets --verbose --features jni_${{ matrix.jni-version }} 40 | - name: Test client crates, jni_${{ matrix.jni-version }} 41 | run: cargo test --all-targets --verbose --manifest-path=test-crates/Cargo.toml --features jni_${{ matrix.jni-version }} 42 | - name: Test book 43 | if: matrix.os == 'ubuntu-latest' 44 | run: ./mdbook test book 45 | 46 | coverage: 47 | runs-on: 'ubuntu-latest' 48 | steps: 49 | - uses: actions/checkout@v3 50 | - uses: actions/setup-java@v3 51 | with: 52 | distribution: 'corretto' 53 | java-version: 17 54 | - uses: Swatinem/rust-cache@v2 55 | - name: install just 56 | run: | 57 | cargo install just 58 | - name: coverage-deps 59 | run: just coverage-tools 60 | - name: 61 | run: just coverage 62 | env: 63 | NO_OPEN: true 64 | - name: Upload Coverage Report 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: ui-coverage-report 68 | path: target/ui-coverage-report/ 69 | -------------------------------------------------------------------------------- /duchess-reflect/src/substitution.rs: -------------------------------------------------------------------------------- 1 | use std::iter::FromIterator; 2 | use std::{collections::BTreeMap, sync::Arc}; 3 | 4 | use crate::class_info::{ClassRef, Id, RefType, Type}; 5 | 6 | pub struct Substitution<'s> { 7 | map: BTreeMap<&'s Id, &'s RefType>, 8 | } 9 | 10 | impl<'s> FromIterator<(&'s Id, &'s RefType)> for Substitution<'s> { 11 | fn from_iter>(iter: T) -> Self { 12 | Substitution { 13 | map: iter.into_iter().collect(), 14 | } 15 | } 16 | } 17 | 18 | pub trait Substitute { 19 | fn substitute(&self, subst: &Substitution<'_>) -> Self; 20 | } 21 | 22 | impl Substitute for RefType { 23 | fn substitute(&self, subst: &Substitution<'_>) -> Self { 24 | match self { 25 | RefType::Class(c) => RefType::Class(c.substitute(subst)), 26 | RefType::Array(a) => RefType::Array(a.substitute(subst)), 27 | RefType::TypeParameter(id) => match subst.map.get(&id) { 28 | Some(v) => RefType::clone(v), 29 | None => self.clone(), 30 | }, 31 | RefType::Extends(e) => RefType::Extends(e.substitute(subst)), 32 | RefType::Super(e) => RefType::Super(e.substitute(subst)), 33 | RefType::Wildcard => RefType::Wildcard, 34 | } 35 | } 36 | } 37 | 38 | impl Substitute for Type { 39 | fn substitute(&self, subst: &Substitution<'_>) -> Self { 40 | match self { 41 | Type::Ref(r) => Type::Ref(r.substitute(subst)), 42 | Type::Scalar(_) => self.clone(), 43 | Type::Repeat(r) => Type::Repeat(r.substitute(subst)), 44 | } 45 | } 46 | } 47 | 48 | impl Substitute for ClassRef { 49 | fn substitute(&self, subst: &Substitution<'_>) -> Self { 50 | let ClassRef { name, generics } = self; 51 | ClassRef { 52 | name: name.clone(), 53 | generics: generics.substitute(subst), 54 | } 55 | } 56 | } 57 | 58 | impl Substitute for Vec 59 | where 60 | F: Substitute, 61 | { 62 | fn substitute(&self, subst: &Substitution<'_>) -> Self { 63 | self.iter().map(|e| e.substitute(subst)).collect() 64 | } 65 | } 66 | 67 | impl Substitute for Arc 68 | where 69 | F: Substitute, 70 | { 71 | fn substitute(&self, subst: &Substitution<'_>) -> Self { 72 | Arc::new(F::substitute(self, subst)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/java-to-rust/java/java_rust_initiated_exceptions/JavaRustExceptions.java: -------------------------------------------------------------------------------- 1 | //@check-pass 2 | 3 | package java_rust_initiated_exceptions; 4 | 5 | public class JavaRustExceptions { 6 | native String raiseNPE(); 7 | native String raiseSliceTooLong(); 8 | native String raiseJvmInternal(); 9 | native String panic(); 10 | 11 | public static void expectNPE(JavaRustExceptions test) { 12 | try { 13 | test.raiseNPE(); 14 | } catch (NullPointerException e) { 15 | return; 16 | } 17 | 18 | throw new RuntimeException("NullPointerException not caught"); 19 | } 20 | 21 | public static void expectSliceTooLong(JavaRustExceptions test) { 22 | String message = "no exception thrown"; 23 | try { 24 | test.raiseSliceTooLong(); 25 | } catch (RuntimeException e) { 26 | message = e.getMessage(); 27 | } 28 | 29 | if (!message.contains("slice was too long")) { 30 | throw new RuntimeException("Caught no exception or the wrong exception: " + message); 31 | } 32 | } 33 | 34 | public static void expectJvmInternal(JavaRustExceptions test) { 35 | String message = "no exception thrown"; 36 | try { 37 | test.raiseJvmInternal(); 38 | } catch (RuntimeException e) { 39 | message = e.getMessage(); 40 | } 41 | 42 | if (!message.equals("JvmInternal")) { 43 | throw new RuntimeException("Caught no exception or the wrong exception: " + message); 44 | } 45 | } 46 | 47 | public static void expectPanicIsRuntimeException(JavaRustExceptions test) { 48 | String message = "no exception thrown"; 49 | try { 50 | test.panic(); 51 | } catch (RuntimeException e) { 52 | message = e.getMessage(); 53 | } 54 | 55 | if (!message.equals("RUST PANIC!")) { 56 | throw new RuntimeException("Caught no exception or the wrong exception: " + message); 57 | } 58 | } 59 | 60 | public static void main(String[] args) { 61 | System.loadLibrary("java_rust_initiated_exceptions"); 62 | JavaRustExceptions test = new JavaRustExceptions(); 63 | 64 | expectNPE(test); 65 | expectSliceTooLong(test); 66 | expectJvmInternal(test); 67 | expectPanicIsRuntimeException(test); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /duchess-build-rs/src/derive_java.rs: -------------------------------------------------------------------------------- 1 | use duchess_reflect::{ 2 | argument::MethodSelector, 3 | parse::{Parse, Parser}, 4 | }; 5 | use proc_macro2::Span; 6 | use syn::{parse::ParseStream, Attribute}; 7 | 8 | use crate::{log, re}; 9 | 10 | /// Process a file and reflect any `#[java(...)]` attributes that were found 11 | pub(crate) fn process_file( 12 | rs_file: &crate::files::File, 13 | reflector: &mut duchess_reflect::reflect::JavapReflector, 14 | ) -> anyhow::Result { 15 | let mut watch_file = false; 16 | for capture in re::java_derive().captures_iter(&rs_file.contents) { 17 | let std::ops::Range { start, end: _ } = capture.get(0).unwrap().range(); 18 | log!( 19 | "Found derive(java) in {}:{}", 20 | rs_file.path.display(), 21 | rs_file.contents[..start].lines().count() 22 | ); 23 | let derive_java_attr: DeriveJavaAttr = match syn::parse_str(rs_file.rust_slice_from(start)) 24 | { 25 | Ok(attr) => attr, 26 | Err(e) => { 27 | log!("Error: failed to parse derive(java) attribute: {}", e); 28 | return Ok(true); 29 | } 30 | }; 31 | reflector.reflect_and_cache( 32 | &derive_java_attr.method_selector.class_name(), 33 | Span::call_site(), 34 | )?; 35 | watch_file = true; 36 | } 37 | Ok(watch_file) 38 | } 39 | 40 | /// Representation of attributes like `#[java(java.lang.Long)]` 41 | #[derive(Debug)] 42 | struct DeriveJavaAttr { 43 | method_selector: MethodSelector, 44 | } 45 | 46 | impl syn::parse::Parse for DeriveJavaAttr { 47 | fn parse(input: ParseStream) -> syn::Result { 48 | let attributes = input.call(Attribute::parse_outer)?; 49 | for attr in attributes { 50 | if !attr.path().is_ident("java") { 51 | continue; 52 | } 53 | let derive_tokens = attr.meta.require_list()?.tokens.clone(); 54 | let mut parser: Parser = derive_tokens.into(); 55 | let method_selector = MethodSelector::parse(&mut parser)?.ok_or(syn::Error::new( 56 | input.span(), 57 | "expected a class in the attribute", 58 | ))?; 59 | return Ok(DeriveJavaAttr { method_selector }); 60 | } 61 | Err(syn::Error::new( 62 | input.span(), 63 | "expected #[java(...)] attribute", 64 | )) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /duchess-build-rs/src/impl_java_trait.rs: -------------------------------------------------------------------------------- 1 | use duchess_reflect::{ 2 | class_info::ClassRef, 3 | reflect::{JavapReflector, Reflect}, 4 | }; 5 | use proc_macro2::{Span, TokenStream}; 6 | use syn::spanned::Spanned; 7 | 8 | use crate::{files::File, java_compiler::JavaCompiler, log, shim_writer::ShimWriter}; 9 | 10 | pub fn process_impl(compiler: &JavaCompiler, file: &File, offset: usize) -> anyhow::Result<()> { 11 | let the_impl: JavaInterfaceImpl = syn::parse_str(file.rust_slice_from(offset))?; 12 | the_impl.generate_shim(compiler)?; 13 | Ok(()) 14 | } 15 | 16 | struct JavaInterfaceImpl { 17 | item: syn::ItemImpl, 18 | } 19 | 20 | impl syn::parse::Parse for JavaInterfaceImpl { 21 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 22 | // we are parsing an input that starts with an impl and then has add'l stuff 23 | let item: syn::ItemImpl = input.parse()?; 24 | 25 | // syn reports an error if there is anything unconsumed, so consume all remaining tokens 26 | // after we parse the impl 27 | let _more_tokens: TokenStream = input.parse()?; 28 | 29 | Ok(Self { item }) 30 | } 31 | } 32 | 33 | impl JavaInterfaceImpl { 34 | fn generate_shim(&self, compiler: &JavaCompiler) -> anyhow::Result<()> { 35 | let mut reflector = JavapReflector::new(compiler.configuration()); 36 | let (java_interface_ref, java_interface_span) = self.java_interface()?; 37 | let java_interface_info = 38 | reflector.reflect(&java_interface_ref.name, java_interface_span)?; 39 | 40 | let shim_name = format!("Shim${}", java_interface_info.name.to_dollar_name()); 41 | let java_file = compiler.java_file("duchess", &shim_name); 42 | ShimWriter::new( 43 | &mut java_file.src_writer()?, 44 | &shim_name, 45 | &java_interface_info, 46 | ) 47 | .emit_shim_class()?; 48 | 49 | compiler.compile_to_rs_file(&java_file)?; 50 | 51 | log!("compiled to {}", java_file.rs_path.display()); 52 | 53 | Ok(()) 54 | } 55 | 56 | fn java_interface(&self) -> anyhow::Result<(ClassRef, Span)> { 57 | let Some((_, trait_path, _)) = &self.item.trait_ else { 58 | return Err(syn::Error::new_spanned(&self.item, "expected an impl of a trait").into()); 59 | }; 60 | let class_ref = ClassRef::from(&self.item.generics, trait_path)?; 61 | Ok((class_ref, trait_path.span())) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{Debug, Display}, 3 | result, 4 | }; 5 | 6 | use thiserror::Error; 7 | 8 | use crate::AsJRef; 9 | use crate::{java::lang::Throwable, Java, Jvm, JvmOp, Local}; 10 | 11 | /// Result returned by most Java operations that may contain a local reference 12 | /// to a thrown exception. 13 | pub type LocalResult<'jvm, T> = result::Result>>; 14 | 15 | /// Result returned by [`crate::Jvm::with()`] that will store any uncaught 16 | /// exception as a global reference. 17 | pub type Result = result::Result>>; 18 | 19 | #[derive(Error)] 20 | pub enum Error> { 21 | /// A reference to an uncaught Java exception 22 | #[error("Java invocation threw: {}", try_extract_message(.0))] 23 | Thrown(T), 24 | 25 | #[error( 26 | "slice was too long (`{0}`) to convert to a Java array, which are limited to `i32::MAX`" 27 | )] 28 | SliceTooLong(usize), 29 | 30 | #[error("attempted to deref a null Java object pointer")] 31 | NullDeref, 32 | 33 | #[error("attempted to nest `Jvm::with` calls")] 34 | NestedUsage, 35 | 36 | #[error("JVM already exists")] 37 | JvmAlreadyExists, 38 | 39 | #[cfg(feature = "dylibjvm")] 40 | #[error(transparent)] 41 | UnableToLoadLibjvm(#[from] Box), 42 | 43 | #[error("{0}")] 44 | JvmInternal(String), 45 | } 46 | 47 | fn try_extract_message(exception: &impl AsJRef) -> String { 48 | let result = 49 | || -> crate::Result<_> { exception.as_jref()?.to_string().assert_not_null().execute() }; 50 | result().unwrap_or_else(|err| format!("failed to get message: {}", err)) 51 | } 52 | 53 | impl Debug for Error 54 | where 55 | T: AsJRef, 56 | { 57 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 58 | Display::fmt(self, f) 59 | } 60 | } 61 | 62 | impl<'jvm> Error> { 63 | pub fn into_global(self, jvm: &mut Jvm<'jvm>) -> Error> { 64 | match self { 65 | Error::Thrown(t) => Error::Thrown(jvm.global(&t)), 66 | Error::SliceTooLong(s) => Error::SliceTooLong(s), 67 | Error::NullDeref => Error::NullDeref, 68 | Error::NestedUsage => Error::NestedUsage, 69 | Error::JvmAlreadyExists => Error::JvmAlreadyExists, 70 | #[cfg(feature = "dylibjvm")] 71 | Error::UnableToLoadLibjvm(e) => Error::UnableToLoadLibjvm(e), 72 | Error::JvmInternal(m) => Error::JvmInternal(m), 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/libjvm.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | 3 | /// Virtual table for top-level libjvm functions that create or get JVMs. 4 | #[allow(non_snake_case)] 5 | pub(crate) struct Libjvm { 6 | pub JNI_CreateJavaVM: unsafe extern "system" fn( 7 | pvm: *mut *mut jni_sys::JavaVM, 8 | penv: *mut *mut std::ffi::c_void, 9 | args: *mut std::ffi::c_void, 10 | ) -> jni_sys::jint, 11 | pub JNI_GetCreatedJavaVMs: unsafe extern "system" fn( 12 | vmBuf: *mut *mut jni_sys::JavaVM, 13 | bufLen: jni_sys::jsize, 14 | nVMs: *mut jni_sys::jsize, 15 | ) -> jni_sys::jint, 16 | } 17 | 18 | #[cfg(feature = "dylibjvm")] 19 | mod dynlib { 20 | use std::path::{Path, PathBuf}; 21 | 22 | use libloading::Library; 23 | use once_cell::sync::OnceCell; 24 | 25 | use super::*; 26 | use crate::Error; 27 | 28 | static LIBJVM: OnceCell = OnceCell::new(); 29 | 30 | #[allow(non_snake_case)] 31 | fn load_libjvm_at(path: &Path) -> Result { 32 | (|| { 33 | let lib = unsafe { Library::new(path) }?; 34 | let JNI_CreateJavaVM = *unsafe { lib.get(b"JNI_CreateJavaVM\0") }?; 35 | let JNI_GetCreatedJavaVMs = *unsafe { lib.get(b"JNI_GetCreatedJavaVMs\0") }?; 36 | std::mem::forget(lib); // We keep the JVM (and therefore libjvm) around through the end of the process 37 | Ok(Libjvm { 38 | JNI_CreateJavaVM, 39 | JNI_GetCreatedJavaVMs, 40 | }) 41 | })() 42 | .map_err(|e: libloading::Error| Error::UnableToLoadLibjvm(Box::new(e))) 43 | } 44 | 45 | pub(crate) fn libjvm_or_load() -> Result<&'static Libjvm> { 46 | LIBJVM.get_or_try_init(|| { 47 | let path: PathBuf = [ 48 | &java_locator::locate_jvm_dyn_library() 49 | .map_err(|e| Error::UnableToLoadLibjvm(Box::new(e)))?, 50 | java_locator::get_jvm_dyn_lib_file_name(), 51 | ] 52 | .into_iter() 53 | .collect(); 54 | 55 | load_libjvm_at(&path) 56 | }) 57 | } 58 | 59 | pub(crate) fn libjvm_or_load_at(path: &Path) -> Result<&'static Libjvm> { 60 | LIBJVM.get_or_try_init(|| load_libjvm_at(path)) 61 | } 62 | } 63 | 64 | #[cfg(feature = "dylibjvm")] 65 | pub(crate) use dynlib::{libjvm_or_load, libjvm_or_load_at}; 66 | 67 | #[cfg(not(feature = "dylibjvm"))] 68 | pub(crate) fn libjvm_or_load() -> Result<&'static Libjvm> { 69 | static LIBJVM: Libjvm = Libjvm { 70 | JNI_CreateJavaVM: jni_sys::JNI_CreateJavaVM, 71 | JNI_GetCreatedJavaVMs: jni_sys::JNI_GetCreatedJavaVMs, 72 | }; 73 | 74 | Ok(&LIBJVM) 75 | } 76 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Experiments with Java-Rust interop. 2 | 3 | mod array; 4 | mod cast; 5 | mod error; 6 | mod find; 7 | mod from_ref; 8 | mod into_rust; 9 | mod jvm; 10 | mod libjvm; 11 | mod link; 12 | mod not_null; 13 | mod null; 14 | mod ops; 15 | mod raw; 16 | mod ref_; 17 | mod refs; 18 | mod str; 19 | mod thread; 20 | mod to_java; 21 | mod try_catch; 22 | 23 | /// Contains reusable declarations for classes distributed by the JDK under the `java.*` packages. 24 | pub mod java; 25 | 26 | pub use duchess_macro::{java_function, java_package, ToJava, ToRust}; 27 | pub use error::{Error, LocalResult, Result}; 28 | pub use into_rust::IntoRust; 29 | pub use jvm::JavaObject; 30 | pub use jvm::JavaType; 31 | pub use jvm::Jvm; 32 | pub use link::JavaFunction; 33 | pub use null::Null; 34 | pub use ref_::{Java, Local}; 35 | pub use refs::{AsJRef, JDeref, NullJRef, Nullable, TryJDeref}; 36 | pub use try_catch::TryCatch; 37 | 38 | pub use prelude::*; 39 | 40 | /// Contains traits with methods expected to be invoked by end-users. 41 | pub mod prelude { 42 | pub use crate::java; 43 | pub use crate::jvm::JvmOp; 44 | pub use crate::link::JavaFn; 45 | pub use crate::ops::{ 46 | IntoJava, IntoScalar, IntoVoid, JavaConstructor, JavaField, JavaMethod, ScalarField, 47 | ScalarMethod, VoidMethod, 48 | }; 49 | pub use crate::refs::{AsJRef, JDeref, TryJDeref}; 50 | pub use crate::to_java::ToJava; 51 | pub use crate::Java; 52 | } 53 | 54 | /// Internal module containing non-semver protected 55 | /// names used by generated code. 56 | #[doc(hidden)] 57 | pub mod semver_unstable { 58 | pub use crate::cast::Upcast; 59 | pub use crate::find::{find_class, find_constructor, find_field, find_method}; 60 | pub use crate::from_ref::FromRef; 61 | pub use crate::jvm::native_function_returning_object; 62 | pub use crate::jvm::native_function_returning_scalar; 63 | pub use crate::jvm::native_function_returning_unit; 64 | pub use crate::jvm::JavaObjectExt; 65 | pub use crate::jvm::JavaView; 66 | pub use crate::jvm::JvmRefOp; 67 | pub use crate::jvm::JvmScalarOp; 68 | pub use crate::link::JavaFn; 69 | pub use crate::link::JavaFunction; 70 | pub use crate::raw::{EnvPtr, FieldPtr, FromJniValue, IntoJniValue, MethodPtr, ObjectPtr}; 71 | pub use crate::refs::NullJRef; 72 | pub use crate::to_java::{ToJavaImpl, ToJavaScalar}; 73 | pub use duchess_macro_rules::{ 74 | argument_impl_trait, field_output_trait, jni_call_fn, jni_static_call_fn, 75 | jni_static_field_get_fn, macro_if, mro, output_trait, output_type, prepare_input, rust_ty, 76 | setup_class, setup_constructor, setup_inherent_object_method, setup_java_function, 77 | setup_obj_method, setup_op_method, setup_static_field_getter, setup_static_method, 78 | view_of_obj, view_of_op, 79 | }; 80 | pub use jni_sys; 81 | pub use once_cell; 82 | } 83 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/tests/rust-to-java/exceptions.rs: -------------------------------------------------------------------------------- 1 | //@run 2 | use duchess::prelude::*; 3 | use duchess::Jvm; 4 | 5 | duchess::java_package! { 6 | package exceptions; 7 | 8 | public class ThrowExceptions { * } 9 | public class DifferentException { * } 10 | } 11 | 12 | pub fn main() -> duchess::Result<()> { 13 | check_exceptions()?; 14 | check_static_fields()?; 15 | catch_exceptions()?; 16 | 17 | Ok(()) 18 | } 19 | 20 | fn check_static_fields() -> duchess::Result<()> { 21 | let result: String = exceptions::ThrowExceptions::get_static_string_not_null() 22 | .execute()? 23 | .unwrap(); 24 | assert_eq!("notnull", result); 25 | Ok(()) 26 | } 27 | 28 | fn catch_exceptions() -> duchess::Result<()> { 29 | let thrower = exceptions::ThrowExceptions::new().execute().unwrap(); 30 | 31 | let caught_exception = thrower 32 | .throw_runtime() 33 | .catch::() 34 | .execute() 35 | .unwrap(); 36 | assert!( 37 | // it matches the expected exception type so, outer is Ok, inner is err 38 | matches!(&caught_exception, Err(_)), 39 | "{:?}", 40 | caught_exception 41 | ); 42 | 43 | let caught_exception = thrower 44 | .null_object() 45 | .catch::() 46 | .execute() 47 | .unwrap() 48 | .expect("returns ok!"); 49 | 50 | // This errors out because `try_extract_exception` calls `Jvm::with`. 51 | let caught_exception = thrower 52 | .throw_runtime() 53 | .catch::() 54 | .execute(); 55 | assert!(matches!(caught_exception, Err(duchess::Error::Thrown(_)))); 56 | 57 | assert_eq!( 58 | format!("{:?}", caught_exception), 59 | "Err(Java invocation threw: java.lang.RuntimeException: something has gone horribly wrong)" 60 | ); 61 | Ok(()) 62 | } 63 | 64 | fn check_exceptions() -> duchess::Result<()> { 65 | let thrower = exceptions::ThrowExceptions::new().execute()?; 66 | 67 | let result = thrower 68 | .throw_runtime() 69 | .execute() 70 | .expect_err("method throws an exception"); 71 | assert!(matches!(result, duchess::Error::Thrown(_))); 72 | let error_message = format!("{}", result); 73 | assert!( 74 | error_message.contains("java.lang.RuntimeException: something has gone horribly wrong"), 75 | "{}", 76 | error_message 77 | ); 78 | 79 | let error = thrower 80 | .null_object() 81 | .to_string() 82 | .execute::>() 83 | .expect_err("returns a null pointer"); 84 | 85 | assert!(matches!(error, duchess::Error::NullDeref)); 86 | 87 | let misbehaved_exception = thrower 88 | .throw_exception_with_crashing_message() 89 | .execute() 90 | .expect_err("method doubly throws an exception"); 91 | assert!(format!("{:?}", misbehaved_exception).contains("My exception threw an exception")); 92 | 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /duchess-reflect/src/class_info/from_syn.rs: -------------------------------------------------------------------------------- 1 | use syn::{spanned::Spanned, PathArguments}; 2 | 3 | use super::{ClassRef, DotId, Id, RefType}; 4 | 5 | impl ClassRef { 6 | /// Convert a Rust path (parsed with syn) into a Java path. 7 | /// 8 | /// Not the ideal way to express Java types etc but used in 9 | /// implementing Java interfaces. 10 | pub fn from(generics: &syn::Generics, path: &syn::Path) -> syn::Result { 11 | let syn::Path { 12 | leading_colon, 13 | segments, 14 | } = path; 15 | 16 | if let Some(c) = leading_colon { 17 | return Err(syn::Error::new(c.span(), "leading colon not accepted")); 18 | } 19 | 20 | let mut names = vec![]; 21 | let mut types = vec![]; 22 | for segment in segments { 23 | names.push(segment.ident.clone()); 24 | 25 | match &segment.arguments { 26 | PathArguments::None => {} 27 | PathArguments::Parenthesized(args) => { 28 | return Err(syn::Error::new( 29 | args.paren_token.span.open(), 30 | "only `<>` type arguments accepted", 31 | )); 32 | } 33 | PathArguments::AngleBracketed(args) => { 34 | if names.len() != segments.len() { 35 | return Err(syn::Error::new( 36 | args.gt_token.span, 37 | "`<>` only permitted in the final segment", 38 | )); 39 | } 40 | 41 | for arg in &args.args { 42 | let ty = super::RefType::from(generics, arg)?; 43 | types.push(ty); 44 | } 45 | } 46 | } 47 | } 48 | 49 | Ok(ClassRef { 50 | name: DotId::from_iter(names.iter().map(|ident| Id::from(ident))), 51 | generics: types, 52 | }) 53 | } 54 | } 55 | 56 | impl RefType { 57 | /// Convert a Rust type into a Java type as best we can. 58 | pub fn from(generics: &syn::Generics, arg: &syn::GenericArgument) -> syn::Result { 59 | if let syn::GenericArgument::Type(ty) = arg { 60 | if let syn::Type::Path(syn::TypePath { qself, path }) = ty { 61 | if let Some(q) = qself { 62 | return Err(syn::Error::new(q.lt_token.span(), "no qualified paths")); 63 | } 64 | 65 | for generic in generics.type_params() { 66 | if path.is_ident(&generic.ident) { 67 | return Ok(RefType::TypeParameter(Id::from(&generic.ident))); 68 | } 69 | } 70 | 71 | Ok(RefType::Class(ClassRef::from(generics, path)?)) 72 | } else { 73 | Err(syn::Error::new(arg.span(), "only paths accepted")) 74 | } 75 | } else { 76 | Err(syn::Error::new(arg.span(), "only types accepted")) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a mdBook site to GitHub Pages 2 | # 3 | # To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html 4 | # 5 | name: Deploy mdBook site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["main"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | env: 32 | MDBOOK_VERSION: 0.4.21 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: Swatinem/rust-cache@v2 36 | with: 37 | cache-all-crates: true 38 | # Needed so that the coverage tests can actually work & rustdoc can successfully build examples 39 | - uses: actions/setup-java@v3 40 | with: 41 | distribution: 'corretto' 42 | java-version: '17' 43 | - name: Install mdBook 44 | run: | 45 | curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.28/mdbook-v0.4.28-x86_64-unknown-linux-gnu.tar.gz | tar -xz 46 | # curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh 47 | # rustup update 48 | # cargo install --version ${MDBOOK_VERSION} mdbook 49 | - name: Setup Pages 50 | id: pages 51 | uses: actions/configure-pages@v3 52 | - name: Build with mdBook 53 | run: ./mdbook build book 54 | #- name: stub 55 | # run: mkdir -p target/ui-coverage-report && echo "hello world!" > target/ui-coverage-report/index.html 56 | - name: install just 57 | run: | 58 | cargo install just 59 | - name: coverage-deps 60 | run: just coverage-tools 61 | - name: 62 | run: just coverage 63 | env: 64 | NO_OPEN: true 65 | # Needed so that mdbook can point directly to rustdoc 66 | - name: Rust rustdoc 67 | run: cargo doc --target-dir book/book/rustdoc 68 | - name: Assemble pages 69 | run: | 70 | mv book/book pages-site 71 | mv target/ui-coverage-report pages-site/coverage 72 | - name: Upload artifact 73 | uses: actions/upload-pages-artifact@v1 74 | with: 75 | path: ./pages-site/ 76 | 77 | # Deployment job 78 | deploy: 79 | environment: 80 | name: github-pages 81 | url: ${{ steps.deployment.outputs.page_url }} 82 | runs-on: ubuntu-latest 83 | needs: build 84 | steps: 85 | - name: Deploy to GitHub Pages 86 | id: deployment 87 | uses: actions/deploy-pages@v2 88 | -------------------------------------------------------------------------------- /duchess-reflect/src/class_info/javap.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // FIXME -- change to `#[expect]` once stabilized, the lalrpop code generates some unused code 2 | 3 | use std::fmt::Display; 4 | 5 | use lalrpop_util::{lalrpop_mod, lexer::Token}; 6 | use proc_macro2::Span; 7 | 8 | use super::{ClassDeclKind, ClassInfo}; 9 | 10 | lalrpop_mod!(pub javap_parser, "/class_info/javap_parser.rs"); // synthesized by LALRPOP 11 | 12 | pub(super) fn parse_class_decl(span: Span, input: &str) -> syn::Result { 13 | match javap_parser::ClassDeclKindParser::new().parse(span, input) { 14 | Ok(v) => Ok(v), 15 | Err(error) => Err(syn::Error::new(span, format_lalrpop_error(input, error))), 16 | } 17 | } 18 | 19 | pub(super) fn parse_class_info(span: Span, input: &str) -> syn::Result { 20 | match javap_parser::ClassInfoParser::new().parse(span, input) { 21 | Ok(v) => Ok(v), 22 | Err(error) => Err(syn::Error::new(span, format_lalrpop_error(input, error))), 23 | } 24 | } 25 | 26 | fn format_lalrpop_error( 27 | input: &str, 28 | error: lalrpop_util::ParseError, impl Display>, 29 | ) -> String { 30 | match error { 31 | lalrpop_util::ParseError::ExtraToken { token } => { 32 | format!("extra token at end of input (`{}`)", token.1) 33 | } 34 | lalrpop_util::ParseError::UnrecognizedEOF { 35 | location: _, 36 | expected, 37 | } => { 38 | format!("unexpected end of input, expected one of `{:?}`", expected) 39 | } 40 | lalrpop_util::ParseError::UnrecognizedToken { 41 | token: (start, _, end), 42 | expected, 43 | } => { 44 | let window_string = window_string(input, start, end); 45 | 46 | format!( 47 | "unexpected token `{}` at offset {}, expected one of `{:?}`", 48 | window_string, start, expected 49 | ) 50 | } 51 | lalrpop_util::ParseError::InvalidToken { location } => { 52 | let ch_len = input[location..].chars().next().unwrap().len_utf8(); 53 | let window_string = window_string(input, location, location + ch_len); 54 | format!("invalid token `{}` at offset {}", window_string, ch_len,) 55 | } 56 | lalrpop_util::ParseError::User { error } => format!("{}", error), 57 | } 58 | } 59 | 60 | fn window_string(input: &str, start: usize, end: usize) -> String { 61 | const WINDOW: usize = 22; 62 | 63 | let mut window_string = String::new(); 64 | 65 | if start < WINDOW { 66 | window_string.push_str(&input[..start]); 67 | } else { 68 | window_string.push_str("... "); 69 | window_string.push_str(&input[start - WINDOW..start]); 70 | } 71 | 72 | window_string.push_str(" <<< "); 73 | window_string.push_str(&input[start..end]); 74 | window_string.push_str(" >>> "); 75 | 76 | let window_end = (end + WINDOW).min(input.len()); 77 | window_string.push_str(&input[end..window_end]); 78 | if input.len() > window_end { 79 | window_string.push_str(" ..."); 80 | } 81 | 82 | window_string 83 | } 84 | -------------------------------------------------------------------------------- /src/refs.rs: -------------------------------------------------------------------------------- 1 | use crate::{cast::Upcast, java::lang::Throwable, Error, Java, JavaObject, Local}; 2 | 3 | /// Possibly null reference to a Java object. 4 | pub trait AsJRef { 5 | fn as_jref(&self) -> Nullable<&U>; 6 | } 7 | 8 | /// Marker type used to indicate an attempt to dereference a null java reference. 9 | /// See [`TryJDeref`][] trait. 10 | pub struct NullJRef; 11 | 12 | pub type Nullable = Result; 13 | 14 | impl<'jvm, T, U> AsJRef for T 15 | where 16 | T: TryJDeref, 17 | T::Java: Upcast, 18 | U: JavaObject, 19 | { 20 | fn as_jref(&self) -> Nullable<&U> { 21 | let this = self.try_jderef()?; 22 | Ok(unsafe { std::mem::transmute(this) }) 23 | } 24 | } 25 | 26 | /// Reference to a Java object that may or may not be null. 27 | /// Implemented both by non-null references like `Java` 28 | /// or `&java::lang::Object` and by maybe-null references like `Option>`. 29 | pub trait TryJDeref { 30 | /// The Java type (e.g., [`java::lang::Object`][`crate::java::lang::Object`]). 31 | type Java: JavaObject; 32 | 33 | /// Dereference to a plain reference to the java object, or `Err` if it is null. 34 | fn try_jderef(&self) -> Nullable<&Self::Java>; 35 | } 36 | 37 | /// Reference to a Java object that cannot be null (e.g., `Java`). 38 | pub trait JDeref: TryJDeref { 39 | /// Dereference to a plain reference to the java object. 40 | fn jderef(&self) -> &Self::Java; 41 | } 42 | 43 | impl TryJDeref for &T 44 | where 45 | T: TryJDeref, 46 | { 47 | type Java = T::Java; 48 | 49 | fn try_jderef(&self) -> Nullable<&T::Java> { 50 | T::try_jderef(self) 51 | } 52 | } 53 | 54 | impl JDeref for &T 55 | where 56 | T: JDeref, 57 | { 58 | fn jderef(&self) -> &T::Java { 59 | T::jderef(self) 60 | } 61 | } 62 | 63 | impl TryJDeref for Local<'_, T> 64 | where 65 | T: JavaObject, 66 | { 67 | type Java = T; 68 | 69 | fn try_jderef(&self) -> Nullable<&T> { 70 | Ok(self) 71 | } 72 | } 73 | 74 | impl JDeref for Local<'_, T> 75 | where 76 | T: JavaObject, 77 | { 78 | fn jderef(&self) -> &T { 79 | self 80 | } 81 | } 82 | 83 | impl TryJDeref for Java 84 | where 85 | T: JavaObject, 86 | { 87 | type Java = T; 88 | 89 | fn try_jderef(&self) -> Nullable<&T> { 90 | Ok(self) 91 | } 92 | } 93 | 94 | impl JDeref for Java 95 | where 96 | T: JavaObject, 97 | { 98 | fn jderef(&self) -> &T { 99 | self 100 | } 101 | } 102 | 103 | impl TryJDeref for Option 104 | where 105 | T: TryJDeref, 106 | { 107 | type Java = T::Java; 108 | 109 | fn try_jderef(&self) -> Result<&T::Java, NullJRef> { 110 | match self { 111 | Some(r) => r.try_jderef(), 112 | None => Err(NullJRef), 113 | } 114 | } 115 | } 116 | 117 | impl From for Error 118 | where 119 | T: AsJRef, 120 | { 121 | fn from(NullJRef: NullJRef) -> Self { 122 | Error::NullDeref 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /macro-rules/src/setup_static_field_getter.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! setup_static_field_getter { 3 | ( 4 | struct_name: [$S:ident], 5 | java_class_generics: [$($G:ident,)*], 6 | rust_field_name: [$F:ident], 7 | field_ty: [$F_ty:tt], 8 | sig_where_clauses: [$($SIG:tt)*], 9 | jni_field: [$jni_field:expr], 10 | jni_descriptor: [$jni_descriptor:expr], 11 | ) => { 12 | pub fn $F() -> duchess::semver_unstable::field_output_trait!($F_ty) 13 | where 14 | $($SIG)* 15 | { 16 | #[allow(non_camel_case_types)] 17 | pub struct $F< 18 | $($G,)* 19 | > { 20 | phantom: ::core::marker::PhantomData<( 21 | $($G,)* 22 | )>, 23 | } 24 | 25 | impl<$($G,)*> duchess::prelude::JvmOp 26 | for $F<$($G,)*> 27 | where 28 | $($G: duchess::JavaObject,)* 29 | $($SIG)* 30 | { 31 | type Output<'jvm> = duchess::semver_unstable::output_type!('jvm, $F_ty); 32 | 33 | fn do_jni<'jvm>( 34 | self, 35 | jvm: &mut duchess::Jvm<'jvm>, 36 | ) -> duchess::LocalResult<'jvm, Self::Output<'jvm>> { 37 | use duchess::semver_unstable::once_cell::sync::OnceCell; 38 | 39 | // Cache the field id for this field -- note that we only have one cache 40 | // no matter how many generic monomorphizations there are. This makes sense 41 | // given Java's erased-based generics system. 42 | static FIELD: OnceCell = OnceCell::new(); 43 | let field = FIELD.get_or_try_init(|| { 44 | let class = <$S<$($G,)*> as duchess::JavaObject>::class(jvm)?; 45 | duchess::semver_unstable::find_field(jvm, &class, $jni_field, $jni_descriptor, true) 46 | })?; 47 | 48 | let class = <$S<$($G,)*> as duchess::JavaObject>::class(jvm)?; 49 | unsafe { 50 | jvm.env().invoke( 51 | duchess::semver_unstable::jni_static_field_get_fn!($F_ty), 52 | |env, f| f( 53 | env, 54 | duchess::semver_unstable::JavaObjectExt::as_raw(&*class).as_ptr(), 55 | field.as_ptr(), 56 | ), 57 | ) 58 | } 59 | } 60 | } 61 | 62 | 63 | impl<$($G,)*> ::core::clone::Clone for $F<$($G,)*> 64 | where 65 | $($G: duchess::JavaObject,)* 66 | $($SIG)* 67 | { 68 | fn clone(&self) -> Self { 69 | $F { 70 | phantom: self.phantom, 71 | } 72 | } 73 | } 74 | 75 | $F { 76 | phantom: ::core::default::Default::default(), 77 | } 78 | } 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /duchess-build-rs/src/files.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use walkdir::WalkDir; 4 | 5 | use crate::log; 6 | 7 | pub(crate) struct File { 8 | pub(crate) path: PathBuf, 9 | pub(crate) contents: String, 10 | } 11 | 12 | pub fn rs_files(path: impl AsRef) -> impl Iterator> { 13 | WalkDir::new(path) 14 | .into_iter() 15 | .filter_map(|entry| -> Option> { 16 | match entry { 17 | Ok(entry) => { 18 | if entry.path().extension().map_or(false, |e| e == "rs") { 19 | Some(Ok(File { 20 | path: entry.path().to_path_buf(), 21 | contents: match std::fs::read_to_string(entry.path()) { 22 | Ok(s) => s, 23 | Err(err) => return Some(Err(err.into())), 24 | }, 25 | })) 26 | } else { 27 | None 28 | } 29 | } 30 | 31 | Err(err) => Some(Err(err.into())), 32 | } 33 | }) 34 | } 35 | 36 | impl File { 37 | /// Return a string that can be used as a slug for error messages. 38 | pub fn slug(&self, offset: usize) -> String { 39 | let line_num = self.contents[..offset].lines().count(); 40 | let column_num = 1 + self.contents[..offset] 41 | .rfind('\n') 42 | .map_or(offset, |i| offset - i - 1); 43 | format!( 44 | "{path}:{line_num}:{column_num}:", 45 | path = self.path.display(), 46 | ) 47 | } 48 | 49 | /// Returns a chunk of rust code starting at `offset` 50 | /// and extending until the end of the current token tree 51 | /// or file, whichever comes first. 52 | /// 53 | /// This is used when we are preprocessing and we find 54 | /// some kind of macro invocation. We want to grab all 55 | /// the text that may be part of it and pass it into `syn`. 56 | // TODO: this should actually return an error, its basically never right to return the whole file 57 | pub fn rust_slice_from(&self, offset: usize) -> &str { 58 | let mut counter = 0; 59 | let terminator = self.contents[offset..].char_indices().find(|&(_, c)| { 60 | if c == '{' || c == '[' || c == '(' { 61 | counter += 1; 62 | } else if c == '}' || c == ']' || c == ')' { 63 | counter -= 1; 64 | 65 | if counter == 0 { 66 | return true; 67 | } 68 | } 69 | 70 | false 71 | }); 72 | if terminator.is_none() { 73 | log!("rust slice ran to end of file {counter}"); 74 | } 75 | match terminator { 76 | Some((i, _)) => &self.contents[offset..offset + i + 1], 77 | None => &self.contents[offset..], 78 | } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod test { 84 | #[test] 85 | fn test_rust_slice() { 86 | for file in super::rs_files("test-files") { 87 | let file = file.unwrap(); 88 | for offset in file.contents.char_indices().map(|(i, _)| i) { 89 | let _ = file.rust_slice_from(offset); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test-crates/viper/src/lib.rs: -------------------------------------------------------------------------------- 1 | duchess::java_package! { 2 | package java.lang; 3 | class Object {} 4 | 5 | package scala; 6 | class AnyVal { * } 7 | interface Function1 {} 8 | class Tuple2 {} 9 | class Option {} 10 | class Some extends scala.Option {} 11 | class "None$" extends scala.Option {} 12 | 13 | package scala.runtime; 14 | class "Nothing$" {} 15 | 16 | package scala.collection; 17 | interface IterableOnceOps { 18 | default scala.collection.immutable.Seq toSeq(); 19 | } 20 | interface IterableOps extends scala.collection.IterableOnceOps {} 21 | interface SeqOps extends scala.collection.IterableOps {} 22 | interface StrictOptimizedSeqOps extends scala.collection.SeqOps {} 23 | 24 | package scala.collection.immutable; 25 | interface Seq {} 26 | 27 | package scala.collection.mutable; 28 | class ArrayBuffer implements scala.collection.StrictOptimizedSeqOps< 29 | A, scala.collection.mutable.ArrayBuffer, scala.collection.mutable.ArrayBuffer 30 | > { 31 | public scala.collection.mutable.ArrayBuffer(); 32 | } 33 | 34 | package viper.silver.ast; 35 | class Bool {} 36 | class Domain {} 37 | class "NoTrafos$" implements viper.silver.ast.ErrorTrafo { 38 | public static viper.silver.ast."NoTrafos$" "MODULE$"; 39 | } 40 | interface ExtensionMember {} 41 | class Field {} 42 | class Function {} 43 | class Int {} 44 | class Method {} 45 | class "NoPosition$" implements viper.silver.ast.Position { 46 | public static viper.silver.ast."NoPosition$" "MODULE$"; 47 | } 48 | class "NoInfo$" implements viper.silver.ast.Info { 49 | public static viper.silver.ast."NoInfo$" "MODULE$"; 50 | } 51 | class Predicate {} 52 | class Program { 53 | public viper.silver.ast.Program( 54 | scala.collection.immutable.Seq, 55 | scala.collection.immutable.Seq, 56 | scala.collection.immutable.Seq, 57 | scala.collection.immutable.Seq, 58 | scala.collection.immutable.Seq, 59 | scala.collection.immutable.Seq, 60 | viper.silver.ast.Position, 61 | viper.silver.ast.Info, 62 | viper.silver.ast.ErrorTrafo 63 | ); 64 | } 65 | class Result {} 66 | interface ErrorTrafo {} 67 | interface Info {} 68 | interface Position {} 69 | 70 | package viper.silver.frontend; 71 | interface SilFrontend {} 72 | 73 | package viper.silver.reporter; 74 | interface Reporter {} 75 | class "NoopReporter$" implements viper.silver.reporter.Reporter { 76 | public static viper.silver.reporter."NoopReporter$" "MODULE$"; 77 | } 78 | 79 | package viper.silicon; 80 | class Silicon { 81 | public viper.silicon.Silicon(); 82 | } 83 | 84 | package viper.carbon; 85 | class CarbonVerifier { 86 | public viper.carbon.CarbonVerifier( 87 | viper.silver.reporter.Reporter, 88 | scala.collection.immutable.Seq> 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /book/src/java_function.md: -------------------------------------------------------------------------------- 1 | # The `java_function` macro 2 | 3 | The `java_function` macro is used to implement native functions. Make sure to read about how you [link these native functions into the JVM](./linking_native_functions.md). 4 | 5 | `java_function` is a low-level primitive. For a more ergonomic, full-featured way to wrap Rust library and call it from Java, 6 | check out [the `gluegun` crate](gluegun). 7 | 8 | [gluegun]: https://gluegun-rs.github.io/gluegun/ 9 | 10 | ## Examples 11 | 12 | Just want to see the code? Read some of the tests: 13 | 14 | * https://github.com/duchess-rs/duchess/tree/main/test-crates/duchess-java-tests/tests/java-to-rust/rust-libraries 15 | 16 | [the `greeting` example](https://github.com/duchess-rs/duchess/blob/main/test-crates/duchess-java-tests/tests/ui/examples/greeting.rs) to see the setup in action. 17 | 18 | ## Specifying which function you are defining 19 | 20 | The `#[java_function(X)]` takes an argument `X` that specifies which Java function is being defined. 21 | This argument `X` can have the following forms: 22 | 23 | * `java.class.Name::method`, identifying a `native` method `method` defined in the class `java.class.Name`. There must be exactly one native method with the given name. 24 | * a partial class definition like `class java.class.Name { native void method(int i); }` which identifies the method name along with its complete signature. This class definition must contain exactly one method as its member, and the types must match what is declared in the Java class. 25 | 26 | ## Expected function arguments and their type 27 | 28 | `#[java_function]` requires the decorated function to have the following arguments: 29 | 30 | * If not static, a `this` parameter -- can have any name, but we recommend `this`, whose type is the Duchess version of the Java type 31 | * One parameter per Java argument -- can have any name, but we recommend matching the names used in Java 32 | 33 | If present, the `this` argument should have the type `&foo::Bar` where `foo::Bar` is the Duchess type of the Java class. i.e., if this is a native method defined on the class `java.lang.String`, you would have `this: &java::lang::String`. 34 | 35 | The other arguments must match the types declared in Java: 36 | 37 | * For Java scalars, use `i32`, `i16`, etc. 38 | * For reference types, use `Option<&J>`, where `J` is the Java type (e.g., `Option<&java::lang::String>`). 39 | * Note that `Option` is required as the Java code can always provide `null`. You can use the [`assert_not_null`][] method on `JvmOp`. 40 | 41 | [`assert_not_null`]: https://duchess-rs.github.io/duchess/rustdoc/doc/duchess/prelude/trait.JvmOp.html#method.assert_not_null 42 | 43 | ## Expected return type 44 | 45 | If the underlying Java function returns a scalar value, your Rust function must return that same scalar value. 46 | 47 | Otherwise, if the underlying Java function returns an object of type `J`, the value returned from your function will be converted to `J` by invoking the [`to_java`](./to_java.md) method. This means your functon can return: 48 | 49 | * a reference to a Java object of type `J` (e.g., `&J` or `Java`); 50 | * a reference to an optional Java object of type `J` (e.g., `Option>`), which permits returning `null`; 51 | * a Rust value that can be converted to `J` via `to_java::`. 52 | 53 | ## Linking your native function into the JVM 54 | 55 | This is covered under a [dedicated page](./linking_native_functions.md). 56 | -------------------------------------------------------------------------------- /book/src/setup.md: -------------------------------------------------------------------------------- 1 | # Setup instructions 2 | 3 | ## TL;DR 4 | 5 | You need to... 6 | 7 | * [Install the JDK](#jdk-and-java_home) 8 | * Install the `cargo-duchess` CLI tool with `cargo install cargo-duchess` 9 | * Run `cargo duchess init` in your package, which will add duches to your `build.rs` file and your `Cargo.toml` 10 | 11 | ## Prequisites 12 | 13 | ### JDK and JAVA_HOME 14 | 15 | You'll need to have a modern JDK installed. We recommend JDK17 or higher. Any JDK distribution will work. Here are some recommended options: 16 | 17 | * Ubuntu: Install one of the following packages... 18 | * `java-20-amazon-corretto-jdk/stable` 19 | * `openjdk-17-jre/stable-security` 20 | * `openjdk-17-jdk-headless` 21 | * Other: 22 | * Download [Amazon Coretto](https://aws.amazon.com/corretto/?filtered-posts.sort-by=item.additionalFields.createdDate&filtered-posts.sort-order=desc) 23 | * Download a [pre-built openjdk package](https://openjdk.org/install/) suitable for your operating system 24 | 25 | **You'll need the `javap` tool from the JDK to build with Duchess.** You'll want to configure the `JAVA_HOME` environment variable to point to your JDK installation. Duchess will use it to locate `javap`. Otherwise, Duchess will search for it on your `PATH`. You can configure the environment variables used at build time via Cargo by creating a `.cargo/config.toml` file (see [this example from duchess itself](https://github.com/duchess-rs/duchess/blob/main/.cargo/config.toml)). 26 | 27 | Duchess relies on `javap` to reflect Java type information at build time. It will *not* be invoked at runtime. 28 | 29 | ## Basic setup 30 | 31 | To use Duchess your project requires a `build.rs` as well as a proc-macro crate. The `build.rs` does the heavy lifting, invoking javap and doing other reflection. The proc-macro crates then do final processing to generate the code. 32 | 33 | You can 34 | 35 | ## Other details 36 | 37 | ## Configuring the CLASSPATH 38 | 39 | You will likely want to configure the `CLASSPATH` for your Rust project as well. Like with `JAVA_HOME`, you can do that via Cargo by creating a `.cargo/config.toml` file. 40 | 41 | If your Rust project uses external JAR files, you may want to configure it to download them as part of the build. The [viper test crate](https://github.com/duchess-rs/duchess/tree/main/test-crates/viper) gives an example of how to do that. It uses a [build.rs](https://github.com/duchess-rs/duchess/blob/main/test-crates/viper/build.rs) file. 42 | 43 | ## Libjvm and linking 44 | 45 | By default, the `dylibjvm` feature is enabled and Duchess will dynamically load and link libjvm at runtime. Like with `javap`, it will first search for libjvm in `JAVA_HOME` if set. Otherwise it will look for `java` on your `PATH` to locate the JRE installation. Non-standard installations can also be configured using `JvmBuilder`. 46 | 47 | Without `dylibjvm`, libjvm must be statically linked. 48 | 49 | ## JNI Versions 50 | 51 | By default, we attempt to load JNI 1.6 when compiling for Android, and JNI 1.8 in all other cases. The JNI version can be selected by using the feature `jni_` and the JNI version concatenated, for any supported JNI version, with underscores replacing periods. 52 | Duchess currently only supports JNI versions 1.6 and 1.8, and only supports 1.6 on Android (the compile will fail if JNI > 1.6 is attempted on Android). Duchess sets the version to the newest version specified by features if features are specified. 53 | If you want Duchess to support a newer JNI API version or locking behavior, cut an issue with your use case, and it may be added to Duchess's next release. 54 | -------------------------------------------------------------------------------- /test-crates/duchess-java-tests/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::{Path, PathBuf}, 3 | process::Command, 4 | }; 5 | 6 | use duchess_build_rs::Configuration; 7 | use walkdir::WalkDir; 8 | 9 | // This controls where this script writes built files 10 | const TARGET_PATH: &str = "../target"; 11 | 12 | fn main() -> anyhow::Result<()> { 13 | // Rerun java build if any source file changes, but then we'll check each file individually below 14 | let java_source_paths = vec!["java", "tests/java-to-rust/java"]; 15 | for source_path in java_source_paths.iter() { 16 | println!("cargo:rerun-if-changed={}", source_path); 17 | } 18 | println!("cargo:rustc-env=CLASSPATH=target/java:target/tests/java-to-rust/java"); 19 | 20 | let target_dir = Path::new(TARGET_PATH); 21 | 22 | for source_path in java_source_paths { 23 | for entry_result in WalkDir::new(source_path) { 24 | let entry = entry_result?; 25 | 26 | if let Some(extension) = entry.path().extension() { 27 | if extension == "java" { 28 | // check if the class file doesn't exist or is older 29 | let source = entry.into_path(); 30 | 31 | // The target class file is basically the same path as the Java source file, relative to the target 32 | // directory 33 | let target = target_dir.join(source.clone()).with_extension("class"); 34 | 35 | let build_file = BuildFile { source, target }; 36 | 37 | if !file_up_to_date(&build_file)? { 38 | build_java(&build_file, source_path)?; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | // This has to run after the compilations above 46 | duchess_build_rs::DuchessBuildRs::new() 47 | .with_configuration( 48 | Configuration::default() 49 | .with_classpath("../target/java:../target/tests/java-to-rust/java"), 50 | ) 51 | .execute()?; 52 | 53 | Ok(()) 54 | } 55 | 56 | // A simple holder for state on a given file 57 | #[derive(Debug)] 58 | struct BuildFile { 59 | source: PathBuf, 60 | target: PathBuf, 61 | } 62 | 63 | /// Determines whether the target file exists and is up-to-date by checking the last modified timestamp 64 | fn file_up_to_date(BuildFile { source, target }: &BuildFile) -> std::io::Result { 65 | Ok(target.exists() && source.metadata()?.modified()? <= target.metadata()?.modified()?) 66 | } 67 | 68 | /// Executes javac to build the specified file 69 | fn build_java(input: &BuildFile, source_path: &str) -> std::io::Result<()> { 70 | // Class files will hav the same path structure as the sources, relative to the target dir 71 | let javac_target_dir = Path::new(TARGET_PATH).join(source_path); 72 | 73 | let output = Command::new("javac") 74 | .args([ 75 | "-d", // Specify the target directory for class files. Javac will create all parents if needed 76 | &javac_target_dir.display().to_string(), 77 | "-sourcepath", // Specify where to find other source files (e.g. dependencies) 78 | source_path, 79 | input.source.to_str().unwrap(), // assuming that we're not dealing with weird filenames 80 | ]) 81 | .output()?; 82 | 83 | if !output.status.success() { 84 | let mut stdout: String = 85 | String::from_utf8(output.stdout).expect("Unable to parse javac output"); 86 | stdout.push_str(String::from_utf8(output.stderr).unwrap().as_str()); 87 | 88 | panic!("Failed to build {:?}: {}", input.source, stdout); 89 | } 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /duchess-build-rs/src/shim_writer.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, io::Write}; 2 | 3 | use duchess_reflect::reflect::JavapClassInfo; 4 | 5 | use crate::code_writer::CodeWriter; 6 | 7 | pub struct ShimWriter<'w> { 8 | cw: CodeWriter<'w>, 9 | shim_name: &'w str, 10 | java_interface_info: &'w JavapClassInfo, 11 | } 12 | 13 | impl<'w> ShimWriter<'w> { 14 | pub fn new( 15 | writer: &'w mut impl Write, 16 | shim_name: &'w str, 17 | java_interface_info: &'w JavapClassInfo, 18 | ) -> Self { 19 | ShimWriter { 20 | cw: CodeWriter::new(writer), 21 | shim_name, 22 | java_interface_info, 23 | } 24 | } 25 | 26 | pub fn emit_shim_class(mut self) -> anyhow::Result<()> { 27 | write!(self.cw, "package duchess;")?; 28 | 29 | write!( 30 | self.cw, 31 | "public class {} implements {} {{", 32 | self.shim_name, self.java_interface_info.name 33 | )?; 34 | 35 | write!(self.cw, "long nativePointer;")?; 36 | write!( 37 | self.cw, 38 | "static java.lang.ref.Cleaner cleaner = java.lang.ref.Cleaner.create();" 39 | )?; 40 | 41 | write!(self.cw, "public {}(long nativePointer) {{", self.shim_name)?; 42 | write!(self.cw, "this.nativePointer = nativePointer;")?; 43 | write!( 44 | self.cw, 45 | "cleaner.register(this, () -> {{ native$drop(nativePointer); }});" 46 | )?; 47 | write!(self.cw, "}}")?; 48 | 49 | write!( 50 | self.cw, 51 | "native static void native$drop(long nativePointer);" 52 | )?; 53 | 54 | for method in &self.java_interface_info.methods { 55 | if !method.generics.is_empty() { 56 | anyhow::bail!( 57 | "generic parameters on method `{}` are not supported", 58 | method.name 59 | ) 60 | } 61 | 62 | let native_method_name = format!("native${}", method.name); 63 | let return_ty: &dyn Display = if let Some(return_ty) = &method.return_ty { 64 | return_ty 65 | } else { 66 | &"void" 67 | }; 68 | 69 | // Emit a native method 70 | write!(self.cw, "native static {return_ty} {native_method_name}(")?; 71 | for (argument_ty, index) in method.argument_tys.iter().zip(0..) { 72 | write!(self.cw, "{argument_ty} arg{index},")?; 73 | } 74 | write!(self.cw, "long nativePointer")?; 75 | write!(self.cw, ");")?; 76 | 77 | // Emit the interface method 78 | let method_name = &method.name; 79 | write!(self.cw, "public {return_ty} {method_name}(")?; 80 | for (argument_ty, index) in method.argument_tys.iter().zip(0..) { 81 | let comma = if index == method.argument_tys.len() - 1 { 82 | "" 83 | } else { 84 | ", " 85 | }; 86 | write!(self.cw, "{argument_ty} arg{index}{comma}")?; 87 | } 88 | write!(self.cw, ") {{")?; 89 | write!(self.cw, "return {native_method_name}(",)?; 90 | for index in 0..method.argument_tys.len() { 91 | write!(self.cw, "arg{index},")?; 92 | } 93 | write!(self.cw, "this.nativePointer")?; 94 | write!(self.cw, ");")?; 95 | write!(self.cw, "}}")?; 96 | } 97 | 98 | write!(self.cw, "}}",)?; 99 | 100 | Ok(()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /duchess-reflect/src/reflect/javap.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, path::Path, process::Command, sync::Arc}; 2 | 3 | use anyhow::{bail, Context}; 4 | use proc_macro2::Span; 5 | 6 | use crate::{ 7 | class_info::{ClassInfo, DotId}, 8 | config::Configuration, 9 | }; 10 | 11 | use super::{reflection_cache, JavapClassInfo, Reflect}; 12 | 13 | /// Reflector that uses JavaP to perform reflection 14 | #[derive(Debug)] 15 | pub struct JavapReflector { 16 | configuration: Configuration, 17 | classes: BTreeMap>, 18 | } 19 | 20 | // This impl is required because we must use the Javap reflector in the main code path for dealing with `*` 21 | impl Reflect for JavapReflector { 22 | fn reflect(&mut self, dot_id: &DotId, span: Span) -> syn::Result> { 23 | JavapReflector::reflect_and_cache(self, dot_id, span) 24 | } 25 | } 26 | 27 | impl JavapReflector { 28 | pub fn new(configuration: &Configuration) -> Self { 29 | Self { 30 | configuration: configuration.clone(), 31 | classes: BTreeMap::new(), 32 | } 33 | } 34 | 35 | fn serialize(&self) -> String { 36 | serde_json::to_string_pretty(&self.classes).expect("failed to serialize JSON") 37 | } 38 | 39 | pub fn dump_to(&self, out_dir: impl AsRef) -> anyhow::Result<()> { 40 | let path = reflection_cache(out_dir); 41 | let json = self.serialize(); 42 | std::fs::write(&path, json) 43 | .with_context(|| format!("writing reflection cache data to {:?}", path))?; 44 | Ok(()) 45 | } 46 | 47 | pub fn len(&self) -> usize { 48 | self.classes.len() 49 | } 50 | 51 | fn reflect_via_javap(&self, class_name: &DotId, span: Span) -> anyhow::Result { 52 | let mut command = Command::new(self.configuration.bin_path("javap")); 53 | 54 | if let Some(classpath) = self.configuration.classpath() { 55 | command.arg("-cp").arg(classpath); 56 | } 57 | 58 | command.arg("-p").arg(format!("{}", class_name)); 59 | 60 | let output_or_err = command.output(); 61 | 62 | let output = match output_or_err { 63 | Ok(o) => o, 64 | Err(err) => { 65 | bail!("failed to execute `{command:?}`: {err}") 66 | } 67 | }; 68 | 69 | if !output.status.success() { 70 | bail!( 71 | "unsuccessful execution of `{command:?}` (exit status: {}): {}", 72 | output.status, 73 | String::from_utf8(output.stderr).unwrap_or(String::from("error")) 74 | ); 75 | } 76 | 77 | let s = match String::from_utf8(output.stdout) { 78 | Ok(o) => o, 79 | Err(err) => { 80 | bail!("failed to parse output of `{command:?}` as utf-8: {err}") 81 | } 82 | }; 83 | 84 | let ci = ClassInfo::parse(&s, span)?; 85 | Ok(JavapClassInfo::from(ci)) 86 | } 87 | 88 | pub fn reflect_and_cache( 89 | // consider using a concurrent map and using `&self` instead. This will eventually allow parallizing the javap invocations 90 | &mut self, 91 | class_name: &DotId, 92 | span: Span, 93 | ) -> syn::Result> { 94 | if let Some(ci) = self.classes.get(class_name) { 95 | return Ok(Arc::clone(ci)); 96 | } 97 | 98 | let class_info = self 99 | .reflect_via_javap(class_name, span) 100 | .map_err(|err| syn::Error::new(span, format!("{}", err)))?; 101 | 102 | let ci = Arc::new(class_info); 103 | 104 | self.classes.insert(class_name.clone(), Arc::clone(&ci)); 105 | 106 | Ok(ci) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/cast.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::Jvm; 4 | use crate::{jvm::JavaObjectExt, refs::AsJRef, JavaObject, JvmOp, Local, TryJDeref}; 5 | 6 | /// A trait to represent safe upcast operations for a [`JavaObject`]. 7 | /// 8 | /// # Safety 9 | /// 10 | /// Inherits the rules of [`JavaObject`], but also `S` must be a valid superclass or implemented interface of `Self`. 11 | /// XX: would this actually allow unsafe behavior in a JNI call? or is it already checked/enforced? 12 | /// 13 | /// XX: having to impl `Upcast` for T on each struct is pretty annoying to get `AsJRef` to work without conflicts 14 | pub unsafe trait Upcast: JavaObject {} 15 | 16 | #[derive_where::derive_where(Clone)] 17 | #[derive_where(Copy; J: Copy)] 18 | pub struct TryDowncast { 19 | op: J, 20 | _marker: PhantomData, 21 | } 22 | 23 | impl TryDowncast 24 | where 25 | J: JvmOp, 26 | for<'jvm> J::Output<'jvm>: TryJDeref, 27 | To: for<'jvm> Upcast< as TryJDeref>::Java>, 28 | { 29 | pub(crate) fn new(op: J) -> Self { 30 | Self { 31 | op, 32 | _marker: PhantomData, 33 | } 34 | } 35 | } 36 | 37 | impl JvmOp for TryDowncast 38 | where 39 | J: JvmOp, 40 | for<'jvm> J::Output<'jvm>: TryJDeref, 41 | To: for<'jvm> Upcast< as TryJDeref>::Java>, 42 | { 43 | type Output<'jvm> = Result, J::Output<'jvm>>; 44 | 45 | fn do_jni<'jvm>(self, jvm: &mut Jvm<'jvm>) -> crate::LocalResult<'jvm, Self::Output<'jvm>> { 46 | let instance = self.op.do_jni(jvm)?; 47 | let instance_raw = instance.try_jderef()?.as_raw(); 48 | 49 | let class = To::class(jvm)?; 50 | let class_raw = class.as_raw(); 51 | 52 | let env = jvm.env(); 53 | let is_inst = unsafe { 54 | env.invoke_unchecked( 55 | |env| env.IsInstanceOf, 56 | |env, f| f(env, instance_raw.as_ptr(), class_raw.as_ptr()), 57 | ) == jni_sys::JNI_TRUE 58 | }; 59 | 60 | if is_inst { 61 | // SAFETY: just shown that jobject instanceof To::class 62 | let casted = unsafe { std::mem::transmute::<&_, &To>(instance.try_jderef()?) }; 63 | Ok(Ok(jvm.local(casted))) 64 | } else { 65 | Ok(Err(instance)) 66 | } 67 | } 68 | } 69 | 70 | #[derive_where::derive_where(Clone)] 71 | #[derive_where(Copy; J: Copy)] 72 | pub struct AsUpcast { 73 | op: J, 74 | _marker: PhantomData, 75 | } 76 | 77 | impl AsUpcast 78 | where 79 | J: JvmOp, 80 | for<'jvm> J::Output<'jvm>: AsJRef, 81 | To: JavaObject, 82 | { 83 | pub(crate) fn new(op: J) -> Self { 84 | Self { 85 | op, 86 | _marker: PhantomData, 87 | } 88 | } 89 | } 90 | 91 | impl JvmOp for AsUpcast 92 | where 93 | J: JvmOp, 94 | for<'jvm> J::Output<'jvm>: AsJRef, 95 | To: JavaObject, 96 | { 97 | type Output<'jvm> = Local<'jvm, To>; 98 | 99 | fn do_jni<'jvm>(self, jvm: &mut Jvm<'jvm>) -> crate::LocalResult<'jvm, Self::Output<'jvm>> { 100 | let instance = self.op.do_jni(jvm)?; 101 | 102 | if cfg!(debug_assertions) { 103 | let class = To::class(jvm)?; 104 | let class_raw = class.as_raw(); 105 | 106 | let instance_raw = instance.as_jref()?.as_raw(); 107 | assert!(unsafe { 108 | jvm.env().invoke_unchecked( 109 | |env| env.IsInstanceOf, 110 | |env, f| f(env, instance_raw.as_ptr(), class_raw.as_ptr()), 111 | ) == jni_sys::JNI_TRUE 112 | }); 113 | } 114 | 115 | // Safety: From: Upcast 116 | Ok(jvm.local(instance.as_jref()?)) 117 | } 118 | } 119 | --------------------------------------------------------------------------------