diff --git a/build.sbt b/build.sbt index 5afc79b..581558e 100644 --- a/build.sbt +++ b/build.sbt @@ -51,9 +51,10 @@ lazy val root = project println(Seq("sh", "-c", cmd) !!) }, cargoBuild := { - val cmd = - Seq("sh", "-c", "cd glue; cargo rustc -- -Awarnings") - println(cmd !!) + println(Seq("sh", "-c", "cd glue; cargo rustc -- -Awarnings") !!) + println( + Seq("sh", "-c", "cd glue; cargo test --quiet") !! + ) // very important: test suite validates interfaces }, fullBuild := { cargoBuild.value diff --git a/glue/src/lib.rs b/glue/src/lib.rs index 8050291..cf13d15 100644 --- a/glue/src/lib.rs +++ b/glue/src/lib.rs @@ -18,7 +18,6 @@ macro_rules! package { (ext $start:ident.$($thing:ident).+) => {concat!(stringify!($start), $("/", stringify!($thing)),+)}; } -pub mod uint; pub mod message_code; pub mod message_opt_ref; pub mod message_opt_value_ref; @@ -26,6 +25,7 @@ pub mod message_ref; pub mod message_type; pub mod retry_strategy; pub mod runtime_config; +pub mod uint; // Class: dev_toad_Runtime // Method: init @@ -39,13 +39,30 @@ pub unsafe extern "system" fn Java_dev_toad_Runtime_init<'local>(mut env: JNIEnv #[cfg(test)] mod tests { - use jni::{InitArgsBuilder, JavaVM}; + use std::sync::Once; + + use jni::{InitArgsBuilder, JNIEnv, JavaVM}; use toad::retry::Strategy; use toad::time::Millis; use crate::retry_strategy::RetryStrategy; use crate::runtime_config::RuntimeConfig; + static INIT: Once = Once::new(); + pub fn init<'a>() -> JNIEnv<'a> { + INIT.call_once(|| { + let jvm = + JavaVM::new(InitArgsBuilder::new().option("--enable-preview") + .option("-Djava.class.path=../target/scala-3.2.2/classes/") + .build() + .unwrap()).unwrap(); + toad_jni::global::init_with(jvm); + }); + + toad_jni::global::jvm().attach_current_thread_permanently(); + toad_jni::global::env() + } + #[test] fn package() { assert_eq!(package!(dev.toad.msg.Foo.Bar.Baz), @@ -54,20 +71,18 @@ mod tests { } #[test] - fn jvm_tests() { - let jvm = - JavaVM::new(InitArgsBuilder::new().option("--enable-preview") - .option("-Djava.class.path=../target/scala-3.2.2/classes/") - .build() - .unwrap()).unwrap(); - jvm.attach_current_thread_permanently(); - toad_jni::global::init_with(jvm); - - let mut e = toad_jni::global::env(); + fn runtime_config() { + let mut e = init(); let e = &mut e; let r = RuntimeConfig::new(e); assert_eq!(r.to_toad(e), Default::default()); + } + + #[test] + fn retry_strategy() { + let mut e = init(); + let e = &mut e; let r = Strategy::Exponential { init_min: Millis::new(0), init_max: Millis::new(100) }; @@ -77,4 +92,24 @@ mod tests { max: Millis::new(100) }; assert_eq!(RetryStrategy::from_toad(e, r).to_toad(e), r); } + + #[test] + fn uint() { + use crate::uint; + + let mut e = init(); + let e = &mut e; + + macro_rules! case { + ($u:ident) => {{ + assert_eq!(uint::$u::from_rust(e, $u::MAX).to_rust(e), $u::MAX); + assert_eq!(uint::$u::from_rust(e, 0).to_rust(e), 0); + }}; + } + + case!(u64); + case!(u32); + case!(u16); + case!(u8); + } } diff --git a/glue/src/retry_strategy.rs b/glue/src/retry_strategy.rs index 393ae4c..6a34981 100644 --- a/glue/src/retry_strategy.rs +++ b/glue/src/retry_strategy.rs @@ -6,6 +6,8 @@ use toad_jni::cls::java; use toad_jni::convert::Object; use toad_jni::Sig; +use crate::uint; + pub struct RetryStrategy(GlobalRef); impl RetryStrategy { @@ -29,13 +31,13 @@ impl RetryStrategy { } pub fn millis_field<'a>(&self, e: &mut JNIEnv<'a>, key: &str) -> Millis { - let o = e.get_field(&self.0, key, Sig::class(java::time::Duration::PATH)) + let o = e.get_field(&self.0, key, Sig::class(uint::u64::PATH)) .unwrap() .l() .unwrap(); let g = e.new_global_ref(o).unwrap(); - let d = java::time::Duration::from_java(g); - Millis::new(d.to_millis(e) as u64) + let d = uint::u64::from_java(g); + Millis::new(d.to_rust(e)) } pub fn to_toad<'a>(self, e: &mut JNIEnv<'a>) -> Strategy { diff --git a/glue/src/uint.rs b/glue/src/uint.rs index c56040a..99940e1 100644 --- a/glue/src/uint.rs +++ b/glue/src/uint.rs @@ -1,58 +1,139 @@ -use jni::{JNIEnv, objects::{JObject, JByteArray}}; +use core::primitive as rust; + +use jni::objects::{GlobalRef, JByteArray, JObject}; +use jni::JNIEnv; +use toad_jni::cls::java; +use toad_jni::convert::Object; use toad_jni::Sig; -pub mod path { - pub const U64: &'static str = package!(dev.toad.ffi.u64); - pub const U32: &'static str = package!(dev.toad.ffi.u32); - pub const U16: &'static str = package!(dev.toad.ffi.u16); - pub const U8: &'static str = package!(dev.toad.ffi.u8); +#[allow(non_camel_case_types)] +pub struct u64(GlobalRef); +impl u64 { + pub const PATH: &'static str = package!(dev.toad.ffi.u64); + pub const CTOR: Sig = Sig::new().arg(Sig::class(java::math::BigInteger::PATH)) + .returning(Sig::VOID); + pub const BIGINT_VALUE: Sig = Sig::new().returning(Sig::class(java::math::BigInteger::PATH)); + + pub fn to_rust<'a>(&self, e: &mut JNIEnv<'a>) -> rust::u64 { + let bi = e.call_method(self.0.as_obj(), "bigintValue", Self::BIGINT_VALUE, &[]) + .unwrap() + .l() + .unwrap(); + let bi = e.new_global_ref(bi).unwrap(); + let bi = java::math::BigInteger::from_java(bi); + bi.to_i128(e) as rust::u64 + } + + pub fn from_rust<'a>(e: &mut JNIEnv<'a>, u: rust::u64) -> Self { + let bi = java::math::BigInteger::from_be_bytes(e, &i128::from(u).to_be_bytes()); + let bi = e.new_object(Self::PATH, Self::CTOR, &[bi.to_java().as_obj().into()]) + .unwrap(); + Self(e.new_global_ref(bi).unwrap()) + } } -pub fn u64<'a>(e: &mut JNIEnv<'a>, o: JObject<'a>) -> u64 { - let bi = e.call_method(o, "bigintValue", Sig::new().returning(Sig::class("java.math.BigInteger")), &[]).unwrap().l().unwrap(); - let barr: JByteArray<'a> = e.call_method(bi, "toByteArray", Sig::new().returning(Sig::array_of(Sig::BYTE)), &[]).unwrap().l().unwrap().try_into().unwrap(); +impl Object for u64 { + fn from_java(jobj: GlobalRef) -> Self { + Self(jobj) + } - let mut bytes = [0i8; 8]; - - // BigInteger is a growable two's complement integer - // - // the "growable" comes from its backing structure being a simple - // int array `int[]`, where bytes are added as needed to afford capacity. - // - // two's-complement means the most significant bit (the first bit of the first byte) - // indicates the sign of the integer, where 0 is positive and 1 is negative. - // - // The rest of the bits are unchanged, meaning the range is from `-(2^(n - 1))` - // to `2^(n - 1) - 1`. - // - // For example, a two's complement i8 would be able to represent `-128` (`0b11111111`), - // `0` (`0b00000000`) to `127` (`0b01111111`). for positive integers, the representation is - // the same as unsigned integers, meaning we simply need to make sure we don't accidentally - // interpret the first bit as part of the integer. - // - // Here we assume whoever is responsible for BigInteger made sure that it's positive, - // so converting the big-endian two's complement int - e.get_byte_array_region(&barr, 0, &mut bytes).unwrap(); - - // https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/math/BigInteger.html#toByteArray() - // - // BigInt.toByteArray actually returns the raw byte representation of the integer, NOT - // two's complement `byte`s as the type signature would lead you to believe. - // - // To interpret these bytes as i8 is incorrect. - let bytes = bytes.map(|i| i8::to_be_bytes(i)[0]); - - u64::from_be_bytes(bytes) + fn to_java(self) -> GlobalRef { + self.0 + } } -pub fn u32<'a>(e: &mut JNIEnv<'a>, o: JObject<'a>) -> u32 { - e.call_method(o, "longValue", Sig::new().returning(Sig::LONG), &[]).unwrap().j().unwrap() as u32 +#[allow(non_camel_case_types)] +pub struct u32(GlobalRef); +impl u32 { + pub const PATH: &'static str = package!(dev.toad.ffi.u32); + pub const CTOR: Sig = Sig::new().arg(Sig::LONG).returning(Sig::VOID); + pub const LONG_VALUE: Sig = Sig::new().returning(Sig::LONG); + + pub fn to_rust<'a>(&self, e: &mut JNIEnv<'a>) -> rust::u32 { + let long = e.call_method(self.0.as_obj(), "longValue", Self::LONG_VALUE, &[]) + .unwrap() + .j() + .unwrap(); + long as rust::u32 + } + + pub fn from_rust<'a>(e: &mut JNIEnv<'a>, u: rust::u32) -> Self { + let bi = e.new_object(Self::PATH, Self::CTOR, &[rust::i64::from(u).into()]) + .unwrap(); + Self(e.new_global_ref(bi).unwrap()) + } } -pub fn u16<'a>(e: &mut JNIEnv<'a>, o: JObject<'a>) -> u16 { - e.call_method(o, "intValue", Sig::new().returning(Sig::INT), &[]).unwrap().i().unwrap() as u16 +impl Object for u32 { + fn from_java(jobj: GlobalRef) -> Self { + Self(jobj) + } + + fn to_java(self) -> GlobalRef { + self.0 + } } -pub fn u8<'a>(e: &mut JNIEnv<'a>, o: JObject<'a>) -> u8 { - e.call_method(o, "shortValue", Sig::new().returning(Sig::SHORT), &[]).unwrap().s().unwrap() as u8 +#[allow(non_camel_case_types)] +pub struct u16(GlobalRef); +impl u16 { + pub const PATH: &'static str = package!(dev.toad.ffi.u16); + pub const CTOR: Sig = Sig::new().arg(Sig::INT).returning(Sig::VOID); + pub const INT_VALUE: Sig = Sig::new().returning(Sig::INT); + + pub fn to_rust<'a>(&self, e: &mut JNIEnv<'a>) -> rust::u16 { + let int = e.call_method(self.0.as_obj(), "intValue", Self::INT_VALUE, &[]) + .unwrap() + .i() + .unwrap(); + int as rust::u16 + } + + pub fn from_rust<'a>(e: &mut JNIEnv<'a>, u: rust::u16) -> Self { + let bi = e.new_object(Self::PATH, Self::CTOR, &[rust::i32::from(u).into()]) + .unwrap(); + Self(e.new_global_ref(bi).unwrap()) + } +} + +impl Object for u16 { + fn from_java(jobj: GlobalRef) -> Self { + Self(jobj) + } + + fn to_java(self) -> GlobalRef { + self.0 + } +} + +#[allow(non_camel_case_types)] +pub struct u8(GlobalRef); +impl u8 { + pub const PATH: &'static str = package!(dev.toad.ffi.u8); + pub const CTOR: Sig = Sig::new().arg(Sig::SHORT).returning(Sig::VOID); + pub const SHORT_VALUE: Sig = Sig::new().returning(Sig::SHORT); + + pub fn to_rust<'a>(&self, e: &mut JNIEnv<'a>) -> rust::u8 { + let int = e.call_method(self.0.as_obj(), "shortValue", Self::SHORT_VALUE, &[]) + .unwrap() + .s() + .unwrap(); + int as rust::u8 + } + + pub fn from_rust<'a>(e: &mut JNIEnv<'a>, u: rust::u8) -> Self { + let bi = e.new_object(Self::PATH, Self::CTOR, &[rust::i16::from(u).into()]) + .unwrap(); + Self(e.new_global_ref(bi).unwrap()) + } +} + +impl Object for u8 { + fn from_java(jobj: GlobalRef) -> Self { + Self(jobj) + } + + fn to_java(self) -> GlobalRef { + self.0 + } } diff --git a/src/main/java/dev.toad/RetryStrategy.java b/src/main/java/dev.toad/RetryStrategy.java index 03437ff..7d61781 100644 --- a/src/main/java/dev.toad/RetryStrategy.java +++ b/src/main/java/dev.toad/RetryStrategy.java @@ -1,3 +1,4 @@ package dev.toad; -public abstract sealed class RetryStrategy permits RetryStrategyDelay, RetryStrategyExponential { } +public abstract sealed class RetryStrategy + permits RetryStrategyDelay, RetryStrategyExponential {} diff --git a/src/main/java/dev.toad/RetryStrategyDelay.java b/src/main/java/dev.toad/RetryStrategyDelay.java index ef2087a..81fec4e 100644 --- a/src/main/java/dev.toad/RetryStrategyDelay.java +++ b/src/main/java/dev.toad/RetryStrategyDelay.java @@ -1,20 +1,31 @@ package dev.toad; +import dev.toad.ffi.*; import java.time.Duration; import java.util.Optional; public final class RetryStrategyDelay extends RetryStrategy { - public final Duration min; - public final Duration max; + public final u64 min; + public final u64 max; private static native RetryStrategyDelay fromRust(byte[] mem); private native byte[] toRust(); public RetryStrategyDelay(Duration min, Duration max) { - this.min = min; - this.max = max; + if (min.isNegative() || max.isNegative()) { + throw new IllegalArgumentException( + String.format( + "{min: %, max: %} neither field may be negative", + min.toMillis(), + max.toMillis() + ) + ); + } + + this.min = new u64(min.toMillis()); + this.max = new u64(max.toMillis()); } @Override diff --git a/src/main/java/dev.toad/RetryStrategyExponential.java b/src/main/java/dev.toad/RetryStrategyExponential.java index 7b979e3..8ce6628 100644 --- a/src/main/java/dev.toad/RetryStrategyExponential.java +++ b/src/main/java/dev.toad/RetryStrategyExponential.java @@ -1,27 +1,39 @@ package dev.toad; +import dev.toad.ffi.*; import java.time.Duration; import java.util.Optional; - public final class RetryStrategyExponential extends RetryStrategy { +public final class RetryStrategyExponential extends RetryStrategy { - public final Duration initMin; - public final Duration initMax; + public final u64 initMin; + public final u64 initMax; - private static native RetryStrategyExponential fromRust(byte[] mem); + private static native RetryStrategyExponential fromRust(byte[] mem); - private native byte[] toRust(); + private native byte[] toRust(); - public RetryStrategyExponential(Duration initMin, Duration initMax) { - this.initMin = initMin; - this.initMax = initMax; + public RetryStrategyExponential(Duration initMin, Duration initMax) { + if (initMin.isNegative() || initMax.isNegative()) { + throw new IllegalArgumentException( + String.format( + "{initMin: %, initMax: %} neither field may be negative", + initMin.toMillis(), + initMax.toMillis() + ) + ); } + this.initMin = new u64(initMin.toMillis()); + this.initMax = new u64(initMax.toMillis()); + } + @Override public boolean equals(Object other) { return switch (other) { - case RetryStrategyExponential e -> e.initMin == this.initMin && e.initMax == this.initMax; + case RetryStrategyExponential e -> e.initMin == this.initMin && + e.initMax == this.initMax; default -> false; }; } - } +} diff --git a/src/main/java/dev.toad/RuntimeOptions.java b/src/main/java/dev.toad/RuntimeOptions.java index c40f63f..6d6849c 100644 --- a/src/main/java/dev.toad/RuntimeOptions.java +++ b/src/main/java/dev.toad/RuntimeOptions.java @@ -51,7 +51,7 @@ public final class RuntimeOptions implements Cloneable { public Net() { this.port = new u16(5683); - this.concurrency = new u8((short)1); + this.concurrency = new u8((short) 1); this.msg = new Msg(); } diff --git a/src/main/java/dev.toad/ffi/u16.java b/src/main/java/dev.toad/ffi/u16.java index f0c4c19..ce4ccc8 100644 --- a/src/main/java/dev.toad/ffi/u16.java +++ b/src/main/java/dev.toad/ffi/u16.java @@ -1,7 +1,8 @@ package dev.toad.ffi; public final class u16 { - public static final int MAX = (int)(Math.pow(2, 16) - 1); + + public static final int MAX = (int) (Math.pow(2, 16) - 1); private final int l; public u16(int l) { diff --git a/src/main/java/dev.toad/ffi/u32.java b/src/main/java/dev.toad/ffi/u32.java index 1f9b70d..c9a56ff 100644 --- a/src/main/java/dev.toad/ffi/u32.java +++ b/src/main/java/dev.toad/ffi/u32.java @@ -1,7 +1,8 @@ package dev.toad.ffi; public final class u32 { - public static final long MAX = (long)(Math.pow(2, 32) - 1); + + public static final long MAX = (long) (Math.pow(2, 32) - 1); private final long l; public u32(long l) { diff --git a/src/main/java/dev.toad/ffi/u64.java b/src/main/java/dev.toad/ffi/u64.java index 050c3d5..149d6c5 100644 --- a/src/main/java/dev.toad/ffi/u64.java +++ b/src/main/java/dev.toad/ffi/u64.java @@ -1,17 +1,24 @@ package dev.toad.ffi; +import java.math.BigInteger; + public final class u64 { - public static final double MAX = Math.pow(2, 64) - 1; - private final double l; - public u64(double l) { + public static final BigInteger MAX = BigInteger.TWO + .pow(64) + .subtract(BigInteger.ONE); + private final BigInteger l; + + public u64(BigInteger l) { uint.assertWithinRange(this.MAX, l); - uint.assertNatural(l); - this.l = l; } - public double doubleValue() { + public u64(long l) { + this(BigInteger.valueOf(l)); + } + + public BigInteger bigintValue() { return this.l; } } diff --git a/src/main/java/dev.toad/ffi/u8.java b/src/main/java/dev.toad/ffi/u8.java index c7649bb..3663e01 100644 --- a/src/main/java/dev.toad/ffi/u8.java +++ b/src/main/java/dev.toad/ffi/u8.java @@ -1,7 +1,8 @@ package dev.toad.ffi; public final class u8 { - public static final short MAX = (short)(Math.pow(2, 8) - 1); + + public static final short MAX = (short) (Math.pow(2, 8) - 1); private final short l; public u8(short l) { diff --git a/src/main/java/dev.toad/ffi/uint.java b/src/main/java/dev.toad/ffi/uint.java index eb63dc7..e97a484 100644 --- a/src/main/java/dev.toad/ffi/uint.java +++ b/src/main/java/dev.toad/ffi/uint.java @@ -1,15 +1,18 @@ package dev.toad.ffi; +import java.math.BigInteger; + public class uint { - public static void assertWithinRange(double max, double n) { - if (n < 0 || n > max) { - throw new IllegalArgumentException(String.format("% must be between 0 and %", n, max)); - } + + public static void assertWithinRange(long max, long n) { + uint.assertWithinRange(BigInteger.valueOf(max), BigInteger.valueOf(n)); } - public static void assertNatural(double n) { - if (n % 1 > 0.0) { - throw new IllegalArgumentException(String.format("% must be a whole integer", n)); + public static void assertWithinRange(BigInteger max, BigInteger n) { + if (n.compareTo(BigInteger.ZERO) < 0 || n.compareTo(max) > 0) { + throw new IllegalArgumentException( + n.toString() + " must be between 0 and " + max.toString() + ); } } }