diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 9de1441e..a36a57e8 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -26,10 +26,12 @@ dependencies { implementation("commons-net:commons-net:3.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("info.picocli:picocli:4.7.4") + implementation("com.google.guava:guava:33.3.1-jre") } tasks { val examples = listOf( + "ZBytesExamples", "ZDelete", "ZGet", "ZGetLiveliness", diff --git a/examples/src/main/java/io/zenoh/ZBytesExamples.java b/examples/src/main/java/io/zenoh/ZBytesExamples.java new file mode 100644 index 00000000..2982e2ec --- /dev/null +++ b/examples/src/main/java/io/zenoh/ZBytesExamples.java @@ -0,0 +1,192 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh; + +import io.zenoh.bytes.ZBytes; +import io.zenoh.ext.ZDeserializer; +import io.zenoh.ext.ZSerializer; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class ZBytesExamples { + + public static void main(String[] args) { + + /* + * ZBytes + * + * A ZBytes instance can be created from a String and from a Byte Array with the `ZBytes.from(string: String)` + * and `ZBytes.from(bytes: byte[])` functions. + * + * A ZBytes can be converted back into a [String] with the functions [ZBytes.toString] and [ZBytes.tryToString]. + * Similarly, with [ZBytes.toBytes] you can get the inner byte representation. + */ + + var exampleString = "example string"; + var zbytesA = ZBytes.from(exampleString); + var outputA = zbytesA.toString(); + assert exampleString.equals(outputA); + + var exampleBytes = new byte[]{1, 2, 3, 4, 5}; + var zbytesB = ZBytes.from(exampleBytes); + var outputB = zbytesB.toBytes(); + assert Arrays.equals(exampleBytes, outputB); + + /* + * Serialization and deserialization. + * + * Additionally, the Zenoh API provides a series of serialization and deserialization utilities for processing + * the received payloads. + * + * Serialization and deserialization supports the following types: + * - Boolean + * - Byte + * - Byte Array + * - Short + * - Int + * - Long + * - Float + * - Double + * - String + * - List + * - Map + * + * For List and Map, the inner types can be a combination of the above types, including themselves. + * + * These serialization and deserialization utilities can be used across the Zenoh ecosystem with Zenoh + * versions based on other supported languages such as Rust, Python, C and C++. + * This works when the types are equivalent (a `Byte` corresponds to an `i8` in Rust, a `Short` to an `i16`, etc). + */ + + // Boolean example + Boolean input1 = true; + ZSerializer serializer1 = new ZSerializer<>() {}; + ZBytes zbytes1 = serializer1.serialize(input1); + + ZDeserializer deserializer1 = new ZDeserializer<>() {}; + Boolean output1 = deserializer1.deserialize(zbytes1); + assert input1.equals(output1); + + // Byte example + Byte input2 = 126; + ZSerializer serializer2 = new ZSerializer<>() {}; + ZBytes zbytes2 = serializer2.serialize(input2); + + ZDeserializer deserializer2 = new ZDeserializer<>() {}; + Byte output2 = deserializer2.deserialize(zbytes2); + assert input2.equals(output2); + + // Short example + Short input3 = 256; + ZSerializer serializer3 = new ZSerializer<>() {}; + ZBytes zbytes3 = serializer3.serialize(input3); + + ZDeserializer deserializer3 = new ZDeserializer<>() {}; + Short output3 = deserializer3.deserialize(zbytes3); + assert input3.equals(output3); + + // Int example + Integer input4 = 123456; + ZSerializer serializer4 = new ZSerializer<>() {}; + ZBytes zbytes4 = serializer4.serialize(input4); + + ZDeserializer deserializer4 = new ZDeserializer<>() {}; + Integer output4 = deserializer4.deserialize(zbytes4); + assert input4.equals(output4); + + // Long example + Long input5 = 123456789L; + ZSerializer serializer5 = new ZSerializer<>() {}; + ZBytes zbytes5 = serializer5.serialize(input5); + + ZDeserializer deserializer5 = new ZDeserializer<>() {}; + Long output5 = deserializer5.deserialize(zbytes5); + assert input5.equals(output5); + + // Float example + Float input6 = 123.45f; + ZSerializer serializer6 = new ZSerializer<>() {}; + ZBytes zbytes6 = serializer6.serialize(input6); + + ZDeserializer deserializer6 = new ZDeserializer<>() {}; + Float output6 = deserializer6.deserialize(zbytes6); + assert input6.equals(output6); + + // Double example + Double input7 = 12345.6789; + ZSerializer serializer7 = new ZSerializer<>() {}; + ZBytes zbytes7 = serializer7.serialize(input7); + + ZDeserializer deserializer7 = new ZDeserializer<>() {}; + Double output7 = deserializer7.deserialize(zbytes7); + assert input7.equals(output7); + + // List example + List input12 = List.of(1, 2, 3, 4, 5); + ZSerializer> serializer12 = new ZSerializer<>() {}; + ZBytes zbytes12 = serializer12.serialize(input12); + + ZDeserializer> deserializer12 = new ZDeserializer<>() {}; + List output12 = deserializer12.deserialize(zbytes12); + assert input12.equals(output12); + + // String example + String input13 = "Hello, World!"; + ZSerializer serializer13 = new ZSerializer<>() {}; + ZBytes zbytes13 = serializer13.serialize(input13); + + ZDeserializer deserializer13 = new ZDeserializer<>() {}; + String output13 = deserializer13.deserialize(zbytes13); + assert input13.equals(output13); + + // ByteArray example + byte[] input14 = new byte[]{1, 2, 3, 4, 5}; + ZSerializer serializer14 = new ZSerializer<>() {}; + ZBytes zbytes14 = serializer14.serialize(input14); + + ZDeserializer deserializer14 = new ZDeserializer<>() {}; + byte[] output14 = deserializer14.deserialize(zbytes14); + assert Arrays.equals(input14, output14); + + // Map example + Map input15 = Map.of("one", 1, "two", 2, "three", 3); + ZSerializer> serializer15 = new ZSerializer<>() {}; + ZBytes zbytes15 = serializer15.serialize(input15); + + ZDeserializer> deserializer15 = new ZDeserializer<>() {}; + Map output15 = deserializer15.deserialize(zbytes15); + assert input15.equals(output15); + + // Nested List example + List> input18 = List.of(List.of(1, 2, 3)); + ZSerializer>> serializer18 = new ZSerializer<>() {}; + ZBytes zbytes18 = serializer18.serialize(input18); + + ZDeserializer>> deserializer18 = new ZDeserializer<>() {}; + List> output18 = deserializer18.deserialize(zbytes18); + assert input18.equals(output18); + + // Combined types example + List> input19 = List.of(Map.of("a", 1, "b", 2)); + ZSerializer>> serializer19 = new ZSerializer<>() {}; + ZBytes zbytes19 = serializer19.serialize(input19); + + ZDeserializer>> deserializer19 = new ZDeserializer<>() {}; + List> output19 = deserializer19.deserialize(zbytes19); + assert input19.equals(output19); + } +} diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index 5ad5203b..12fde33a 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -63,6 +63,7 @@ kotlin { val commonMain by getting { dependencies { implementation("commons-net:commons-net:3.9.0") + implementation("com.google.guava:guava:33.3.1-jre") } } val commonTest by getting { diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt new file mode 100644 index 00000000..bbae67d2 --- /dev/null +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt @@ -0,0 +1,110 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh.ext + +import com.google.common.reflect.TypeToken +import io.zenoh.bytes.ZBytes +import io.zenoh.jni.JNIZBytes + +/** + * Zenoh deserializer. + * + * This class is a utility for deserializing [ZBytes] into elements of type [T]. + * + * This class supports the following types: + * - [Boolean] + * - [Byte] + * - [Short] + * - [Int] + * - [Long] + * - [Float] + * - [Double] + * - [List] + * - [String] + * - [ByteArray] + * - [Map] + * + * For List and Map, the inner types can be a combination of the above types, including themselves. + * + * Due to Java's type erasure, an actual implementation of this abstract class needs to be created (see the examples below). + * + * This deserialization utility can be used across the Zenoh ecosystem with Zenoh + * versions based on other supported languages such as Rust, Python, C and C++. + * This works when the types are equivalent (a `Byte` corresponds to an `i8` in Rust, a `Short` to an `i16`, etc). + * + * # Examples + * + * Example for a basic type, in this case an integer: + * ```java + * Integer input = 123456; + * ZSerializer serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input); + * + * ZDeserializer deserializer = new ZDeserializer<>() {}; + * Integer output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * Examples for parameterized types: + * - List + * ```java + * List input = List.of(1, 2, 3, 4, 5); + * ZSerializer> serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input12); + * + * ZDeserializer> deserializer = new ZDeserializer<>() {}; + * List output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * - Map + * ```java + * Map input = Map.of("one", 1, "two", 2, "three", 3); + * ZSerializer> serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input); + * + * ZDeserializer> deserializer = new ZDeserializer<>() {}; + * Map output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * As mentioned, for List and Map, the inner types can be a combination of the above types, including themselves. + * Here's an example with a List of Maps: + * ```java + * List> input = List.of(Map.of("a", 1, "b", 2)); + * ZSerializer>> serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input); + * + * ZDeserializer>> deserializer = new ZDeserializer<>() {}; + * List> output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * For more examples, see the ZBytesExamples in the examples. + * + * @param T The deserialization type. + * @see ZBytes + * @see ZSerializer + */ +abstract class ZDeserializer: TypeToken() { + + /** + * Deserialize the [zbytes] into an element of type [T]. + */ + fun deserialize(zbytes: ZBytes): T { + @Suppress("UNCHECKED_CAST") + return JNIZBytes.deserializeViaJNI(zbytes, this.type) as T + } +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt new file mode 100644 index 00000000..8ee4a1b7 --- /dev/null +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt @@ -0,0 +1,109 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh.ext + +import com.google.common.reflect.TypeToken +import io.zenoh.bytes.ZBytes +import io.zenoh.jni.JNIZBytes + +/** + * Zenoh serializer. + * + * This class is a utility for serializing elements of type [T] into [ZBytes]. + * + * This class supports the following types: + * - [Boolean] + * - [Byte] + * - [Short] + * - [Int] + * - [Long] + * - [Float] + * - [Double] + * - [List] + * - [String] + * - [ByteArray] + * - [Map] + * + * For List and Map, the inner types can be a combination of the above types, including themselves. + * + * Due to Java's type erasure, an actual implementation of this abstract class needs to be created (see the examples below). + * + * This serialization utility can be used across the Zenoh ecosystem with Zenoh + * versions based on other supported languages such as Rust, Python, C and C++. + * This works when the types are equivalent (a `Byte` corresponds to an `i8` in Rust, a `Short` to an `i16`, etc). + * + * # Examples + * + * Example for a basic type, in this case an integer: + * ```java + * Integer input = 123456; + * ZSerializer serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input); + * + * ZDeserializer deserializer = new ZDeserializer<>() {}; + * Integer output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * Examples for parameterized types: + * - List + * ```java + * List input = List.of(1, 2, 3, 4, 5); + * ZSerializer> serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input12); + * + * ZDeserializer> deserializer = new ZDeserializer<>() {}; + * List output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * - Map + * ```java + * Map input = Map.of("one", 1, "two", 2, "three", 3); + * ZSerializer> serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input); + * + * ZDeserializer> deserializer = new ZDeserializer<>() {}; + * Map output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * As mentioned, for List and Map, the inner types can be a combination of the above types, including themselves. + * Here's an example with a List of Maps: + * ```java + * List> input = List.of(Map.of("a", 1, "b", 2)); + * ZSerializer>> serializer = new ZSerializer<>() {}; + * ZBytes zbytes = serializer.serialize(input); + * + * ZDeserializer>> deserializer = new ZDeserializer<>() {}; + * List> output = deserializer.deserialize(zbytes); + * assert input.equals(output); + * ``` + * + * For more examples, see the ZBytesExamples in the examples. + * + * @param T The type to be serialized. + * @see ZBytes + * @see ZDeserializer + */ +abstract class ZSerializer: TypeToken() { + + /** + * Serialize [t] into a [ZBytes]. + */ + fun serialize(t: T): ZBytes { + return JNIZBytes.serializeViaJNI(t as Any, this.type) + } +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt new file mode 100644 index 00000000..799b9cac --- /dev/null +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt @@ -0,0 +1,33 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh.jni + +import io.zenoh.ZenohLoad +import io.zenoh.bytes.ZBytes +import java.lang.reflect.Type + +@PublishedApi +internal object JNIZBytes { + + init { + ZenohLoad + } + + @JvmStatic + external fun serializeViaJNI(any: Any, type: Type): ZBytes + + @JvmStatic + external fun deserializeViaJNI(zBytes: ZBytes, type: Type): Any +} diff --git a/zenoh-java/src/jvmTest/java/io/zenoh/LivelinessTest.java b/zenoh-java/src/jvmTest/java/io/zenoh/LivelinessTest.java index a43741c6..967b09a7 100644 --- a/zenoh-java/src/jvmTest/java/io/zenoh/LivelinessTest.java +++ b/zenoh-java/src/jvmTest/java/io/zenoh/LivelinessTest.java @@ -35,6 +35,7 @@ public void getLivelinessTest() throws ZError, InterruptedException { var keyExpr = KeyExpr.tryFrom("test/liveliness"); LivelinessToken token = sessionA.liveliness().declareToken(keyExpr); + Thread.sleep(1000); Reply[] receivedReply = new Reply[1]; sessionB.liveliness().get(KeyExpr.tryFrom("test/**"), reply -> receivedReply[0] = reply); @@ -55,7 +56,7 @@ public void livelinessSubscriberTest() throws ZError, InterruptedException { Sample[] receivedSample = new Sample[1]; var subscriber = sessionA.liveliness().declareSubscriber(KeyExpr.tryFrom("test/**"), sample -> receivedSample[0] = sample); - + Thread.sleep(1000); var token = sessionB.liveliness().declareToken(KeyExpr.tryFrom("test/liveliness")); Thread.sleep(1000); diff --git a/zenoh-java/src/jvmTest/java/io/zenoh/ZBytesTests.java b/zenoh-java/src/jvmTest/java/io/zenoh/ZBytesTests.java new file mode 100644 index 00000000..5c0bedf3 --- /dev/null +++ b/zenoh-java/src/jvmTest/java/io/zenoh/ZBytesTests.java @@ -0,0 +1,200 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh; + +import io.zenoh.ext.ZDeserializer; +import io.zenoh.ext.ZSerializer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(JUnit4.class) +public class ZBytesTests { + + /*********************************************** + * Standard serialization and deserialization. * + ***********************************************/ + + @Test + public void testIntSerializationAndDeserialization() { + int intInput = 1234; + var serializer = new ZSerializer() {}; + var payload = serializer.serialize(intInput); + + var deserializer = new ZDeserializer() {}; + int intOutput = deserializer.deserialize(payload); + assertEquals(intInput, intOutput); + } + + @Test + public void testFloatSerializationAndDeserialization() { + float floatInput = 3.1415f; + + ZSerializer serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(floatInput); + + ZDeserializer deserializer = new ZDeserializer<>() {}; + float floatOutput = deserializer.deserialize(payload); + + assertEquals(floatInput, floatOutput, 0.0001); + } + + @Test + public void testStringSerializationAndDeserialization() { + String stringInput = "example"; + + ZSerializer serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(stringInput); + + ZDeserializer deserializer = new ZDeserializer<>() {}; + String stringOutput = deserializer.deserialize(payload); + + assertEquals(stringInput, stringOutput); + } + + @Test + public void testByteArraySerializationAndDeserialization() { + byte[] byteArrayInput = "example".getBytes(); + + ZSerializer serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(byteArrayInput); + + ZDeserializer deserializer = new ZDeserializer<>() {}; + byte[] byteArrayOutput = deserializer.deserialize(payload); + + assertTrue(Arrays.equals(byteArrayInput, byteArrayOutput)); + } + + @Test + public void testListOfStringsSerializationAndDeserialization() { + List inputList = List.of("sample1", "sample2", "sample3"); + + ZSerializer> serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(inputList); + + ZDeserializer> deserializer = new ZDeserializer<>() {}; + List outputList = deserializer.deserialize(payload); + + assertEquals(inputList, outputList); + } + + @Test + public void testListOfByteArraysSerializationAndDeserialization() { + List inputList = Stream.of("sample1", "sample2", "sample3") + .map(String::getBytes) + .collect(Collectors.toList()); + + ZSerializer> serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(inputList); + + ZDeserializer> deserializer = new ZDeserializer<>() {}; + List outputList = deserializer.deserialize(payload); + + assertTrue(compareByteArrayLists(inputList, outputList)); + } + + @Test + public void testMapOfStringsSerializationAndDeserialization() { + Map inputMap = Map.of("key1", "value1", "key2", "value2", "key3", "value3"); + + // Create serializer + ZSerializer> serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(inputMap); + + // Create deserializer + ZDeserializer> deserializer = new ZDeserializer<>() {}; + Map outputMap = deserializer.deserialize(payload); + + assertEquals(inputMap, outputMap); + } + + /********************************************** + * Additional test cases for new Kotlin types * + **********************************************/ + + @Test + public void testBooleanSerializationAndDeserialization() { + boolean booleanInput = true; + + // Create serializer + ZSerializer serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(booleanInput); + + // Create deserializer + ZDeserializer deserializer = new ZDeserializer<>() {}; + boolean booleanOutput = deserializer.deserialize(payload); + + assertEquals(booleanInput, booleanOutput); + } + + /********************************************** + * Tests for collections with new types * + **********************************************/ + + @Test + public void testListOfBooleansSerializationAndDeserialization() { + List listBooleanInput = List.of(true, false, true); + + // Create serializer + ZSerializer> serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(listBooleanInput); + + // Create deserializer + ZDeserializer> deserializer = new ZDeserializer<>() {}; + List listBooleanOutput = deserializer.deserialize(payload); + + assertEquals(listBooleanInput, listBooleanOutput); + } + + @Test + public void testMapOfStringToListOfIntSerializationAndDeserialization() { + Map> mapOfListInput = Map.of("numbers", List.of(1, 2, 3, 4, 5)); + + // Create serializer + ZSerializer>> serializer = new ZSerializer<>() {}; + var payload = serializer.serialize(mapOfListInput); + + // Create deserializer + ZDeserializer>> deserializer = new ZDeserializer<>() {}; + Map> mapOfListOutput = deserializer.deserialize(payload); + + assertEquals(mapOfListInput, mapOfListOutput); + } + + /***************** + * Testing utils * + *****************/ + + private boolean compareByteArrayLists(List list1, List list2) { + if (list1.size() != list2.size()) { + return false; + } + for (int i = 0; i < list1.size(); i++) { + if (!Arrays.equals(list1.get(i), list2.get(i))) { + return false; + } + } + return true; + } +} diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index bf012f13..77138bdf 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -24,6 +24,8 @@ mod scouting; mod session; mod subscriber; mod utils; +#[cfg(feature = "zenoh-ext")] +mod zbytes; mod zenoh_id; // Test should be runned with `cargo test --no-default-features` diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs new file mode 100644 index 00000000..627bb8b0 --- /dev/null +++ b/zenoh-jni/src/zbytes.rs @@ -0,0 +1,451 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use jni::{ + objects::{JByteArray, JClass, JList, JMap, JObject, JObjectArray, JString, JValue}, + sys::jobject, + JNIEnv, +}; +use zenoh::bytes::ZBytes; +use zenoh_ext::{VarInt, ZDeserializeError, ZDeserializer, ZSerializer}; + +use crate::{ + errors::ZResult, + throw_exception, + utils::{bytes_to_java_array, decode_byte_array}, + zerror, +}; + +enum JavaType { + Boolean, + String, + ByteArray, + Byte, + Short, + Int, + Long, + Float, + Double, + List(Box), + Map(Box, Box), +} + +fn decode_token_type(env: &mut JNIEnv, type_obj: JObject) -> ZResult { + let type_name_jobject = env + .call_method(&type_obj, "getTypeName", "()Ljava/lang/String;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + + let qualified_name: String = env + .get_string(&JString::from(type_name_jobject)) + .map_err(|err| zerror!(err))? + .into(); + + match qualified_name.as_str() { + "java.lang.Boolean" => Ok(JavaType::Boolean), + "java.lang.String" => Ok(JavaType::String), + "byte[]" => Ok(JavaType::ByteArray), + "java.lang.Byte" => Ok(JavaType::Byte), + "java.lang.Short" => Ok(JavaType::Short), + "java.lang.Integer" => Ok(JavaType::Int), + "java.lang.Long" => Ok(JavaType::Long), + "java.lang.Float" => Ok(JavaType::Float), + "java.lang.Double" => Ok(JavaType::Double), + _ => { + let type_token_class = env + .find_class("com/google/common/reflect/TypeToken") + .map_err(|err| zerror!(err))?; + let token_type = env + .call_static_method( + type_token_class, + "of", + "(Ljava/lang/reflect/Type;)Lcom/google/common/reflect/TypeToken;", + &[JValue::Object(&type_obj)], + ) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + let map_class: JObject = env + .find_class("java/util/Map") + .map_err(|err| zerror!(err))? + .into(); + let is_map_subtype = env + .call_method( + &token_type, + "isSubtypeOf", + "(Ljava/lang/reflect/Type;)Z", + &[JValue::Object(&map_class)], + ) + .map_err(|err| zerror!(err))? + .z() + .map_err(|err| zerror!(err))?; + + if is_map_subtype { + let args = env + .call_method( + &type_obj, + "getActualTypeArguments", + "()[Ljava/lang/reflect/Type;", + &[], + ) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + let jobject_array = JObjectArray::from(args); + let arg1 = env + .get_object_array_element(&jobject_array, 0) + .map_err(|err| zerror!(err))?; + let arg2 = env + .get_object_array_element(&jobject_array, 1) + .map_err(|err| zerror!(err))?; + + return Ok(JavaType::Map( + Box::new(decode_token_type(env, arg1)?), + Box::new(decode_token_type(env, arg2)?), + )); + } + + let list_class: JObject = env + .find_class("java/util/List") + .map_err(|err| zerror!(err))? + .into(); + let is_list_subtype = env + .call_method( + &token_type, + "isSubtypeOf", + "(Ljava/lang/reflect/Type;)Z", + &[JValue::Object(&list_class)], + ) + .map_err(|err| zerror!(err))? + .z() + .map_err(|err| zerror!(err))?; + + if is_list_subtype { + let args = env + .call_method( + &type_obj, + "getActualTypeArguments", + "()[Ljava/lang/reflect/Type;", + &[], + ) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + let jobject_array = JObjectArray::from(args); + let arg1 = env + .get_object_array_element(&jobject_array, 0) + .map_err(|err| zerror!(err))?; + + return Ok(JavaType::List(Box::new(decode_token_type(env, arg1)?))); + } + + Err(zerror!("Unsupported type: {}", qualified_name)) + } + } +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeViaJNI( + mut env: JNIEnv, + _class: JClass, + any: JObject, + token_type: JObject, +) -> jobject { + || -> ZResult { + let mut serializer = ZSerializer::new(); + let jtype = decode_token_type(&mut env, token_type)?; + serialize(&mut env, &mut serializer, any, &jtype)?; + let zbytes = serializer.finish(); + + let byte_array = bytes_to_java_array(&env, &zbytes).map_err(|err| zerror!(err))?; + let zbytes_obj = env + .new_object( + "io/zenoh/bytes/ZBytes", + "([B)V", + &[JValue::Object(&JObject::from(byte_array))], + ) + .map_err(|err| zerror!(err))?; + + Ok(zbytes_obj.as_raw()) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + JObject::default().as_raw() + }) +} + +fn serialize( + env: &mut JNIEnv, + serializer: &mut ZSerializer, + any: JObject, + jtype: &JavaType, +) -> ZResult<()> { + match jtype { + JavaType::Byte => { + let byte_value = env + .call_method(any, "byteValue", "()B", &[]) + .map_err(|err| zerror!(err))? + .b() + .map_err(|err| zerror!(err))?; + serializer.serialize(byte_value); + } + JavaType::Short => { + let short_value = env + .call_method(any, "shortValue", "()S", &[]) + .map_err(|err| zerror!(err))? + .s() + .map_err(|err| zerror!(err))?; + serializer.serialize(short_value); + } + JavaType::Int => { + let int_value = env + .call_method(any, "intValue", "()I", &[]) + .map_err(|err| zerror!(err))? + .i() + .map_err(|err| zerror!(err))?; + serializer.serialize(int_value); + } + JavaType::Long => { + let long_value = env + .call_method(any, "longValue", "()J", &[]) + .map_err(|err| zerror!(err))? + .j() + .map_err(|err| zerror!(err))?; + serializer.serialize(long_value); + } + JavaType::Float => { + let float_value = env + .call_method(any, "floatValue", "()F", &[]) + .map_err(|err| zerror!(err))? + .f() + .map_err(|err| zerror!(err))?; + serializer.serialize(float_value); + } + JavaType::Double => { + let double_value = env + .call_method(any, "doubleValue", "()D", &[]) + .map_err(|err| zerror!(err))? + .d() + .map_err(|err| zerror!(err))?; + serializer.serialize(double_value); + } + JavaType::Boolean => { + let boolean_value = env + .call_method(any, "booleanValue", "()Z", &[]) + .map_err(|err| zerror!(err))? + .z() + .map_err(|err| zerror!(err))?; + serializer.serialize(boolean_value); + } + JavaType::String => { + let jstring = JString::from(any); + let string_value: String = env.get_string(&jstring).map_err(|err| zerror!(err))?.into(); + serializer.serialize(string_value); + } + JavaType::ByteArray => { + let jbyte_array = JByteArray::from(any); + let bytes = decode_byte_array(env, jbyte_array).map_err(|err| zerror!(err))?; + serializer.serialize(bytes); + } + JavaType::List(kotlin_type) => { + let jlist: JList<'_, '_, '_> = + JList::from_env(env, &any).map_err(|err| zerror!(err))?; + let mut iterator = jlist.iter(env).map_err(|err| zerror!(err))?; + let list_size = jlist.size(env).map_err(|err| zerror!(err))?; + serializer.serialize(zenoh_ext::VarInt(list_size as usize)); + while let Some(value) = iterator.next(env).map_err(|err| zerror!(err))? { + serialize(env, serializer, value, kotlin_type)?; + } + } + JavaType::Map(key_type, value_type) => { + let jmap = JMap::from_env(env, &any).map_err(|err| zerror!(err))?; + + let map_size = env + .call_method(&jmap, "size", "()I", &[]) + .map_err(|err| zerror!(err))? + .i() + .map_err(|err| zerror!(err))?; + + serializer.serialize(zenoh_ext::VarInt(map_size as usize)); + + let mut iterator = jmap.iter(env).map_err(|err| zerror!(err))?; + while let Some((key, value)) = iterator.next(env).map_err(|err| zerror!(err))? { + serialize(env, serializer, key, key_type)?; + serialize(env, serializer, value, value_type)?; + } + } + } + Ok(()) +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeViaJNI( + mut env: JNIEnv, + _class: JClass, + zbytes: JObject, + jtype: JObject, +) -> jobject { + || -> ZResult { + let payload = env + .get_field(zbytes, "bytes", "[B") + .map_err(|err| zerror!(err))?; + let decoded_bytes: Vec = decode_byte_array( + &env, + JByteArray::from(payload.l().map_err(|err| zerror!(err))?), + )?; + let zbytes = ZBytes::from(decoded_bytes); + let mut deserializer = ZDeserializer::new(&zbytes); + let jtype = decode_token_type(&mut env, jtype)?; + let obj = deserialize(&mut env, &mut deserializer, &jtype)?; + if !deserializer.done() { + return Err(zerror!(ZDeserializeError)); + } + Ok(obj) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + JObject::default().as_raw() + }) +} + +fn deserialize( + env: &mut JNIEnv, + deserializer: &mut ZDeserializer, + jtype: &JavaType, +) -> ZResult { + match jtype { + JavaType::Byte => { + let byte = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let byte_obj = env + .new_object("java/lang/Byte", "(B)V", &[JValue::Byte(byte)]) + .map_err(|err| zerror!(err))?; + Ok(byte_obj.as_raw()) + } + JavaType::Short => { + let short = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let short_obj = env + .new_object("java/lang/Short", "(S)V", &[JValue::Short(short)]) + .map_err(|err| zerror!(err))?; + Ok(short_obj.as_raw()) + } + JavaType::Int => { + let integer = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let integer_obj = env + .new_object("java/lang/Integer", "(I)V", &[JValue::Int(integer)]) + .map_err(|err| zerror!(err))?; + Ok(integer_obj.as_raw()) + } + JavaType::Long => { + let long = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let long_obj = env + .new_object("java/lang/Long", "(J)V", &[JValue::Long(long)]) + .map_err(|err| zerror!(err))?; + Ok(long_obj.as_raw()) + } + JavaType::Float => { + let float = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let float_obj = env + .new_object("java/lang/Float", "(F)V", &[JValue::Float(float)]) + .map_err(|err| zerror!(err))?; + Ok(float_obj.as_raw()) + } + JavaType::Double => { + let double = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let double_obj = env + .new_object("java/lang/Double", "(D)V", &[JValue::Double(double)]) + .map_err(|err| zerror!(err))?; + Ok(double_obj.as_raw()) + } + JavaType::Boolean => { + let boolean_value = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let jboolean = if boolean_value { 1u8 } else { 0u8 }; + let boolean_obj = env + .new_object("java/lang/Boolean", "(Z)V", &[JValue::Bool(jboolean)]) + .map_err(|err| zerror!(err))?; + Ok(boolean_obj.as_raw()) + } + JavaType::String => { + let deserialized_string = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let jstring = env + .new_string(&deserialized_string) + .map_err(|err| zerror!(err))?; + Ok(jstring.into_raw()) + } + JavaType::ByteArray => { + let deserialized_bytes = deserializer + .deserialize::>() + .map_err(|err| zerror!(err))?; + let jbytes = env + .byte_array_from_slice(deserialized_bytes.as_slice()) + .map_err(|err| zerror!(err))?; + Ok(jbytes.into_raw()) + } + JavaType::List(kotlin_type) => { + let list_size = deserializer + .deserialize::>() + .map_err(|err| zerror!(err))? + .0; + let array_list = env + .new_object("java/util/ArrayList", "()V", &[]) + .map_err(|err| zerror!(err))?; + let jlist = JList::from_env(env, &array_list).map_err(|err| zerror!(err))?; + + for _ in 0..list_size { + let item = deserialize(env, deserializer, kotlin_type)?; + let item_obj = unsafe { JObject::from_raw(item) }; + jlist.add(env, &item_obj).map_err(|err| zerror!(err))?; + } + Ok(array_list.as_raw()) + } + JavaType::Map(key_type, value_type) => { + let map_size = deserializer + .deserialize::>() + .map_err(|err| zerror!(err))? + .0; + let map = env + .new_object("java/util/HashMap", "()V", &[]) + .map_err(|err| zerror!(err))?; + let jmap = JMap::from_env(env, &map).map_err(|err| zerror!(err))?; + + for _ in 0..map_size { + let key = deserialize(env, deserializer, key_type)?; + let key_obj = unsafe { JObject::from_raw(key) }; + let value = deserialize(env, deserializer, value_type)?; + let value_obj = unsafe { JObject::from_raw(value) }; + jmap.put(env, &key_obj, &value_obj) + .map_err(|err| zerror!(err))?; + } + Ok(map.as_raw()) + } + } +}