feat: ffi uint types

This commit is contained in:
Orion Kindel 2023-04-05 10:08:25 -07:00
parent 42c30b84dd
commit 8eee5f3827
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
13 changed files with 252 additions and 96 deletions

View File

@ -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

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -1,3 +1,4 @@
package dev.toad;
public abstract sealed class RetryStrategy permits RetryStrategyDelay, RetryStrategyExponential { }
public abstract sealed class RetryStrategy
permits RetryStrategyDelay, RetryStrategyExponential {}

View File

@ -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

View File

@ -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;
};
}
}
}

View File

@ -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();
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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()
);
}
}
}