From 6c254c31c4bd7a481f3e15eaaff6eef153a9eb64 Mon Sep 17 00:00:00 2001 From: milyin Date: Tue, 14 Apr 2026 15:32:10 +0200 Subject: [PATCH 001/126] chore: add branch placeholder zbobr_fix-68-adjust-zenoh-java-functionality-to-zenoh-kotlin From d58386cf525221d869e866d6b37e6ca3e349ba91 Mon Sep 17 00:00:00 2001 From: milyin Date: Tue, 14 Apr 2026 16:24:09 +0200 Subject: [PATCH 002/126] feat: add JNI exports for zenoh-kotlin compatibility (advanced pub/sub) - Fix zenoh-ext Cargo.toml to include "unstable" feature (required for AdvancedPublisher/AdvancedSubscriber) - Add owned_object.rs: safe Arc accessor to prevent early drops - Add sample_callback.rs: SetJniSampleCallback trait for JNI callback wiring - Add ext/ module with JNI exports for AdvancedPublisher, AdvancedSubscriber, MatchingListener, SampleMissListener - Add openSessionViaJNI instance-method variant for zenoh-kotlin compatibility - Add prepare_publisher_builder and prepare_subscriber_builder helpers - Add declareAdvancedPublisherViaJNI and declareAdvancedSubscriberViaJNI Co-Authored-By: Claude Sonnet 4.6 --- zenoh-jni/Cargo.toml | 2 +- zenoh-jni/src/ext/advanced_publisher.rs | 339 ++++++++++++++++++++ zenoh-jni/src/ext/advanced_subscriber.rs | 359 ++++++++++++++++++++++ zenoh-jni/src/ext/matching_listener.rs | 41 +++ zenoh-jni/src/ext/mod.rs | 18 ++ zenoh-jni/src/ext/sample_miss_listener.rs | 41 +++ zenoh-jni/src/lib.rs | 4 + zenoh-jni/src/owned_object.rs | 46 +++ zenoh-jni/src/sample_callback.rs | 138 +++++++++ zenoh-jni/src/session.rs | 331 +++++++++++++++++++- 10 files changed, 1317 insertions(+), 2 deletions(-) create mode 100644 zenoh-jni/src/ext/advanced_publisher.rs create mode 100644 zenoh-jni/src/ext/advanced_subscriber.rs create mode 100644 zenoh-jni/src/ext/matching_listener.rs create mode 100644 zenoh-jni/src/ext/mod.rs create mode 100644 zenoh-jni/src/ext/sample_miss_listener.rs create mode 100644 zenoh-jni/src/owned_object.rs create mode 100644 zenoh-jni/src/sample_callback.rs diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index 975fc0fc..a7ff42ab 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -37,7 +37,7 @@ uhlc = "0.8.0" json5 = "0.4.1" serde_yaml = "0.9.19" zenoh = { version = "1.8.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } -zenoh-ext = { version = "1.8.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["internal"], default-features = false, optional = true } +zenoh-ext = { version = "1.8.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false, optional = true } tracing = { version = "0.1" , features = ["log"] } [lib] name = "zenoh_jni" diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs new file mode 100644 index 00000000..c3d0c7c1 --- /dev/null +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -0,0 +1,339 @@ +// +// Copyright (c) 2026 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 std::sync::Arc; + +use jni::objects::JValue; +use jni::{ + objects::{JByteArray, JClass, JString}, + sys::jint, + JNIEnv, +}; +use zenoh::handlers::{Callback, DefaultHandler}; +use zenoh::Wait; +use zenoh_ext::AdvancedPublisher; + +use crate::owned_object::OwnedObject; +use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; + +use crate::throw_exception; +use crate::{ + errors::ZResult, + utils::{decode_byte_array, decode_encoding}, + zerror, +}; +use jni::sys::jboolean; +use std::ptr::null; + +use jni::objects::JObject; +use zenoh::matching::{MatchingListener, MatchingListenerBuilder, MatchingStatus}; + +trait SetJniMatchingStatusCallback { + type WithCallback; + + unsafe fn set_jni_matching_status_callback( + self, + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, + ) -> ZResult; +} + +impl<'a> SetJniMatchingStatusCallback for MatchingListenerBuilder<'a, DefaultHandler> { + type WithCallback = MatchingListenerBuilder<'a, Callback>; + + unsafe fn set_jni_matching_status_callback( + self, + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, + ) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let on_close = load_on_close(&java_vm, on_close_global_ref); + + let builder = self.callback(move |matching_status| { + on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure + let _ = || -> ZResult<()> { + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + zerror!("Unable to attach thread for matching listener: {}", err) + })?; + + env.call_method( + &callback_global_ref, + "run", + "(Z)V", + &[JValue::from(matching_status.matching())], + ) + .map_err(|err| zerror!(err))?; + Ok(()) + }() + .map_err(|err| tracing::error!("On matching listener callback error: {err}")); + }); + Ok(builder) + } +} + +/// Declare a MatchingListener for [AdvancedPublisher] via JNI. +/// +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `advanced_publisher_ptr`: The raw pointer to an [AdvancedPublisher]. +/// - `callback`: The callback function as an instance of the `JNIMatchingListenerCallback` interface in Java/Kotlin. +/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon undeclaring the [MatchingListener]. +/// +/// Returns: +/// - A raw pointer to the declared [MatchingListener]. In case of failure, an exception is thrown and null is returned. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided [AdvancedPublisher] pointer is valid and has not been modified or freed. +/// - The [AdvancedPublisher] pointer remains valid and the ownership of the [AdvancedPublisher] is not transferred, +/// allowing safe usage of the [AdvancedPublisher] after this function call. +/// - The callback function passed as `callback` must be a valid instance of the `JNIMatchingListenerCallback` interface +/// in Java/Kotlin, matching the specified signature. +/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_declareMatchingListenerViaJNI( + mut env: JNIEnv, + _class: JClass, + advanced_publisher_ptr: *const AdvancedPublisher, + + callback: JObject, + on_close: JObject, +) -> *const MatchingListener<()> { + let advanced_publisher = OwnedObject::from_raw(advanced_publisher_ptr); + + || -> ZResult<*const MatchingListener<()>> { + tracing::debug!( + "Declaring matching listener on '{}'...", + advanced_publisher.key_expr() + ); + + let matching_listener = advanced_publisher + .matching_listener() + .set_jni_matching_status_callback(&mut env, callback, on_close)? + .wait() + .map_err(|err| zerror!("Unable to declare matching listener: {}", err))?; + + tracing::debug!( + "Matching listener declared on '{}'...", + advanced_publisher.key_expr() + ); + Ok(Arc::into_raw(Arc::new(matching_listener))) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }) +} + +/// Declare a background matching listener for [AdvancedPublisher] via JNI. +/// Register the listener callback to be run in background until the [AdvancedPublisher] is undeclared. +/// +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `advanced_publisher_ptr`: The raw pointer to an [AdvancedPublisher]. +/// - `callback`: The callback function as an instance of the `JNIMatchingListenerCallback` interface in Java/Kotlin. +/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon undeclaring the [AdvancedPublisher]. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided [AdvancedPublisher] pointer is valid and has not been modified or freed. +/// - The [AdvancedPublisher] pointer remains valid and the ownership of the [AdvancedPublisher] is not transferred, +/// allowing safe usage of the [AdvancedPublisher] after this function call. +/// - The callback function passed as `callback` must be a valid instance of the `JNIMatchingListenerCallback` interface +/// in Java/Kotlin, matching the specified signature. +/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_declareBackgroundMatchingListenerViaJNI( + mut env: JNIEnv, + _class: JClass, + advanced_publisher_ptr: *const AdvancedPublisher, + + callback: JObject, + on_close: JObject, +) { + let advanced_publisher = OwnedObject::from_raw(advanced_publisher_ptr); + + || -> ZResult<()> { + tracing::debug!( + "Declaring background matching listener on '{}'...", + advanced_publisher.key_expr() + ); + + advanced_publisher + .matching_listener() + .set_jni_matching_status_callback(&mut env, callback, on_close)? + .background() + .wait() + .map_err(|err| zerror!("Unable to declare background matching listener: {}", err))?; + + tracing::debug!( + "Background matching listener declared on '{}'...", + advanced_publisher.key_expr() + ); + Ok(()) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + }); +} + +/// Return the matching status of the [AdvancedPublisher]. +/// +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `advanced_publisher_ptr`: The raw pointer to an [AdvancedPublisher]. +/// +/// Returns: +/// - will return true if there exist Subscribers matching the Publisher's key expression and false otherwise. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided [AdvancedPublisher] pointer is valid and has not been modified or freed. +/// - The [AdvancedPublisher] pointer remains valid and the ownership of the [AdvancedPublisher] is not transferred, +/// allowing safe usage of the [AdvancedPublisher] after this function call. +/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_getMatchingStatusViaJNI( + mut env: JNIEnv, + _class: JClass, + advanced_publisher_ptr: *const AdvancedPublisher, +) -> jboolean { + use crate::errors::ZError; + + let advanced_publisher = OwnedObject::from_raw(advanced_publisher_ptr); + advanced_publisher + .matching_status() + .wait() + .map(|val| val.matching() as jboolean) + .map_err(|e| zerror!(e.to_string())) + .unwrap_or_else(|err: ZError| { + throw_exception!(env, err); + false as jboolean + }) +} + +/// Performs a PUT operation on an [AdvancedPublisher] via JNI. +/// +/// # Parameters +/// - `env`: The JNI environment pointer. +/// - `_class`: The Java class reference (unused). +/// - `payload`: The byte array to be published. +/// - `encoding_id`: The encoding ID of the payload. +/// - `encoding_schema`: Nullable encoding schema string of the payload. +/// - `attachment`: Nullble byte array for the attachment. +/// - `publisher_ptr`: The raw pointer to the [AdvancedPublisher]. +/// +/// # Safety +/// - This function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - Assumes that the provided [AdvancedPublisher] pointer is valid and has not been modified or freed. +/// - The [AdvancedPublisher] pointer remains valid after this function call. +/// - May throw an exception in case of failure, which must be handled by the caller. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_putViaJNI( + mut env: JNIEnv, + _class: JClass, + payload: JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, + attachment: /*nullable*/ JByteArray, + publisher_ptr: *const AdvancedPublisher<'static>, +) { + let publisher = OwnedObject::from_raw(publisher_ptr); + let _ = || -> ZResult<()> { + let payload = decode_byte_array(&env, payload)?; + let mut publication = publisher.put(payload); + let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + publication = publication.encoding(encoding); + if !attachment.is_null() { + let attachment = decode_byte_array(&env, attachment)?; + publication = publication.attachment::>(attachment) + }; + publication.wait().map_err(|err| zerror!(err)) + }() + .map_err(|err| throw_exception!(env, err)); +} + +/// Performs a DELETE operation on an [AdvancedPublisher] via JNI. +/// +/// # Parameters +/// - `env`: The JNI environment pointer. +/// - `_class`: The Java class reference (unused). +/// - `attachment`: Nullble byte array for the attachment. +/// - `publisher_ptr`: The raw pointer to the [AdvancedPublisher]. +/// +/// # Safety +/// - This function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - Assumes that the provided [AdvancedPublisher] pointer is valid and has not been modified or freed. +/// - The [AdvancedPublisher] pointer remains valid after this function call. +/// - May throw an exception in case of failure, which must be handled by the caller. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_deleteViaJNI( + mut env: JNIEnv, + _class: JClass, + attachment: /*nullable*/ JByteArray, + publisher_ptr: *const AdvancedPublisher<'static>, +) { + let publisher = OwnedObject::from_raw(publisher_ptr); + let _ = || -> ZResult<()> { + let mut delete = publisher.delete(); + if !attachment.is_null() { + let attachment = decode_byte_array(&env, attachment)?; + delete = delete.attachment::>(attachment) + }; + delete.wait().map_err(|err| zerror!(err)) + }() + .map_err(|err| throw_exception!(env, err)); +} + +/// Frees the [AdvancedPublisher]. +/// +/// # Parameters: +/// - `_env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `publisher_ptr`: The raw pointer to the [AdvancedPublisher]. +/// +/// # Safety: +/// - The function is marked as unsafe due to raw pointer manipulation. +/// - It assumes that the provided [AdvancedPublisher] pointer is valid and has not been modified or freed. +/// - After calling this function, the [AdvancedPublisher] pointer becomes invalid and should not be used anymore. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_freePtrViaJNI( + _env: JNIEnv, + _: JClass, + publisher_ptr: *const AdvancedPublisher, +) { + Arc::from_raw(publisher_ptr); +} diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs new file mode 100644 index 00000000..4fbc624a --- /dev/null +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -0,0 +1,359 @@ +// +// Copyright (c) 2026 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 std::sync::Arc; + +use jni::sys::jboolean; +use jni::{objects::JClass, JNIEnv}; +use zenoh::handlers::{Callback, DefaultHandler}; +use zenoh::pubsub::Subscriber; +use zenoh_ext::SampleMissListener; +use zenoh_ext::{AdvancedSubscriber, Miss, SampleMissListenerBuilder}; + +use crate::sample_callback::SetJniSampleCallback; +use jni::objects::JObject; + +use crate::errors::ZResult; +use jni::objects::JValue; +use zenoh::Wait; + +use crate::owned_object::OwnedObject; + +use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; +use crate::zerror; +use std::ptr::null; + +use crate::throw_exception; + +trait SetJniSampleMissListenerCallback { + type WithCallback; + + unsafe fn set_jni_sample_miss_callback( + self, + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, + ) -> ZResult; +} + +impl<'a> SetJniSampleMissListenerCallback for SampleMissListenerBuilder<'a, DefaultHandler> { + type WithCallback = SampleMissListenerBuilder<'a, Callback>; + + unsafe fn set_jni_sample_miss_callback( + self, + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, + ) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let on_close = load_on_close(&java_vm, on_close_global_ref); + + let builder = self.callback(move |miss| { + on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure + let _ = || -> ZResult<()> { + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + zerror!("Unable to attach thread for sample miss listener: {}", err) + })?; + + let (zid_lower, zid_upper, eid) = { + let id = miss.source(); + + let zid = id.zid().to_le_bytes(); + let zid_lower = i64::from_le_bytes(zid[0..8].try_into().unwrap()); + let zid_upper = i64::from_le_bytes(zid[8..16].try_into().unwrap()); + + (zid_lower, zid_upper, id.eid()) + }; + let missed_count = miss.nb(); + + env.call_method( + &callback_global_ref, + "run", + "(JJJJ)V", + &[ + JValue::from(zid_lower), + JValue::from(zid_upper), + JValue::from(eid as i64), + JValue::from(missed_count as i64), + ], + ) + .map_err(|err| zerror!(err))?; + Ok(()) + }() + .map_err(|err| tracing::error!("On sample miss listener callback error: {err}")); + }); + Ok(builder) + } +} + +/// Declares a subscriber to detect matching publishers for an [AdvancedSubscriber] via JNI. +/// +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `advanced_subscriber_ptr`: The raw pointer to the [AdvancedSubscriber]. +/// - `callback`: The callback function as an instance of the `JNISubscriberCallback` interface in Java/Kotlin. +/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the subscriber. +/// +/// Returns: +/// - A raw pointer to the declared [Subscriber]. In case of failure, an exception is thrown and null is returned. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided [AdvancedSubscriber] pointer is valid and has not been modified or freed. +/// - The [AdvancedSubscriber] pointer remains valid and the ownership of the [AdvancedSubscriber] is not transferred, +/// allowing safe usage of the [AdvancedSubscriber] after this function call. +/// - The callback function passed as `callback` must be a valid instance of the `JNISubscriberCallback` interface +/// in Java/Kotlin, matching the specified signature. +/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareDetectPublishersSubscriberViaJNI( + mut env: JNIEnv, + _class: JClass, + advanced_subscriber_ptr: *const AdvancedSubscriber<()>, + history: jboolean, + callback: JObject, + on_close: JObject, +) -> *const Subscriber<()> { + let advanced_subscriber = OwnedObject::from_raw(advanced_subscriber_ptr); + + || -> ZResult<*const Subscriber<()>> { + tracing::debug!( + "Declaring detect publishers subscriber on '{}'...", + advanced_subscriber.key_expr() + ); + + let detect_publishers_subscriber = advanced_subscriber + .detect_publishers() + .history(history != 0) + .set_jni_sample_callback(&mut env, callback, on_close)? + .wait() + .map_err(|err| zerror!("Unable to declare detect publishers subscriber: {}", err))?; + + tracing::debug!( + "Detect publishers subscriber declared on '{}'...", + advanced_subscriber.key_expr() + ); + Ok(Arc::into_raw(Arc::new(detect_publishers_subscriber))) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }) +} + +/// Declares a background subscriber to detect matching publishers for an [AdvancedSubscriber] via JNI. +/// +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `advanced_subscriber_ptr`: The raw pointer to the [AdvancedSubscriber]. +/// - `callback`: The callback function as an instance of the `JNISubscriberCallback` interface in Java/Kotlin. +/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the subscriber. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided [AdvancedSubscriber] pointer is valid and has not been modified or freed. +/// - The [AdvancedSubscriber] pointer remains valid and the ownership of the [AdvancedSubscriber] is not transferred, +/// allowing safe usage of the [AdvancedSubscriber] after this function call. +/// - The callback function passed as `callback` must be a valid instance of the `JNISubscriberCallback` interface +/// in Java/Kotlin, matching the specified signature. +/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareBackgroundDetectPublishersSubscriberViaJNI( + mut env: JNIEnv, + _class: JClass, + advanced_subscriber_ptr: *const AdvancedSubscriber<()>, + history: jboolean, + callback: JObject, + on_close: JObject, +) { + let advanced_subscriber = OwnedObject::from_raw(advanced_subscriber_ptr); + + || -> ZResult<()> { + tracing::debug!( + "Declaring background detect publishers subscriber on '{}'...", + advanced_subscriber.key_expr() + ); + + advanced_subscriber + .detect_publishers() + .history(history != 0) + .set_jni_sample_callback(&mut env, callback, on_close)? + .background() + .wait() + .map_err(|err| { + zerror!( + "Unable to declare background detect publishers subscriber: {}", + err + ) + })?; + + tracing::debug!( + "Background detect publishers subscriber declared on '{}'...", + advanced_subscriber.key_expr() + ); + Ok(()) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + }); +} + +/// Declares a [SampleMissListener] to detect missed samples for an [AdvancedSubscriber] via JNI. +/// +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `advanced_subscriber_ptr`: The raw pointer to the [AdvancedSubscriber]. +/// - `callback`: The callback function as an instance of the `JNISampleMissListenerCallback` interface in Java/Kotlin. +/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the subscriber. +/// +/// Returns: +/// - A raw pointer to the declared [SampleMissListener]. In case of failure, an exception is thrown and null is returned. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided [AdvancedSubscriber] pointer is valid and has not been modified or freed. +/// - The [AdvancedSubscriber] pointer remains valid and the ownership of the [AdvancedSubscriber] is not transferred, +/// allowing safe usage of the [AdvancedSubscriber] after this function call. +/// - The callback function passed as `callback` must be a valid instance of the `JNISampleMissListenerCallback` interface +/// in Java/Kotlin, matching the specified signature. +/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareSampleMissListenerViaJNI( + mut env: JNIEnv, + _class: JClass, + advanced_subscriber_ptr: *const AdvancedSubscriber<()>, + + callback: JObject, + on_close: JObject, +) -> *const SampleMissListener<()> { + let advanced_subscriber = OwnedObject::from_raw(advanced_subscriber_ptr); + + || -> ZResult<*const SampleMissListener<()>> { + tracing::debug!( + "Declaring sample miss listener on '{}'...", + advanced_subscriber.key_expr() + ); + + let result = advanced_subscriber + .sample_miss_listener() + .set_jni_sample_miss_callback(&mut env, callback, on_close)? + .wait(); + + let sample_miss_listener = + result.map_err(|err| zerror!("Unable to declare sample miss listener: {}", err))?; + + tracing::debug!( + "Matching listener declared on '{}'...", + advanced_subscriber.key_expr() + ); + Ok(Arc::into_raw(Arc::new(sample_miss_listener))) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }) +} + +/// Declare a background sample miss listener for [AdvancedSubscriber] via JNI. +/// Register the listener callback to be run in background until the [AdvancedSubscriber] is undeclared. +/// +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `advanced_subscriber_ptr`: The raw pointer to an [AdvancedSubscriber]. +/// - `callback`: The callback function as an instance of the `JNISampleMissListenerCallback` interface in Java/Kotlin. +/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon undeclaring the [AdvancedSubscriber]. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided [AdvancedSubscriber] pointer is valid and has not been modified or freed. +/// - The [AdvancedSubscriber] pointer remains valid and the ownership of the [AdvancedSubscriber] is not transferred, +/// allowing safe usage of the [AdvancedSubscriber] after this function call. +/// - The callback function passed as `callback` must be a valid instance of the `JNISampleMissListenerCallback` interface +/// in Java/Kotlin, matching the specified signature. +/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareBackgroundSampleMissListenerViaJNI( + mut env: JNIEnv, + _class: JClass, + advanced_subscriber_ptr: *const AdvancedSubscriber<()>, + + callback: JObject, + on_close: JObject, +) { + let advanced_subscriber = OwnedObject::from_raw(advanced_subscriber_ptr); + + || -> ZResult<()> { + tracing::debug!( + "Declaring background sample miss listener on '{}'...", + advanced_subscriber.key_expr() + ); + + advanced_subscriber + .sample_miss_listener() + .set_jni_sample_miss_callback(&mut env, callback, on_close)? + .background() + .wait() + .map_err(|err| zerror!("Unable to declare background sample miss listener: {}", err))?; + + tracing::debug!( + "Background sample miss listener declared on '{}'...", + advanced_subscriber.key_expr() + ); + Ok(()) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + }) +} + +/// Frees the [AdvancedSubscriber]. +/// +/// # Parameters: +/// - `_env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `subscriber_ptr`: The raw pointer to the [AdvancedSubscriber]. +/// +/// # Safety: +/// - The function is marked as unsafe due to raw pointer manipulation. +/// - It assumes that the provided [AdvancedSubscriber] pointer is valid and has not been modified or freed. +/// - The function takes ownership of the raw pointer and releases the associated memory. +/// - After calling this function, the [AdvancedSubscriber] pointer becomes invalid and should not be used anymore. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_freePtrViaJNI( + _env: JNIEnv, + _: JClass, + subscriber_ptr: *const AdvancedSubscriber<()>, +) { + Arc::from_raw(subscriber_ptr); +} diff --git a/zenoh-jni/src/ext/matching_listener.rs b/zenoh-jni/src/ext/matching_listener.rs new file mode 100644 index 00000000..737d0203 --- /dev/null +++ b/zenoh-jni/src/ext/matching_listener.rs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2026 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 std::sync::Arc; + +use jni::{objects::JClass, JNIEnv}; +use zenoh::matching::MatchingListener; + +/// Frees the [MatchingListener]. +/// +/// # Parameters: +/// - `_env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `matching_listener_ptr`: The raw pointer to the [MatchingListener]. +/// +/// # Safety: +/// - The function is marked as unsafe due to raw pointer manipulation. +/// - It assumes that the provided [MatchingListener] pointer is valid and has not been modified or freed. +/// - The function takes ownership of the raw pointer and releases the associated memory. +/// - After calling this function, the [MatchingListener] pointer becomes invalid and should not be used anymore. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIMatchingListener_freePtrViaJNI( + _env: JNIEnv, + _: JClass, + matching_listener_ptr: *const MatchingListener<()>, +) { + Arc::from_raw(matching_listener_ptr); +} diff --git a/zenoh-jni/src/ext/mod.rs b/zenoh-jni/src/ext/mod.rs new file mode 100644 index 00000000..8a4a30b9 --- /dev/null +++ b/zenoh-jni/src/ext/mod.rs @@ -0,0 +1,18 @@ +// +// Copyright (c) 2026 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, +// + +mod advanced_publisher; +mod advanced_subscriber; +mod matching_listener; +mod sample_miss_listener; diff --git a/zenoh-jni/src/ext/sample_miss_listener.rs b/zenoh-jni/src/ext/sample_miss_listener.rs new file mode 100644 index 00000000..5fc3ddbb --- /dev/null +++ b/zenoh-jni/src/ext/sample_miss_listener.rs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2026 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 std::sync::Arc; + +use jni::{objects::JClass, JNIEnv}; +use zenoh_ext::SampleMissListener; + +/// Frees the [SampleMissListener]. +/// +/// # Parameters: +/// - `_env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `sample_miss_listener_ptr`: The raw pointer to the [SampleMissListener]. +/// +/// # Safety: +/// - The function is marked as unsafe due to raw pointer manipulation. +/// - It assumes that the provided [SampleMissListener] pointer is valid and has not been modified or freed. +/// - The function takes ownership of the raw pointer and releases the associated memory. +/// - After calling this function, the [SampleMissListener] pointer becomes invalid and should not be used anymore. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNISampleMissListener_freePtrViaJNI( + _env: JNIEnv, + _: JClass, + sample_miss_listener_ptr: *const SampleMissListener<()>, +) { + Arc::from_raw(sample_miss_listener_ptr); +} diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index ff3981a4..507fa54f 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -17,10 +17,12 @@ mod errors; mod key_expr; mod liveliness; mod logger; +pub(crate) mod owned_object; mod publisher; mod querier; mod query; mod queryable; +pub(crate) mod sample_callback; mod scouting; mod session; mod subscriber; @@ -28,6 +30,8 @@ mod utils; #[cfg(feature = "zenoh-ext")] mod zbytes; mod zenoh_id; +#[cfg(feature = "zenoh-ext")] +mod ext; // Test should be runned with `cargo test --no-default-features` #[test] diff --git a/zenoh-jni/src/owned_object.rs b/zenoh-jni/src/owned_object.rs new file mode 100644 index 00000000..326230f6 --- /dev/null +++ b/zenoh-jni/src/owned_object.rs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2026 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 std::{ops::Deref, sync::Arc}; + +/// Safe accessor to refocounted ([Arc]) owned objects. +/// Helps to avoid early drop by offloading [std::mem::forget] from user +pub(crate) struct OwnedObject { + inner: Option>, +} + +impl Deref for OwnedObject { + type Target = T; + + fn deref(&self) -> &Self::Target { + // SAFETY: inner is always initialized + unsafe { self.inner.as_ref().unwrap_unchecked() } + } +} + +impl Drop for OwnedObject { + fn drop(&mut self) { + // SAFETY: inner is always initialized + let inner = unsafe { self.inner.take().unwrap_unchecked() }; + std::mem::forget(inner); + } +} + +impl OwnedObject { + pub(crate) unsafe fn from_raw(ptr: *const T) -> Self { + Self { + inner: Some(Arc::from_raw(ptr)), + } + } +} diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs new file mode 100644 index 00000000..fdf677b8 --- /dev/null +++ b/zenoh-jni/src/sample_callback.rs @@ -0,0 +1,138 @@ +// +// Copyright (c) 2026 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 std::sync::Arc; + +use jni::{ + objects::{JByteArray, JObject, JString, JValue}, + sys::jint, + JNIEnv, +}; +use zenoh::{ + handlers::{Callback, DefaultHandler}, + liveliness::LivelinessSubscriberBuilder, + pubsub::SubscriberBuilder, + sample::Sample, +}; + +use crate::{errors::ZResult, utils::*, zerror}; + +pub(crate) trait SetJniSampleCallback: Sized + HasSampleCallbackSetter { + unsafe fn set_jni_sample_callback( + self, + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, + ) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let on_close = load_on_close(&java_vm, on_close_global_ref); + + let builder = self.set_callback(move |sample: Sample| { + on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure + let _ = || -> ZResult<()> { + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + zerror!("Unable to attach thread for sample callback: {}", err) + })?; + let byte_array = bytes_to_java_array(&env, sample.payload()) + .map(|array| env.auto_local(array))?; + + let encoding_id: jint = sample.encoding().id() as jint; + let encoding_schema = match sample.encoding().schema() { + Some(schema) => slice_to_java_string(&env, schema)?, + None => JString::default(), + }; + let kind = sample.kind() as jint; + let (timestamp, is_valid) = sample + .timestamp() + .map(|timestamp| (timestamp.get_time().as_u64(), true)) + .unwrap_or((0, false)); + + let attachment_bytes = sample + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(&env, attachment), + ) + .map(|array| env.auto_local(array)) + .map_err(|err| zerror!("Error processing attachment: {}", err))?; + + let key_expr_str = env.auto_local( + env.new_string(sample.key_expr().to_string()) + .map_err(|err| zerror!("Error processing sample key expr: {}", err))?, + ); + + let express = sample.express(); + let priority = sample.priority() as jint; + let cc = sample.congestion_control() as jint; + + env.call_method( + &callback_global_ref, + "run", + "(Ljava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&key_expr_str), + JValue::from(&byte_array), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + JValue::from(kind), + JValue::from(timestamp as i64), + JValue::from(is_valid), + JValue::from(&attachment_bytes), + JValue::from(express), + JValue::from(priority), + JValue::from(cc), + ], + ) + .map_err(|err| zerror!(err))?; + Ok(()) + }() + .map_err(|err| tracing::error!("On sample callback error: {err}")); + }); + Ok(builder) + } +} + +impl SetJniSampleCallback for T {} + +pub(crate) trait HasSampleCallbackSetter { + type BuilderWithCallback; + + fn set_callback(self, callback: F) -> Self::BuilderWithCallback + where + F: Fn(Sample) + Send + Sync + 'static; +} + +impl<'a, 'b> HasSampleCallbackSetter for SubscriberBuilder<'a, 'b, DefaultHandler> { + type BuilderWithCallback = SubscriberBuilder<'a, 'b, Callback>; + + fn set_callback(self, callback: F) -> Self::BuilderWithCallback + where + F: Fn(Sample) + Send + Sync + 'static, + { + self.callback(callback) + } +} + +impl<'a, 'b> HasSampleCallbackSetter for LivelinessSubscriberBuilder<'a, 'b, DefaultHandler> { + type BuilderWithCallback = LivelinessSubscriberBuilder<'a, 'b, Callback>; + + fn set_callback(self, callback: F) -> Self::BuilderWithCallback + where + F: Fn(Sample) + Send + Sync + 'static, + { + self.callback(callback) + } +} diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 5f0848b2..1950cc06 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -21,14 +21,26 @@ use jni::{ }; use zenoh::{ config::Config, + handlers::Callback, key_expr::KeyExpr, - pubsub::{Publisher, Subscriber}, + pubsub::{Publisher, PublisherBuilder, Subscriber, SubscriberBuilder}, query::{Querier, Query, Queryable, ReplyError, ReplyKeyExpr, Selector}, sample::Sample, session::{EntityGlobalId, Session, ZenohId}, Wait, }; +use crate::owned_object::OwnedObject; +use crate::sample_callback::SetJniSampleCallback; +#[cfg(feature = "zenoh-ext")] +use jni::sys::jdouble; +#[cfg(feature = "zenoh-ext")] +use zenoh_ext::{ + AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, + AdvancedSubscriberBuilderExt, CacheConfig, HistoryConfig, MissDetectionConfig, RecoveryConfig, + RepliesConfig, +}; + use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, zerror, }; @@ -1199,3 +1211,320 @@ fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result< } Ok(array_list.as_raw()) } + +/// Open a Zenoh session via JNI (instance-method variant for zenoh-kotlin compatibility). +/// +/// This is an alias for the companion-object variant, allowing zenoh-kotlin to call +/// `openSessionViaJNI` as an instance method on `JNISession`. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class (parameter required by the JNI interface but unused). +/// - `config_ptr`: Pointer to the Zenoh config. If null, the default configuration will be loaded. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( + mut env: JNIEnv, + _class: JClass, + config_ptr: *const Config, +) -> *const Session { + let session = open_session(config_ptr); + match session { + Ok(session) => Arc::into_raw(Arc::new(session)), + Err(err) => { + tracing::error!("Unable to open session: {}", err); + throw_exception!(env, zerror!(err)); + null() + } + } +} + +#[allow(clippy::too_many_arguments)] +unsafe fn prepare_publisher_builder<'a, 'b>( + env: &mut JNIEnv, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: &JString, + session: &'a Session, + congestion_control: jint, + priority: jint, + is_express: jboolean, + reliability: jint, +) -> ZResult> { + let key_expr = process_kotlin_key_expr(env, key_expr_str, key_expr_ptr)?; + let congestion_control = decode_congestion_control(congestion_control)?; + let priority = decode_priority(priority)?; + let reliability = decode_reliability(reliability)?; + let builder = session + .declare_publisher(key_expr) + .congestion_control(congestion_control) + .priority(priority) + .express(is_express != 0) + .reliability(reliability); + Ok(builder) +} + +unsafe fn prepare_subscriber_builder<'a, 'b>( + env: &mut JNIEnv, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: &JString, + session: &'a Session, + callback: JObject, + on_close: JObject, + entity_name: &str, +) -> ZResult>> { + let key_expr = process_kotlin_key_expr(env, key_expr_str, key_expr_ptr)?; + tracing::debug!("Declaring {entity_name} on '{}'...", key_expr); + + let builder = session + .declare_subscriber(key_expr.to_owned()) + .set_jni_sample_callback(env, callback, on_close)?; + + tracing::debug!("{entity_name} declared on '{}'.", key_expr); + Ok(builder) +} + +/// Declare an advanced Zenoh subscriber via JNI. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `key_expr_ptr`: Raw pointer to the [KeyExpr], may be null. +/// - `key_expr_str`: String representation of the [KeyExpr]. +/// - `history_config_enabled`: Whether history config is enabled. +/// - `history_detect_late_publishers`: Whether to detect late publishers in history. +/// - `history_max_samples`: Max samples in history (<=0 means unlimited). +/// - `history_max_age_seconds`: Max age in seconds for history (<=0.0 means unlimited). +/// - `recovery_config_enabled`: Whether recovery config is enabled. +/// - `recovery_config_is_heartbeat`: Whether to use heartbeat recovery. +/// - `recovery_query_period_ms`: Query period for periodic recovery in milliseconds. +/// - `subscriber_detection`: Allow this subscriber to be detected through liveliness. +/// - `session_ptr`: The raw pointer to the Zenoh session. +/// - `callback`: The callback function as an instance of the `JNISubscriberCallback` interface. +/// - `on_close`: A `JNIOnCloseCallback` to be called upon closing the subscriber. +/// +/// Returns: +/// - A raw pointer to the declared [AdvancedSubscriber]. In case of failure, an exception is thrown and null is returned. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberViaJNI( + mut env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + // HistoryConfig + history_config_enabled: jboolean, + history_detect_late_publishers: jboolean, + history_max_samples: jlong, + history_max_age_seconds: jdouble, + // RecoveryConfig + recovery_config_enabled: jboolean, + recovery_config_is_heartbeat: jboolean, + recovery_query_period_ms: jlong, + + subscriber_detection: jboolean, + + callback: JObject, + on_close: JObject, +) -> *const AdvancedSubscriber<()> { + let session = Arc::from_raw(session_ptr); + let subscriber_ptr = || -> ZResult<*const AdvancedSubscriber<()>> { + let mut builder = prepare_subscriber_builder( + &mut env, + key_expr_ptr, + &key_expr_str, + &session, + callback, + on_close, + "advanced subscriber", + )? + .advanced(); + + if history_config_enabled != 0 { + let mut history = match history_detect_late_publishers != 0 { + true => HistoryConfig::default().detect_late_publishers(), + false => HistoryConfig::default(), + }; + + if history_max_samples > 0 { + history = history.max_samples( + history_max_samples + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, + ); + } + + if history_max_age_seconds > 0.0 { + history = history.max_age(history_max_age_seconds); + } + + builder = builder.history(history); + } + + if recovery_config_enabled != 0 { + let recovery = if recovery_config_is_heartbeat != 0 { + RecoveryConfig::default().heartbeat() + } else { + let dur = Duration::from_millis( + recovery_query_period_ms + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, + ); + RecoveryConfig::default().periodic_queries(dur) + }; + builder = builder.recovery(recovery); + } + + if subscriber_detection != 0 { + builder = builder.subscriber_detection(); + } + + builder + .wait() + .map(|s| Arc::into_raw(Arc::new(s))) + .map_err(|err| zerror!("Unable to declare advanced subscriber: {}", err)) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }); + std::mem::forget(session); + subscriber_ptr +} + +/// Declare an advanced Zenoh publisher via JNI. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the publisher, may be null. +/// - `key_expr_str`: String representation of the [KeyExpr]. +/// - `session_ptr`: Raw pointer to the Zenoh [Session] to be used for the publisher. +/// - `congestion_control`: The [CongestionControl] configuration as an ordinal. +/// - `priority`: The [Priority] configuration as an ordinal. +/// - `is_express`: The express config of the publisher. +/// - `reliability`: The reliability value as an ordinal. +/// - `cache_enabled`: If true, attach a cache to the [AdvancedPublisher]. +/// - `cache_max_samples`: How many samples to keep for each resource. +/// - `cache_replies_priority`: The [Priority] for cache replies as an ordinal. +/// - `cache_replies_congestion_control`: The [CongestionControl] for cache replies as an ordinal. +/// - `cache_replies_is_express`: The express config for cache replies. +/// - `sample_miss_detection_enabled`: Enables sample miss detection functionality. +/// - `sample_miss_detection_enable_heartbeat`: Use heartbeat for miss detection. +/// - `sample_miss_detection_heartbeat_ms`: Heartbeat period in milliseconds. +/// - `sample_miss_detection_heartbeat_is_sporadic`: Whether heartbeat is sporadic. +/// - `publisher_detection`: Enables publisher detection. +/// +/// # Returns: +/// - A raw pointer to the declared [AdvancedPublisher] or null in case of failure. +/// +/// # Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// +#[cfg(feature = "zenoh-ext")] +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedPublisherViaJNI( + mut env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + congestion_control: jint, + priority: jint, + is_express: jboolean, + reliability: jint, + // CacheConfig + cache_enabled: jboolean, + cache_max_samples: jlong, + cache_replies_priority: jint, + cache_replies_congestion_control: jint, + cache_replies_is_express: jboolean, + // MissDetectionConfig + sample_miss_detection_enabled: jboolean, + sample_miss_detection_enable_heartbeat: jboolean, + sample_miss_detection_heartbeat_ms: jlong, + sample_miss_detection_heartbeat_is_sporadic: jboolean, + + publisher_detection: jboolean, +) -> *const AdvancedPublisher<'static> { + let session = OwnedObject::from_raw(session_ptr); + let publisher_ptr = || -> ZResult<*const AdvancedPublisher<'static>> { + let mut builder = prepare_publisher_builder( + &mut env, + key_expr_ptr, + &key_expr_str, + &session, + congestion_control, + priority, + is_express, + reliability, + )? + .advanced(); + + // fill CacheConfig + if cache_enabled != 0 { + let cache_congestion_control = + decode_congestion_control(cache_replies_congestion_control)?; + + let cache_priority = decode_priority(cache_replies_priority)?; + + let replies_config = RepliesConfig::default() + .priority(cache_priority) + .congestion_control(cache_congestion_control) + .express(cache_replies_is_express != 0); + + let cache_config = CacheConfig::default() + .max_samples( + cache_max_samples + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, + ) + .replies_config(replies_config); + + builder = builder.cache(cache_config); + } + + // fill MissDetectionConfig + if sample_miss_detection_enabled != 0 { + let miss_detection_config = { + let mut result = MissDetectionConfig::default(); + if sample_miss_detection_enable_heartbeat != 0 { + let duration = Duration::from_millis( + sample_miss_detection_heartbeat_ms + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, + ); + + result = match sample_miss_detection_heartbeat_is_sporadic != 0 { + true => result.sporadic_heartbeat(duration), + false => result.heartbeat(duration), + }; + } + result + }; + builder = builder.sample_miss_detection(miss_detection_config); + } + + if publisher_detection != 0 { + builder = builder.publisher_detection(); + } + + let result = builder.wait(); + match result { + Ok(publisher) => Ok(Arc::into_raw(Arc::new(publisher))), + Err(err) => Err(zerror!(err)), + } + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }); + publisher_ptr +} From 3919692d01a4bbcd46e31e7b33aa8166572adb5f Mon Sep 17 00:00:00 2001 From: milyin Date: Tue, 14 Apr 2026 17:04:12 +0200 Subject: [PATCH 003/126] fix: restore unstable feature for zenoh-ext and reorder modules The ext module requires unstable features from zenoh-ext. The merge removed this feature but the ext module still uses it, causing compilation errors. Also reordered module declarations to group feature-gated modules together per cargo fmt output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- zenoh-jni/Cargo.toml | 2 +- zenoh-jni/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index a88e31ec..1afcceee 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -37,7 +37,7 @@ uhlc = "0.8.0" json5 = "0.4.1" serde_yaml = "0.9.19" zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } -zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["internal"], default-features = false, optional = true } +zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false, optional = true } tracing = { version = "0.1" , features = ["log"] } [lib] name = "zenoh_jni" diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index 507fa54f..93765edb 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -14,6 +14,8 @@ mod config; mod errors; +#[cfg(feature = "zenoh-ext")] +mod ext; mod key_expr; mod liveliness; mod logger; @@ -30,8 +32,6 @@ mod utils; #[cfg(feature = "zenoh-ext")] mod zbytes; mod zenoh_id; -#[cfg(feature = "zenoh-ext")] -mod ext; // Test should be runned with `cargo test --no-default-features` #[test] From eba165a6c8a9ca830a4519c5e9ea06ccc2b753e4 Mon Sep 17 00:00:00 2001 From: milyin Date: Tue, 14 Apr 2026 17:44:00 +0200 Subject: [PATCH 004/126] fix: unify openSessionViaJNI JNI symbol between zenoh-java and zenoh-kotlin Add @JvmStatic to openSessionViaJNI in JNISession companion object so Kotlin emits Java_io_zenoh_jni_JNISession_openSessionViaJNI (outer-class form) instead of the $Companion-suffixed variant. Remove the now-redundant Java_io_zenoh_jni_JNISession_00024Companion_openSessionViaJNI from session.rs, leaving the single canonical JNI binding that both zenoh-java and zenoh-kotlin share. Co-Authored-By: Claude Sonnet 4.6 --- .../kotlin/io/zenoh/jni/JNISession.kt | 1 + zenoh-jni/src/session.rs | 40 +++---------------- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 096a6c59..7026ebd6 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -53,6 +53,7 @@ internal class JNISession(val sessionPtr: Long) { return JNISession(sessionPtr) } + @JvmStatic @Throws(ZError::class) private external fun openSessionViaJNI(configPtr: Long): Long } diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 1950cc06..0b5ae57f 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -45,37 +45,6 @@ use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, zerror, }; -/// Open a Zenoh session via JNI. -/// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. -/// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `config_path`: Nullable path to the Zenoh config file. If null, the default configuration will be loaded. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_00024Companion_openSessionViaJNI( - mut env: JNIEnv, - _class: JClass, - config_ptr: *const Config, -) -> *const Session { - let session = open_session(config_ptr); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - null() - } - } -} - /// Open a Zenoh session with the configuration pointed out by `config_path`. /// /// If the config path provided is null then the default configuration is loaded. @@ -1212,10 +1181,13 @@ fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result< Ok(array_list.as_raw()) } -/// Open a Zenoh session via JNI (instance-method variant for zenoh-kotlin compatibility). +/// Open a Zenoh session via JNI. +/// +/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute +/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require +/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. /// -/// This is an alias for the companion-object variant, allowing zenoh-kotlin to call -/// `openSessionViaJNI` as an instance method on `JNISession`. +/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. /// /// # Parameters: /// - `env`: The JNI environment. From f0cd490162fc0a254c855c3731fe5c06362d40b7 Mon Sep 17 00:00:00 2001 From: milyin Date: Wed, 15 Apr 2026 14:27:46 +0200 Subject: [PATCH 005/126] fix: use OwnedObject::from_raw in declareAdvancedSubscriberViaJNI for consistency --- zenoh-jni/src/session.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 0b5ae57f..c07c4dea 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -1305,7 +1305,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV callback: JObject, on_close: JObject, ) -> *const AdvancedSubscriber<()> { - let session = Arc::from_raw(session_ptr); + let session = OwnedObject::from_raw(session_ptr); let subscriber_ptr = || -> ZResult<*const AdvancedSubscriber<()>> { let mut builder = prepare_subscriber_builder( &mut env, @@ -1365,9 +1365,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV .unwrap_or_else(|err| { throw_exception!(env, err); null() - }); - std::mem::forget(session); - subscriber_ptr + }) } /// Declare an advanced Zenoh publisher via JNI. From b619f14ebd5ccf17db7c8fa8142df52cb83af886 Mon Sep 17 00:00:00 2001 From: milyin Date: Wed, 15 Apr 2026 15:08:05 +0200 Subject: [PATCH 006/126] feat: create zenoh-jni-runtime module and migrate JNI adapters from zenoh-java - Create zenoh-jni-runtime Gradle subproject with public primitive-only JNI adapters - Move JNI adapter classes (JNIConfig, JNIKeyExpr, JNISession, JNIPublisher, JNIQuery, JNIQuerier, JNIScout, JNILiveliness, JNISubscriber, JNIQueryable, JNILivelinessToken, JNIZenohId) from zenoh-java to zenoh-jni-runtime with public visibility and primitive APIs - Add new JNIAdvancedPublisher, JNIAdvancedSubscriber, JNIMatchingListener, JNISampleMissListener - Add new callback interfaces including JNIMatchingListenerCallback and JNISampleMissListenerCallback - Add public ZenohLoad expect/actual objects in zenoh-jni-runtime (jvm and android) - Add public Target enum in zenoh-jni-runtime jvmMain - Update zenoh-java/build.gradle.kts: depend on zenoh-jni-runtime, remove duplicate native build - Update Session.kt to inline callback assembly using runtime JNI adapters - Update Publisher.kt, Query.kt, Querier.kt, Liveliness.kt, Zenoh.kt to use primitive APIs - Fix Rust bug in session.rs: remove spurious let binding in declareAdvancedSubscriberViaJNI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- settings.gradle.kts | 1 + zenoh-java/build.gradle.kts | 59 +- .../src/commonMain/kotlin/io/zenoh/Config.kt | 12 +- .../src/commonMain/kotlin/io/zenoh/Session.kt | 284 ++++++++- .../src/commonMain/kotlin/io/zenoh/Zenoh.kt | 38 +- .../kotlin/io/zenoh/jni/JNILiveliness.kt | 188 ------ .../kotlin/io/zenoh/jni/JNILivelinessToken.kt | 12 - .../kotlin/io/zenoh/jni/JNIPublisher.kt | 70 --- .../kotlin/io/zenoh/jni/JNIQuerier.kt | 137 ----- .../kotlin/io/zenoh/jni/JNIQuery.kt | 106 ---- .../kotlin/io/zenoh/jni/JNIScout.kt | 87 --- .../kotlin/io/zenoh/jni/JNISession.kt | 549 ------------------ .../kotlin/io/zenoh/keyexpr/KeyExpr.kt | 14 +- .../kotlin/io/zenoh/liveliness/Liveliness.kt | 130 ++++- .../kotlin/io/zenoh/pubsub/Publisher.kt | 7 +- .../kotlin/io/zenoh/query/Querier.kt | 76 ++- .../commonMain/kotlin/io/zenoh/query/Query.kt | 41 +- zenoh-jni-runtime/build.gradle.kts | 240 ++++++++ .../androidMain/kotlin/io/zenoh/ZenohLoad.kt | 7 +- .../commonMain/kotlin/io/zenoh/ZenohLoad.kt | 20 + .../kotlin/io/zenoh/exceptions/ZError.kt | 20 + .../io/zenoh/jni/JNIAdvancedPublisher.kt | 75 +++ .../io/zenoh/jni/JNIAdvancedSubscriber.kt | 80 +++ .../kotlin/io/zenoh/jni/JNIConfig.kt | 38 +- .../kotlin/io/zenoh/jni/JNIKeyExpr.kt | 51 +- .../kotlin/io/zenoh/jni/JNILiveliness.kt | 47 ++ .../kotlin/io/zenoh/jni/JNILivelinessToken.kt | 27 + .../io/zenoh/jni/JNIMatchingListener.kt | 25 + .../kotlin/io/zenoh/jni/JNIPublisher.kt | 49 ++ .../kotlin/io/zenoh/jni/JNIQuerier.kt | 43 ++ .../kotlin/io/zenoh/jni/JNIQuery.kt | 96 +++ .../kotlin/io/zenoh/jni/JNIQueryable.kt | 7 +- .../io/zenoh/jni/JNISampleMissListener.kt | 25 + .../kotlin/io/zenoh/jni/JNIScout.kt | 48 ++ .../kotlin/io/zenoh/jni/JNISession.kt | 194 +++++++ .../kotlin/io/zenoh/jni/JNISubscriber.kt | 8 +- .../kotlin/io/zenoh/jni/JNIZenohId.kt | 6 +- .../io/zenoh/jni/callbacks/JNIGetCallback.kt | 4 +- .../callbacks/JNIMatchingListenerCallback.kt | 20 + .../zenoh/jni/callbacks/JNIOnCloseCallback.kt | 4 +- .../jni/callbacks/JNIQueryableCallback.kt | 22 +- .../JNISampleMissListenerCallback.kt | 20 + .../zenoh/jni/callbacks/JNIScoutCallback.kt | 4 +- .../jni/callbacks/JNISubscriberCallback.kt | 4 +- .../src/jvmMain/kotlin/io/zenoh/Target.kt | 4 +- .../src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 39 +- zenoh-jni/src/session.rs | 2 +- 47 files changed, 1599 insertions(+), 1441 deletions(-) delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt create mode 100644 zenoh-jni-runtime/build.gradle.kts rename zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt => zenoh-jni-runtime/src/androidMain/kotlin/io/zenoh/ZenohLoad.kt (79%) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/ZenohLoad.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt (63%) rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt (55%) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt (79%) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt (78%) rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt (80%) rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt (91%) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIMatchingListenerCallback.kt rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIOnCloseCallback.kt (84%) rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt (56%) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISampleMissListenerCallback.kt rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt (85%) rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt (89%) rename {zenoh-java => zenoh-jni-runtime}/src/jvmMain/kotlin/io/zenoh/Target.kt (93%) rename zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt => zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt (81%) diff --git a/settings.gradle.kts b/settings.gradle.kts index 2d17a6a4..776fcd77 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ pluginManagement { rootProject.name = "zenoh-java" include(":zenoh-java") +include(":zenoh-jni-runtime") include(":examples") include(":zenoh-jni") diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index 123d7499..74bd48af 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -30,7 +30,7 @@ val release = project.findProperty("release")?.toString()?.toBoolean() == true // Modifying this property will affect the release workflows! val isRemotePublication = project.findProperty("remotePublication")?.toString()?.toBoolean() == true -var buildMode = if (release) BuildMode.RELEASE else BuildMode.DEBUG +var buildMode = if (release) "release" else "debug" if (androidEnabled) { apply(plugin = "com.android.library") @@ -66,6 +66,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + implementation(project(":zenoh-jni-runtime")) implementation("commons-net:commons-net:3.9.0") implementation("com.google.guava:guava:33.3.1-jre") } @@ -82,18 +83,9 @@ kotlin { } } } - val jvmMain by getting { - if (isRemotePublication) { - // The line below is intended to load the native libraries that are crosscompiled on GitHub actions when publishing a JVM package. - resources.srcDir("../jni-libs").include("*/**") - } else { - resources.srcDir("../zenoh-jni/target/$buildMode").include(arrayListOf("*.dylib", "*.so", "*.dll")) - } - } + val jvmMain by getting {} - val jvmTest by getting { - resources.srcDir("../zenoh-jni/target/$buildMode").include(arrayListOf("*.dylib", "*.so", "*.dll")) - } + val jvmTest by getting {} } val javadocJar by tasks.registering(Jar::class) { @@ -168,48 +160,7 @@ tasks.whenObjectAdded { } tasks.named("compileKotlinJvm") { - dependsOn("buildZenohJni") -} - -tasks.register("buildZenohJni") { - doLast { - if (!isRemotePublication) { - // This is intended for local publications. For publications done through GitHub workflows, - // the zenoh-jni build is achieved and loaded differently from the CI - buildZenohJNI(buildMode) - } - } -} - -fun buildZenohJNI(mode: BuildMode = BuildMode.DEBUG) { - val cargoCommand = mutableListOf("cargo", "build") - - if (mode == BuildMode.RELEASE) { - cargoCommand.add("--release") - } - - val result = project.exec { - commandLine(*(cargoCommand.toTypedArray()), "--manifest-path", "../zenoh-jni/Cargo.toml") - } - - if (result.exitValue != 0) { - throw GradleException("Failed to build Zenoh-JNI.") - } - - Thread.sleep(1000) -} - -enum class BuildMode { - DEBUG { - override fun toString(): String { - return "debug" - } - }, - RELEASE { - override fun toString(): String { - return "release" - } - } + dependsOn(":zenoh-jni-runtime:buildZenohJni") } fun Project.configureAndroid() { diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt index 91bc0e01..9443c731 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt @@ -46,7 +46,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic fun loadDefault(): Config { - return JNIConfig.loadDefaultConfig() + return Config(JNIConfig(JNIConfig.loadDefaultConfig())) } /** @@ -59,7 +59,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromFile(file: File): Config { - return JNIConfig.loadConfigFile(file) + return Config(JNIConfig(JNIConfig.loadConfigFile(file.toString()))) } /** @@ -72,7 +72,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromFile(path: Path): Config { - return JNIConfig.loadConfigFile(path) + return Config(JNIConfig(JNIConfig.loadConfigFile(path.toString()))) } /** @@ -87,7 +87,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromJson(config: String): Config { - return JNIConfig.loadJsonConfig(config) + return Config(JNIConfig(JNIConfig.loadJsonConfig(config))) } /** @@ -102,7 +102,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromJson5(config: String): Config { - return JNIConfig.loadJson5Config(config) + return Config(JNIConfig(JNIConfig.loadJson5Config(config))) } /** @@ -117,7 +117,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromYaml(config: String): Config { - return JNIConfig.loadYamlConfig(config) + return Config(JNIConfig(JNIConfig.loadYamlConfig(config))) } /** diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index d3b125f9..440d80b4 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -15,23 +15,40 @@ package io.zenoh import io.zenoh.annotations.Unstable +import io.zenoh.bytes.Encoding import io.zenoh.bytes.IntoZBytes import io.zenoh.bytes.ZBytes +import io.zenoh.bytes.into +import io.zenoh.config.EntityGlobalId import io.zenoh.config.ZenohId import io.zenoh.exceptions.ZError import io.zenoh.handlers.BlockingQueueHandler import io.zenoh.handlers.Callback import io.zenoh.handlers.Handler +import io.zenoh.jni.JNIKeyExpr +import io.zenoh.jni.JNIPublisher +import io.zenoh.jni.JNIQuery +import io.zenoh.jni.JNIQuerier +import io.zenoh.jni.JNIQueryable import io.zenoh.jni.JNISession +import io.zenoh.jni.JNISubscriber +import io.zenoh.jni.callbacks.JNIGetCallback +import io.zenoh.jni.callbacks.JNIQueryableCallback +import io.zenoh.jni.callbacks.JNISubscriberCallback import io.zenoh.keyexpr.KeyExpr import io.zenoh.liveliness.Liveliness import io.zenoh.pubsub.* +import io.zenoh.qos.CongestionControl +import io.zenoh.qos.Priority +import io.zenoh.qos.QoS import io.zenoh.query.* import io.zenoh.query.Query import io.zenoh.query.Queryable import io.zenoh.sample.Sample +import io.zenoh.sample.SampleKind import io.zenoh.session.SessionDeclaration import io.zenoh.session.SessionInfo +import org.apache.commons.net.ntp.TimeStamp import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.BlockingQueue @@ -383,9 +400,10 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) fun declareKeyExpr(keyExpr: String): KeyExpr { return jniSession?.run { - val keyexpr = declareKeyExpr(keyExpr) - strongDeclarations.add(keyexpr) - keyexpr + val ptr = declareKeyExprViaJNI(sessionPtr, keyExpr) + val ke = KeyExpr(keyExpr, JNIKeyExpr(ptr)) + strongDeclarations.add(ke) + ke } ?: throw sessionClosedException } @@ -400,8 +418,11 @@ class Session private constructor(private val config: Config) : AutoCloseable { */ @Throws(ZError::class) fun undeclare(keyExpr: KeyExpr) { - return jniSession?.run { - undeclareKeyExpr(keyExpr) + jniSession?.run { + keyExpr.jniKeyExpr?.run { + undeclareKeyExprViaJNI(sessionPtr, ptr) + keyExpr.jniKeyExpr = null + } ?: throw ZError("Attempting to undeclare a non declared key expression.") } ?: throw (sessionClosedException) } @@ -576,7 +597,22 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) internal fun resolvePublisher(keyExpr: KeyExpr, options: PublisherOptions): Publisher { return jniSession?.run { - val publisher = declarePublisher(keyExpr, options) + val publisherRawPtr = declarePublisherViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + sessionPtr, + options.congestionControl.value, + options.priority.value, + options.express, + options.reliability.ordinal + ) + val publisher = Publisher( + keyExpr, + options.congestionControl, + options.priority, + options.encoding, + JNIPublisher(publisherRawPtr) + ) weakDeclarations.add(WeakReference(publisher)) publisher } ?: throw (sessionClosedException) @@ -587,7 +623,25 @@ class Session private constructor(private val config: Config) : AutoCloseable { keyExpr: KeyExpr, handler: Handler ): HandlerSubscriber { return jniSession?.run { - val subscriber = declareSubscriberWithHandler(keyExpr, handler) + val subCallback = + JNISubscriberCallback { keyExpr1, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + handler.handle( + Sample( + KeyExpr(keyExpr1, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } + val subscriberRawPtr = declareSubscriberViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, subCallback, handler::onClose + ) + val subscriber = HandlerSubscriber(keyExpr, JNISubscriber(subscriberRawPtr), handler.receiver()) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -598,7 +652,25 @@ class Session private constructor(private val config: Config) : AutoCloseable { keyExpr: KeyExpr, callback: Callback ): CallbackSubscriber { return jniSession?.run { - val subscriber = declareSubscriberWithCallback(keyExpr, callback) + val subCallback = + JNISubscriberCallback { keyExpr1, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + callback.run( + Sample( + KeyExpr(keyExpr1, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } + val subscriberRawPtr = declareSubscriberViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, subCallback, fun() {} + ) + val subscriber = CallbackSubscriber(keyExpr, JNISubscriber(subscriberRawPtr)) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -609,7 +681,27 @@ class Session private constructor(private val config: Config) : AutoCloseable { keyExpr: KeyExpr, handler: Handler, options: QueryableOptions ): HandlerQueryable { return jniSession?.run { - val queryable = declareQueryableWithHandler(keyExpr, handler, options) + val queryCallback = + JNIQueryableCallback { keyExpr1, selectorParams, payload, encodingId, encodingSchema, attachmentBytes, queryPtr, acceptReplies -> + val jniQuery = JNIQuery(queryPtr) + val keyExpr2 = KeyExpr(keyExpr1, null) + val selector = if (selectorParams.isEmpty()) Selector(keyExpr2) else Selector(keyExpr2, Parameters.from(selectorParams)) + handler.handle( + Query( + keyExpr2, + selector, + payload?.into(), + payload?.let { Encoding(encodingId, schema = encodingSchema) }, + attachmentBytes?.into(), + ReplyKeyExpr.entries[acceptReplies], + jniQuery + ) + ) + } + val queryableRawPtr = declareQueryableViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, queryCallback, handler::onClose, options.complete + ) + val queryable = HandlerQueryable(keyExpr, JNIQueryable(queryableRawPtr), handler.receiver()) strongDeclarations.add(queryable) queryable } ?: throw (sessionClosedException) @@ -620,18 +712,55 @@ class Session private constructor(private val config: Config) : AutoCloseable { keyExpr: KeyExpr, callback: Callback, options: QueryableOptions ): CallbackQueryable { return jniSession?.run { - val queryable = declareQueryableWithCallback(keyExpr, callback, options) + val queryCallback = + JNIQueryableCallback { keyExpr1, selectorParams, payload, encodingId, encodingSchema, attachmentBytes, queryPtr, acceptReplies -> + val jniQuery = JNIQuery(queryPtr) + val keyExpr2 = KeyExpr(keyExpr1, null) + val selector = if (selectorParams.isEmpty()) Selector(keyExpr2) else Selector(keyExpr2, Parameters.from(selectorParams)) + callback.run( + Query( + keyExpr2, + selector, + payload?.into(), + payload?.let { Encoding(encodingId, schema = encodingSchema) }, + attachmentBytes?.into(), + ReplyKeyExpr.entries[acceptReplies], + jniQuery + ) + ) + } + val queryableRawPtr = declareQueryableViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, queryCallback, fun() {}, options.complete + ) + val queryable = CallbackQueryable(keyExpr, JNIQueryable(queryableRawPtr)) strongDeclarations.add(queryable) queryable } ?: throw (sessionClosedException) } + @OptIn(Unstable::class) private fun resolveQuerier( keyExpr: KeyExpr, options: QuerierOptions ): Querier { return jniSession?.run { - val querier = declareQuerier(keyExpr, options) + val querierRawPtr = declareQuerierViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + sessionPtr, + options.target.ordinal, + options.consolidationMode.ordinal, + options.congestionControl.value, + options.priority.value, + options.express, + options.timeout.toMillis(), + options.acceptReplies.ordinal + ) + val querier = Querier( + keyExpr, + QoS(congestionControl = options.congestionControl, priority = options.priority, express = options.express), + JNIQuerier(querierRawPtr) + ) weakDeclarations.add(WeakReference(querier)) querier } ?: throw sessionClosedException @@ -643,11 +772,49 @@ class Session private constructor(private val config: Config) : AutoCloseable { handler: Handler, options: GetOptions ): R { - return jniSession?.performGetWithHandler( - selector, - handler, - options - ) ?: throw sessionClosedException + return jniSession?.run { + val getCallback = JNIGetCallback { replierZid, replierEid, success, keyExpr, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val reply: Reply = if (success) { + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + Reply.Success( + replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + Sample( + KeyExpr(keyExpr!!, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } else { + Reply.Error(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, payload.into(), Encoding(encodingId, schema = encodingSchema)) + } + handler.handle(reply) + } + val sel = selector.into() + getViaJNI( + sel.keyExpr.jniKeyExpr?.ptr ?: 0, + sel.keyExpr.keyExpr, + sel.parameters?.toString(), + sessionPtr, + getCallback, + handler::onClose, + options.timeout.toMillis(), + options.target.ordinal, + options.consolidation.ordinal, + options.attachment?.into()?.bytes, + options.payload?.into()?.bytes, + options.encoding?.id ?: Encoding.defaultEncoding().id, + options.encoding?.schema, + options.qos.congestionControl.value, + options.qos.priority.value, + options.qos.express, + options.acceptReplies.ordinal + ) + handler.receiver() + } ?: throw sessionClosedException } @Throws(ZError::class) @@ -656,42 +823,105 @@ class Session private constructor(private val config: Config) : AutoCloseable { callback: Callback, options: GetOptions ) { - return jniSession?.performGetWithCallback( - selector, - callback, - options - ) ?: throw sessionClosedException + jniSession?.run { + val getCallback = JNIGetCallback { replierZid, replierEid, success, keyExpr, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val reply: Reply = if (success) { + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + Reply.Success( + replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + Sample( + KeyExpr(keyExpr!!, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } else { + Reply.Error(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, payload.into(), Encoding(encodingId, schema = encodingSchema)) + } + callback.run(reply) + } + val sel = selector.into() + getViaJNI( + sel.keyExpr.jniKeyExpr?.ptr ?: 0, + sel.keyExpr.keyExpr, + sel.parameters?.toString(), + sessionPtr, + getCallback, + fun() {}, + options.timeout.toMillis(), + options.target.ordinal, + options.consolidation.ordinal, + options.attachment?.into()?.bytes, + options.payload?.into()?.bytes, + options.encoding?.id ?: Encoding.defaultEncoding().id, + options.encoding?.schema, + options.qos.congestionControl.value, + options.qos.priority.value, + options.qos.express, + options.acceptReplies.ordinal + ) + } ?: throw sessionClosedException } @Throws(ZError::class) internal fun resolvePut(keyExpr: KeyExpr, payload: IntoZBytes, putOptions: PutOptions) { - jniSession?.run { performPut(keyExpr, payload, putOptions) } + jniSession?.run { + val encoding = putOptions.encoding ?: Encoding.defaultEncoding() + putViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + sessionPtr, + payload.into().bytes, + encoding.id, + encoding.schema, + putOptions.congestionControl.value, + putOptions.priority.value, + putOptions.express, + putOptions.attachment?.into()?.bytes, + putOptions.reliability.ordinal + ) + } } @Throws(ZError::class) internal fun resolveDelete(keyExpr: KeyExpr, deleteOptions: DeleteOptions) { - jniSession?.run { performDelete(keyExpr, deleteOptions) } + jniSession?.run { + deleteViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + sessionPtr, + deleteOptions.congestionControl.value, + deleteOptions.priority.value, + deleteOptions.express, + deleteOptions.attachment?.into()?.bytes, + deleteOptions.reliability.ordinal + ) + } } @Throws(ZError::class) internal fun zid(): ZenohId { - return jniSession?.zid() ?: throw sessionClosedException + return jniSession?.run { ZenohId(getZidViaJNI(sessionPtr)) } ?: throw sessionClosedException } @Throws(ZError::class) internal fun getPeersId(): List { - return jniSession?.peersZid() ?: throw sessionClosedException + return jniSession?.run { getPeersZidViaJNI(sessionPtr).map { ZenohId(it) } } ?: throw sessionClosedException } @Throws(ZError::class) internal fun getRoutersId(): List { - return jniSession?.routersZid() ?: throw sessionClosedException + return jniSession?.run { getRoutersZidViaJNI(sessionPtr).map { ZenohId(it) } } ?: throw sessionClosedException } /** Launches the session through the jni session, returning the [Session] on success. */ @Throws(ZError::class) private fun launch(): Session { - this.jniSession = JNISession.open(config) + this.jniSession = JNISession.open(config.jniConfig.ptr) return this } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt index 7a5931f8..7a5500d6 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt @@ -15,11 +15,14 @@ package io.zenoh import io.zenoh.Logger.Companion.LOG_ENV +import io.zenoh.config.WhatAmI +import io.zenoh.config.ZenohId import io.zenoh.exceptions.ZError import io.zenoh.handlers.BlockingQueueHandler import io.zenoh.handlers.Callback import io.zenoh.handlers.Handler import io.zenoh.jni.JNIScout +import io.zenoh.jni.callbacks.JNIScoutCallback import io.zenoh.scouting.* import java.util.* import java.util.concurrent.BlockingQueue @@ -53,10 +56,12 @@ object Zenoh { @Throws(ZError::class) fun scout(scoutOptions: ScoutOptions = ScoutOptions()): HandlerScout>> { val handler = BlockingQueueHandler(LinkedBlockingDeque>()) - return JNIScout.scoutWithHandler( - scoutOptions.whatAmI, handler::handle, fun() { handler.onClose() }, - receiver = handler.receiver(), config = scoutOptions.config - ) + val scoutCallback = JNIScoutCallback { whatAmI, id, locators -> + handler.handle(Hello(WhatAmI.fromInt(whatAmI), ZenohId(id), locators)) + } + val binaryWhatAmI = scoutOptions.whatAmI.map { it.value }.reduce { acc, it -> acc or it } + val ptr = JNIScout.scoutViaJNI(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig?.ptr ?: 0) + return HandlerScout(JNIScout(ptr), handler.receiver()) } /** @@ -74,10 +79,12 @@ object Zenoh { @JvmStatic @Throws(ZError::class) fun scout(handler: Handler, scoutOptions: ScoutOptions = ScoutOptions()): HandlerScout { - return JNIScout.scoutWithHandler( - scoutOptions.whatAmI, handler::handle, fun() { handler.onClose() }, - receiver = handler.receiver(), config = scoutOptions.config - ) + val scoutCallback = JNIScoutCallback { whatAmI, id, locators -> + handler.handle(Hello(WhatAmI.fromInt(whatAmI), ZenohId(id), locators)) + } + val binaryWhatAmI = scoutOptions.whatAmI.map { it.value }.reduce { acc, it -> acc or it } + val ptr = JNIScout.scoutViaJNI(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig?.ptr ?: 0) + return HandlerScout(JNIScout(ptr), handler.receiver()) } /** @@ -94,9 +101,12 @@ object Zenoh { @JvmStatic @Throws(ZError::class) fun scout(callback: Callback, scoutOptions: ScoutOptions = ScoutOptions()): CallbackScout { - return JNIScout.scoutWithCallback( - scoutOptions.whatAmI, callback, config = scoutOptions.config - ) + val scoutCallback = JNIScoutCallback { whatAmI, id, locators -> + callback.run(Hello(WhatAmI.fromInt(whatAmI), ZenohId(id), locators)) + } + val binaryWhatAmI = scoutOptions.whatAmI.map { it.value }.reduce { acc, it -> acc or it } + val ptr = JNIScout.scoutViaJNI(binaryWhatAmI, scoutCallback, fun() {}, scoutOptions.config?.jniConfig?.ptr ?: 0) + return CallbackScout(JNIScout(ptr)) } /** @@ -136,9 +146,3 @@ object Zenoh { logLevelProp?.let { Logger.start(it) } ?: Logger.start(fallbackFilter) } } - -/** - * Static singleton class to load the Zenoh native library once and only once, as well as the logger in function of the - * log level configuration. - */ -internal expect object ZenohLoad diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt deleted file mode 100644 index 0225c32b..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt +++ /dev/null @@ -1,188 +0,0 @@ -// -// 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.bytes.Encoding -import io.zenoh.bytes.into -import io.zenoh.config.EntityGlobalId -import io.zenoh.config.ZenohId -import io.zenoh.exceptions.ZError -import io.zenoh.handlers.Callback -import io.zenoh.jni.callbacks.JNIGetCallback -import io.zenoh.jni.callbacks.JNIOnCloseCallback -import io.zenoh.jni.callbacks.JNISubscriberCallback -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.liveliness.LivelinessToken -import io.zenoh.pubsub.CallbackSubscriber -import io.zenoh.pubsub.HandlerSubscriber -import io.zenoh.qos.CongestionControl -import io.zenoh.qos.Priority -import io.zenoh.qos.QoS -import io.zenoh.query.Reply -import io.zenoh.sample.Sample -import io.zenoh.sample.SampleKind -import org.apache.commons.net.ntp.TimeStamp -import java.time.Duration - -internal object JNILiveliness { - - @Throws(ZError::class) - fun get( - jniSession: JNISession, - keyExpr: KeyExpr, - callback: Callback, - receiver: R, - timeout: Duration, - onClose: Runnable - ): R { - val getCallback = JNIGetCallback { - replierZid: ByteArray?, - replierEid: Int, - success: Boolean, - keyExpr2: String?, - payload: ByteArray, - encodingId: Int, - encodingSchema: String?, - kind: Int, - timestampNTP64: Long, - timestampIsValid: Boolean, - attachmentBytes: ByteArray?, - express: Boolean, - priority: Int, - congestionControl: Int, - -> - val reply: Reply - if (success) { - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr2!!, null), - payload.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) - } else { - reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, - payload.into(), - Encoding(encodingId, schema = encodingSchema) - ) - } - callback.run(reply) - } - getViaJNI( - jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - getCallback, - timeout.toMillis(), - onClose::run - ) - return receiver - } - - fun declareToken(jniSession: JNISession, keyExpr: KeyExpr): LivelinessToken { - val ptr = declareTokenViaJNI(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr) - return LivelinessToken(JNILivelinessToken(ptr)) - } - - fun declareSubscriber( - jniSession: JNISession, - keyExpr: KeyExpr, - callback: Callback, - history: Boolean, - onClose: () -> Unit - ): CallbackSubscriber { - val subCallback = - JNISubscriberCallback { keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr2, null), - payload.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - callback.run(sample) - } - val ptr = declareSubscriberViaJNI( - jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - subCallback, - history, - onClose - ) - return CallbackSubscriber(keyExpr, JNISubscriber(ptr)) - } - - fun declareSubscriber( - jniSession: JNISession, - keyExpr: KeyExpr, - callback: Callback, - receiver: R, - history: Boolean, - onClose: () -> Unit - ): HandlerSubscriber { - val subCallback = - JNISubscriberCallback { keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr2, null), - payload.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - callback.run(sample) - } - val ptr = declareSubscriberViaJNI( - jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - subCallback, - history, - onClose - ) - return HandlerSubscriber(keyExpr, JNISubscriber(ptr), receiver) - } - - private external fun getViaJNI( - sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, - callback: JNIGetCallback, - timeoutMs: Long, - onClose: JNIOnCloseCallback - ) - - private external fun declareTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long - - private external fun declareSubscriberViaJNI( - sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, - callback: JNISubscriberCallback, - history: Boolean, - onClose: JNIOnCloseCallback - ): Long -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt deleted file mode 100644 index 991c860a..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.zenoh.jni - -internal class JNILivelinessToken(val ptr: Long) { - - fun undeclare() { - undeclareViaJNI(this.ptr) - } - - companion object { - external fun undeclareViaJNI(ptr: Long) - } -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt deleted file mode 100644 index 6e694271..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt +++ /dev/null @@ -1,70 +0,0 @@ -// -// 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.exceptions.ZError -import io.zenoh.bytes.Encoding -import io.zenoh.bytes.IntoZBytes - -/** - * Adapter class to handle the interactions with Zenoh through JNI for a [io.zenoh.pubsub.Publisher]. - * - * @property ptr: raw pointer to the underlying native Publisher. - */ -internal class JNIPublisher(private val ptr: Long) { - - /** - * Put operation. - * - * @param payload Payload of the put. - * @param encoding Encoding of the payload. - * @param attachment Optional attachment. - */ - @Throws(ZError::class) - fun put(payload: IntoZBytes, encoding: Encoding?, attachment: IntoZBytes?) { - val resolvedEncoding = encoding ?: Encoding.defaultEncoding() - putViaJNI(payload.into().bytes, resolvedEncoding.id, resolvedEncoding.schema, attachment?.into()?.bytes, ptr) - } - - /** - * Delete operation. - * - * @param attachment Optional attachment. - */ - @Throws(ZError::class) - fun delete(attachment: IntoZBytes?) { - deleteViaJNI(attachment?.into()?.bytes, ptr) - } - - /** - * Close and free the underlying publisher pointer. - * - * Further operations with this publisher should not be performed anymore. - */ - fun close() { - freePtrViaJNI(ptr) - } - - @Throws(ZError::class) - private external fun putViaJNI( - valuePayload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?, ptr: Long - ) - - @Throws(ZError::class) - private external fun deleteViaJNI(attachment: ByteArray?, ptr: Long) - - private external fun freePtrViaJNI(ptr: Long) - -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt deleted file mode 100644 index c026ed1c..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt +++ /dev/null @@ -1,137 +0,0 @@ -// -// 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.annotations.Unstable -import io.zenoh.bytes.Encoding -import io.zenoh.bytes.IntoZBytes -import io.zenoh.bytes.into -import io.zenoh.config.EntityGlobalId -import io.zenoh.config.ZenohId -import io.zenoh.exceptions.ZError -import io.zenoh.handlers.Callback -import io.zenoh.handlers.Handler -import io.zenoh.jni.callbacks.JNIGetCallback -import io.zenoh.jni.callbacks.JNIOnCloseCallback -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.qos.CongestionControl -import io.zenoh.qos.Priority -import io.zenoh.qos.QoS -import io.zenoh.query.Parameters -import io.zenoh.query.Querier -import io.zenoh.query.Reply -import io.zenoh.sample.Sample -import io.zenoh.sample.SampleKind -import org.apache.commons.net.ntp.TimeStamp - -internal class JNIQuerier(val ptr: Long) { - - @OptIn(Unstable::class) - @Throws(ZError::class) - fun performGetWithCallback(keyExpr: KeyExpr, callback: Callback, options: Querier.GetOptions) { - performGet(keyExpr, options.parameters, callback, fun() {}, Unit, options.attachment, options.payload, options.encoding) - } - - @OptIn(Unstable::class) - @Throws(ZError::class) - fun performGetWithHandler(keyExpr: KeyExpr, handler: Handler, options: Querier.GetOptions): R { - return performGet(keyExpr, options.parameters, handler::handle, handler::onClose, handler.receiver(), options.attachment, options.payload, options.encoding) - } - - @Throws(ZError::class) - private fun performGet( - keyExpr: KeyExpr, - parameters: Parameters?, - callback: Callback, - onClose: () -> Unit, - receiver: R, - attachment: IntoZBytes?, - payload: IntoZBytes?, - encoding: Encoding? - ): R { - val getCallback = JNIGetCallback { - replierZid: ByteArray?, - replierEid: Int, - success: Boolean, - keyExpr2: String?, - payload2: ByteArray, - encodingId: Int, - encodingSchema: String?, - kind: Int, - timestampNTP64: Long, - timestampIsValid: Boolean, - attachmentBytes: ByteArray?, - express: Boolean, - priority: Int, - congestionControl: Int, - -> - val reply: Reply - if (success) { - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr2!!, null), - payload2.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) - } else { - reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, - payload2.into(), - Encoding(encodingId, schema = encodingSchema) - ) - } - callback.run(reply) - } - - getViaJNI(this.ptr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - parameters?.toString(), - getCallback, - onClose, - attachment?.into()?.bytes, - payload?.into()?.bytes, - encoding?.id ?: Encoding.defaultEncoding().id, - encoding?.schema - ) - return receiver - } - - fun close() { - freePtrViaJNI(ptr) - } - - @Throws(ZError::class) - private external fun getViaJNI( - querierPtr: Long, - keyExprPtr: Long, - keyExprString: String, - parameters: String?, - callback: JNIGetCallback, - onClose: JNIOnCloseCallback, - attachmentBytes: ByteArray?, - payload: ByteArray?, - encodingId: Int, - encodingSchema: String?, - ) - - private external fun freePtrViaJNI(ptr: Long) - -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt deleted file mode 100644 index 0b1683f0..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt +++ /dev/null @@ -1,106 +0,0 @@ -// -// 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.exceptions.ZError -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.bytes.Encoding -import io.zenoh.qos.QoS -import io.zenoh.bytes.IntoZBytes -import io.zenoh.sample.Sample -import org.apache.commons.net.ntp.TimeStamp - -/** - * Adapter class for interacting with a Query using JNI. - * - * This class serves as an adapter for interacting with a Query through JNI (Java Native Interface). - * - * @property ptr The raw pointer to the underlying native query. - */ -internal class JNIQuery(private val ptr: Long) { - - fun replySuccess(sample: Sample) { - val timestampEnabled = sample.timestamp != null - replySuccessViaJNI( - ptr, - sample.keyExpr.jniKeyExpr?.ptr ?: 0, - sample.keyExpr.keyExpr, - sample.payload.bytes, - sample.encoding.id, - sample.encoding.schema, - timestampEnabled, - if (timestampEnabled) sample.timestamp!!.ntpValue() else 0, - sample.attachment?.bytes, - sample.qos.express - ) - } - - fun replyError(error: IntoZBytes, encoding: Encoding) { - replyErrorViaJNI(ptr, error.into().bytes, encoding.id, encoding.schema) - } - - fun replyDelete(keyExpr: KeyExpr, timestamp: TimeStamp?, attachment: IntoZBytes?, qos: QoS) { - val timestampEnabled = timestamp != null - replyDeleteViaJNI( - ptr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - timestampEnabled, - if (timestampEnabled) timestamp!!.ntpValue() else 0, - attachment?.into()?.bytes, - qos.express - ) - } - - fun close() { - freePtrViaJNI(ptr) - } - - @Throws(ZError::class) - private external fun replySuccessViaJNI( - queryPtr: Long, - keyExprPtr: Long, - keyExprString: String, - valuePayload: ByteArray, - valueEncodingId: Int, - valueEncodingSchema: String?, - timestampEnabled: Boolean, - timestampNtp64: Long, - attachment: ByteArray?, - qosExpress: Boolean, - ) - - @Throws(ZError::class) - private external fun replyErrorViaJNI( - queryPtr: Long, - errorValuePayload: ByteArray, - errorValueEncoding: Int, - encodingSchema: String?, - ) - - @Throws(ZError::class) - private external fun replyDeleteViaJNI( - queryPtr: Long, - keyExprPtr: Long, - keyExprString: String, - timestampEnabled: Boolean, - timestampNtp64: Long, - attachment: ByteArray?, - qosExpress: Boolean, - ) - - /** Frees the underlying native Query. */ - private external fun freePtrViaJNI(ptr: Long) -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt deleted file mode 100644 index f2e1c49c..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt +++ /dev/null @@ -1,87 +0,0 @@ -// -// 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.Config -import io.zenoh.ZenohLoad -import io.zenoh.exceptions.ZError -import io.zenoh.handlers.Callback -import io.zenoh.jni.callbacks.JNIScoutCallback -import io.zenoh.config.ZenohId -import io.zenoh.scouting.Hello -import io.zenoh.config.WhatAmI -import io.zenoh.jni.callbacks.JNIOnCloseCallback -import io.zenoh.scouting.CallbackScout -import io.zenoh.scouting.HandlerScout - -/** - * Adapter class to handle the interactions with Zenoh through JNI for a [io.zenoh.scouting.Scout] - * - * @property ptr: raw pointer to the underlying native scout. - */ -internal class JNIScout(private val ptr: Long) { - - companion object { - - init { - ZenohLoad - } - - @Throws(ZError::class) - fun scoutWithHandler( - whatAmI: Set, - callback: Callback, - onClose: () -> Unit, - config: Config?, - receiver: R - ): HandlerScout { - val scoutCallback = JNIScoutCallback { whatAmI2: Int, id: ByteArray, locators: List -> - callback.run(Hello(WhatAmI.fromInt(whatAmI2), ZenohId(id), locators)) - } - val binaryWhatAmI: Int = whatAmI.map { it.value }.reduce { acc, it -> acc or it } - val ptr = scoutViaJNI(binaryWhatAmI, scoutCallback, onClose,config?.jniConfig?.ptr ?: 0) - return HandlerScout(JNIScout(ptr), receiver) - } - - @Throws(ZError::class) - fun scoutWithCallback( - whatAmI: Set, - callback: Callback, - config: Config?, - ): CallbackScout { - val scoutCallback = JNIScoutCallback { whatAmI2: Int, id: ByteArray, locators: List -> - callback.run(Hello(WhatAmI.fromInt(whatAmI2), ZenohId(id), locators)) - } - val binaryWhatAmI: Int = whatAmI.map { it.value }.reduce { acc, it -> acc or it } - val ptr = scoutViaJNI(binaryWhatAmI, scoutCallback, fun() {},config?.jniConfig?.ptr ?: 0) - return CallbackScout(JNIScout(ptr)) - } - - @Throws(ZError::class) - private external fun scoutViaJNI( - whatAmI: Int, - callback: JNIScoutCallback, - onClose: JNIOnCloseCallback, - configPtr: Long, - ): Long - - @Throws(ZError::class) - external fun freePtrViaJNI(ptr: Long) - } - - fun close() { - freePtrViaJNI(ptr) - } -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt deleted file mode 100644 index 7026ebd6..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ /dev/null @@ -1,549 +0,0 @@ -// -// 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.* -import io.zenoh.bytes.Encoding -import io.zenoh.exceptions.ZError -import io.zenoh.handlers.Callback -import io.zenoh.jni.callbacks.JNIOnCloseCallback -import io.zenoh.jni.callbacks.JNIGetCallback -import io.zenoh.jni.callbacks.JNIQueryableCallback -import io.zenoh.jni.callbacks.JNISubscriberCallback -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.bytes.IntoZBytes -import io.zenoh.config.ZenohId -import io.zenoh.bytes.into -import io.zenoh.Config -import io.zenoh.annotations.Unstable -import io.zenoh.config.EntityGlobalId -import io.zenoh.handlers.Handler -import io.zenoh.pubsub.* -import io.zenoh.qos.CongestionControl -import io.zenoh.qos.Priority -import io.zenoh.qos.QoS -import io.zenoh.query.* -import io.zenoh.sample.Sample -import io.zenoh.sample.SampleKind -import org.apache.commons.net.ntp.TimeStamp - -/** Adapter class to handle the communication with the Zenoh JNI code for a [Session]. */ -internal class JNISession(val sessionPtr: Long) { - - companion object { - init { - ZenohLoad - } - - @Throws(ZError::class) - fun open(config: Config): JNISession { - val sessionPtr = openSessionViaJNI(config.jniConfig.ptr) - return JNISession(sessionPtr) - } - - @JvmStatic - @Throws(ZError::class) - private external fun openSessionViaJNI(configPtr: Long): Long - } - - fun close() { - closeSessionViaJNI(sessionPtr) - } - - @Throws(ZError::class) - fun declarePublisher(keyExpr: KeyExpr, publisherOptions: PublisherOptions): Publisher { - val publisherRawPtr = declarePublisherViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - publisherOptions.congestionControl.value, - publisherOptions.priority.value, - publisherOptions.express, - publisherOptions.reliability.ordinal - ) - return Publisher( - keyExpr, - publisherOptions.congestionControl, - publisherOptions.priority, - publisherOptions.encoding, - JNIPublisher(publisherRawPtr), - ) - } - - @Throws(ZError::class) - fun declareSubscriberWithHandler( - keyExpr: KeyExpr, handler: Handler - ): HandlerSubscriber { - val subCallback = - JNISubscriberCallback { keyExpr1, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr1, null), - payload.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - handler.handle(sample) - } - val subscriberRawPtr = declareSubscriberViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, subCallback, handler::onClose - ) - return HandlerSubscriber(keyExpr, JNISubscriber(subscriberRawPtr), handler.receiver()) - } - - @Throws(ZError::class) - fun declareSubscriberWithCallback( - keyExpr: KeyExpr, callback: Callback - ): CallbackSubscriber { - val subCallback = - JNISubscriberCallback { keyExpr1, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr1, null), - payload.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - callback.run(sample) - } - val subscriberRawPtr = declareSubscriberViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - subCallback, - fun() {} - ) - return CallbackSubscriber(keyExpr, JNISubscriber(subscriberRawPtr)) - } - - @Throws(ZError::class) - fun declareQueryableWithCallback( - keyExpr: KeyExpr, callback: Callback, config: QueryableOptions - ): CallbackQueryable { - val queryCallback = - JNIQueryableCallback { keyExpr1: String, selectorParams: String, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long, acceptReplies: Int -> - val jniQuery = JNIQuery(queryPtr) - val keyExpr2 = KeyExpr(keyExpr1, null) - val selector = if (selectorParams.isEmpty()) { - Selector(keyExpr2) - } else { - Selector(keyExpr2, Parameters.from(selectorParams)) - } - val replyKeyExpr = ReplyKeyExpr.entries[acceptReplies] - val query = Query( - keyExpr2, - selector, - payload?.into(), - payload?.let { Encoding(encodingId, schema = encodingSchema) }, - attachmentBytes?.into(), - replyKeyExpr, - jniQuery - ) - callback.run(query) - } - val queryableRawPtr = declareQueryableViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - queryCallback, - fun() {}, - config.complete - ) - return CallbackQueryable(keyExpr, JNIQueryable(queryableRawPtr)) - } - - @Throws(ZError::class) - fun declareQueryableWithHandler( - keyExpr: KeyExpr, handler: Handler, config: QueryableOptions - ): HandlerQueryable { - val queryCallback = - JNIQueryableCallback { keyExpr1: String, selectorParams: String, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long, acceptReplies: Int -> - val jniQuery = JNIQuery(queryPtr) - val keyExpr2 = KeyExpr(keyExpr1, null) - val selector = if (selectorParams.isEmpty()) { - Selector(keyExpr2) - } else { - Selector(keyExpr2, Parameters.from(selectorParams)) - } - val replyKeyExpr = ReplyKeyExpr.entries[acceptReplies] - val query = Query( - keyExpr2, - selector, - payload?.into(), - payload?.let { Encoding(encodingId, schema = encodingSchema) }, - attachmentBytes?.into(), - replyKeyExpr, - jniQuery - ) - handler.handle(query) - } - val queryableRawPtr = declareQueryableViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - queryCallback, - handler::onClose, - config.complete - ) - return HandlerQueryable(keyExpr, JNIQueryable(queryableRawPtr), handler.receiver()) - } - - @OptIn(Unstable::class) - fun declareQuerier( - keyExpr: KeyExpr, - options: QuerierOptions - ): Querier { - val querierRawPtr = declareQuerierViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - options.target.ordinal, - options.consolidationMode.ordinal, - options.congestionControl.value, - options.priority.value, - options.express, - options.timeout.toMillis(), - options.acceptReplies.ordinal - ) - return Querier( - keyExpr, - QoS( - congestionControl = options.congestionControl, - priority = options.priority, - express = options.express - ), - JNIQuerier(querierRawPtr) - ) - } - - @Throws(ZError::class) - fun performGetWithCallback( - intoSelector: IntoSelector, - callback: Callback, - options: GetOptions - ) { - val getCallback = JNIGetCallback { - replierZid: ByteArray?, - replierEid: Int, - success: Boolean, - keyExpr: String?, - payload1: ByteArray, - encodingId: Int, - encodingSchema: String?, - kind: Int, - timestampNTP64: Long, - timestampIsValid: Boolean, - attachmentBytes: ByteArray?, - express: Boolean, - priority: Int, - congestionControl: Int, - -> - val reply: Reply - if (success) { - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr!!, null), - payload1.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) - } else { - reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, - payload1.into(), - Encoding(encodingId, schema = encodingSchema) - ) - } - callback.run(reply) - } - - val selector = intoSelector.into() - getViaJNI( - selector.keyExpr.jniKeyExpr?.ptr ?: 0, - selector.keyExpr.keyExpr, - selector.parameters?.toString(), - sessionPtr, - getCallback, - fun() {}, - options.timeout.toMillis(), - options.target.ordinal, - options.consolidation.ordinal, - options.attachment?.into()?.bytes, - options.payload?.into()?.bytes, - options.encoding?.id ?: Encoding.defaultEncoding().id, - options.encoding?.schema, - options.qos.congestionControl.value, - options.qos.priority.value, - options.qos.express, - options.acceptReplies.ordinal - ) - } - - @Throws(ZError::class) - fun performGetWithHandler( - intoSelector: IntoSelector, - handler: Handler, - options: GetOptions - ): R { - val getCallback = JNIGetCallback { - replierZid: ByteArray?, - replierEid: Int, - success: Boolean, - keyExpr: String?, - payload1: ByteArray, - encodingId: Int, - encodingSchema: String?, - kind: Int, - timestampNTP64: Long, - timestampIsValid: Boolean, - attachmentBytes: ByteArray?, - express: Boolean, - priority: Int, - congestionControl: Int, - -> - val reply: Reply - if (success) { - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val sample = Sample( - KeyExpr(keyExpr!!, null), - payload1.into(), - Encoding(encodingId, schema = encodingSchema), - SampleKind.fromInt(kind), - timestamp, - QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), - attachmentBytes?.into() - ) - reply = Reply.Success(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, sample) - } else { - reply = Reply.Error( - replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, - payload1.into(), - Encoding(encodingId, schema = encodingSchema) - ) - } - handler.handle(reply) - } - - val selector = intoSelector.into() - getViaJNI( - selector.keyExpr.jniKeyExpr?.ptr ?: 0, - selector.keyExpr.keyExpr, - selector.parameters?.toString(), - sessionPtr, - getCallback, - handler::onClose, - options.timeout.toMillis(), - options.target.ordinal, - options.consolidation.ordinal, - options.attachment?.into()?.bytes, - options.payload?.into()?.bytes, - options.encoding?.id ?: Encoding.defaultEncoding().id, - options.encoding?.schema, - options.qos.congestionControl.value, - options.qos.priority.value, - options.qos.express, - options.acceptReplies.ordinal - ) - return handler.receiver() - } - - @Throws(ZError::class) - fun declareKeyExpr(keyExpr: String): KeyExpr { - val ptr = declareKeyExprViaJNI(sessionPtr, keyExpr) - return KeyExpr(keyExpr, JNIKeyExpr(ptr)) - } - - @Throws(ZError::class) - fun undeclareKeyExpr(keyExpr: KeyExpr) { - keyExpr.jniKeyExpr?.run { - undeclareKeyExprViaJNI(sessionPtr, this.ptr) - keyExpr.jniKeyExpr = null - } ?: throw ZError("Attempting to undeclare a non declared key expression.") - } - - @Throws(ZError::class) - fun performPut( - keyExpr: KeyExpr, - payload: IntoZBytes, - options: PutOptions, - ) { - val encoding = options.encoding ?: Encoding.defaultEncoding() - putViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - payload.into().bytes, - encoding.id, - encoding.schema, - options.congestionControl.value, - options.priority.value, - options.express, - options.attachment?.into()?.bytes, - options.reliability.ordinal - ) - } - - @Throws(ZError::class) - fun performDelete( - keyExpr: KeyExpr, - options: DeleteOptions, - ) { - deleteViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - options.congestionControl.value, - options.priority.value, - options.express, - options.attachment?.into()?.bytes, - options.reliability.ordinal - ) - } - - @Throws(ZError::class) - fun zid(): ZenohId { - return ZenohId(getZidViaJNI(sessionPtr)) - } - - @Throws(ZError::class) - fun peersZid(): List { - return getPeersZidViaJNI(sessionPtr).map { ZenohId(it) } - } - - @Throws(ZError::class) - fun routersZid(): List { - return getRoutersZidViaJNI(sessionPtr).map { ZenohId(it) } - } - - @Throws(ZError::class) - private external fun getZidViaJNI(ptr: Long): ByteArray - - @Throws(ZError::class) - private external fun getPeersZidViaJNI(ptr: Long): List - - @Throws(ZError::class) - private external fun getRoutersZidViaJNI(ptr: Long): List - - @Throws(ZError::class) - private external fun closeSessionViaJNI(ptr: Long) - - @Throws(ZError::class) - private external fun declarePublisherViaJNI( - keyExprPtr: Long, - keyExprString: String, - sessionPtr: Long, - congestionControl: Int, - priority: Int, - express: Boolean, - reliability: Int - ): Long - - @Throws(ZError::class) - private external fun declareSubscriberViaJNI( - keyExprPtr: Long, - keyExprString: String, - sessionPtr: Long, - callback: JNISubscriberCallback, - onClose: JNIOnCloseCallback, - ): Long - - @Throws(ZError::class) - private external fun declareQueryableViaJNI( - keyExprPtr: Long, - keyExprString: String, - sessionPtr: Long, - callback: JNIQueryableCallback, - onClose: JNIOnCloseCallback, - complete: Boolean - ): Long - - @Throws(ZError::class) - private external fun declareQuerierViaJNI( - keyExprPtr: Long, - keyExprString: String, - sessionPtr: Long, - target: Int, - consolidation: Int, - congestionControl: Int, - priority: Int, - express: Boolean, - timeoutMs: Long, - acceptReplies: Int - ): Long - - @Throws(ZError::class) - private external fun declareKeyExprViaJNI(sessionPtr: Long, keyExpr: String): Long - - @Throws(ZError::class) - private external fun undeclareKeyExprViaJNI(sessionPtr: Long, keyExprPtr: Long) - - @Throws(ZError::class) - private external fun getViaJNI( - keyExprPtr: Long, - keyExprString: String, - selectorParams: String?, - sessionPtr: Long, - callback: JNIGetCallback, - onClose: JNIOnCloseCallback, - timeoutMs: Long, - target: Int, - consolidation: Int, - attachmentBytes: ByteArray?, - payload: ByteArray?, - encodingId: Int, - encodingSchema: String?, - congestionControl: Int, - priority: Int, - express: Boolean, - acceptReplies: Int, - ) - - @Throws(ZError::class) - private external fun putViaJNI( - keyExprPtr: Long, - keyExprString: String, - sessionPtr: Long, - valuePayload: ByteArray, - valueEncoding: Int, - valueEncodingSchema: String?, - congestionControl: Int, - priority: Int, - express: Boolean, - attachmentBytes: ByteArray?, - reliability: Int - ) - - @Throws(ZError::class) - private external fun deleteViaJNI( - keyExprPtr: Long, - keyExprString: String, - sessionPtr: Long, - congestionControl: Int, - priority: Int, - express: Boolean, - attachmentBytes: ByteArray?, - reliability: Int - ) -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt index b5fe6110..fda83d39 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt @@ -79,7 +79,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn @JvmStatic @Throws(ZError::class) fun tryFrom(keyExpr: String): KeyExpr { - return JNIKeyExpr.tryFrom(keyExpr) + return KeyExpr(JNIKeyExpr.tryFrom(keyExpr)) } /** @@ -95,7 +95,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn @JvmStatic @Throws(ZError::class) fun autocanonize(keyExpr: String): KeyExpr { - return JNIKeyExpr.autocanonize(keyExpr) + return KeyExpr(JNIKeyExpr.autocanonize(keyExpr)) } } @@ -106,7 +106,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun intersects(other: KeyExpr): Boolean { - return JNIKeyExpr.intersects(this, other) + return JNIKeyExpr.intersects(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) } /** @@ -116,7 +116,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun includes(other: KeyExpr): Boolean { - return JNIKeyExpr.includes(this, other) + return JNIKeyExpr.includes(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) } /** @@ -126,7 +126,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun relationTo(other: KeyExpr): SetIntersectionLevel { - return JNIKeyExpr.relationTo(this, other) + return SetIntersectionLevel.fromInt(JNIKeyExpr.relationTo(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr)) } /** @@ -135,7 +135,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun join(other: String): KeyExpr { - return JNIKeyExpr.joinViaJNI(this, other) + return KeyExpr(JNIKeyExpr.join(jniKeyExpr?.ptr ?: 0, keyExpr, other)) } /** @@ -144,7 +144,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun concat(other: String): KeyExpr { - return JNIKeyExpr.concatViaJNI(this, other) + return KeyExpr(JNIKeyExpr.concat(jniKeyExpr?.ptr ?: 0, keyExpr, other)) } override fun toString(): String { diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt index 96711557..be811bba 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt @@ -15,17 +15,30 @@ package io.zenoh.liveliness import io.zenoh.Session +import io.zenoh.bytes.Encoding +import io.zenoh.bytes.into +import io.zenoh.config.EntityGlobalId +import io.zenoh.config.ZenohId import io.zenoh.exceptions.ZError import io.zenoh.handlers.BlockingQueueHandler import io.zenoh.handlers.Callback import io.zenoh.handlers.Handler import io.zenoh.jni.JNILiveliness +import io.zenoh.jni.JNILivelinessToken +import io.zenoh.jni.JNISubscriber +import io.zenoh.jni.callbacks.JNIGetCallback +import io.zenoh.jni.callbacks.JNISubscriberCallback import io.zenoh.keyexpr.KeyExpr import io.zenoh.pubsub.CallbackSubscriber import io.zenoh.pubsub.HandlerSubscriber import io.zenoh.pubsub.Subscriber +import io.zenoh.qos.CongestionControl +import io.zenoh.qos.Priority +import io.zenoh.qos.QoS import io.zenoh.query.Reply import io.zenoh.sample.Sample +import io.zenoh.sample.SampleKind +import org.apache.commons.net.ntp.TimeStamp import java.time.Duration import java.util.* import java.util.concurrent.BlockingQueue @@ -49,7 +62,8 @@ class Liveliness internal constructor(private val session: Session) { @Throws(ZError::class) fun declareToken(keyExpr: KeyExpr): LivelinessToken { val jniSession = session.jniSession ?: throw Session.sessionClosedException - return JNILiveliness.declareToken(jniSession, keyExpr) + val ptr = JNILiveliness.declareTokenViaJNI(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr) + return LivelinessToken(JNILivelinessToken(ptr)) } /** @@ -66,14 +80,16 @@ class Liveliness internal constructor(private val session: Session) { ): BlockingQueue> { val jniSession = session.jniSession ?: throw Session.sessionClosedException val handler = BlockingQueueHandler(LinkedBlockingDeque()) - return JNILiveliness.get( - jniSession, - keyExpr, - handler::handle, - receiver = handler.receiver(), - timeout, - onClose = handler::onClose + val getCallback = buildGetCallback(handler::handle) + JNILiveliness.getViaJNI( + jniSession.sessionPtr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + getCallback, + timeout.toMillis(), + handler::onClose ) + return handler.receiver() } /** @@ -89,7 +105,14 @@ class Liveliness internal constructor(private val session: Session) { keyExpr: KeyExpr, callback: Callback, timeout: Duration = Duration.ofMillis(10000) ) { val jniSession = session.jniSession ?: throw Session.sessionClosedException - return JNILiveliness.get(jniSession, keyExpr, callback, Unit, timeout, {}) + JNILiveliness.getViaJNI( + jniSession.sessionPtr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + buildGetCallback(callback), + timeout.toMillis(), + fun() {} + ) } /** @@ -106,17 +129,39 @@ class Liveliness internal constructor(private val session: Session) { keyExpr: KeyExpr, handler: Handler, timeout: Duration = Duration.ofMillis(10000) ): R { val jniSession = session.jniSession ?: throw Session.sessionClosedException - val callback = handler::handle - return JNILiveliness.get( - jniSession, - keyExpr, - callback, - handler.receiver(), - timeout, - onClose = handler::onClose + JNILiveliness.getViaJNI( + jniSession.sessionPtr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + buildGetCallback(handler::handle), + timeout.toMillis(), + handler::onClose ) + return handler.receiver() } + private fun buildGetCallback(callback: Callback): JNIGetCallback = + JNIGetCallback { replierZid, replierEid, success, keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val reply: Reply = if (success) { + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + Reply.Success( + replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + Sample( + KeyExpr(keyExpr2!!, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } else { + Reply.Error(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, payload.into(), Encoding(encodingId, schema = encodingSchema)) + } + callback.run(reply) + } + /** * Create a [Subscriber] for liveliness changes matching the given key expression. * @@ -131,14 +176,16 @@ class Liveliness internal constructor(private val session: Session) { ): HandlerSubscriber>> { val handler = BlockingQueueHandler(LinkedBlockingDeque()) val jniSession = session.jniSession ?: throw Session.sessionClosedException - return JNILiveliness.declareSubscriber( - jniSession, - keyExpr, - handler::handle, - handler.receiver(), + val subCallback = buildSubscriberCallback(handler::handle) + val ptr = JNILiveliness.declareSubscriberViaJNI( + jniSession.sessionPtr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + subCallback, options.history, handler::onClose ) + return HandlerSubscriber(keyExpr, JNISubscriber(ptr), handler.receiver()) } /** @@ -156,13 +203,16 @@ class Liveliness internal constructor(private val session: Session) { options: LivelinessSubscriberOptions = LivelinessSubscriberOptions() ): CallbackSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException - return JNILiveliness.declareSubscriber( - jniSession, - keyExpr, - callback, + val subCallback = buildSubscriberCallback(callback) + val ptr = JNILiveliness.declareSubscriberViaJNI( + jniSession.sessionPtr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + subCallback, options.history, fun() {} ) + return CallbackSubscriber(keyExpr, JNISubscriber(ptr)) } /** @@ -181,15 +231,33 @@ class Liveliness internal constructor(private val session: Session) { options: LivelinessSubscriberOptions = LivelinessSubscriberOptions() ): HandlerSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException - return JNILiveliness.declareSubscriber( - jniSession, - keyExpr, - handler::handle, - handler.receiver(), + val subCallback = buildSubscriberCallback(handler::handle) + val ptr = JNILiveliness.declareSubscriberViaJNI( + jniSession.sessionPtr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + subCallback, options.history, handler::onClose ) + return HandlerSubscriber(keyExpr, JNISubscriber(ptr), handler.receiver()) } + + private fun buildSubscriberCallback(callback: Callback): JNISubscriberCallback = + JNISubscriberCallback { keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + callback.run( + Sample( + KeyExpr(keyExpr2, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } } /** diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt index d0a9de2e..9ec8641a 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt @@ -79,13 +79,14 @@ class Publisher internal constructor( /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @Throws(ZError::class) fun put(payload: IntoZBytes) { - jniPublisher?.put(payload, encoding, null) ?: throw publisherNotValid + jniPublisher?.put(payload.into().bytes, encoding.id, encoding.schema, null) ?: throw publisherNotValid } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @Throws(ZError::class) fun put(payload: IntoZBytes, options: PutOptions) { - jniPublisher?.put(payload, options.encoding ?: this.encoding, options.attachment) ?: throw publisherNotValid + val enc = options.encoding ?: this.encoding + jniPublisher?.put(payload.into().bytes, enc.id, enc.schema, options.attachment?.into()?.bytes) ?: throw publisherNotValid } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @@ -102,7 +103,7 @@ class Publisher internal constructor( @JvmOverloads @Throws(ZError::class) fun delete(options: DeleteOptions = DeleteOptions()) { - jniPublisher?.delete(options.attachment) ?: throw(publisherNotValid) + jniPublisher?.delete(options.attachment?.into()?.bytes) ?: throw(publisherNotValid) } /** diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt index 541d056b..27cc6acb 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt @@ -18,16 +18,23 @@ import io.zenoh.annotations.Unstable import io.zenoh.bytes.Encoding import io.zenoh.bytes.IntoZBytes import io.zenoh.bytes.ZBytes +import io.zenoh.bytes.into +import io.zenoh.config.EntityGlobalId +import io.zenoh.config.ZenohId import io.zenoh.exceptions.ZError import io.zenoh.handlers.BlockingQueueHandler import io.zenoh.handlers.Callback import io.zenoh.handlers.Handler import io.zenoh.jni.JNIQuerier +import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.keyexpr.KeyExpr import io.zenoh.qos.CongestionControl import io.zenoh.qos.Priority import io.zenoh.qos.QoS +import io.zenoh.sample.Sample +import io.zenoh.sample.SampleKind import io.zenoh.session.SessionDeclaration +import org.apache.commons.net.ntp.TimeStamp import java.time.Duration import java.util.Optional import java.util.concurrent.BlockingQueue @@ -143,11 +150,76 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v } private fun resolveGetWithCallback(keyExpr: KeyExpr, callback: Callback, options: GetOptions) { - jniQuerier?.performGetWithCallback(keyExpr, callback, options) ?: throw ZError("Querier is not valid.") + val jni = jniQuerier ?: throw ZError("Querier is not valid.") + val getCallback = JNIGetCallback { replierZid, replierEid, success, keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val reply: Reply = if (success) { + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + Reply.Success( + replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + Sample( + KeyExpr(keyExpr2!!, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } else { + Reply.Error(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, payload.into(), Encoding(encodingId, schema = encodingSchema)) + } + callback.run(reply) + } + jni.getViaJNI( + jni.ptr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + options.parameters?.toString(), + getCallback, + fun() {}, + options.attachment?.into()?.bytes, + options.payload?.into()?.bytes, + options.encoding?.id ?: Encoding.defaultEncoding().id, + options.encoding?.schema + ) } private fun resolveGetWithHandler(keyExpr: KeyExpr, handler: Handler, options: GetOptions): R { - return jniQuerier?.performGetWithHandler(keyExpr, handler, options) ?: throw ZError("Querier is not valid.") + val jni = jniQuerier ?: throw ZError("Querier is not valid.") + val getCallback = JNIGetCallback { replierZid, replierEid, success, keyExpr2, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express, priority, congestionControl -> + val reply: Reply = if (success) { + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + Reply.Success( + replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, + Sample( + KeyExpr(keyExpr2!!, null), + payload.into(), + Encoding(encodingId, schema = encodingSchema), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + ) + } else { + Reply.Error(replierZid?.let { EntityGlobalId(ZenohId(it), replierEid.toUInt()) }, payload.into(), Encoding(encodingId, schema = encodingSchema)) + } + handler.handle(reply) + } + jni.getViaJNI( + jni.ptr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + options.parameters?.toString(), + getCallback, + handler::onClose, + options.attachment?.into()?.bytes, + options.payload?.into()?.bytes, + options.encoding?.id ?: Encoding.defaultEncoding().id, + options.encoding?.schema + ) + return handler.receiver() } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt index 3a8184da..9731983d 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt @@ -24,6 +24,7 @@ import io.zenoh.keyexpr.KeyExpr import io.zenoh.qos.QoS import io.zenoh.sample.Sample import io.zenoh.sample.SampleKind +import org.apache.commons.net.ntp.TimeStamp /** * Represents a Zenoh Query in Kotlin. @@ -61,17 +62,22 @@ class Query internal constructor( @Throws(ZError::class) @JvmOverloads fun reply(keyExpr: KeyExpr, payload: IntoZBytes, options: ReplyOptions = ReplyOptions()) { - val sample = Sample( - keyExpr, - payload.into(), - options.encoding, - SampleKind.PUT, - options.timeStamp, - QoS(options.congestionControl, options.priority, options.express), - options.attachment?.into() - ) + val zbytes = payload.into() + val encoding = options.encoding + val timestamp = options.timeStamp + val timestampEnabled = timestamp != null jniQuery?.apply { - replySuccess(sample) + replySuccess( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + zbytes.bytes, + encoding?.id ?: Encoding.defaultEncoding().id, + encoding?.schema, + timestampEnabled, + if (timestampEnabled) timestamp!!.ntpValue() else 0, + options.attachment?.into()?.bytes, + QoS(options.congestionControl, options.priority, options.express).express + ) jniQuery = null } ?: throw (ZError("Query is invalid")) } @@ -98,12 +104,16 @@ class Query internal constructor( @JvmOverloads @Throws(ZError::class) fun replyDel(keyExpr: KeyExpr, options: ReplyDelOptions = ReplyDelOptions()) { + val timestamp = options.timeStamp + val timestampEnabled = timestamp != null jniQuery?.apply { replyDelete( - keyExpr, - options.timeStamp, - options.attachment, - QoS(options.congestionControl, options.priority, options.express), + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + timestampEnabled, + if (timestampEnabled) timestamp!!.ntpValue() else 0, + options.attachment?.into()?.bytes, + QoS(options.congestionControl, options.priority, options.express).express ) jniQuery = null } ?: throw (ZError("Query is invalid")) @@ -118,8 +128,9 @@ class Query internal constructor( @JvmOverloads @Throws(ZError::class) fun replyErr(message: IntoZBytes, options: ReplyErrOptions = ReplyErrOptions()) { + val encoding = options.encoding jniQuery?.apply { - replyError(message.into(), options.encoding) + replyError(message.into().bytes, encoding?.id ?: Encoding.defaultEncoding().id, encoding?.schema) jniQuery = null } ?: throw (ZError("Query is invalid")) } diff --git a/zenoh-jni-runtime/build.gradle.kts b/zenoh-jni-runtime/build.gradle.kts new file mode 100644 index 00000000..dc997ce5 --- /dev/null +++ b/zenoh-jni-runtime/build.gradle.kts @@ -0,0 +1,240 @@ +// +// Copyright (c) 2026 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, +// + +import com.nishtahir.CargoExtension + +plugins { + kotlin("multiplatform") + `maven-publish` + signing +} + +val androidEnabled = project.findProperty("android")?.toString()?.toBoolean() == true +val release = project.findProperty("release")?.toString()?.toBoolean() == true + +// If the publication is meant to be done on a remote repository (Maven central). +// Modifying this property will affect the release workflows! +val isRemotePublication = project.findProperty("remotePublication")?.toString()?.toBoolean() == true + +var buildMode = if (release) BuildMode.RELEASE else BuildMode.DEBUG + +if (androidEnabled) { + apply(plugin = "com.android.library") + apply(plugin = "org.mozilla.rust-android-gradle.rust-android") + + configureCargo() + configureAndroid() +} + +kotlin { + jvmToolchain(11) + jvm { + compilations.all { + kotlinOptions.jvmTarget = "11" + } + testRuns["test"].executionTask.configure { + val zenohPaths = "../zenoh-jni/target/$buildMode" + jvmArgs("-Djava.library.path=$zenohPaths") + } + if (!androidEnabled) { + withJava() + } + } + if (androidEnabled) { + androidTarget { + publishLibraryVariants("release") + } + } + + @Suppress("Unused") + sourceSets { + val commonMain by getting {} + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + if (androidEnabled) { + val androidUnitTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + } + val jvmMain by getting { + if (isRemotePublication) { + resources.srcDir("../jni-libs").include("*/**") + } else { + resources.srcDir("../zenoh-jni/target/$buildMode").include(arrayListOf("*.dylib", "*.so", "*.dll")) + } + } + + val jvmTest by getting { + resources.srcDir("../zenoh-jni/target/$buildMode").include(arrayListOf("*.dylib", "*.so", "*.dll")) + } + } + + publishing { + publications.withType { + groupId = "org.eclipse.zenoh" + artifactId = "zenoh-jni-runtime" + version = rootProject.version.toString() + + pom { + name.set("Zenoh JNI Runtime") + description.set("The Eclipse Zenoh JNI runtime layer for zenoh-java and zenoh-kotlin.") + url.set("https://zenoh.io/") + + licenses { + license { + name.set("Eclipse Public License 2.0 OR Apache License 2.0") + url.set("http://www.eclipse.org/legal/epl-2.0") + } + } + developers { + developer { + id.set("ZettaScale") + name.set("ZettaScale Zenoh Team") + email.set("zenoh@zettascale.tech") + } + } + scm { + connection.set("scm:git:https://github.com/eclipse-zenoh/zenoh-java.git") + developerConnection.set("scm:git:https://github.com/eclipse-zenoh/zenoh-java.git") + url.set("https://github.com/eclipse-zenoh/zenoh-java") + } + } + } + } +} + +signing { + isRequired = isRemotePublication + useInMemoryPgpKeys(System.getenv("ORG_GPG_SUBKEY_ID"), System.getenv("ORG_GPG_PRIVATE_KEY"), System.getenv("ORG_GPG_PASSPHRASE")) + sign(publishing.publications) +} + +tasks.withType().configureEach { + dependsOn(tasks.withType()) +} + +tasks.withType { + doFirst { + systemProperty("java.library.path", "../zenoh-jni/target/$buildMode") + } +} + +tasks.whenObjectAdded { + if ((this.name == "mergeDebugJniLibFolders" || this.name == "mergeReleaseJniLibFolders")) { + this.dependsOn("cargoBuild") + } +} + +tasks.named("compileKotlinJvm") { + dependsOn("buildZenohJni") +} + +tasks.register("buildZenohJni") { + doLast { + if (!isRemotePublication) { + buildZenohJNI(buildMode) + } + } +} + +fun buildZenohJNI(mode: BuildMode = BuildMode.DEBUG) { + val cargoCommand = mutableListOf("cargo", "build") + + if (mode == BuildMode.RELEASE) { + cargoCommand.add("--release") + } + + val result = project.exec { + commandLine(*(cargoCommand.toTypedArray()), "--manifest-path", "../zenoh-jni/Cargo.toml") + } + + if (result.exitValue != 0) { + throw GradleException("Failed to build Zenoh-JNI.") + } + + Thread.sleep(1000) +} + +enum class BuildMode { + DEBUG { + override fun toString(): String { + return "debug" + } + }, + RELEASE { + override fun toString(): String { + return "release" + } + } +} + +fun Project.configureAndroid() { + extensions.configure("android") { + namespace = "io.zenoh.jni" + compileSdk = 30 + + ndkVersion = "26.0.10792818" + + defaultConfig { + minSdk = 30 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + getByName("debug") { + isMinifyEnabled = false + } + } + sourceSets { + getByName("main") { + manifest.srcFile("src/androidMain/AndroidManifest.xml") + } + } + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } + } +} + +fun Project.configureCargo() { + extensions.configure("cargo") { + pythonCommand = "python3" + module = "../zenoh-jni" + libname = "zenoh-jni" + targetIncludes = arrayOf("libzenoh_jni.so") + targetDirectory = "../zenoh-jni/target/" + profile = "release" + targets = arrayListOf( + "arm", + "arm64", + "x86", + "x86_64", + ) + } +} diff --git a/zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt b/zenoh-jni-runtime/src/androidMain/kotlin/io/zenoh/ZenohLoad.kt similarity index 79% rename from zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt rename to zenoh-jni-runtime/src/androidMain/kotlin/io/zenoh/ZenohLoad.kt index 74d42e51..15de9e38 100644 --- a/zenoh-java/src/androidMain/kotlin/io.zenoh/Zenoh.kt +++ b/zenoh-jni-runtime/src/androidMain/kotlin/io/zenoh/ZenohLoad.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -15,10 +15,9 @@ package io.zenoh /** - * Static singleton class to load the Zenoh native library once and only once, as well as the logger in function of the - * log level configuration. + * Static singleton class to load the Zenoh native library once and only once. */ -internal actual object ZenohLoad { +public actual object ZenohLoad { private const val ZENOH_LIB_NAME = "zenoh_jni" init { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/ZenohLoad.kt new file mode 100644 index 00000000..a2d21403 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/ZenohLoad.kt @@ -0,0 +1,20 @@ +// +// Copyright (c) 2026 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 + +/** + * Static singleton object to load the Zenoh native library once and only once. + */ +public expect object ZenohLoad diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt new file mode 100644 index 00000000..1984bd23 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt @@ -0,0 +1,20 @@ +// +// Copyright (c) 2026 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.exceptions + +/** + * A Zenoh Error. + */ +class ZError(override val message: String? = null): Exception() diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt new file mode 100644 index 00000000..d27b0826 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt @@ -0,0 +1,75 @@ +// +// Copyright (c) 2026 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.exceptions.ZError +import io.zenoh.jni.callbacks.JNIMatchingListenerCallback +import io.zenoh.jni.callbacks.JNIOnCloseCallback + +/** + * Adapter class for a native Zenoh AdvancedPublisher. + * + * @property ptr Raw pointer to the underlying native AdvancedPublisher. + */ +public class JNIAdvancedPublisher(public val ptr: Long) { + + @Throws(ZError::class) + fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { + putViaJNI(payload, encodingId, encodingSchema, attachment, ptr) + } + + @Throws(ZError::class) + fun delete(attachment: ByteArray?) { + deleteViaJNI(attachment, ptr) + } + + @Throws(ZError::class) + fun declareMatchingListener(callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback): Long = + declareMatchingListenerViaJNI(ptr, callback, onClose) + + @Throws(ZError::class) + fun declareBackgroundMatchingListener(callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback) = + declareBackgroundMatchingListenerViaJNI(ptr, callback, onClose) + + @Throws(ZError::class) + fun getMatchingStatus(): Boolean = getMatchingStatusViaJNI(ptr) + + fun close() { + freePtrViaJNI(ptr) + } + + @Throws(ZError::class) + private external fun putViaJNI( + payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?, ptr: Long + ) + + @Throws(ZError::class) + private external fun deleteViaJNI(attachment: ByteArray?, ptr: Long) + + @Throws(ZError::class) + private external fun declareMatchingListenerViaJNI( + ptr: Long, callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback + ): Long + + @Throws(ZError::class) + private external fun declareBackgroundMatchingListenerViaJNI( + ptr: Long, callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback + ) + + @Throws(ZError::class) + private external fun getMatchingStatusViaJNI(ptr: Long): Boolean + + private external fun freePtrViaJNI(ptr: Long) +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt new file mode 100644 index 00000000..c76d1dbb --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt @@ -0,0 +1,80 @@ +// +// Copyright (c) 2026 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.exceptions.ZError +import io.zenoh.jni.callbacks.JNIOnCloseCallback +import io.zenoh.jni.callbacks.JNISampleMissListenerCallback +import io.zenoh.jni.callbacks.JNISubscriberCallback + +/** + * Adapter class for a native Zenoh AdvancedSubscriber. + * + * @property ptr Raw pointer to the underlying native AdvancedSubscriber. + */ +public class JNIAdvancedSubscriber(public val ptr: Long) { + + @Throws(ZError::class) + fun declareDetectPublishersSubscriber( + history: Boolean, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): Long = declareDetectPublishersSubscriberViaJNI(ptr, history, callback, onClose) + + @Throws(ZError::class) + fun declareBackgroundDetectPublishersSubscriber( + history: Boolean, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ) = declareBackgroundDetectPublishersSubscriberViaJNI(ptr, history, callback, onClose) + + @Throws(ZError::class) + fun declareSampleMissListener( + callback: JNISampleMissListenerCallback, + onClose: JNIOnCloseCallback, + ): Long = declareSampleMissListenerViaJNI(ptr, callback, onClose) + + @Throws(ZError::class) + fun declareBackgroundSampleMissListener( + callback: JNISampleMissListenerCallback, + onClose: JNIOnCloseCallback, + ) = declareBackgroundSampleMissListenerViaJNI(ptr, callback, onClose) + + fun close() { + freePtrViaJNI(ptr) + } + + @Throws(ZError::class) + private external fun declareDetectPublishersSubscriberViaJNI( + ptr: Long, history: Boolean, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback + ): Long + + @Throws(ZError::class) + private external fun declareBackgroundDetectPublishersSubscriberViaJNI( + ptr: Long, history: Boolean, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback + ) + + @Throws(ZError::class) + private external fun declareSampleMissListenerViaJNI( + ptr: Long, callback: JNISampleMissListenerCallback, onClose: JNIOnCloseCallback + ): Long + + @Throws(ZError::class) + private external fun declareBackgroundSampleMissListenerViaJNI( + ptr: Long, callback: JNISampleMissListenerCallback, onClose: JNIOnCloseCallback + ) + + private external fun freePtrViaJNI(ptr: Long) +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt similarity index 63% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt index ea278988..39a15756 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -14,13 +14,11 @@ package io.zenoh.jni -import io.zenoh.Config import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError -import java.io.File -import java.nio.file.Path -internal class JNIConfig(internal val ptr: Long) { +/** Adapter for the native Zenoh config. Factory methods return raw pointers for use by facade classes. */ +public class JNIConfig(public val ptr: Long) { companion object { @@ -28,36 +26,29 @@ internal class JNIConfig(internal val ptr: Long) { ZenohLoad } - fun loadDefaultConfig(): Config { - val cfgPtr = loadDefaultConfigViaJNI() - return Config(JNIConfig(cfgPtr)) - } - @Throws(ZError::class) - fun loadConfigFile(path: Path): Config { - val cfgPtr = loadConfigFileViaJNI(path.toString()) - return Config(JNIConfig(cfgPtr)) + fun loadDefaultConfig(): Long { + return loadDefaultConfigViaJNI() } @Throws(ZError::class) - fun loadConfigFile(file: File): Config = loadConfigFile(file.toPath()) + fun loadConfigFile(path: String): Long { + return loadConfigFileViaJNI(path) + } @Throws(ZError::class) - fun loadJsonConfig(rawConfig: String): Config { - val cfgPtr = loadJsonConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) + fun loadJsonConfig(rawConfig: String): Long { + return loadJsonConfigViaJNI(rawConfig) } @Throws(ZError::class) - fun loadJson5Config(rawConfig: String): Config { - val cfgPtr = loadJsonConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) + fun loadYamlConfig(rawConfig: String): Long { + return loadYamlConfigViaJNI(rawConfig) } @Throws(ZError::class) - fun loadYamlConfig(rawConfig: String): Config { - val cfgPtr = loadYamlConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) + fun loadJson5Config(rawConfig: String): Long { + return loadJsonConfigViaJNI(rawConfig) } @Throws(ZError::class) @@ -78,7 +69,6 @@ internal class JNIConfig(internal val ptr: Long) { @Throws(ZError::class) private external fun insertJson5ViaJNI(ptr: Long, key: String, value: String): Long - /** Frees the underlying native config. */ private external fun freePtrViaJNI(ptr: Long) @Throws(ZError::class) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt similarity index 55% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt index 29e419e3..ce5f7126 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -16,10 +16,9 @@ package io.zenoh.jni import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.keyexpr.SetIntersectionLevel -internal class JNIKeyExpr(internal val ptr: Long) { +/** Adapter for native Zenoh key expressions. Factory methods return raw primitives. */ +public class JNIKeyExpr(public val ptr: Long) { companion object { init { @@ -27,50 +26,37 @@ internal class JNIKeyExpr(internal val ptr: Long) { } @Throws(ZError::class) - fun tryFrom(keyExpr: String): KeyExpr { - return KeyExpr(tryFromViaJNI(keyExpr)) + fun tryFrom(keyExpr: String): String { + return tryFromViaJNI(keyExpr) } @Throws(ZError::class) - fun autocanonize(keyExpr: String): KeyExpr { - return KeyExpr(autocanonizeViaJNI(keyExpr)) + fun autocanonize(keyExpr: String): String { + return autocanonizeViaJNI(keyExpr) } @Throws(ZError::class) - fun intersects(keyExprA: KeyExpr, keyExprB: KeyExpr): Boolean = intersectsViaJNI( - keyExprA.jniKeyExpr?.ptr ?: 0, - keyExprA.keyExpr, - keyExprB.jniKeyExpr?.ptr ?: 0, - keyExprB.keyExpr - ) + fun intersects(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean = + intersectsViaJNI(ptrA, keyExprA, ptrB, keyExprB) @Throws(ZError::class) - fun includes(keyExprA: KeyExpr, keyExprB: KeyExpr): Boolean = includesViaJNI( - keyExprA.jniKeyExpr?.ptr ?: 0, - keyExprA.keyExpr, - keyExprB.jniKeyExpr?.ptr ?: 0, - keyExprB.keyExpr - ) + fun includes(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean = + includesViaJNI(ptrA, keyExprA, ptrB, keyExprB) + /** Returns SetIntersectionLevel ordinal as Int. Callers convert to SetIntersectionLevel. */ @Throws(ZError::class) - fun relationTo(keyExpr: KeyExpr, other: KeyExpr): SetIntersectionLevel { - val intersection = relationToViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - other.jniKeyExpr?.ptr ?: 0, - other.keyExpr - ) - return SetIntersectionLevel.fromInt(intersection) + fun relationTo(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int { + return relationToViaJNI(ptrA, keyExprA, ptrB, keyExprB) } @Throws(ZError::class) - fun joinViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(joinViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) + fun join(ptrA: Long, keyExprA: String, other: String): String { + return joinViaJNI(ptrA, keyExprA, other) } @Throws(ZError::class) - fun concatViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(concatViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) + fun concat(ptrA: Long, keyExprA: String, other: String): String { + return concatViaJNI(ptrA, keyExprA, other) } @Throws(ZError::class) @@ -99,6 +85,5 @@ internal class JNIKeyExpr(internal val ptr: Long) { freePtrViaJNI(ptr) } - /** Frees the underlying native KeyExpr. */ private external fun freePtrViaJNI(ptr: Long) } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt new file mode 100644 index 00000000..580ffcc1 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt @@ -0,0 +1,47 @@ +// +// Copyright (c) 2026 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.exceptions.ZError +import io.zenoh.jni.callbacks.JNIGetCallback +import io.zenoh.jni.callbacks.JNIOnCloseCallback +import io.zenoh.jni.callbacks.JNISubscriberCallback + +/** Adapter object for interacting with Zenoh Liveliness through JNI. */ +public object JNILiveliness { + + @Throws(ZError::class) + external fun declareTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long + + @Throws(ZError::class) + external fun declareSubscriberViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + callback: JNISubscriberCallback, + history: Boolean, + onClose: JNIOnCloseCallback, + ): Long + + @Throws(ZError::class) + external fun getViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + callback: JNIGetCallback, + timeoutMs: Long, + onClose: JNIOnCloseCallback, + ) +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt new file mode 100644 index 00000000..b9d8dbeb --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt @@ -0,0 +1,27 @@ +// +// Copyright (c) 2026 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 + +/** Adapter class for a native Zenoh LivelinessToken. */ +public class JNILivelinessToken(public val ptr: Long) { + + fun undeclare() { + undeclareViaJNI(ptr) + } + + companion object { + external fun undeclareViaJNI(ptr: Long) + } +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt new file mode 100644 index 00000000..d41814ac --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt @@ -0,0 +1,25 @@ +// +// Copyright (c) 2026 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 + +/** Adapter class for a native Zenoh MatchingListener. */ +public class JNIMatchingListener(public val ptr: Long) { + + fun close() { + freePtrViaJNI(ptr) + } + + private external fun freePtrViaJNI(ptr: Long) +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt new file mode 100644 index 00000000..edfddc97 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt @@ -0,0 +1,49 @@ +// +// Copyright (c) 2026 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.exceptions.ZError + +/** + * Adapter class for a native Zenoh publisher. Uses primitive types for put/delete. + * + * @property ptr Raw pointer to the underlying native Publisher. + */ +public class JNIPublisher(public val ptr: Long) { + + @Throws(ZError::class) + fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { + putViaJNI(payload, encodingId, encodingSchema, attachment, ptr) + } + + @Throws(ZError::class) + fun delete(attachment: ByteArray?) { + deleteViaJNI(attachment, ptr) + } + + fun close() { + freePtrViaJNI(ptr) + } + + @Throws(ZError::class) + private external fun putViaJNI( + valuePayload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?, ptr: Long + ) + + @Throws(ZError::class) + private external fun deleteViaJNI(attachment: ByteArray?, ptr: Long) + + private external fun freePtrViaJNI(ptr: Long) +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt new file mode 100644 index 00000000..07baadeb --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -0,0 +1,43 @@ +// +// Copyright (c) 2026 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.exceptions.ZError +import io.zenoh.jni.callbacks.JNIGetCallback +import io.zenoh.jni.callbacks.JNIOnCloseCallback + +/** Adapter class for a native Zenoh querier. */ +public class JNIQuerier(public val ptr: Long) { + + @Throws(ZError::class) + external fun getViaJNI( + querierPtr: Long, + keyExprPtr: Long, + keyExprString: String, + parameters: String?, + callback: JNIGetCallback, + onClose: JNIOnCloseCallback, + attachmentBytes: ByteArray?, + payload: ByteArray?, + encodingId: Int, + encodingSchema: String?, + ) + + external fun freePtrViaJNI(ptr: Long) + + fun close() { + freePtrViaJNI(ptr) + } +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt new file mode 100644 index 00000000..d2711638 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -0,0 +1,96 @@ +// +// Copyright (c) 2026 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.exceptions.ZError + +/** + * Adapter class for interacting with a native Zenoh Query using JNI. + * + * @property ptr The raw pointer to the underlying native query. + */ +public class JNIQuery(private val ptr: Long) { + + @Throws(ZError::class) + fun replySuccess( + keyExprPtr: Long, + keyExprString: String, + payload: ByteArray, + encodingId: Int, + encodingSchema: String?, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) { + replySuccessViaJNI(ptr, keyExprPtr, keyExprString, payload, encodingId, encodingSchema, timestampEnabled, timestampNtp64, attachment, qosExpress) + } + + @Throws(ZError::class) + fun replyError(errorPayload: ByteArray, encodingId: Int, encodingSchema: String?) { + replyErrorViaJNI(ptr, errorPayload, encodingId, encodingSchema) + } + + @Throws(ZError::class) + fun replyDelete( + keyExprPtr: Long, + keyExprString: String, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) { + replyDeleteViaJNI(ptr, keyExprPtr, keyExprString, timestampEnabled, timestampNtp64, attachment, qosExpress) + } + + fun close() { + freePtrViaJNI(ptr) + } + + @Throws(ZError::class) + private external fun replySuccessViaJNI( + queryPtr: Long, + keyExprPtr: Long, + keyExprString: String, + valuePayload: ByteArray, + valueEncodingId: Int, + valueEncodingSchema: String?, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) + + @Throws(ZError::class) + private external fun replyErrorViaJNI( + queryPtr: Long, + errorValuePayload: ByteArray, + errorValueEncoding: Int, + encodingSchema: String?, + ) + + @Throws(ZError::class) + private external fun replyDeleteViaJNI( + queryPtr: Long, + keyExprPtr: Long, + keyExprString: String, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) + + private external fun freePtrViaJNI(ptr: Long) +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt similarity index 79% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt index e5f7d3ce..7299b67e 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -15,16 +15,15 @@ package io.zenoh.jni /** - * Adapter class to handle the interactions with Zenoh through JNI for a [io.zenoh.query.Queryable] + * Adapter class to handle the interactions with Zenoh through JNI for a Queryable. * * @property ptr: raw pointer to the underlying native Queryable. */ -internal class JNIQueryable(val ptr: Long) { +public class JNIQueryable(public val ptr: Long) { fun close() { freePtrViaJNI(ptr) } - /** Frees the underlying native Queryable. */ private external fun freePtrViaJNI(ptr: Long) } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt new file mode 100644 index 00000000..08edf3d3 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt @@ -0,0 +1,25 @@ +// +// Copyright (c) 2026 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 + +/** Adapter class for a native Zenoh SampleMissListener. */ +public class JNISampleMissListener(public val ptr: Long) { + + fun close() { + freePtrViaJNI(ptr) + } + + private external fun freePtrViaJNI(ptr: Long) +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt new file mode 100644 index 00000000..0f2db057 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt @@ -0,0 +1,48 @@ +// +// Copyright (c) 2026 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.exceptions.ZError +import io.zenoh.jni.callbacks.JNIOnCloseCallback +import io.zenoh.jni.callbacks.JNIScoutCallback + +/** + * Adapter class to handle the interactions with Zenoh through JNI for a Scout. + * + * @property ptr: raw pointer to the underlying native scout. + */ +public class JNIScout(private val ptr: Long) { + + companion object { + init { + ZenohLoad + } + + @Throws(ZError::class) + external fun scoutViaJNI( + whatAmI: Int, + callback: JNIScoutCallback, + onClose: JNIOnCloseCallback, + configPtr: Long, + ): Long + + external fun freePtrViaJNI(ptr: Long) + } + + fun close() { + freePtrViaJNI(ptr) + } +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt new file mode 100644 index 00000000..9b3ad49b --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -0,0 +1,194 @@ +// +// Copyright (c) 2026 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.exceptions.ZError +import io.zenoh.jni.callbacks.JNIGetCallback +import io.zenoh.jni.callbacks.JNIOnCloseCallback +import io.zenoh.jni.callbacks.JNIQueryableCallback +import io.zenoh.jni.callbacks.JNISubscriberCallback + +/** Adapter class to handle communication with the Zenoh JNI code for a Session. */ +public class JNISession(public val sessionPtr: Long) { + + companion object { + init { + ZenohLoad + } + + @Throws(ZError::class) + fun open(configPtr: Long): JNISession { + val sessionPtr = openSessionViaJNI(configPtr) + return JNISession(sessionPtr) + } + + @JvmStatic + @Throws(ZError::class) + private external fun openSessionViaJNI(configPtr: Long): Long + } + + @Throws(ZError::class) + external fun closeSessionViaJNI(ptr: Long) + + @Throws(ZError::class) + external fun declarePublisherViaJNI( + keyExprPtr: Long, + keyExprString: String, + sessionPtr: Long, + congestionControl: Int, + priority: Int, + express: Boolean, + reliability: Int + ): Long + + @Throws(ZError::class) + external fun declareSubscriberViaJNI( + keyExprPtr: Long, + keyExprString: String, + sessionPtr: Long, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): Long + + @Throws(ZError::class) + external fun declareQueryableViaJNI( + keyExprPtr: Long, + keyExprString: String, + sessionPtr: Long, + callback: JNIQueryableCallback, + onClose: JNIOnCloseCallback, + complete: Boolean + ): Long + + @Throws(ZError::class) + external fun declareQuerierViaJNI( + keyExprPtr: Long, + keyExprString: String, + sessionPtr: Long, + target: Int, + consolidation: Int, + congestionControl: Int, + priority: Int, + express: Boolean, + timeoutMs: Long, + acceptReplies: Int + ): Long + + @Throws(ZError::class) + external fun declareKeyExprViaJNI(sessionPtr: Long, keyExpr: String): Long + + @Throws(ZError::class) + external fun undeclareKeyExprViaJNI(sessionPtr: Long, keyExprPtr: Long) + + @Throws(ZError::class) + external fun getViaJNI( + keyExprPtr: Long, + keyExprString: String, + selectorParams: String?, + sessionPtr: Long, + callback: JNIGetCallback, + onClose: JNIOnCloseCallback, + timeoutMs: Long, + target: Int, + consolidation: Int, + attachmentBytes: ByteArray?, + payload: ByteArray?, + encodingId: Int, + encodingSchema: String?, + congestionControl: Int, + priority: Int, + express: Boolean, + acceptReplies: Int, + ) + + @Throws(ZError::class) + external fun putViaJNI( + keyExprPtr: Long, + keyExprString: String, + sessionPtr: Long, + valuePayload: ByteArray, + valueEncoding: Int, + valueEncodingSchema: String?, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) + + @Throws(ZError::class) + external fun deleteViaJNI( + keyExprPtr: Long, + keyExprString: String, + sessionPtr: Long, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) + + @Throws(ZError::class) + external fun getZidViaJNI(ptr: Long): ByteArray + + @Throws(ZError::class) + external fun getPeersZidViaJNI(ptr: Long): List + + @Throws(ZError::class) + external fun getRoutersZidViaJNI(ptr: Long): List + + @Throws(ZError::class) + external fun declareAdvancedSubscriberViaJNI( + keyExprPtr: Long, + keyExprStr: String, + sessionPtr: Long, + historyConfigEnabled: Boolean, + historyDetectLatePublishers: Boolean, + historyMaxSamples: Long, + historyMaxAgeSeconds: Double, + recoveryConfigEnabled: Boolean, + recoveryConfigIsHeartbeat: Boolean, + recoveryQueryPeriodMs: Long, + subscriberDetection: Boolean, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): Long + + @Throws(ZError::class) + external fun declareAdvancedPublisherViaJNI( + keyExprPtr: Long, + keyExprStr: String, + sessionPtr: Long, + congestionControl: Int, + priority: Int, + isExpress: Boolean, + reliability: Int, + cacheEnabled: Boolean, + cacheMaxSamples: Long, + cacheRepliesPriority: Int, + cacheRepliesCongestionControl: Int, + cacheRepliesIsExpress: Boolean, + sampleMissDetectionEnabled: Boolean, + sampleMissDetectionEnableHeartbeat: Boolean, + sampleMissDetectionHeartbeatMs: Long, + sampleMissDetectionHeartbeatIsSporadic: Boolean, + publisherDetection: Boolean, + ): Long + + fun close() { + closeSessionViaJNI(sessionPtr) + } +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt similarity index 78% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt index 1bb80543..7999854e 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -15,17 +15,15 @@ package io.zenoh.jni /** - * Adapter class to handle the interactions with Zenoh through JNI for a [io.zenoh.pubsub.Subscriber] + * Adapter class to handle the interactions with Zenoh through JNI for a Subscriber. * * @property ptr: raw pointer to the underlying native Subscriber. */ -internal class JNISubscriber(private val ptr: Long) { +public class JNISubscriber(public val ptr: Long) { fun close() { freePtrViaJNI(ptr) } - /** Frees the underlying native Subscriber. */ private external fun freePtrViaJNI(ptr: Long) - } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt similarity index 80% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt index 53ecb5dc..9bb02cb3 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -16,12 +16,12 @@ package io.zenoh.jni import io.zenoh.ZenohLoad -internal object JNIZenohId { +/** Adapter object for interacting with Zenoh IDs through JNI. */ +public object JNIZenohId { init { ZenohLoad } external fun toStringViaJNI(bytes: ByteArray): String - } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt similarity index 91% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt index 14f3c8e9..fe7da688 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -14,7 +14,7 @@ package io.zenoh.jni.callbacks -internal fun interface JNIGetCallback { +public fun interface JNIGetCallback { fun run( replierZid: ByteArray?, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIMatchingListenerCallback.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIMatchingListenerCallback.kt new file mode 100644 index 00000000..4b133b08 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIMatchingListenerCallback.kt @@ -0,0 +1,20 @@ +// +// Copyright (c) 2026 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.callbacks + +/** Callback for matching listener notifications; receives true when matching subscribers exist. */ +public fun interface JNIMatchingListenerCallback { + fun run(matching: Boolean) +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIOnCloseCallback.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIOnCloseCallback.kt similarity index 84% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIOnCloseCallback.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIOnCloseCallback.kt index b58fa23d..62760b54 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIOnCloseCallback.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIOnCloseCallback.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -14,7 +14,7 @@ package io.zenoh.jni.callbacks -internal fun interface JNIOnCloseCallback { +public fun interface JNIOnCloseCallback { fun run() diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt similarity index 56% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt index addf1430..8dc3bce8 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -14,13 +14,15 @@ package io.zenoh.jni.callbacks -internal fun interface JNIQueryableCallback { - fun run(keyExpr: String, - selectorParams: String, - payload: ByteArray?, - encodingId: Int, - encodingSchema: String?, - attachmentBytes: ByteArray?, - queryPtr: Long, - acceptReplies: Int) +public fun interface JNIQueryableCallback { + fun run( + keyExpr: String, + selectorParams: String, + payload: ByteArray?, + encodingId: Int, + encodingSchema: String?, + attachmentBytes: ByteArray?, + queryPtr: Long, + acceptReplies: Int, + ) } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISampleMissListenerCallback.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISampleMissListenerCallback.kt new file mode 100644 index 00000000..bc5f09cc --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISampleMissListenerCallback.kt @@ -0,0 +1,20 @@ +// +// Copyright (c) 2026 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.callbacks + +/** Callback for sample miss listener notifications. Parameters encode the source entity global id and missed count. */ +public fun interface JNISampleMissListenerCallback { + fun run(zidLower: Long, zidUpper: Long, eid: Long, nb: Long) +} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt similarity index 85% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt index 0a8b20e9..57e2390f 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIScoutCallback.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -14,7 +14,7 @@ package io.zenoh.jni.callbacks -internal fun interface JNIScoutCallback { +public fun interface JNIScoutCallback { fun run(whatAmI: Int, zid: ByteArray, locators: List) } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt similarity index 89% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt index 76373c72..013fc6ac 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -14,7 +14,7 @@ package io.zenoh.jni.callbacks -internal fun interface JNISubscriberCallback { +public fun interface JNISubscriberCallback { fun run( keyExpr: String, payload: ByteArray, diff --git a/zenoh-java/src/jvmMain/kotlin/io/zenoh/Target.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt similarity index 93% rename from zenoh-java/src/jvmMain/kotlin/io/zenoh/Target.kt rename to zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt index f3f28256..ad35d536 100644 --- a/zenoh-java/src/jvmMain/kotlin/io/zenoh/Target.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -14,7 +14,7 @@ package io.zenoh -internal enum class Target { +public enum class Target { WINDOWS_X86_64_MSVC, WINDOWS_AARCH64_MSVC, LINUX_X86_64, diff --git a/zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt similarity index 81% rename from zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt rename to zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 8da47656..8e4fcdd3 100644 --- a/zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2023 ZettaScale Technology +// Copyright (c) 2026 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 @@ -21,10 +21,9 @@ import java.io.InputStream import java.util.zip.ZipInputStream /** - * Static singleton class to load the Zenoh native library once and only once, as well as the logger in function of the - * log level configuration. + * Static singleton class to load the Zenoh native library once and only once. */ -internal actual object ZenohLoad { +public actual object ZenohLoad { private const val ZENOH_LIB_NAME = "zenoh_jni" init { @@ -36,13 +35,6 @@ internal actual object ZenohLoad { } } - /** - * Determine target - * - * Determines the [Target] corresponding to the machine on top of which the native code will run. - * - * @return A result with the target. - */ private fun determineTarget(): Result = runCatching { val osName = System.getProperty("os.name").lowercase() val osArch = System.getProperty("os.arch").lowercase() @@ -83,18 +75,6 @@ internal actual object ZenohLoad { return Result.success(target) } - /** - * Unzip library. - * - * The Zenoh libraries are stored within the JAR as compressed ZIP files. - * The location of the zipped files is expected to be under target/target.zip. - * It is expected that the zip file only contains the compressed library. - * - * The uncompressed library will be stored temporarily and deleted on exit. - * - * @param compressedLib Input stream pointing to the compressed library. - * @return A result with the uncompressed library file. - */ private fun unzipLibrary(compressedLib: InputStream): Result = runCatching { val zipInputStream = ZipInputStream(compressedLib) val buffer = ByteArray(1024) @@ -140,24 +120,11 @@ internal actual object ZenohLoad { System.load(tempLib.absolutePath) } - /** - * Load library from jar package. - * - * Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar. - * - * @param target - */ private fun tryLoadingLibraryFromJarPackage(target: Target): Result = runCatching { val lib: Result = loadLibraryAsInputStream(target) lib.onSuccess { loadZenohJNI(it) }.onFailure { throw Exception("Unable to load Zenoh JNI: $it") } } - /** - * Try loading local library. - * - * This function aims to load the default library that is usually included when building the zenoh kotlin library - * locally. - */ private fun tryLoadingLocalLibrary(): Result = runCatching { val lib = ClassLoader.getSystemClassLoader().findLibraryStream(ZENOH_LIB_NAME) ?: javaClass.classLoader.findLibraryStream( diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index c07c4dea..1cd4b7fd 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -1306,7 +1306,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV on_close: JObject, ) -> *const AdvancedSubscriber<()> { let session = OwnedObject::from_raw(session_ptr); - let subscriber_ptr = || -> ZResult<*const AdvancedSubscriber<()>> { + || -> ZResult<*const AdvancedSubscriber<()>> { let mut builder = prepare_subscriber_builder( &mut env, key_expr_ptr, From f3ea4bc92a72a4777f471689c1217ee7170a2895 Mon Sep 17 00:00:00 2001 From: milyin Date: Wed, 15 Apr 2026 15:19:35 +0200 Subject: [PATCH 007/126] Add ZenohLoad safeguard to Logger.start() Ensure native library is loaded before calling startLogsViaJNI by touching ZenohLoad in Logger.start(). This makes Logger robust against direct internal calls that might bypass the usual Zenoh.kt initialization path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt index eba6681c..53a6caef 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt @@ -25,6 +25,7 @@ internal class Logger { @Throws(ZError::class) fun start(filter: String) { + ZenohLoad startLogsViaJNI(filter) } From 9e3770bef7b86fed1c15fc663ac3480d29fc75f8 Mon Sep 17 00:00:00 2001 From: milyin Date: Wed, 15 Apr 2026 15:31:14 +0200 Subject: [PATCH 008/126] Add missing AndroidManifest.xml to zenoh-jni-runtime Android source set Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- zenoh-jni-runtime/src/androidMain/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 zenoh-jni-runtime/src/androidMain/AndroidManifest.xml diff --git a/zenoh-jni-runtime/src/androidMain/AndroidManifest.xml b/zenoh-jni-runtime/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..4fb03756 --- /dev/null +++ b/zenoh-jni-runtime/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + From e881630de98a32b5d80eade147a680f6397bf188 Mon Sep 17 00:00:00 2001 From: milyin Date: Wed, 15 Apr 2026 16:26:08 +0200 Subject: [PATCH 009/126] fix: remove duplicate ZError from zenoh-java, expose via api() dep ZError was defined in both zenoh-jni-runtime and zenoh-java under the same FQCN (io.zenoh.exceptions.ZError). This caused duplicate-class packaging failures on JVM/Android when both artifacts were on the classpath. Fix: delete zenoh-java's copy so zenoh-jni-runtime is the single owner, and change the runtime dependency from implementation() to api() so that ZError (used in @Throws across zenoh-java's public API) is properly re-exported to consumers. Co-Authored-By: Claude Sonnet 4.6 --- zenoh-java/build.gradle.kts | 2 +- .../kotlin/io/zenoh/exceptions/ZError.kt | 20 ------------------- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index 74bd48af..d752d8c8 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -66,7 +66,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(project(":zenoh-jni-runtime")) + api(project(":zenoh-jni-runtime")) implementation("commons-net:commons-net:3.9.0") implementation("com.google.guava:guava:33.3.1-jre") } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt deleted file mode 100644 index c63a19ce..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt +++ /dev/null @@ -1,20 +0,0 @@ -// -// 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.exceptions - -/** - * A Zenoh Error. - */ -class ZError(override val message: String? = null): Exception() From b26fba747a387c0bed9bdc49b0a726aa9ee314ba Mon Sep 17 00:00:00 2001 From: milyin Date: Wed, 15 Apr 2026 16:48:27 +0200 Subject: [PATCH 010/126] Remove Android JNI packaging from zenoh-java zenoh-jni-runtime is the single owner of Android Rust/JNI packaging. Remove the rust-android-gradle plugin, configureCargo() call, and mergeDebugJniLibFolders/mergeReleaseJniLibFolders task hooks from zenoh-java/build.gradle.kts so there is no duplication. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- zenoh-java/build.gradle.kts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index d752d8c8..81ea2335 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -12,8 +12,6 @@ // ZettaScale Zenoh Team, // -import com.nishtahir.CargoExtension - plugins { kotlin("multiplatform") kotlin("plugin.serialization") @@ -34,9 +32,7 @@ var buildMode = if (release) "release" else "debug" if (androidEnabled) { apply(plugin = "com.android.library") - apply(plugin = "org.mozilla.rust-android-gradle.rust-android") - configureCargo() configureAndroid() } @@ -153,12 +149,6 @@ tasks.withType { } } -tasks.whenObjectAdded { - if ((this.name == "mergeDebugJniLibFolders" || this.name == "mergeReleaseJniLibFolders")) { - this.dependsOn("cargoBuild") - } -} - tasks.named("compileKotlinJvm") { dependsOn(":zenoh-jni-runtime:buildZenohJni") } @@ -200,20 +190,3 @@ fun Project.configureAndroid() { } } } - -fun Project.configureCargo() { - extensions.configure("cargo") { - pythonCommand = "python3" - module = "../zenoh-jni" - libname = "zenoh-jni" - targetIncludes = arrayOf("libzenoh_jni.so") - targetDirectory = "../zenoh-jni/target/" - profile = "release" - targets = arrayListOf( - "arm", - "arm64", - "x86", - "x86_64", - ) - } -} From 57e4d927eee03dbda2451aeda45c6e4be05be33d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 01:05:55 +0200 Subject: [PATCH 011/126] uniformly use OwnedObject --- zenoh-jni/src/config.rs | 9 ++--- zenoh-jni/src/key_expr.rs | 8 ++-- zenoh-jni/src/liveliness.rs | 15 +++----- zenoh-jni/src/publisher.rs | 7 ++-- zenoh-jni/src/querier.rs | 4 +- zenoh-jni/src/scouting.rs | 7 ++-- zenoh-jni/src/session.rs | 76 ++++++++++++++----------------------- 7 files changed, 49 insertions(+), 77 deletions(-) diff --git a/zenoh-jni/src/config.rs b/zenoh-jni/src/config.rs index 0ada1340..b64a78a4 100644 --- a/zenoh-jni/src/config.rs +++ b/zenoh-jni/src/config.rs @@ -21,6 +21,7 @@ use jni::{ }; use zenoh::Config; +use crate::owned_object::OwnedObject; use crate::{errors::ZResult, zerror}; use crate::{throw_exception, utils::decode_string}; @@ -130,8 +131,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_getJsonViaJN cfg_ptr: *const Config, key: JString, ) -> jstring { - let arc_cfg: Arc = Arc::from_raw(cfg_ptr); - let result = || -> ZResult { + let arc_cfg = OwnedObject::from_raw(cfg_ptr); + || -> ZResult { let key = decode_string(&mut env, &key)?; let json = arc_cfg.get_json(&key).map_err(|err| zerror!(err))?; let java_json = env.new_string(json).map_err(|err| zerror!(err))?; @@ -140,9 +141,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_getJsonViaJN .unwrap_or_else(|err| { throw_exception!(env, err); JString::default().as_raw() - }); - std::mem::forget(arc_cfg); - result + }) } /// Inserts a json5 value associated to the provided [key]. May throw an exception in case of failure, which must be handled diff --git a/zenoh-jni/src/key_expr.rs b/zenoh-jni/src/key_expr.rs index 11d39d47..dedbe565 100644 --- a/zenoh-jni/src/key_expr.rs +++ b/zenoh-jni/src/key_expr.rs @@ -12,7 +12,6 @@ // ZettaScale Zenoh Team, // -use std::ops::Deref; use std::sync::Arc; use jni::objects::JClass; @@ -21,6 +20,7 @@ use jni::{objects::JString, JNIEnv}; use zenoh::key_expr::KeyExpr; use crate::errors::ZResult; +use crate::owned_object::OwnedObject; use crate::utils::decode_string; use crate::{throw_exception, zerror}; @@ -325,9 +325,7 @@ pub(crate) unsafe fn process_kotlin_key_expr( .map_err(|err| zerror!("Unable to get key expression string value: '{}'.", err))?; Ok(KeyExpr::from_string_unchecked(key_expr)) } else { - let key_expr = Arc::from_raw(key_expr_ptr); - let key_expr_clone = key_expr.deref().clone(); - std::mem::forget(key_expr); - Ok(key_expr_clone) + let key_expr = OwnedObject::from_raw(key_expr_ptr); + Ok((*key_expr).clone()) } } diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 8b05c925..4ad099d6 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -28,6 +28,7 @@ use zenoh::{ use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, + owned_object::OwnedObject, session::{on_reply_error, on_reply_success}, throw_exception, utils::{ @@ -49,7 +50,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( timeout_ms: jlong, on_close: JObject, ) { - let session = unsafe { Arc::from_raw(session_ptr) }; + let session = unsafe { OwnedObject::from_raw(session_ptr) }; let _ = || -> ZResult<()> { let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; let java_vm = Arc::new(get_java_vm(&mut env)?); @@ -98,7 +99,6 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( .map_err(|err| { throw_exception!(env, err); }); - std::mem::forget(session); } #[no_mangle] @@ -110,8 +110,8 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI( key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, ) -> *const LivelinessToken { - let session = unsafe { Arc::from_raw(session_ptr) }; - let ptr = || -> ZResult<*const LivelinessToken> { + let session = unsafe { OwnedObject::from_raw(session_ptr) }; + || -> ZResult<*const LivelinessToken> { let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; tracing::trace!("Declaring liveliness token on '{key_expr}'."); let token = session @@ -124,9 +124,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI( .unwrap_or_else(|err| { throw_exception!(env, err); null() - }); - std::mem::forget(session); - ptr + }) } #[no_mangle] @@ -151,7 +149,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( history: jboolean, on_close: JObject, ) -> *const Subscriber<()> { - let session = unsafe { Arc::from_raw(session_ptr) }; + let session = unsafe { OwnedObject::from_raw(session_ptr) }; || -> ZResult<*const Subscriber<()>> { let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; @@ -232,7 +230,6 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( result.map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; tracing::debug!("Subscriber declared on '{}'.", key_expr); - std::mem::forget(session); Ok(Arc::into_raw(Arc::new(subscriber))) }() .unwrap_or_else(|err| { diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index ead60c3f..80738de2 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -21,6 +21,7 @@ use jni::{ }; use zenoh::{pubsub::Publisher, Wait}; +use crate::owned_object::OwnedObject; use crate::throw_exception; use crate::{ errors::ZResult, @@ -56,7 +57,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( attachment: /*nullable*/ JByteArray, publisher_ptr: *const Publisher<'static>, ) { - let publisher = Arc::from_raw(publisher_ptr); + let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { let payload = decode_byte_array(&env, payload)?; let mut publication = publisher.put(payload); @@ -69,7 +70,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( publication.wait().map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(publisher); } /// Performs a DELETE operation on a Zenoh publisher via JNI. @@ -94,7 +94,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( attachment: /*nullable*/ JByteArray, publisher_ptr: *const Publisher<'static>, ) { - let publisher = Arc::from_raw(publisher_ptr); + let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { let mut delete = publisher.delete(); if !attachment.is_null() { @@ -104,7 +104,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( delete.wait().map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(publisher) } /// Frees the publisher. diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 8c239498..27bf7a63 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -24,6 +24,7 @@ use zenoh::{key_expr::KeyExpr, query::Querier, Wait}; use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, + owned_object::OwnedObject, session::{on_reply_error, on_reply_success}, throw_exception, utils::{ @@ -69,7 +70,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( encoding_id: jint, encoding_schema: /*nullable*/ JString, ) { - let querier = Arc::from_raw(querier_ptr); + let querier = OwnedObject::from_raw(querier_ptr); let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let java_vm = Arc::new(get_java_vm(&mut env)?); @@ -118,7 +119,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(querier); } /// diff --git a/zenoh-jni/src/scouting.rs b/zenoh-jni/src/scouting.rs index b0a665c1..cc45aee7 100644 --- a/zenoh-jni/src/scouting.rs +++ b/zenoh-jni/src/scouting.rs @@ -22,6 +22,7 @@ use jni::{ use zenoh::{config::WhatAmIMatcher, Wait}; use zenoh::{scouting::Scout, Config}; +use crate::owned_object::OwnedObject; use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; use crate::{errors::ZResult, throw_exception, zerror}; @@ -54,10 +55,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIScout_00024Companion_scoutViaJNI( let config = if config_ptr.is_null() { Config::default() } else { - let arc_cfg = Arc::from_raw(config_ptr); - let config_clone = arc_cfg.as_ref().clone(); - std::mem::forget(arc_cfg); - config_clone + let arc_cfg = OwnedObject::from_raw(config_ptr); + (*arc_cfg).clone() }; zenoh::scout(whatAmIMatcher, config) .callback(move |hello| { diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 1cd4b7fd..13a082af 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -use std::{mem, ops::Deref, ptr::null, sync::Arc, time::Duration}; +use std::{ops::Deref, ptr::null, sync::Arc, time::Duration}; use jni::{ objects::{GlobalRef, JByteArray, JClass, JList, JObject, JString, JValue}, @@ -50,12 +50,10 @@ use crate::{ /// If the config path provided is null then the default configuration is loaded. /// unsafe fn open_session(config_ptr: *const Config) -> ZResult { - let config = Arc::from_raw(config_ptr); - let result = zenoh::open(config.as_ref().clone()) + let config = OwnedObject::from_raw(config_ptr); + zenoh::open((*config).clone()) .wait() - .map_err(|err| zerror!(err)); - mem::forget(config); - result + .map_err(|err: zenoh::Error| zerror!(err)) } /// Open a Zenoh session with a JSON configuration. @@ -206,8 +204,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( is_express: jboolean, reliability: jint, ) -> *const Publisher<'static> { - let session = Arc::from_raw(session_ptr); - let publisher_ptr = || -> ZResult<*const Publisher<'static>> { + let session = OwnedObject::from_raw(session_ptr); + || -> ZResult<*const Publisher<'static>> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; @@ -227,9 +225,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( .unwrap_or_else(|err| { throw_exception!(env, err); null() - }); - std::mem::forget(session); - publisher_ptr + }) } /// Performs a `put` operation in the Zenoh session via JNI. @@ -275,7 +271,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( attachment: JByteArray, reliability: jint, ) { - let session = Arc::from_raw(session_ptr); + let session = OwnedObject::from_raw(session_ptr); let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let payload = decode_byte_array(&env, payload)?; @@ -303,7 +299,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); } /// Performs a `delete` operation in the Zenoh session via JNI. @@ -344,7 +339,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( attachment: JByteArray, reliability: jint, ) { - let session = Arc::from_raw(session_ptr); + let session = OwnedObject::from_raw(session_ptr); let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; @@ -369,7 +364,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); } /// Declare a Zenoh subscriber via JNI. @@ -408,7 +402,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( callback: JObject, on_close: JObject, ) -> *const Subscriber<()> { - let session = Arc::from_raw(session_ptr); + let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Subscriber<()>> { let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; @@ -486,7 +480,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( let subscriber = result.map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; tracing::debug!("Subscriber declared on '{}'.", key_expr); - std::mem::forget(session); Ok(Arc::into_raw(Arc::new(subscriber))) }() .unwrap_or_else(|err| { @@ -528,7 +521,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( timeout_ms: jlong, accept_replies: jint, ) -> *const Querier<'static> { - let session = Arc::from_raw(session_ptr); + let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Querier<'static>> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let query_target = decode_query_target(target)?; @@ -552,7 +545,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( .map_err(|err| zerror!(err))?; tracing::debug!("Querier declared on '{}'.", key_expr); - std::mem::forget(session); Ok(Arc::into_raw(Arc::new(querier))) }() .unwrap_or_else(|err| { @@ -601,8 +593,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( on_close: JObject, complete: jboolean, ) -> *const Queryable<()> { - let session = Arc::from_raw(session_ptr); - let query_ptr = || -> ZResult<*const Queryable<()>> { + let session = OwnedObject::from_raw(session_ptr); + || -> ZResult<*const Queryable<()>> { let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; @@ -638,9 +630,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( .unwrap_or_else(|err| { throw_exception!(env, err); null() - }); - std::mem::forget(session); - query_ptr + }) } fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> ZResult<()> { @@ -757,8 +747,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( session_ptr: *const Session, key_expr_str: JString, ) -> *const KeyExpr<'static> { - let session: Arc = Arc::from_raw(session_ptr); - let key_expr_ptr = || -> ZResult<*const KeyExpr<'static>> { + let session = OwnedObject::from_raw(session_ptr); + || -> ZResult<*const KeyExpr<'static>> { let key_expr_str = decode_string(&mut env, &key_expr_str)?; let key_expr = session .declare_keyexpr(key_expr_str.to_owned()) @@ -775,9 +765,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( .unwrap_or_else(|err| { throw_exception!(env, err); null() - }); - mem::forget(session); - key_expr_ptr + }) } /// Undeclare a [KeyExpr] through a [Session] via JNI. @@ -808,7 +796,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( session_ptr: *const Session, key_expr_ptr: *const KeyExpr<'static>, ) { - let session = Arc::from_raw(session_ptr); + let session = OwnedObject::from_raw(session_ptr); let key_expr = Arc::from_raw(key_expr_ptr); let key_expr_clone = key_expr.deref().clone(); match session.undeclare(key_expr_clone).wait() { @@ -820,7 +808,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( ); } } - std::mem::forget(session); // `key_expr` is intentionally left to be freed by Rust } @@ -881,7 +868,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( is_express: jboolean, accept_replies: jint, ) { - let session = Arc::from_raw(session_ptr); + let session = OwnedObject::from_raw(session_ptr); let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let java_vm = Arc::new(get_java_vm(&mut env)?); @@ -952,7 +939,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( .map_err(|err| zerror!(err)) }() .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); } pub(crate) fn on_reply_success( @@ -1111,8 +1097,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getPeersZidViaJNI( _class: JClass, session_ptr: *const Session, ) -> jobject { - let session = Arc::from_raw(session_ptr); - let ids = { + let session = OwnedObject::from_raw(session_ptr); + { let peers_zid = session.info().peers_zid().wait(); let ids = peers_zid.collect::>(); ids_to_java_list(&mut env, ids).map_err(|err| zerror!(err)) @@ -1120,9 +1106,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getPeersZidViaJNI( .unwrap_or_else(|err| { throw_exception!(env, err); JObject::default().as_raw() - }); - std::mem::forget(session); - ids + }) } /// Returns a list of zenoh ids as byte arrays corresponding to the routers connected to the session provided. @@ -1134,8 +1118,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI( _class: JClass, session_ptr: *const Session, ) -> jobject { - let session = Arc::from_raw(session_ptr); - let ids = { + let session = OwnedObject::from_raw(session_ptr); + { let peers_zid = session.info().routers_zid().wait(); let ids = peers_zid.collect::>(); ids_to_java_list(&mut env, ids).map_err(|err| zerror!(err)) @@ -1143,9 +1127,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI( .unwrap_or_else(|err| { throw_exception!(env, err); JObject::default().as_raw() - }); - std::mem::forget(session); - ids + }) } /// Returns the Zenoh ID as a byte array of the session. @@ -1156,8 +1138,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getZidViaJNI( _class: JClass, session_ptr: *const Session, ) -> jbyteArray { - let session = Arc::from_raw(session_ptr); - let ids = { + let session = OwnedObject::from_raw(session_ptr); + { let zid = session.info().zid().wait(); env.byte_array_from_slice(&zid.to_le_bytes()) .map(|x| x.as_raw()) @@ -1166,9 +1148,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getZidViaJNI( .unwrap_or_else(|err| { throw_exception!(env, err); JByteArray::default().as_raw() - }); - std::mem::forget(session); - ids + }) } fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result { From 0b8d396ca4521630f653db6bd403325d2fcbddfb Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 16:20:54 +0200 Subject: [PATCH 012/126] moved open funciton back to place --- zenoh-jni/src/session.rs | 62 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 13a082af..f36935f8 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -45,6 +45,37 @@ use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, zerror, }; +/// Open a Zenoh session via JNI. +/// +/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute +/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require +/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. +/// +/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class (parameter required by the JNI interface but unused). +/// - `config_ptr`: Pointer to the Zenoh config. If null, the default configuration will be loaded. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( + mut env: JNIEnv, + _class: JClass, + config_ptr: *const Config, +) -> *const Session { + let session = open_session(config_ptr); + match session { + Ok(session) => Arc::into_raw(Arc::new(session)), + Err(err) => { + tracing::error!("Unable to open session: {}", err); + throw_exception!(env, zerror!(err)); + null() + } + } +} + /// Open a Zenoh session with the configuration pointed out by `config_path`. /// /// If the config path provided is null then the default configuration is loaded. @@ -1161,37 +1192,6 @@ fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result< Ok(array_list.as_raw()) } -/// Open a Zenoh session via JNI. -/// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. -/// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `config_ptr`: Pointer to the Zenoh config. If null, the default configuration will be loaded. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( - mut env: JNIEnv, - _class: JClass, - config_ptr: *const Config, -) -> *const Session { - let session = open_session(config_ptr); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - null() - } - } -} - #[allow(clippy::too_many_arguments)] unsafe fn prepare_publisher_builder<'a, 'b>( env: &mut JNIEnv, From a137fc2ebfadc0c65fbafca75ddd63bc1af457ed Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 16:37:49 +0200 Subject: [PATCH 013/126] advanced pub/sub creation simplified --- zenoh-jni/src/session.rs | 86 +++++++++------------------------------- 1 file changed, 19 insertions(+), 67 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index f36935f8..9a7da616 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -21,9 +21,8 @@ use jni::{ }; use zenoh::{ config::Config, - handlers::Callback, key_expr::KeyExpr, - pubsub::{Publisher, PublisherBuilder, Subscriber, SubscriberBuilder}, + pubsub::{Publisher, Subscriber}, query::{Querier, Query, Queryable, ReplyError, ReplyKeyExpr, Selector}, sample::Sample, session::{EntityGlobalId, Session, ZenohId}, @@ -1192,50 +1191,6 @@ fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result< Ok(array_list.as_raw()) } -#[allow(clippy::too_many_arguments)] -unsafe fn prepare_publisher_builder<'a, 'b>( - env: &mut JNIEnv, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: &JString, - session: &'a Session, - congestion_control: jint, - priority: jint, - is_express: jboolean, - reliability: jint, -) -> ZResult> { - let key_expr = process_kotlin_key_expr(env, key_expr_str, key_expr_ptr)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let reliability = decode_reliability(reliability)?; - let builder = session - .declare_publisher(key_expr) - .congestion_control(congestion_control) - .priority(priority) - .express(is_express != 0) - .reliability(reliability); - Ok(builder) -} - -unsafe fn prepare_subscriber_builder<'a, 'b>( - env: &mut JNIEnv, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: &JString, - session: &'a Session, - callback: JObject, - on_close: JObject, - entity_name: &str, -) -> ZResult>> { - let key_expr = process_kotlin_key_expr(env, key_expr_str, key_expr_ptr)?; - tracing::debug!("Declaring {entity_name} on '{}'...", key_expr); - - let builder = session - .declare_subscriber(key_expr.to_owned()) - .set_jni_sample_callback(env, callback, on_close)?; - - tracing::debug!("{entity_name} declared on '{}'.", key_expr); - Ok(builder) -} - /// Declare an advanced Zenoh subscriber via JNI. /// /// # Parameters: @@ -1287,16 +1242,13 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV ) -> *const AdvancedSubscriber<()> { let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const AdvancedSubscriber<()>> { - let mut builder = prepare_subscriber_builder( - &mut env, - key_expr_ptr, - &key_expr_str, - &session, - callback, - on_close, - "advanced subscriber", - )? - .advanced(); + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + tracing::debug!("Declaring advanced subscriber on '{}'...", key_expr); + let mut builder = session + .declare_subscriber(key_expr.to_owned()) + .set_jni_sample_callback(&mut env, callback, on_close)? + .advanced(); + tracing::debug!("Advanced subscriber declared on '{}'.", key_expr); if history_config_enabled != 0 { let mut history = match history_detect_late_publishers != 0 { @@ -1406,17 +1358,17 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedPublisherVi ) -> *const AdvancedPublisher<'static> { let session = OwnedObject::from_raw(session_ptr); let publisher_ptr = || -> ZResult<*const AdvancedPublisher<'static>> { - let mut builder = prepare_publisher_builder( - &mut env, - key_expr_ptr, - &key_expr_str, - &session, - congestion_control, - priority, - is_express, - reliability, - )? - .advanced(); + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let congestion_control = decode_congestion_control(congestion_control)?; + let priority = decode_priority(priority)?; + let reliability = decode_reliability(reliability)?; + let mut builder = session + .declare_publisher(key_expr) + .congestion_control(congestion_control) + .priority(priority) + .express(is_express != 0) + .reliability(reliability) + .advanced(); // fill CacheConfig if cache_enabled != 0 { From 401a5ab22f36a1c20b807cf1b2e3202500a50d3c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 16:43:31 +0200 Subject: [PATCH 014/126] reuse set_jni_sample_callback --- zenoh-jni/src/session.rs | 74 +++------------------------------------- 1 file changed, 4 insertions(+), 70 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 9a7da616..cea91223 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -434,80 +434,14 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( ) -> *const Subscriber<()> { let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Subscriber<()>> { - let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; tracing::debug!("Declaring subscriber on '{}'...", key_expr); - let result = session + let subscriber = session .declare_subscriber(key_expr.to_owned()) - .callback(move |sample: Sample| { - on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure - let _ = || -> ZResult<()> { - let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { - zerror!("Unable to attach thread for subscriber: {}", err) - })?; - let byte_array = bytes_to_java_array(&env, sample.payload()) - .map(|array| env.auto_local(array))?; - - let encoding_id: jint = sample.encoding().id() as jint; - let encoding_schema = match sample.encoding().schema() { - Some(schema) => slice_to_java_string(&env, schema)?, - None => JString::default(), - }; - let kind = sample.kind() as jint; - let (timestamp, is_valid) = sample - .timestamp() - .map(|timestamp| (timestamp.get_time().as_u64(), true)) - .unwrap_or((0, false)); - - let attachment_bytes = sample - .attachment() - .map_or_else( - || Ok(JByteArray::default()), - |attachment| bytes_to_java_array(&env, attachment), - ) - .map(|array| env.auto_local(array)) - .map_err(|err| zerror!("Error processing attachment: {}", err))?; - - let key_expr_str = env.auto_local( - env.new_string(sample.key_expr().to_string()) - .map_err(|err| zerror!("Error processing sample key expr: {}", err))?, - ); - - let express = sample.express(); - let priority = sample.priority() as jint; - let cc = sample.congestion_control() as jint; - - env.call_method( - &callback_global_ref, - "run", - "(Ljava/lang/String;[BILjava/lang/String;IJZ[BZII)V", - &[ - JValue::from(&key_expr_str), - JValue::from(&byte_array), - JValue::from(encoding_id), - JValue::from(&encoding_schema), - JValue::from(kind), - JValue::from(timestamp as i64), - JValue::from(is_valid), - JValue::from(&attachment_bytes), - JValue::from(express), - JValue::from(priority), - JValue::from(cc), - ], - ) - .map_err(|err| zerror!(err))?; - Ok(()) - }() - .map_err(|err| tracing::error!("On subscriber callback error: {err}")); - }) - .wait(); - - let subscriber = result.map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; + .set_jni_sample_callback(&mut env, callback, on_close)? + .wait() + .map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; tracing::debug!("Subscriber declared on '{}'.", key_expr); Ok(Arc::into_raw(Arc::new(subscriber))) From 11a38a7fd49fa6015398a80f6d91d867270aa315 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 16:48:01 +0200 Subject: [PATCH 015/126] reuse set_jni_sample_callback in liveliness --- zenoh-jni/src/liveliness.rs | 95 ++++++------------------------------- 1 file changed, 14 insertions(+), 81 deletions(-) diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 4ad099d6..650a4301 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -15,26 +15,24 @@ use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ - objects::{JByteArray, JClass, JObject, JString, JValue}, - sys::{jboolean, jint, jlong}, + objects::{JClass, JObject, JString}, + sys::{jboolean, jlong}, JNIEnv, }; use zenoh::{ internal::runtime::ZRuntime, key_expr::KeyExpr, liveliness::LivelinessToken, - pubsub::Subscriber, sample::Sample, Session, Wait, + pubsub::Subscriber, Session, Wait, }; use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, owned_object::OwnedObject, + sample_callback::SetJniSampleCallback, session::{on_reply_error, on_reply_success}, throw_exception, - utils::{ - bytes_to_java_array, get_callback_global_ref, get_java_vm, load_on_close, - slice_to_java_string, - }, + utils::{get_callback_global_ref, get_java_vm, load_on_close}, zerror, }; @@ -151,83 +149,18 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( ) -> *const Subscriber<()> { let session = unsafe { OwnedObject::from_raw(session_ptr) }; || -> ZResult<*const Subscriber<()>> { - let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); - let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; tracing::debug!("Declaring liveliness subscriber on '{}'...", key_expr); - let result = session - .liveliness() - .declare_subscriber(key_expr.to_owned()) - .history(history != 0) - .callback(move |sample: Sample| { - let _ = || -> ZResult<()> { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { - zerror!("Unable to attach thread for liveliness subscriber: {}", err) - })?; - let byte_array = bytes_to_java_array(&env, sample.payload()) - .map(|array| env.auto_local(array))?; - - let encoding_id: jint = sample.encoding().id() as jint; - let encoding_schema = match sample.encoding().schema() { - Some(schema) => slice_to_java_string(&env, schema)?, - None => JString::default(), - }; - let kind = sample.kind() as jint; - let (timestamp, is_valid) = sample - .timestamp() - .map(|timestamp| (timestamp.get_time().as_u64(), true)) - .unwrap_or((0, false)); - - let attachment_bytes = sample - .attachment() - .map_or_else( - || Ok(JByteArray::default()), - |attachment| bytes_to_java_array(&env, attachment), - ) - .map(|array| env.auto_local(array)) - .map_err(|err| zerror!("Error processing attachment: {}", err))?; - - let key_expr_str = env.auto_local( - env.new_string(sample.key_expr().to_string()) - .map_err(|err| zerror!("Error processing sample key expr: {}", err))?, - ); - - let express = sample.express(); - let priority = sample.priority() as jint; - let cc = sample.congestion_control() as jint; - - env.call_method( - &callback_global_ref, - "run", - "(Ljava/lang/String;[BILjava/lang/String;IJZ[BZII)V", - &[ - JValue::from(&key_expr_str), - JValue::from(&byte_array), - JValue::from(encoding_id), - JValue::from(&encoding_schema), - JValue::from(kind), - JValue::from(timestamp as i64), - JValue::from(is_valid), - JValue::from(&attachment_bytes), - JValue::from(express), - JValue::from(priority), - JValue::from(cc), - ], - ) - .map_err(|err| zerror!(err))?; - Ok(()) - }() - .map_err(|err| tracing::error!("On liveliness subscriber callback error: {err}")); - }) - .wait(); - - let subscriber = - result.map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; + let subscriber = unsafe { + session + .liveliness() + .declare_subscriber(key_expr.to_owned()) + .history(history != 0) + .set_jni_sample_callback(&mut env, callback, on_close) + }? + .wait() + .map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; tracing::debug!("Subscriber declared on '{}'.", key_expr); Ok(Arc::into_raw(Arc::new(subscriber))) From ccc1463b36561c1874d9d9f5c6d9718c44b1fbcd Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 16:49:19 +0200 Subject: [PATCH 016/126] unsafe in function prototype --- zenoh-jni/src/liveliness.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 650a4301..3aaf1a98 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -137,7 +137,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILivelinessToken_00024Companion_undeclareV #[no_mangle] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, @@ -147,19 +147,17 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( history: jboolean, on_close: JObject, ) -> *const Subscriber<()> { - let session = unsafe { OwnedObject::from_raw(session_ptr) }; + let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Subscriber<()>> { - let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; tracing::debug!("Declaring liveliness subscriber on '{}'...", key_expr); - let subscriber = unsafe { - session - .liveliness() - .declare_subscriber(key_expr.to_owned()) - .history(history != 0) - .set_jni_sample_callback(&mut env, callback, on_close) - }? - .wait() + let subscriber = session + .liveliness() + .declare_subscriber(key_expr.to_owned()) + .history(history != 0) + .set_jni_sample_callback(&mut env, callback, on_close)? + .wait() .map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; tracing::debug!("Subscriber declared on '{}'.", key_expr); From 8861ded6eec4228b40ac3158637c4d791d49d146 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 18:26:41 +0200 Subject: [PATCH 017/126] useless config wrappers removed --- .../src/commonMain/kotlin/io/zenoh/Config.kt | 12 +++---- .../kotlin/io/zenoh/jni/JNIConfig.kt | 33 +++---------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt index 9443c731..e392c019 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt @@ -46,7 +46,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic fun loadDefault(): Config { - return Config(JNIConfig(JNIConfig.loadDefaultConfig())) + return Config(JNIConfig(JNIConfig.loadDefaultConfigViaJNI())) } /** @@ -59,7 +59,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromFile(file: File): Config { - return Config(JNIConfig(JNIConfig.loadConfigFile(file.toString()))) + return Config(JNIConfig(JNIConfig.loadConfigFileViaJNI(file.toString()))) } /** @@ -72,7 +72,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromFile(path: Path): Config { - return Config(JNIConfig(JNIConfig.loadConfigFile(path.toString()))) + return Config(JNIConfig(JNIConfig.loadConfigFileViaJNI(path.toString()))) } /** @@ -87,7 +87,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromJson(config: String): Config { - return Config(JNIConfig(JNIConfig.loadJsonConfig(config))) + return Config(JNIConfig(JNIConfig.loadJsonConfigViaJNI(config))) } /** @@ -102,7 +102,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromJson5(config: String): Config { - return Config(JNIConfig(JNIConfig.loadJson5Config(config))) + return Config(JNIConfig(JNIConfig.loadJsonConfigViaJNI(config))) } /** @@ -117,7 +117,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromYaml(config: String): Config { - return Config(JNIConfig(JNIConfig.loadYamlConfig(config))) + return Config(JNIConfig(JNIConfig.loadYamlConfigViaJNI(config))) } /** diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt index 39a15756..a12fff4a 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt @@ -27,41 +27,16 @@ public class JNIConfig(public val ptr: Long) { } @Throws(ZError::class) - fun loadDefaultConfig(): Long { - return loadDefaultConfigViaJNI() - } - - @Throws(ZError::class) - fun loadConfigFile(path: String): Long { - return loadConfigFileViaJNI(path) - } - - @Throws(ZError::class) - fun loadJsonConfig(rawConfig: String): Long { - return loadJsonConfigViaJNI(rawConfig) - } - - @Throws(ZError::class) - fun loadYamlConfig(rawConfig: String): Long { - return loadYamlConfigViaJNI(rawConfig) - } - - @Throws(ZError::class) - fun loadJson5Config(rawConfig: String): Long { - return loadJsonConfigViaJNI(rawConfig) - } - - @Throws(ZError::class) - private external fun loadDefaultConfigViaJNI(): Long + internal external fun loadDefaultConfigViaJNI(): Long @Throws(ZError::class) - private external fun loadConfigFileViaJNI(path: String): Long + internal external fun loadConfigFileViaJNI(path: String): Long @Throws(ZError::class) - private external fun loadJsonConfigViaJNI(rawConfig: String): Long + internal external fun loadJsonConfigViaJNI(rawConfig: String): Long @Throws(ZError::class) - private external fun loadYamlConfigViaJNI(rawConfig: String): Long + internal external fun loadYamlConfigViaJNI(rawConfig: String): Long @Throws(ZError::class) private external fun getIdViaJNI(ptr: Long): ByteArray From 823d698e1eb5adbc2c8b524ff5f38eb881e44dd2 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 18:34:58 +0200 Subject: [PATCH 018/126] useless methods removed --- .../kotlin/io/zenoh/keyexpr/KeyExpr.kt | 14 +++--- .../kotlin/io/zenoh/jni/JNIKeyExpr.kt | 47 +++---------------- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt index fda83d39..3e6ff24a 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt @@ -79,7 +79,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn @JvmStatic @Throws(ZError::class) fun tryFrom(keyExpr: String): KeyExpr { - return KeyExpr(JNIKeyExpr.tryFrom(keyExpr)) + return KeyExpr(JNIKeyExpr.tryFromViaJNI(keyExpr)) } /** @@ -95,7 +95,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn @JvmStatic @Throws(ZError::class) fun autocanonize(keyExpr: String): KeyExpr { - return KeyExpr(JNIKeyExpr.autocanonize(keyExpr)) + return KeyExpr(JNIKeyExpr.autocanonizeViaJNI(keyExpr)) } } @@ -106,7 +106,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun intersects(other: KeyExpr): Boolean { - return JNIKeyExpr.intersects(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) + return JNIKeyExpr.intersectsViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) } /** @@ -116,7 +116,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun includes(other: KeyExpr): Boolean { - return JNIKeyExpr.includes(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) + return JNIKeyExpr.includesViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) } /** @@ -126,7 +126,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun relationTo(other: KeyExpr): SetIntersectionLevel { - return SetIntersectionLevel.fromInt(JNIKeyExpr.relationTo(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr)) + return SetIntersectionLevel.fromInt(JNIKeyExpr.relationToViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr)) } /** @@ -135,7 +135,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun join(other: String): KeyExpr { - return KeyExpr(JNIKeyExpr.join(jniKeyExpr?.ptr ?: 0, keyExpr, other)) + return KeyExpr(JNIKeyExpr.joinViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other)) } /** @@ -144,7 +144,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun concat(other: String): KeyExpr { - return KeyExpr(JNIKeyExpr.concat(jniKeyExpr?.ptr ?: 0, keyExpr, other)) + return KeyExpr(JNIKeyExpr.concatViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other)) } override fun toString(): String { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt index ce5f7126..2ccaa4ef 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt @@ -26,59 +26,26 @@ public class JNIKeyExpr(public val ptr: Long) { } @Throws(ZError::class) - fun tryFrom(keyExpr: String): String { - return tryFromViaJNI(keyExpr) - } + internal external fun tryFromViaJNI(keyExpr: String): String @Throws(ZError::class) - fun autocanonize(keyExpr: String): String { - return autocanonizeViaJNI(keyExpr) - } + internal external fun autocanonizeViaJNI(keyExpr: String): String @Throws(ZError::class) - fun intersects(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean = - intersectsViaJNI(ptrA, keyExprA, ptrB, keyExprB) + internal external fun intersectsViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean @Throws(ZError::class) - fun includes(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean = - includesViaJNI(ptrA, keyExprA, ptrB, keyExprB) + internal external fun includesViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean /** Returns SetIntersectionLevel ordinal as Int. Callers convert to SetIntersectionLevel. */ @Throws(ZError::class) - fun relationTo(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int { - return relationToViaJNI(ptrA, keyExprA, ptrB, keyExprB) - } - - @Throws(ZError::class) - fun join(ptrA: Long, keyExprA: String, other: String): String { - return joinViaJNI(ptrA, keyExprA, other) - } - - @Throws(ZError::class) - fun concat(ptrA: Long, keyExprA: String, other: String): String { - return concatViaJNI(ptrA, keyExprA, other) - } - - @Throws(ZError::class) - private external fun tryFromViaJNI(keyExpr: String): String - - @Throws(ZError::class) - private external fun autocanonizeViaJNI(keyExpr: String): String - - @Throws(ZError::class) - private external fun intersectsViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean - - @Throws(ZError::class) - private external fun includesViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean - - @Throws(ZError::class) - private external fun relationToViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int + internal external fun relationToViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int @Throws(ZError::class) - private external fun joinViaJNI(ptrA: Long, keyExprA: String, other: String): String + internal external fun joinViaJNI(ptrA: Long, keyExprA: String, other: String): String @Throws(ZError::class) - private external fun concatViaJNI(ptrA: Long, keyExprA: String, other: String): String + internal external fun concatViaJNI(ptrA: Long, keyExprA: String, other: String): String } fun close() { From c790cd48d7072c7b6773b7396e646723fe93cf29 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 18:40:59 +0200 Subject: [PATCH 019/126] made wrapped undeclare methods private --- .../src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNISession.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt index b9d8dbeb..9f847b9f 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt @@ -22,6 +22,6 @@ public class JNILivelinessToken(public val ptr: Long) { } companion object { - external fun undeclareViaJNI(ptr: Long) + private external fun undeclareViaJNI(ptr: Long) } } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt index 07baadeb..20db11f9 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -35,7 +35,7 @@ public class JNIQuerier(public val ptr: Long) { encodingSchema: String?, ) - external fun freePtrViaJNI(ptr: Long) + private external fun freePtrViaJNI(ptr: Long) fun close() { freePtrViaJNI(ptr) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt index 0f2db057..0fd624cd 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt @@ -39,7 +39,7 @@ public class JNIScout(private val ptr: Long) { configPtr: Long, ): Long - external fun freePtrViaJNI(ptr: Long) + private external fun freePtrViaJNI(ptr: Long) } fun close() { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 9b3ad49b..e0474649 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -41,7 +41,7 @@ public class JNISession(public val sessionPtr: Long) { } @Throws(ZError::class) - external fun closeSessionViaJNI(ptr: Long) + private external fun closeSessionViaJNI(ptr: Long) @Throws(ZError::class) external fun declarePublisherViaJNI( From 766950f29cd60cd2f5e2ffaaad2fd89d1fe3249c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 18:47:36 +0200 Subject: [PATCH 020/126] get in querier --- .../commonMain/kotlin/io/zenoh/query/Querier.kt | 6 ++---- .../kotlin/io/zenoh/jni/JNIQuerier.kt | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt index 27cc6acb..d7e3704e 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt @@ -171,8 +171,7 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v } callback.run(reply) } - jni.getViaJNI( - jni.ptr, + jni.get( keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, options.parameters?.toString(), @@ -207,8 +206,7 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v } handler.handle(reply) } - jni.getViaJNI( - jni.ptr, + jni.get( keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, options.parameters?.toString(), diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt index 20db11f9..988a2a8a 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -22,7 +22,22 @@ import io.zenoh.jni.callbacks.JNIOnCloseCallback public class JNIQuerier(public val ptr: Long) { @Throws(ZError::class) - external fun getViaJNI( + fun get( + keyExprPtr: Long, + keyExprString: String, + parameters: String?, + callback: JNIGetCallback, + onClose: JNIOnCloseCallback, + attachmentBytes: ByteArray?, + payload: ByteArray?, + encodingId: Int, + encodingSchema: String?, + ) { + getViaJNI(ptr, keyExprPtr, keyExprString, parameters, callback, onClose, attachmentBytes, payload, encodingId, encodingSchema) + } + + @Throws(ZError::class) + private external fun getViaJNI( querierPtr: Long, keyExprPtr: Long, keyExprString: String, From da92aa8171a3cc11090d42ab89330caa9531ddc5 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 19:09:53 +0200 Subject: [PATCH 021/126] declare jni method made private --- .../src/commonMain/kotlin/io/zenoh/Session.kt | 62 +++++++------------ .../kotlin/io/zenoh/liveliness/Liveliness.kt | 35 ++--------- .../kotlin/io/zenoh/jni/JNILiveliness.kt | 18 +++++- .../kotlin/io/zenoh/jni/JNISession.kt | 48 ++++++++++++-- 4 files changed, 87 insertions(+), 76 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index 440d80b4..4a474240 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -597,21 +597,19 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) internal fun resolvePublisher(keyExpr: KeyExpr, options: PublisherOptions): Publisher { return jniSession?.run { - val publisherRawPtr = declarePublisherViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - options.congestionControl.value, - options.priority.value, - options.express, - options.reliability.ordinal - ) val publisher = Publisher( keyExpr, options.congestionControl, options.priority, options.encoding, - JNIPublisher(publisherRawPtr) + declarePublisher( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + options.congestionControl.value, + options.priority.value, + options.express, + options.reliability.ordinal + ) ) weakDeclarations.add(WeakReference(publisher)) publisher @@ -638,10 +636,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val subscriberRawPtr = declareSubscriberViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, subCallback, handler::onClose - ) - val subscriber = HandlerSubscriber(keyExpr, JNISubscriber(subscriberRawPtr), handler.receiver()) + val subscriber = HandlerSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, handler::onClose), handler.receiver()) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -667,10 +662,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val subscriberRawPtr = declareSubscriberViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, subCallback, fun() {} - ) - val subscriber = CallbackSubscriber(keyExpr, JNISubscriber(subscriberRawPtr)) + val subscriber = CallbackSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, fun() {})) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -698,10 +690,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val queryableRawPtr = declareQueryableViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, queryCallback, handler::onClose, options.complete - ) - val queryable = HandlerQueryable(keyExpr, JNIQueryable(queryableRawPtr), handler.receiver()) + val queryable = HandlerQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, queryCallback, handler::onClose, options.complete), handler.receiver()) strongDeclarations.add(queryable) queryable } ?: throw (sessionClosedException) @@ -729,10 +718,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val queryableRawPtr = declareQueryableViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr, queryCallback, fun() {}, options.complete - ) - val queryable = CallbackQueryable(keyExpr, JNIQueryable(queryableRawPtr)) + val queryable = CallbackQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, queryCallback, fun() {}, options.complete)) strongDeclarations.add(queryable) queryable } ?: throw (sessionClosedException) @@ -744,22 +730,20 @@ class Session private constructor(private val config: Config) : AutoCloseable { options: QuerierOptions ): Querier { return jniSession?.run { - val querierRawPtr = declareQuerierViaJNI( - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - sessionPtr, - options.target.ordinal, - options.consolidationMode.ordinal, - options.congestionControl.value, - options.priority.value, - options.express, - options.timeout.toMillis(), - options.acceptReplies.ordinal - ) val querier = Querier( keyExpr, QoS(congestionControl = options.congestionControl, priority = options.priority, express = options.express), - JNIQuerier(querierRawPtr) + declareQuerier( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + options.target.ordinal, + options.consolidationMode.ordinal, + options.congestionControl.value, + options.priority.value, + options.express, + options.timeout.toMillis(), + options.acceptReplies.ordinal + ) ) weakDeclarations.add(WeakReference(querier)) querier diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt index be811bba..f32c9247 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt @@ -24,8 +24,6 @@ import io.zenoh.handlers.BlockingQueueHandler import io.zenoh.handlers.Callback import io.zenoh.handlers.Handler import io.zenoh.jni.JNILiveliness -import io.zenoh.jni.JNILivelinessToken -import io.zenoh.jni.JNISubscriber import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNISubscriberCallback import io.zenoh.keyexpr.KeyExpr @@ -62,8 +60,7 @@ class Liveliness internal constructor(private val session: Session) { @Throws(ZError::class) fun declareToken(keyExpr: KeyExpr): LivelinessToken { val jniSession = session.jniSession ?: throw Session.sessionClosedException - val ptr = JNILiveliness.declareTokenViaJNI(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr) - return LivelinessToken(JNILivelinessToken(ptr)) + return LivelinessToken(JNILiveliness.declareToken(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr)) } /** @@ -177,15 +174,7 @@ class Liveliness internal constructor(private val session: Session) { val handler = BlockingQueueHandler(LinkedBlockingDeque()) val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(handler::handle) - val ptr = JNILiveliness.declareSubscriberViaJNI( - jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - subCallback, - options.history, - handler::onClose - ) - return HandlerSubscriber(keyExpr, JNISubscriber(ptr), handler.receiver()) + return HandlerSubscriber(keyExpr, JNILiveliness.declareSubscriber(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) } /** @@ -204,15 +193,7 @@ class Liveliness internal constructor(private val session: Session) { ): CallbackSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(callback) - val ptr = JNILiveliness.declareSubscriberViaJNI( - jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - subCallback, - options.history, - fun() {} - ) - return CallbackSubscriber(keyExpr, JNISubscriber(ptr)) + return CallbackSubscriber(keyExpr, JNILiveliness.declareSubscriber(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, fun() {})) } /** @@ -232,15 +213,7 @@ class Liveliness internal constructor(private val session: Session) { ): HandlerSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(handler::handle) - val ptr = JNILiveliness.declareSubscriberViaJNI( - jniSession.sessionPtr, - keyExpr.jniKeyExpr?.ptr ?: 0, - keyExpr.keyExpr, - subCallback, - options.history, - handler::onClose - ) - return HandlerSubscriber(keyExpr, JNISubscriber(ptr), handler.receiver()) + return HandlerSubscriber(keyExpr, JNILiveliness.declareSubscriber(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) } private fun buildSubscriberCallback(callback: Callback): JNISubscriberCallback = diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt index 580ffcc1..59f3b342 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt @@ -23,10 +23,24 @@ import io.zenoh.jni.callbacks.JNISubscriberCallback public object JNILiveliness { @Throws(ZError::class) - external fun declareTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long + fun declareToken(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): JNILivelinessToken = + JNILivelinessToken(declareTokenViaJNI(sessionPtr, keyExprPtr, keyExprString)) @Throws(ZError::class) - external fun declareSubscriberViaJNI( + private external fun declareTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long + + @Throws(ZError::class) + fun declareSubscriber( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + callback: JNISubscriberCallback, + history: Boolean, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, history, onClose)) + + @Throws(ZError::class) + private external fun declareSubscriberViaJNI( sessionPtr: Long, keyExprPtr: Long, keyExprString: String, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index e0474649..4afdc3a0 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -44,7 +44,17 @@ public class JNISession(public val sessionPtr: Long) { private external fun closeSessionViaJNI(ptr: Long) @Throws(ZError::class) - external fun declarePublisherViaJNI( + fun declarePublisher( + keyExprPtr: Long, + keyExprString: String, + congestionControl: Int, + priority: Int, + express: Boolean, + reliability: Int + ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(keyExprPtr, keyExprString, sessionPtr, congestionControl, priority, express, reliability)) + + @Throws(ZError::class) + private external fun declarePublisherViaJNI( keyExprPtr: Long, keyExprString: String, sessionPtr: Long, @@ -55,7 +65,15 @@ public class JNISession(public val sessionPtr: Long) { ): Long @Throws(ZError::class) - external fun declareSubscriberViaJNI( + fun declareSubscriber( + keyExprPtr: Long, + keyExprString: String, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(keyExprPtr, keyExprString, sessionPtr, callback, onClose)) + + @Throws(ZError::class) + private external fun declareSubscriberViaJNI( keyExprPtr: Long, keyExprString: String, sessionPtr: Long, @@ -64,7 +82,16 @@ public class JNISession(public val sessionPtr: Long) { ): Long @Throws(ZError::class) - external fun declareQueryableViaJNI( + fun declareQueryable( + keyExprPtr: Long, + keyExprString: String, + callback: JNIQueryableCallback, + onClose: JNIOnCloseCallback, + complete: Boolean + ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(keyExprPtr, keyExprString, sessionPtr, callback, onClose, complete)) + + @Throws(ZError::class) + private external fun declareQueryableViaJNI( keyExprPtr: Long, keyExprString: String, sessionPtr: Long, @@ -74,7 +101,20 @@ public class JNISession(public val sessionPtr: Long) { ): Long @Throws(ZError::class) - external fun declareQuerierViaJNI( + fun declareQuerier( + keyExprPtr: Long, + keyExprString: String, + target: Int, + consolidation: Int, + congestionControl: Int, + priority: Int, + express: Boolean, + timeoutMs: Long, + acceptReplies: Int + ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(keyExprPtr, keyExprString, sessionPtr, target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) + + @Throws(ZError::class) + private external fun declareQuerierViaJNI( keyExprPtr: Long, keyExprString: String, sessionPtr: Long, From 6ff42a54e8a71d7bfbc5b8b113e8b114489a72ee Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 19:17:43 +0200 Subject: [PATCH 022/126] declare/undeclare keyexpr wrapped --- zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt | 5 ++--- .../src/commonMain/kotlin/io/zenoh/jni/JNISession.kt | 10 ++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index 4a474240..4f15b591 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -400,8 +400,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) fun declareKeyExpr(keyExpr: String): KeyExpr { return jniSession?.run { - val ptr = declareKeyExprViaJNI(sessionPtr, keyExpr) - val ke = KeyExpr(keyExpr, JNIKeyExpr(ptr)) + val ke = KeyExpr(keyExpr, declareKeyExpr(keyExpr)) strongDeclarations.add(ke) ke } ?: throw sessionClosedException @@ -420,7 +419,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { fun undeclare(keyExpr: KeyExpr) { jniSession?.run { keyExpr.jniKeyExpr?.run { - undeclareKeyExprViaJNI(sessionPtr, ptr) + undeclareKeyExpr(ptr) keyExpr.jniKeyExpr = null } ?: throw ZError("Attempting to undeclare a non declared key expression.") } ?: throw (sessionClosedException) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 4afdc3a0..342b6736 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -128,10 +128,16 @@ public class JNISession(public val sessionPtr: Long) { ): Long @Throws(ZError::class) - external fun declareKeyExprViaJNI(sessionPtr: Long, keyExpr: String): Long + fun declareKeyExpr(keyExpr: String): JNIKeyExpr = JNIKeyExpr(declareKeyExprViaJNI(sessionPtr, keyExpr)) @Throws(ZError::class) - external fun undeclareKeyExprViaJNI(sessionPtr: Long, keyExprPtr: Long) + private external fun declareKeyExprViaJNI(sessionPtr: Long, keyExpr: String): Long + + @Throws(ZError::class) + fun undeclareKeyExpr(keyExprPtr: Long) = undeclareKeyExprViaJNI(sessionPtr, keyExprPtr) + + @Throws(ZError::class) + private external fun undeclareKeyExprViaJNI(sessionPtr: Long, keyExprPtr: Long) @Throws(ZError::class) external fun getViaJNI( From 7304cbafca8e3e87576712413644146fc50226b4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 19:20:47 +0200 Subject: [PATCH 023/126] get/put wrapped --- .../src/commonMain/kotlin/io/zenoh/Session.kt | 12 ++--- .../kotlin/io/zenoh/jni/JNISession.kt | 51 +++++++++++++++++-- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index 4f15b591..09972b92 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -777,11 +777,10 @@ class Session private constructor(private val config: Config) : AutoCloseable { handler.handle(reply) } val sel = selector.into() - getViaJNI( + get( sel.keyExpr.jniKeyExpr?.ptr ?: 0, sel.keyExpr.keyExpr, sel.parameters?.toString(), - sessionPtr, getCallback, handler::onClose, options.timeout.toMillis(), @@ -828,11 +827,10 @@ class Session private constructor(private val config: Config) : AutoCloseable { callback.run(reply) } val sel = selector.into() - getViaJNI( + get( sel.keyExpr.jniKeyExpr?.ptr ?: 0, sel.keyExpr.keyExpr, sel.parameters?.toString(), - sessionPtr, getCallback, fun() {}, options.timeout.toMillis(), @@ -854,10 +852,9 @@ class Session private constructor(private val config: Config) : AutoCloseable { internal fun resolvePut(keyExpr: KeyExpr, payload: IntoZBytes, putOptions: PutOptions) { jniSession?.run { val encoding = putOptions.encoding ?: Encoding.defaultEncoding() - putViaJNI( + put( keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, - sessionPtr, payload.into().bytes, encoding.id, encoding.schema, @@ -873,10 +870,9 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) internal fun resolveDelete(keyExpr: KeyExpr, deleteOptions: DeleteOptions) { jniSession?.run { - deleteViaJNI( + delete( keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, - sessionPtr, deleteOptions.congestionControl.value, deleteOptions.priority.value, deleteOptions.express, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 342b6736..72061f95 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -140,7 +140,27 @@ public class JNISession(public val sessionPtr: Long) { private external fun undeclareKeyExprViaJNI(sessionPtr: Long, keyExprPtr: Long) @Throws(ZError::class) - external fun getViaJNI( + fun get( + keyExprPtr: Long, + keyExprString: String, + selectorParams: String?, + callback: JNIGetCallback, + onClose: JNIOnCloseCallback, + timeoutMs: Long, + target: Int, + consolidation: Int, + attachmentBytes: ByteArray?, + payload: ByteArray?, + encodingId: Int, + encodingSchema: String?, + congestionControl: Int, + priority: Int, + express: Boolean, + acceptReplies: Int, + ) = getViaJNI(keyExprPtr, keyExprString, selectorParams, sessionPtr, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encodingId, encodingSchema, congestionControl, priority, express, acceptReplies) + + @Throws(ZError::class) + private external fun getViaJNI( keyExprPtr: Long, keyExprString: String, selectorParams: String?, @@ -161,7 +181,21 @@ public class JNISession(public val sessionPtr: Long) { ) @Throws(ZError::class) - external fun putViaJNI( + fun put( + keyExprPtr: Long, + keyExprString: String, + valuePayload: ByteArray, + valueEncoding: Int, + valueEncodingSchema: String?, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) = putViaJNI(keyExprPtr, keyExprString, sessionPtr, valuePayload, valueEncoding, valueEncodingSchema, congestionControl, priority, express, attachmentBytes, reliability) + + @Throws(ZError::class) + private external fun putViaJNI( keyExprPtr: Long, keyExprString: String, sessionPtr: Long, @@ -176,7 +210,18 @@ public class JNISession(public val sessionPtr: Long) { ) @Throws(ZError::class) - external fun deleteViaJNI( + fun delete( + keyExprPtr: Long, + keyExprString: String, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) = deleteViaJNI(keyExprPtr, keyExprString, sessionPtr, congestionControl, priority, express, attachmentBytes, reliability) + + @Throws(ZError::class) + private external fun deleteViaJNI( keyExprPtr: Long, keyExprString: String, sessionPtr: Long, From 63e9866988628d19f086831765124dc5d89635c8 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 19:23:50 +0200 Subject: [PATCH 024/126] session ptr is first parameter --- .../kotlin/io/zenoh/jni/JNISession.kt | 32 +++++++++---------- zenoh-jni/src/session.rs | 18 +++++------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 72061f95..bcc7a28d 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -51,13 +51,13 @@ public class JNISession(public val sessionPtr: Long) { priority: Int, express: Boolean, reliability: Int - ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(keyExprPtr, keyExprString, sessionPtr, congestionControl, priority, express, reliability)) + ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, keyExprPtr, keyExprString, congestionControl, priority, express, reliability)) @Throws(ZError::class) private external fun declarePublisherViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprString: String, - sessionPtr: Long, congestionControl: Int, priority: Int, express: Boolean, @@ -70,13 +70,13 @@ public class JNISession(public val sessionPtr: Long) { keyExprString: String, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, - ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(keyExprPtr, keyExprString, sessionPtr, callback, onClose)) + ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, onClose)) @Throws(ZError::class) private external fun declareSubscriberViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprString: String, - sessionPtr: Long, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, ): Long @@ -88,13 +88,13 @@ public class JNISession(public val sessionPtr: Long) { callback: JNIQueryableCallback, onClose: JNIOnCloseCallback, complete: Boolean - ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(keyExprPtr, keyExprString, sessionPtr, callback, onClose, complete)) + ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, onClose, complete)) @Throws(ZError::class) private external fun declareQueryableViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprString: String, - sessionPtr: Long, callback: JNIQueryableCallback, onClose: JNIOnCloseCallback, complete: Boolean @@ -111,13 +111,13 @@ public class JNISession(public val sessionPtr: Long) { express: Boolean, timeoutMs: Long, acceptReplies: Int - ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(keyExprPtr, keyExprString, sessionPtr, target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) + ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(sessionPtr, keyExprPtr, keyExprString, target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) @Throws(ZError::class) private external fun declareQuerierViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprString: String, - sessionPtr: Long, target: Int, consolidation: Int, congestionControl: Int, @@ -157,14 +157,14 @@ public class JNISession(public val sessionPtr: Long) { priority: Int, express: Boolean, acceptReplies: Int, - ) = getViaJNI(keyExprPtr, keyExprString, selectorParams, sessionPtr, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encodingId, encodingSchema, congestionControl, priority, express, acceptReplies) + ) = getViaJNI(sessionPtr, keyExprPtr, keyExprString, selectorParams, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encodingId, encodingSchema, congestionControl, priority, express, acceptReplies) @Throws(ZError::class) private external fun getViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprString: String, selectorParams: String?, - sessionPtr: Long, callback: JNIGetCallback, onClose: JNIOnCloseCallback, timeoutMs: Long, @@ -192,13 +192,13 @@ public class JNISession(public val sessionPtr: Long) { express: Boolean, attachmentBytes: ByteArray?, reliability: Int - ) = putViaJNI(keyExprPtr, keyExprString, sessionPtr, valuePayload, valueEncoding, valueEncodingSchema, congestionControl, priority, express, attachmentBytes, reliability) + ) = putViaJNI(sessionPtr, keyExprPtr, keyExprString, valuePayload, valueEncoding, valueEncodingSchema, congestionControl, priority, express, attachmentBytes, reliability) @Throws(ZError::class) private external fun putViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprString: String, - sessionPtr: Long, valuePayload: ByteArray, valueEncoding: Int, valueEncodingSchema: String?, @@ -218,13 +218,13 @@ public class JNISession(public val sessionPtr: Long) { express: Boolean, attachmentBytes: ByteArray?, reliability: Int - ) = deleteViaJNI(keyExprPtr, keyExprString, sessionPtr, congestionControl, priority, express, attachmentBytes, reliability) + ) = deleteViaJNI(sessionPtr, keyExprPtr, keyExprString, congestionControl, priority, express, attachmentBytes, reliability) @Throws(ZError::class) private external fun deleteViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprString: String, - sessionPtr: Long, congestionControl: Int, priority: Int, express: Boolean, @@ -243,9 +243,9 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) external fun declareAdvancedSubscriberViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprStr: String, - sessionPtr: Long, historyConfigEnabled: Boolean, historyDetectLatePublishers: Boolean, historyMaxSamples: Long, @@ -260,9 +260,9 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) external fun declareAdvancedPublisherViaJNI( + sessionPtr: Long, keyExprPtr: Long, keyExprStr: String, - sessionPtr: Long, congestionControl: Int, priority: Int, isExpress: Boolean, diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index cea91223..15a23a50 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -226,9 +226,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, congestion_control: jint, priority: jint, is_express: jboolean, @@ -289,9 +289,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, payload: JByteArray, encoding_id: jint, encoding_schema: JString, @@ -360,9 +360,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, congestion_control: jint, priority: jint, is_express: jboolean, @@ -426,9 +426,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, callback: JObject, on_close: JObject, ) -> *const Subscriber<()> { @@ -474,9 +474,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, target: jint, consolidation: jint, congestion_control: jint, @@ -550,9 +550,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, callback: JObject, on_close: JObject, complete: jboolean, @@ -814,10 +814,10 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, selector_params: /*nullable*/ JString, - session_ptr: *const Session, callback: JObject, on_close: JObject, timeout_ms: jlong, @@ -1156,9 +1156,9 @@ fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result< pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, // HistoryConfig history_config_enabled: jboolean, history_detect_late_publishers: jboolean, @@ -1269,9 +1269,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedPublisherViaJNI( mut env: JNIEnv, _class: JClass, + session_ptr: *const Session, key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const Session, congestion_control: jint, priority: jint, is_express: jboolean, From b2c6e98feff23810a6d9cd287cbd6d9aa46ede72 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 19:27:48 +0200 Subject: [PATCH 025/126] ptr at the first place for publisher --- .../kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt | 8 ++++---- .../src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt | 8 ++++---- zenoh-jni/src/ext/advanced_publisher.rs | 4 ++-- zenoh-jni/src/publisher.rs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt index d27b0826..aa5d1aab 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt @@ -27,12 +27,12 @@ public class JNIAdvancedPublisher(public val ptr: Long) { @Throws(ZError::class) fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { - putViaJNI(payload, encodingId, encodingSchema, attachment, ptr) + putViaJNI(ptr, payload, encodingId, encodingSchema, attachment) } @Throws(ZError::class) fun delete(attachment: ByteArray?) { - deleteViaJNI(attachment, ptr) + deleteViaJNI(ptr, attachment) } @Throws(ZError::class) @@ -52,11 +52,11 @@ public class JNIAdvancedPublisher(public val ptr: Long) { @Throws(ZError::class) private external fun putViaJNI( - payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?, ptr: Long + ptr: Long, payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray? ) @Throws(ZError::class) - private external fun deleteViaJNI(attachment: ByteArray?, ptr: Long) + private external fun deleteViaJNI(ptr: Long, attachment: ByteArray?) @Throws(ZError::class) private external fun declareMatchingListenerViaJNI( diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt index edfddc97..c3844d02 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt @@ -25,12 +25,12 @@ public class JNIPublisher(public val ptr: Long) { @Throws(ZError::class) fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { - putViaJNI(payload, encodingId, encodingSchema, attachment, ptr) + putViaJNI(ptr, payload, encodingId, encodingSchema, attachment) } @Throws(ZError::class) fun delete(attachment: ByteArray?) { - deleteViaJNI(attachment, ptr) + deleteViaJNI(ptr, attachment) } fun close() { @@ -39,11 +39,11 @@ public class JNIPublisher(public val ptr: Long) { @Throws(ZError::class) private external fun putViaJNI( - valuePayload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?, ptr: Long + ptr: Long, valuePayload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray? ) @Throws(ZError::class) - private external fun deleteViaJNI(attachment: ByteArray?, ptr: Long) + private external fun deleteViaJNI(ptr: Long, attachment: ByteArray?) private external fun freePtrViaJNI(ptr: Long) } diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index c3d0c7c1..51500880 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -261,11 +261,11 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_getMatchingStatu pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_putViaJNI( mut env: JNIEnv, _class: JClass, + publisher_ptr: *const AdvancedPublisher<'static>, payload: JByteArray, encoding_id: jint, encoding_schema: /*nullable*/ JString, attachment: /*nullable*/ JByteArray, - publisher_ptr: *const AdvancedPublisher<'static>, ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { @@ -301,8 +301,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_putViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_deleteViaJNI( mut env: JNIEnv, _class: JClass, - attachment: /*nullable*/ JByteArray, publisher_ptr: *const AdvancedPublisher<'static>, + attachment: /*nullable*/ JByteArray, ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index 80738de2..e2e76339 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -51,11 +51,11 @@ use crate::{ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( mut env: JNIEnv, _class: JClass, + publisher_ptr: *const Publisher<'static>, payload: JByteArray, encoding_id: jint, encoding_schema: /*nullable*/ JString, attachment: /*nullable*/ JByteArray, - publisher_ptr: *const Publisher<'static>, ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { @@ -91,8 +91,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( mut env: JNIEnv, _class: JClass, - attachment: /*nullable*/ JByteArray, publisher_ptr: *const Publisher<'static>, + attachment: /*nullable*/ JByteArray, ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { From a1745b3ea99df809ec255b1b85ee0ca0ba969fdb Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 19:33:04 +0200 Subject: [PATCH 026/126] wrapped other session functiuons --- .../src/commonMain/kotlin/io/zenoh/Session.kt | 4 +- .../kotlin/io/zenoh/jni/JNISession.kt | 50 +++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index 09972b92..b1768a07 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -884,12 +884,12 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) internal fun zid(): ZenohId { - return jniSession?.run { ZenohId(getZidViaJNI(sessionPtr)) } ?: throw sessionClosedException + return jniSession?.run { ZenohId(getZid()) } ?: throw sessionClosedException } @Throws(ZError::class) internal fun getPeersId(): List { - return jniSession?.run { getPeersZidViaJNI(sessionPtr).map { ZenohId(it) } } ?: throw sessionClosedException + return jniSession?.run { getPeersZid().map { ZenohId(it) } } ?: throw sessionClosedException } @Throws(ZError::class) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index bcc7a28d..d934366c 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -233,16 +233,38 @@ public class JNISession(public val sessionPtr: Long) { ) @Throws(ZError::class) - external fun getZidViaJNI(ptr: Long): ByteArray + fun getZid(): ByteArray = getZidViaJNI(sessionPtr) @Throws(ZError::class) - external fun getPeersZidViaJNI(ptr: Long): List + private external fun getZidViaJNI(ptr: Long): ByteArray + + @Throws(ZError::class) + fun getPeersZid(): List = getPeersZidViaJNI(sessionPtr) + + @Throws(ZError::class) + private external fun getPeersZidViaJNI(ptr: Long): List @Throws(ZError::class) external fun getRoutersZidViaJNI(ptr: Long): List @Throws(ZError::class) - external fun declareAdvancedSubscriberViaJNI( + fun declareAdvancedSubscriber( + keyExprPtr: Long, + keyExprStr: String, + historyConfigEnabled: Boolean, + historyDetectLatePublishers: Boolean, + historyMaxSamples: Long, + historyMaxAgeSeconds: Double, + recoveryConfigEnabled: Boolean, + recoveryConfigIsHeartbeat: Boolean, + recoveryQueryPeriodMs: Long, + subscriberDetection: Boolean, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): JNIAdvancedSubscriber = JNIAdvancedSubscriber(declareAdvancedSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprStr, historyConfigEnabled, historyDetectLatePublishers, historyMaxSamples, historyMaxAgeSeconds, recoveryConfigEnabled, recoveryConfigIsHeartbeat, recoveryQueryPeriodMs, subscriberDetection, callback, onClose)) + + @Throws(ZError::class) + private external fun declareAdvancedSubscriberViaJNI( sessionPtr: Long, keyExprPtr: Long, keyExprStr: String, @@ -259,7 +281,27 @@ public class JNISession(public val sessionPtr: Long) { ): Long @Throws(ZError::class) - external fun declareAdvancedPublisherViaJNI( + fun declareAdvancedPublisher( + keyExprPtr: Long, + keyExprStr: String, + congestionControl: Int, + priority: Int, + isExpress: Boolean, + reliability: Int, + cacheEnabled: Boolean, + cacheMaxSamples: Long, + cacheRepliesPriority: Int, + cacheRepliesCongestionControl: Int, + cacheRepliesIsExpress: Boolean, + sampleMissDetectionEnabled: Boolean, + sampleMissDetectionEnableHeartbeat: Boolean, + sampleMissDetectionHeartbeatMs: Long, + sampleMissDetectionHeartbeatIsSporadic: Boolean, + publisherDetection: Boolean, + ): JNIAdvancedPublisher = JNIAdvancedPublisher(declareAdvancedPublisherViaJNI(sessionPtr, keyExprPtr, keyExprStr, congestionControl, priority, isExpress, reliability, cacheEnabled, cacheMaxSamples, cacheRepliesPriority, cacheRepliesCongestionControl, cacheRepliesIsExpress, sampleMissDetectionEnabled, sampleMissDetectionEnableHeartbeat, sampleMissDetectionHeartbeatMs, sampleMissDetectionHeartbeatIsSporadic, publisherDetection)) + + @Throws(ZError::class) + private external fun declareAdvancedPublisherViaJNI( sessionPtr: Long, keyExprPtr: Long, keyExprStr: String, From d50b088666951592030ca4d9eb7e011bed0325d1 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 19:41:47 +0200 Subject: [PATCH 027/126] liveliness moved to session --- .../kotlin/io/zenoh/liveliness/Liveliness.kt | 18 +++--- .../kotlin/io/zenoh/jni/JNILiveliness.kt | 61 ------------------- .../kotlin/io/zenoh/jni/JNISession.kt | 45 ++++++++++++++ zenoh-jni/src/liveliness.rs | 6 +- 4 files changed, 55 insertions(+), 75 deletions(-) delete mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt index f32c9247..23e8beef 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt @@ -23,7 +23,6 @@ import io.zenoh.exceptions.ZError import io.zenoh.handlers.BlockingQueueHandler import io.zenoh.handlers.Callback import io.zenoh.handlers.Handler -import io.zenoh.jni.JNILiveliness import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNISubscriberCallback import io.zenoh.keyexpr.KeyExpr @@ -60,7 +59,7 @@ class Liveliness internal constructor(private val session: Session) { @Throws(ZError::class) fun declareToken(keyExpr: KeyExpr): LivelinessToken { val jniSession = session.jniSession ?: throw Session.sessionClosedException - return LivelinessToken(JNILiveliness.declareToken(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr)) + return LivelinessToken(jniSession.declareLivelinessToken(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr)) } /** @@ -78,8 +77,7 @@ class Liveliness internal constructor(private val session: Session) { val jniSession = session.jniSession ?: throw Session.sessionClosedException val handler = BlockingQueueHandler(LinkedBlockingDeque()) val getCallback = buildGetCallback(handler::handle) - JNILiveliness.getViaJNI( - jniSession.sessionPtr, + jniSession.livelinessGet( keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, getCallback, @@ -102,8 +100,7 @@ class Liveliness internal constructor(private val session: Session) { keyExpr: KeyExpr, callback: Callback, timeout: Duration = Duration.ofMillis(10000) ) { val jniSession = session.jniSession ?: throw Session.sessionClosedException - JNILiveliness.getViaJNI( - jniSession.sessionPtr, + jniSession.livelinessGet( keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, buildGetCallback(callback), @@ -126,8 +123,7 @@ class Liveliness internal constructor(private val session: Session) { keyExpr: KeyExpr, handler: Handler, timeout: Duration = Duration.ofMillis(10000) ): R { val jniSession = session.jniSession ?: throw Session.sessionClosedException - JNILiveliness.getViaJNI( - jniSession.sessionPtr, + jniSession.livelinessGet( keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, buildGetCallback(handler::handle), @@ -174,7 +170,7 @@ class Liveliness internal constructor(private val session: Session) { val handler = BlockingQueueHandler(LinkedBlockingDeque()) val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(handler::handle) - return HandlerSubscriber(keyExpr, JNILiveliness.declareSubscriber(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) + return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) } /** @@ -193,7 +189,7 @@ class Liveliness internal constructor(private val session: Session) { ): CallbackSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(callback) - return CallbackSubscriber(keyExpr, JNILiveliness.declareSubscriber(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, fun() {})) + return CallbackSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, fun() {})) } /** @@ -213,7 +209,7 @@ class Liveliness internal constructor(private val session: Session) { ): HandlerSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(handler::handle) - return HandlerSubscriber(keyExpr, JNILiveliness.declareSubscriber(jniSession.sessionPtr, keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) + return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) } private fun buildSubscriberCallback(callback: Callback): JNISubscriberCallback = diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt deleted file mode 100644 index 59f3b342..00000000 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILiveliness.kt +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright (c) 2026 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.exceptions.ZError -import io.zenoh.jni.callbacks.JNIGetCallback -import io.zenoh.jni.callbacks.JNIOnCloseCallback -import io.zenoh.jni.callbacks.JNISubscriberCallback - -/** Adapter object for interacting with Zenoh Liveliness through JNI. */ -public object JNILiveliness { - - @Throws(ZError::class) - fun declareToken(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): JNILivelinessToken = - JNILivelinessToken(declareTokenViaJNI(sessionPtr, keyExprPtr, keyExprString)) - - @Throws(ZError::class) - private external fun declareTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long - - @Throws(ZError::class) - fun declareSubscriber( - sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, - callback: JNISubscriberCallback, - history: Boolean, - onClose: JNIOnCloseCallback, - ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, history, onClose)) - - @Throws(ZError::class) - private external fun declareSubscriberViaJNI( - sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, - callback: JNISubscriberCallback, - history: Boolean, - onClose: JNIOnCloseCallback, - ): Long - - @Throws(ZError::class) - external fun getViaJNI( - sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, - callback: JNIGetCallback, - timeoutMs: Long, - onClose: JNIOnCloseCallback, - ) -} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index d934366c..644e0e2b 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -321,6 +321,51 @@ public class JNISession(public val sessionPtr: Long) { publisherDetection: Boolean, ): Long + @Throws(ZError::class) + fun declareLivelinessToken(keyExprPtr: Long, keyExprString: String): JNILivelinessToken = + JNILivelinessToken(declareLivelinessTokenViaJNI(sessionPtr, keyExprPtr, keyExprString)) + + @Throws(ZError::class) + private external fun declareLivelinessTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long + + @Throws(ZError::class) + fun declareLivelinessSubscriber( + keyExprPtr: Long, + keyExprString: String, + callback: JNISubscriberCallback, + history: Boolean, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(declareLivelinessSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, history, onClose)) + + @Throws(ZError::class) + private external fun declareLivelinessSubscriberViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + callback: JNISubscriberCallback, + history: Boolean, + onClose: JNIOnCloseCallback, + ): Long + + @Throws(ZError::class) + fun livelinessGet( + keyExprPtr: Long, + keyExprString: String, + callback: JNIGetCallback, + timeoutMs: Long, + onClose: JNIOnCloseCallback, + ) = livelinessGetViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, timeoutMs, onClose) + + @Throws(ZError::class) + private external fun livelinessGetViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + callback: JNIGetCallback, + timeoutMs: Long, + onClose: JNIOnCloseCallback, + ) + fun close() { closeSessionViaJNI(sessionPtr) } diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 3aaf1a98..37e0925b 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -38,7 +38,7 @@ use crate::{ #[no_mangle] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( +pub extern "C" fn Java_io_zenoh_jni_JNISession_livelinessGetViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, @@ -101,7 +101,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( #[no_mangle] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareTokenViaJNI( +pub extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessTokenViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, @@ -137,7 +137,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNILivelinessToken_00024Companion_undeclareV #[no_mangle] #[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNILiveliness_declareSubscriberViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessSubscriberViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, From ca09547a6ca5a39b678b2a9d5f2a2a904451a865 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 21:11:32 +0200 Subject: [PATCH 028/126] config pointer wrapped --- .../src/commonMain/kotlin/io/zenoh/Config.kt | 12 +++---- .../src/commonMain/kotlin/io/zenoh/Session.kt | 2 +- .../kotlin/io/zenoh/jni/JNIConfig.kt | 31 +++++++++++++------ .../kotlin/io/zenoh/jni/JNISession.kt | 4 +-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt index e392c019..a2a334cc 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt @@ -46,7 +46,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { */ @JvmStatic fun loadDefault(): Config { - return Config(JNIConfig(JNIConfig.loadDefaultConfigViaJNI())) + return Config(JNIConfig.loadDefault()) } /** @@ -59,7 +59,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromFile(file: File): Config { - return Config(JNIConfig(JNIConfig.loadConfigFileViaJNI(file.toString()))) + return Config(JNIConfig.loadFromFile(file.toString())) } /** @@ -72,7 +72,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromFile(path: Path): Config { - return Config(JNIConfig(JNIConfig.loadConfigFileViaJNI(path.toString()))) + return Config(JNIConfig.loadFromFile(path.toString())) } /** @@ -87,7 +87,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromJson(config: String): Config { - return Config(JNIConfig(JNIConfig.loadJsonConfigViaJNI(config))) + return Config(JNIConfig.loadFromJson(config)) } /** @@ -102,7 +102,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromJson5(config: String): Config { - return Config(JNIConfig(JNIConfig.loadJsonConfigViaJNI(config))) + return Config(JNIConfig.loadFromJson(config)) } /** @@ -117,7 +117,7 @@ class Config internal constructor(internal val jniConfig: JNIConfig) { @JvmStatic @Throws(ZError::class) fun fromYaml(config: String): Config { - return Config(JNIConfig(JNIConfig.loadYamlConfigViaJNI(config))) + return Config(JNIConfig.loadFromYaml(config)) } /** diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index b1768a07..3236ada0 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -900,7 +900,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { /** Launches the session through the jni session, returning the [Session] on success. */ @Throws(ZError::class) private fun launch(): Session { - this.jniSession = JNISession.open(config.jniConfig.ptr) + this.jniSession = JNISession.open(config.jniConfig) return this } } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt index a12fff4a..bf4978f2 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt @@ -17,7 +17,7 @@ package io.zenoh.jni import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError -/** Adapter for the native Zenoh config. Factory methods return raw pointers for use by facade classes. */ +/** Adapter for the native Zenoh config. */ public class JNIConfig(public val ptr: Long) { companion object { @@ -27,16 +27,28 @@ public class JNIConfig(public val ptr: Long) { } @Throws(ZError::class) - internal external fun loadDefaultConfigViaJNI(): Long + fun loadDefault(): JNIConfig = JNIConfig(loadDefaultConfigViaJNI()) @Throws(ZError::class) - internal external fun loadConfigFileViaJNI(path: String): Long + fun loadFromFile(path: String): JNIConfig = JNIConfig(loadConfigFileViaJNI(path)) @Throws(ZError::class) - internal external fun loadJsonConfigViaJNI(rawConfig: String): Long + fun loadFromJson(rawConfig: String): JNIConfig = JNIConfig(loadJsonConfigViaJNI(rawConfig)) @Throws(ZError::class) - internal external fun loadYamlConfigViaJNI(rawConfig: String): Long + fun loadFromYaml(rawConfig: String): JNIConfig = JNIConfig(loadYamlConfigViaJNI(rawConfig)) + + @Throws(ZError::class) + private external fun loadDefaultConfigViaJNI(): Long + + @Throws(ZError::class) + private external fun loadConfigFileViaJNI(path: String): Long + + @Throws(ZError::class) + private external fun loadJsonConfigViaJNI(rawConfig: String): Long + + @Throws(ZError::class) + private external fun loadYamlConfigViaJNI(rawConfig: String): Long @Throws(ZError::class) private external fun getIdViaJNI(ptr: Long): ByteArray @@ -55,12 +67,13 @@ public class JNIConfig(public val ptr: Long) { } @Throws(ZError::class) - fun getJson(key: String): String { - return getJsonViaJNI(ptr, key) - } + fun getId(): ByteArray = getIdViaJNI(ptr) + + @Throws(ZError::class) + fun getJson(key: String): String = getJsonViaJNI(ptr, key) @Throws(ZError::class) fun insertJson5(key: String, value: String) { - insertJson5ViaJNI(this.ptr, key, value) + insertJson5ViaJNI(ptr, key, value) } } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 644e0e2b..2492a610 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -30,8 +30,8 @@ public class JNISession(public val sessionPtr: Long) { } @Throws(ZError::class) - fun open(configPtr: Long): JNISession { - val sessionPtr = openSessionViaJNI(configPtr) + fun open(config: JNIConfig): JNISession { + val sessionPtr = openSessionViaJNI(config.ptr) return JNISession(sessionPtr) } From 3b05665eb099715c81515bfb8f9147466a91fa2b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 16 Apr 2026 21:43:53 +0200 Subject: [PATCH 029/126] ptr made private/internal --- zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt | 9 +++------ .../kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt | 2 +- .../kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt | 2 +- .../kotlin/io/zenoh/jni/JNILivelinessToken.kt | 2 +- .../kotlin/io/zenoh/jni/JNIMatchingListener.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt | 2 +- .../kotlin/io/zenoh/jni/JNISampleMissListener.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt | 10 +++++++++- .../commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt | 2 +- 12 files changed, 22 insertions(+), 17 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt index 7a5500d6..e0c60c6c 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Zenoh.kt @@ -60,8 +60,7 @@ object Zenoh { handler.handle(Hello(WhatAmI.fromInt(whatAmI), ZenohId(id), locators)) } val binaryWhatAmI = scoutOptions.whatAmI.map { it.value }.reduce { acc, it -> acc or it } - val ptr = JNIScout.scoutViaJNI(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig?.ptr ?: 0) - return HandlerScout(JNIScout(ptr), handler.receiver()) + return HandlerScout(JNIScout.scout(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig), handler.receiver()) } /** @@ -83,8 +82,7 @@ object Zenoh { handler.handle(Hello(WhatAmI.fromInt(whatAmI), ZenohId(id), locators)) } val binaryWhatAmI = scoutOptions.whatAmI.map { it.value }.reduce { acc, it -> acc or it } - val ptr = JNIScout.scoutViaJNI(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig?.ptr ?: 0) - return HandlerScout(JNIScout(ptr), handler.receiver()) + return HandlerScout(JNIScout.scout(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig), handler.receiver()) } /** @@ -105,8 +103,7 @@ object Zenoh { callback.run(Hello(WhatAmI.fromInt(whatAmI), ZenohId(id), locators)) } val binaryWhatAmI = scoutOptions.whatAmI.map { it.value }.reduce { acc, it -> acc or it } - val ptr = JNIScout.scoutViaJNI(binaryWhatAmI, scoutCallback, fun() {}, scoutOptions.config?.jniConfig?.ptr ?: 0) - return CallbackScout(JNIScout(ptr)) + return CallbackScout(JNIScout.scout(binaryWhatAmI, scoutCallback, fun() {}, scoutOptions.config?.jniConfig)) } /** diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt index aa5d1aab..eee392b6 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt @@ -23,7 +23,7 @@ import io.zenoh.jni.callbacks.JNIOnCloseCallback * * @property ptr Raw pointer to the underlying native AdvancedPublisher. */ -public class JNIAdvancedPublisher(public val ptr: Long) { +public class JNIAdvancedPublisher(private val ptr: Long) { @Throws(ZError::class) fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt index c76d1dbb..13df29a8 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt @@ -24,7 +24,7 @@ import io.zenoh.jni.callbacks.JNISubscriberCallback * * @property ptr Raw pointer to the underlying native AdvancedSubscriber. */ -public class JNIAdvancedSubscriber(public val ptr: Long) { +public class JNIAdvancedSubscriber(private val ptr: Long) { @Throws(ZError::class) fun declareDetectPublishersSubscriber( diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt index bf4978f2..9cdd403c 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIConfig.kt @@ -18,7 +18,7 @@ import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError /** Adapter for the native Zenoh config. */ -public class JNIConfig(public val ptr: Long) { +public class JNIConfig(internal val ptr: Long) { companion object { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt index 9f847b9f..dcf14b6b 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt @@ -15,7 +15,7 @@ package io.zenoh.jni /** Adapter class for a native Zenoh LivelinessToken. */ -public class JNILivelinessToken(public val ptr: Long) { +public class JNILivelinessToken(private val ptr: Long) { fun undeclare() { undeclareViaJNI(ptr) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt index d41814ac..7c58edee 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIMatchingListener.kt @@ -15,7 +15,7 @@ package io.zenoh.jni /** Adapter class for a native Zenoh MatchingListener. */ -public class JNIMatchingListener(public val ptr: Long) { +public class JNIMatchingListener(private val ptr: Long) { fun close() { freePtrViaJNI(ptr) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt index c3844d02..7f800597 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt @@ -21,7 +21,7 @@ import io.zenoh.exceptions.ZError * * @property ptr Raw pointer to the underlying native Publisher. */ -public class JNIPublisher(public val ptr: Long) { +public class JNIPublisher(private val ptr: Long) { @Throws(ZError::class) fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt index 988a2a8a..a79a293b 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -19,7 +19,7 @@ import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNIOnCloseCallback /** Adapter class for a native Zenoh querier. */ -public class JNIQuerier(public val ptr: Long) { +public class JNIQuerier(private val ptr: Long) { @Throws(ZError::class) fun get( diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt index 7299b67e..ba64f7e9 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQueryable.kt @@ -19,7 +19,7 @@ package io.zenoh.jni * * @property ptr: raw pointer to the underlying native Queryable. */ -public class JNIQueryable(public val ptr: Long) { +public class JNIQueryable(private val ptr: Long) { fun close() { freePtrViaJNI(ptr) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt index 08edf3d3..dea27edf 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISampleMissListener.kt @@ -15,7 +15,7 @@ package io.zenoh.jni /** Adapter class for a native Zenoh SampleMissListener. */ -public class JNISampleMissListener(public val ptr: Long) { +public class JNISampleMissListener(private val ptr: Long) { fun close() { freePtrViaJNI(ptr) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt index 0fd624cd..1b3d1bd0 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt @@ -32,7 +32,15 @@ public class JNIScout(private val ptr: Long) { } @Throws(ZError::class) - external fun scoutViaJNI( + fun scout( + whatAmI: Int, + callback: JNIScoutCallback, + onClose: JNIOnCloseCallback, + config: JNIConfig?, + ): JNIScout = JNIScout(scoutViaJNI(whatAmI, callback, onClose, config?.ptr ?: 0)) + + @Throws(ZError::class) + private external fun scoutViaJNI( whatAmI: Int, callback: JNIScoutCallback, onClose: JNIOnCloseCallback, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt index 7999854e..b9877d70 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISubscriber.kt @@ -19,7 +19,7 @@ package io.zenoh.jni * * @property ptr: raw pointer to the underlying native Subscriber. */ -public class JNISubscriber(public val ptr: Long) { +public class JNISubscriber(private val ptr: Long) { fun close() { freePtrViaJNI(ptr) From ca7d96459fc41b12a11608cba5ff76460996a10b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 10:19:38 +0200 Subject: [PATCH 030/126] made ptr in keyexpr internal --- .../src/commonMain/kotlin/io/zenoh/Session.kt | 22 ++++---- .../kotlin/io/zenoh/keyexpr/KeyExpr.kt | 10 ++-- .../kotlin/io/zenoh/liveliness/Liveliness.kt | 14 +++--- .../kotlin/io/zenoh/query/Querier.kt | 4 +- .../commonMain/kotlin/io/zenoh/query/Query.kt | 4 +- .../kotlin/io/zenoh/jni/JNIKeyExpr.kt | 34 ++++++++++--- .../kotlin/io/zenoh/jni/JNIQuerier.kt | 4 +- .../kotlin/io/zenoh/jni/JNIQuery.kt | 8 +-- .../kotlin/io/zenoh/jni/JNISession.kt | 50 +++++++++---------- 9 files changed, 85 insertions(+), 65 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index 3236ada0..92f6e22b 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -419,7 +419,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { fun undeclare(keyExpr: KeyExpr) { jniSession?.run { keyExpr.jniKeyExpr?.run { - undeclareKeyExpr(ptr) + undeclareKeyExpr(this) keyExpr.jniKeyExpr = null } ?: throw ZError("Attempting to undeclare a non declared key expression.") } ?: throw (sessionClosedException) @@ -602,7 +602,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { options.priority, options.encoding, declarePublisher( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, options.congestionControl.value, options.priority.value, @@ -635,7 +635,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val subscriber = HandlerSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, handler::onClose), handler.receiver()) + val subscriber = HandlerSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, handler::onClose), handler.receiver()) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -661,7 +661,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val subscriber = CallbackSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, fun() {})) + val subscriber = CallbackSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, fun() {})) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -689,7 +689,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val queryable = HandlerQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, queryCallback, handler::onClose, options.complete), handler.receiver()) + val queryable = HandlerQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr, keyExpr.keyExpr, queryCallback, handler::onClose, options.complete), handler.receiver()) strongDeclarations.add(queryable) queryable } ?: throw (sessionClosedException) @@ -717,7 +717,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { ) ) } - val queryable = CallbackQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, queryCallback, fun() {}, options.complete)) + val queryable = CallbackQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr, keyExpr.keyExpr, queryCallback, fun() {}, options.complete)) strongDeclarations.add(queryable) queryable } ?: throw (sessionClosedException) @@ -733,7 +733,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { keyExpr, QoS(congestionControl = options.congestionControl, priority = options.priority, express = options.express), declareQuerier( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, options.target.ordinal, options.consolidationMode.ordinal, @@ -778,7 +778,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { } val sel = selector.into() get( - sel.keyExpr.jniKeyExpr?.ptr ?: 0, + sel.keyExpr.jniKeyExpr, sel.keyExpr.keyExpr, sel.parameters?.toString(), getCallback, @@ -828,7 +828,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { } val sel = selector.into() get( - sel.keyExpr.jniKeyExpr?.ptr ?: 0, + sel.keyExpr.jniKeyExpr, sel.keyExpr.keyExpr, sel.parameters?.toString(), getCallback, @@ -853,7 +853,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { jniSession?.run { val encoding = putOptions.encoding ?: Encoding.defaultEncoding() put( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, payload.into().bytes, encoding.id, @@ -871,7 +871,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { internal fun resolveDelete(keyExpr: KeyExpr, deleteOptions: DeleteOptions) { jniSession?.run { delete( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, deleteOptions.congestionControl.value, deleteOptions.priority.value, diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt index 3e6ff24a..427f643c 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt @@ -106,7 +106,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun intersects(other: KeyExpr): Boolean { - return JNIKeyExpr.intersectsViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) + return JNIKeyExpr.intersects(jniKeyExpr, keyExpr, other.jniKeyExpr, other.keyExpr) } /** @@ -116,7 +116,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun includes(other: KeyExpr): Boolean { - return JNIKeyExpr.includesViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr) + return JNIKeyExpr.includes(jniKeyExpr, keyExpr, other.jniKeyExpr, other.keyExpr) } /** @@ -126,7 +126,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun relationTo(other: KeyExpr): SetIntersectionLevel { - return SetIntersectionLevel.fromInt(JNIKeyExpr.relationToViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other.jniKeyExpr?.ptr ?: 0, other.keyExpr)) + return SetIntersectionLevel.fromInt(JNIKeyExpr.relationTo(jniKeyExpr, keyExpr, other.jniKeyExpr, other.keyExpr)) } /** @@ -135,7 +135,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun join(other: String): KeyExpr { - return KeyExpr(JNIKeyExpr.joinViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other)) + return KeyExpr(JNIKeyExpr.join(jniKeyExpr, keyExpr, other)) } /** @@ -144,7 +144,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn */ @Throws(ZError::class) fun concat(other: String): KeyExpr { - return KeyExpr(JNIKeyExpr.concatViaJNI(jniKeyExpr?.ptr ?: 0, keyExpr, other)) + return KeyExpr(JNIKeyExpr.concat(jniKeyExpr, keyExpr, other)) } override fun toString(): String { diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt index 23e8beef..62aff6c5 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/liveliness/Liveliness.kt @@ -59,7 +59,7 @@ class Liveliness internal constructor(private val session: Session) { @Throws(ZError::class) fun declareToken(keyExpr: KeyExpr): LivelinessToken { val jniSession = session.jniSession ?: throw Session.sessionClosedException - return LivelinessToken(jniSession.declareLivelinessToken(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr)) + return LivelinessToken(jniSession.declareLivelinessToken(keyExpr.jniKeyExpr, keyExpr.keyExpr)) } /** @@ -78,7 +78,7 @@ class Liveliness internal constructor(private val session: Session) { val handler = BlockingQueueHandler(LinkedBlockingDeque()) val getCallback = buildGetCallback(handler::handle) jniSession.livelinessGet( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, getCallback, timeout.toMillis(), @@ -101,7 +101,7 @@ class Liveliness internal constructor(private val session: Session) { ) { val jniSession = session.jniSession ?: throw Session.sessionClosedException jniSession.livelinessGet( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, buildGetCallback(callback), timeout.toMillis(), @@ -124,7 +124,7 @@ class Liveliness internal constructor(private val session: Session) { ): R { val jniSession = session.jniSession ?: throw Session.sessionClosedException jniSession.livelinessGet( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, buildGetCallback(handler::handle), timeout.toMillis(), @@ -170,7 +170,7 @@ class Liveliness internal constructor(private val session: Session) { val handler = BlockingQueueHandler(LinkedBlockingDeque()) val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(handler::handle) - return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) + return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) } /** @@ -189,7 +189,7 @@ class Liveliness internal constructor(private val session: Session) { ): CallbackSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(callback) - return CallbackSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, fun() {})) + return CallbackSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, options.history, fun() {})) } /** @@ -209,7 +209,7 @@ class Liveliness internal constructor(private val session: Session) { ): HandlerSubscriber { val jniSession = session.jniSession ?: throw Session.sessionClosedException val subCallback = buildSubscriberCallback(handler::handle) - return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) + return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) } private fun buildSubscriberCallback(callback: Callback): JNISubscriberCallback = diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt index d7e3704e..faca37a8 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt @@ -172,7 +172,7 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v callback.run(reply) } jni.get( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, options.parameters?.toString(), getCallback, @@ -207,7 +207,7 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v handler.handle(reply) } jni.get( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, options.parameters?.toString(), getCallback, diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt index 9731983d..c2128efa 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt @@ -68,7 +68,7 @@ class Query internal constructor( val timestampEnabled = timestamp != null jniQuery?.apply { replySuccess( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, zbytes.bytes, encoding?.id ?: Encoding.defaultEncoding().id, @@ -108,7 +108,7 @@ class Query internal constructor( val timestampEnabled = timestamp != null jniQuery?.apply { replyDelete( - keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.jniKeyExpr, keyExpr.keyExpr, timestampEnabled, if (timestampEnabled) timestamp!!.ntpValue() else 0, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt index 2ccaa4ef..831e92c6 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt @@ -17,8 +17,8 @@ package io.zenoh.jni import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError -/** Adapter for native Zenoh key expressions. Factory methods return raw primitives. */ -public class JNIKeyExpr(public val ptr: Long) { +/** Adapter for native Zenoh key expressions. */ +public class JNIKeyExpr(internal val ptr: Long) { companion object { init { @@ -32,20 +32,40 @@ public class JNIKeyExpr(public val ptr: Long) { internal external fun autocanonizeViaJNI(keyExpr: String): String @Throws(ZError::class) - internal external fun intersectsViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean + fun intersects(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Boolean = + intersectsViaJNI(a?.ptr ?: 0, aStr, b?.ptr ?: 0, bStr) @Throws(ZError::class) - internal external fun includesViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean + fun includes(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Boolean = + includesViaJNI(a?.ptr ?: 0, aStr, b?.ptr ?: 0, bStr) /** Returns SetIntersectionLevel ordinal as Int. Callers convert to SetIntersectionLevel. */ @Throws(ZError::class) - internal external fun relationToViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int + fun relationTo(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Int = + relationToViaJNI(a?.ptr ?: 0, aStr, b?.ptr ?: 0, bStr) @Throws(ZError::class) - internal external fun joinViaJNI(ptrA: Long, keyExprA: String, other: String): String + fun join(a: JNIKeyExpr?, aStr: String, other: String): String = + joinViaJNI(a?.ptr ?: 0, aStr, other) @Throws(ZError::class) - internal external fun concatViaJNI(ptrA: Long, keyExprA: String, other: String): String + fun concat(a: JNIKeyExpr?, aStr: String, other: String): String = + concatViaJNI(a?.ptr ?: 0, aStr, other) + + @Throws(ZError::class) + private external fun intersectsViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean + + @Throws(ZError::class) + private external fun includesViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean + + @Throws(ZError::class) + private external fun relationToViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int + + @Throws(ZError::class) + private external fun joinViaJNI(ptrA: Long, keyExprA: String, other: String): String + + @Throws(ZError::class) + private external fun concatViaJNI(ptrA: Long, keyExprA: String, other: String): String } fun close() { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt index a79a293b..7757c086 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -23,7 +23,7 @@ public class JNIQuerier(private val ptr: Long) { @Throws(ZError::class) fun get( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, parameters: String?, callback: JNIGetCallback, @@ -33,7 +33,7 @@ public class JNIQuerier(private val ptr: Long) { encodingId: Int, encodingSchema: String?, ) { - getViaJNI(ptr, keyExprPtr, keyExprString, parameters, callback, onClose, attachmentBytes, payload, encodingId, encodingSchema) + getViaJNI(ptr, jniKeyExpr?.ptr ?: 0, keyExprString, parameters, callback, onClose, attachmentBytes, payload, encodingId, encodingSchema) } @Throws(ZError::class) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt index d2711638..78bd5dc3 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -25,7 +25,7 @@ public class JNIQuery(private val ptr: Long) { @Throws(ZError::class) fun replySuccess( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, payload: ByteArray, encodingId: Int, @@ -35,7 +35,7 @@ public class JNIQuery(private val ptr: Long) { attachment: ByteArray?, qosExpress: Boolean, ) { - replySuccessViaJNI(ptr, keyExprPtr, keyExprString, payload, encodingId, encodingSchema, timestampEnabled, timestampNtp64, attachment, qosExpress) + replySuccessViaJNI(ptr, jniKeyExpr?.ptr ?: 0, keyExprString, payload, encodingId, encodingSchema, timestampEnabled, timestampNtp64, attachment, qosExpress) } @Throws(ZError::class) @@ -45,14 +45,14 @@ public class JNIQuery(private val ptr: Long) { @Throws(ZError::class) fun replyDelete( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, timestampEnabled: Boolean, timestampNtp64: Long, attachment: ByteArray?, qosExpress: Boolean, ) { - replyDeleteViaJNI(ptr, keyExprPtr, keyExprString, timestampEnabled, timestampNtp64, attachment, qosExpress) + replyDeleteViaJNI(ptr, jniKeyExpr?.ptr ?: 0, keyExprString, timestampEnabled, timestampNtp64, attachment, qosExpress) } fun close() { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 2492a610..cebe1b97 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -45,13 +45,13 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun declarePublisher( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, congestionControl: Int, priority: Int, express: Boolean, reliability: Int - ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, keyExprPtr, keyExprString, congestionControl, priority, express, reliability)) + ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, congestionControl, priority, express, reliability)) @Throws(ZError::class) private external fun declarePublisherViaJNI( @@ -66,11 +66,11 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun declareSubscriber( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, - ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, onClose)) + ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, onClose)) @Throws(ZError::class) private external fun declareSubscriberViaJNI( @@ -83,12 +83,12 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun declareQueryable( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, callback: JNIQueryableCallback, onClose: JNIOnCloseCallback, complete: Boolean - ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, onClose, complete)) + ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, onClose, complete)) @Throws(ZError::class) private external fun declareQueryableViaJNI( @@ -102,7 +102,7 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun declareQuerier( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, target: Int, consolidation: Int, @@ -111,7 +111,7 @@ public class JNISession(public val sessionPtr: Long) { express: Boolean, timeoutMs: Long, acceptReplies: Int - ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(sessionPtr, keyExprPtr, keyExprString, target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) + ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) @Throws(ZError::class) private external fun declareQuerierViaJNI( @@ -134,14 +134,14 @@ public class JNISession(public val sessionPtr: Long) { private external fun declareKeyExprViaJNI(sessionPtr: Long, keyExpr: String): Long @Throws(ZError::class) - fun undeclareKeyExpr(keyExprPtr: Long) = undeclareKeyExprViaJNI(sessionPtr, keyExprPtr) + fun undeclareKeyExpr(jniKeyExpr: JNIKeyExpr) = undeclareKeyExprViaJNI(sessionPtr, jniKeyExpr.ptr) @Throws(ZError::class) private external fun undeclareKeyExprViaJNI(sessionPtr: Long, keyExprPtr: Long) @Throws(ZError::class) fun get( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, selectorParams: String?, callback: JNIGetCallback, @@ -157,7 +157,7 @@ public class JNISession(public val sessionPtr: Long) { priority: Int, express: Boolean, acceptReplies: Int, - ) = getViaJNI(sessionPtr, keyExprPtr, keyExprString, selectorParams, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encodingId, encodingSchema, congestionControl, priority, express, acceptReplies) + ) = getViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, selectorParams, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encodingId, encodingSchema, congestionControl, priority, express, acceptReplies) @Throws(ZError::class) private external fun getViaJNI( @@ -182,7 +182,7 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun put( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, valuePayload: ByteArray, valueEncoding: Int, @@ -192,7 +192,7 @@ public class JNISession(public val sessionPtr: Long) { express: Boolean, attachmentBytes: ByteArray?, reliability: Int - ) = putViaJNI(sessionPtr, keyExprPtr, keyExprString, valuePayload, valueEncoding, valueEncodingSchema, congestionControl, priority, express, attachmentBytes, reliability) + ) = putViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, valuePayload, valueEncoding, valueEncodingSchema, congestionControl, priority, express, attachmentBytes, reliability) @Throws(ZError::class) private external fun putViaJNI( @@ -211,14 +211,14 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun delete( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, congestionControl: Int, priority: Int, express: Boolean, attachmentBytes: ByteArray?, reliability: Int - ) = deleteViaJNI(sessionPtr, keyExprPtr, keyExprString, congestionControl, priority, express, attachmentBytes, reliability) + ) = deleteViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, congestionControl, priority, express, attachmentBytes, reliability) @Throws(ZError::class) private external fun deleteViaJNI( @@ -249,7 +249,7 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun declareAdvancedSubscriber( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprStr: String, historyConfigEnabled: Boolean, historyDetectLatePublishers: Boolean, @@ -261,7 +261,7 @@ public class JNISession(public val sessionPtr: Long) { subscriberDetection: Boolean, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, - ): JNIAdvancedSubscriber = JNIAdvancedSubscriber(declareAdvancedSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprStr, historyConfigEnabled, historyDetectLatePublishers, historyMaxSamples, historyMaxAgeSeconds, recoveryConfigEnabled, recoveryConfigIsHeartbeat, recoveryQueryPeriodMs, subscriberDetection, callback, onClose)) + ): JNIAdvancedSubscriber = JNIAdvancedSubscriber(declareAdvancedSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprStr, historyConfigEnabled, historyDetectLatePublishers, historyMaxSamples, historyMaxAgeSeconds, recoveryConfigEnabled, recoveryConfigIsHeartbeat, recoveryQueryPeriodMs, subscriberDetection, callback, onClose)) @Throws(ZError::class) private external fun declareAdvancedSubscriberViaJNI( @@ -282,7 +282,7 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun declareAdvancedPublisher( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprStr: String, congestionControl: Int, priority: Int, @@ -298,7 +298,7 @@ public class JNISession(public val sessionPtr: Long) { sampleMissDetectionHeartbeatMs: Long, sampleMissDetectionHeartbeatIsSporadic: Boolean, publisherDetection: Boolean, - ): JNIAdvancedPublisher = JNIAdvancedPublisher(declareAdvancedPublisherViaJNI(sessionPtr, keyExprPtr, keyExprStr, congestionControl, priority, isExpress, reliability, cacheEnabled, cacheMaxSamples, cacheRepliesPriority, cacheRepliesCongestionControl, cacheRepliesIsExpress, sampleMissDetectionEnabled, sampleMissDetectionEnableHeartbeat, sampleMissDetectionHeartbeatMs, sampleMissDetectionHeartbeatIsSporadic, publisherDetection)) + ): JNIAdvancedPublisher = JNIAdvancedPublisher(declareAdvancedPublisherViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprStr, congestionControl, priority, isExpress, reliability, cacheEnabled, cacheMaxSamples, cacheRepliesPriority, cacheRepliesCongestionControl, cacheRepliesIsExpress, sampleMissDetectionEnabled, sampleMissDetectionEnableHeartbeat, sampleMissDetectionHeartbeatMs, sampleMissDetectionHeartbeatIsSporadic, publisherDetection)) @Throws(ZError::class) private external fun declareAdvancedPublisherViaJNI( @@ -322,20 +322,20 @@ public class JNISession(public val sessionPtr: Long) { ): Long @Throws(ZError::class) - fun declareLivelinessToken(keyExprPtr: Long, keyExprString: String): JNILivelinessToken = - JNILivelinessToken(declareLivelinessTokenViaJNI(sessionPtr, keyExprPtr, keyExprString)) + fun declareLivelinessToken(jniKeyExpr: JNIKeyExpr?, keyExprString: String): JNILivelinessToken = + JNILivelinessToken(declareLivelinessTokenViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString)) @Throws(ZError::class) private external fun declareLivelinessTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long @Throws(ZError::class) fun declareLivelinessSubscriber( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, callback: JNISubscriberCallback, history: Boolean, onClose: JNIOnCloseCallback, - ): JNISubscriber = JNISubscriber(declareLivelinessSubscriberViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, history, onClose)) + ): JNISubscriber = JNISubscriber(declareLivelinessSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, history, onClose)) @Throws(ZError::class) private external fun declareLivelinessSubscriberViaJNI( @@ -349,12 +349,12 @@ public class JNISession(public val sessionPtr: Long) { @Throws(ZError::class) fun livelinessGet( - keyExprPtr: Long, + jniKeyExpr: JNIKeyExpr?, keyExprString: String, callback: JNIGetCallback, timeoutMs: Long, onClose: JNIOnCloseCallback, - ) = livelinessGetViaJNI(sessionPtr, keyExprPtr, keyExprString, callback, timeoutMs, onClose) + ) = livelinessGetViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, timeoutMs, onClose) @Throws(ZError::class) private external fun livelinessGetViaJNI( From 7f89a4681fdc12ba343d7e72e90ab72bae19987e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 10:26:49 +0200 Subject: [PATCH 031/126] logger in zenoh-jni-runtime --- .../src/commonMain/kotlin/io/zenoh/Logger.kt | 9 +---- .../kotlin/io/zenoh/jni/JNILogger.kt | 37 +++++++++++++++++++ zenoh-jni/src/logger.rs | 2 +- 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILogger.kt diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt index 53a6caef..1b6101bf 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt @@ -15,6 +15,7 @@ package io.zenoh import io.zenoh.exceptions.ZError +import io.zenoh.jni.JNILogger /** Logger class to redirect the Rust logs from Zenoh to the kotlin environment. */ internal class Logger { @@ -23,12 +24,6 @@ internal class Logger { internal const val LOG_ENV: String = "RUST_LOG" - @Throws(ZError::class) - fun start(filter: String) { - ZenohLoad - startLogsViaJNI(filter) - } - /** * Redirects the rust logs either to logcat for Android systems or to the standard output (for non-android * systems). @@ -36,6 +31,6 @@ internal class Logger { * See https://docs.rs/env_logger/latest/env_logger/index.html for accepted filter format. */ @Throws(ZError::class) - private external fun startLogsViaJNI(filter: String) + fun start(filter: String) = JNILogger.startLogs(filter) } } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILogger.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILogger.kt new file mode 100644 index 00000000..47c0de53 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILogger.kt @@ -0,0 +1,37 @@ +// +// Copyright (c) 2026 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.exceptions.ZError + +/** Adapter for initializing Rust logging through JNI. */ +public object JNILogger { + + init { + ZenohLoad + } + + /** + * Redirects Rust logs either to logcat (Android) or standard output. + * + * See https://docs.rs/env_logger/latest/env_logger/index.html for accepted filter format. + */ + @Throws(ZError::class) + fun startLogs(filter: String) = startLogsViaJNI(filter) + + @Throws(ZError::class) + private external fun startLogsViaJNI(filter: String) +} diff --git a/zenoh-jni/src/logger.rs b/zenoh-jni/src/logger.rs index 785a6cd1..dd64548f 100644 --- a/zenoh-jni/src/logger.rs +++ b/zenoh-jni/src/logger.rs @@ -37,7 +37,7 @@ use crate::{errors::ZResult, throw_exception, zerror}; /// #[no_mangle] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_Logger_00024Companion_startLogsViaJNI( +pub extern "C" fn Java_io_zenoh_jni_JNILogger_startLogsViaJNI( mut env: JNIEnv, _class: JClass, filter: JString, From 8f2bf56e54ada8e8adc70641355fb49d5ade1d3a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 10:31:58 +0200 Subject: [PATCH 032/126] getRoutersZid wrapped --- zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNISession.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index 92f6e22b..e60e6b4f 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -894,7 +894,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) internal fun getRoutersId(): List { - return jniSession?.run { getRoutersZidViaJNI(sessionPtr).map { ZenohId(it) } } ?: throw sessionClosedException + return jniSession?.run { getRoutersZid().map { ZenohId(it) } } ?: throw sessionClosedException } /** Launches the session through the jni session, returning the [Session] on success. */ diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index cebe1b97..841f6306 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -245,7 +245,10 @@ public class JNISession(public val sessionPtr: Long) { private external fun getPeersZidViaJNI(ptr: Long): List @Throws(ZError::class) - external fun getRoutersZidViaJNI(ptr: Long): List + fun getRoutersZid(): List = getRoutersZidViaJNI(sessionPtr) + + @Throws(ZError::class) + private external fun getRoutersZidViaJNI(ptr: Long): List @Throws(ZError::class) fun declareAdvancedSubscriber( From 7ad83e8fa7afea3b91d66fadf53396a552b02e8b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 10:37:04 +0200 Subject: [PATCH 033/126] wrapped remaining ViaJNI functions --- .../src/commonMain/kotlin/io/zenoh/config/ZenohId.kt | 2 +- .../commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt | 2 +- .../src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt | 8 ++++++-- .../src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt | 4 ++-- .../src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt | 10 ++++++++-- .../src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt | 4 +++- 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt index 9787fced..53304190 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt @@ -23,7 +23,7 @@ import kotlin.math.absoluteValue data class ZenohId internal constructor(internal val bytes: ByteArray) { override fun toString(): String { - return JNIZenohId.toStringViaJNI(bytes) + return JNIZenohId.toString(bytes) } override fun equals(other: Any?): Boolean { diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt index 1d039cbe..45e91db9 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt @@ -106,6 +106,6 @@ abstract class ZDeserializer: TypeToken() { */ fun deserialize(zbytes: IntoZBytes): T { @Suppress("UNCHECKED_CAST") - return JNIZBytes.deserializeViaJNI(zbytes.into(), this.type) as T + return JNIZBytes.deserialize(zbytes.into(), 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 index 8ee4a1b7..997814ff 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt @@ -104,6 +104,6 @@ abstract class ZSerializer: TypeToken() { * Serialize [t] into a [ZBytes]. */ fun serialize(t: T): ZBytes { - return JNIZBytes.serializeViaJNI(t as Any, this.type) + return JNIZBytes.serialize(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 index 799b9cac..e9c61964 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt @@ -25,9 +25,13 @@ internal object JNIZBytes { ZenohLoad } + fun serialize(any: Any, type: Type): ZBytes = serializeViaJNI(any, type) + + fun deserialize(zBytes: ZBytes, type: Type): Any = deserializeViaJNI(zBytes, type) + @JvmStatic - external fun serializeViaJNI(any: Any, type: Type): ZBytes + private external fun serializeViaJNI(any: Any, type: Type): ZBytes @JvmStatic - external fun deserializeViaJNI(zBytes: ZBytes, type: Type): Any + private external fun deserializeViaJNI(zBytes: ZBytes, type: Type): Any } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt index 427f643c..bf0461a2 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt @@ -79,7 +79,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn @JvmStatic @Throws(ZError::class) fun tryFrom(keyExpr: String): KeyExpr { - return KeyExpr(JNIKeyExpr.tryFromViaJNI(keyExpr)) + return KeyExpr(JNIKeyExpr.tryFrom(keyExpr)) } /** @@ -95,7 +95,7 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn @JvmStatic @Throws(ZError::class) fun autocanonize(keyExpr: String): KeyExpr { - return KeyExpr(JNIKeyExpr.autocanonizeViaJNI(keyExpr)) + return KeyExpr(JNIKeyExpr.autocanonize(keyExpr)) } } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt index 831e92c6..c13395c2 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt @@ -26,10 +26,16 @@ public class JNIKeyExpr(internal val ptr: Long) { } @Throws(ZError::class) - internal external fun tryFromViaJNI(keyExpr: String): String + fun tryFrom(keyExpr: String): String = tryFromViaJNI(keyExpr) @Throws(ZError::class) - internal external fun autocanonizeViaJNI(keyExpr: String): String + fun autocanonize(keyExpr: String): String = autocanonizeViaJNI(keyExpr) + + @Throws(ZError::class) + private external fun tryFromViaJNI(keyExpr: String): String + + @Throws(ZError::class) + private external fun autocanonizeViaJNI(keyExpr: String): String @Throws(ZError::class) fun intersects(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Boolean = diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt index 9bb02cb3..0eb1f394 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt @@ -23,5 +23,7 @@ public object JNIZenohId { ZenohLoad } - external fun toStringViaJNI(bytes: ByteArray): String + fun toString(bytes: ByteArray): String = toStringViaJNI(bytes) + + private external fun toStringViaJNI(bytes: ByteArray): String } From 44dfafe42e456a628562533fdb7bdb13147fe7f7 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:13:49 +0200 Subject: [PATCH 034/126] ZBytes only on zenoh-java level --- .../kotlin/io/zenoh/ext/ZDeserializer.kt | 2 +- .../kotlin/io/zenoh/ext/ZSerializer.kt | 2 +- .../kotlin/io/zenoh/jni/JNIZBytes.kt | 9 ++++----- zenoh-jni/src/zbytes.rs | 20 +++---------------- 4 files changed, 9 insertions(+), 24 deletions(-) rename {zenoh-java => zenoh-jni-runtime}/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt (72%) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt index 45e91db9..fadbac89 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt @@ -106,6 +106,6 @@ abstract class ZDeserializer: TypeToken() { */ fun deserialize(zbytes: IntoZBytes): T { @Suppress("UNCHECKED_CAST") - return JNIZBytes.deserialize(zbytes.into(), this.type) as T + return JNIZBytes.deserialize(zbytes.into().bytes, 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 index 997814ff..5f47b7eb 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt @@ -104,6 +104,6 @@ abstract class ZSerializer: TypeToken() { * Serialize [t] into a [ZBytes]. */ fun serialize(t: T): ZBytes { - return JNIZBytes.serialize(t as Any, this.type) + return ZBytes(JNIZBytes.serialize(t as Any, this.type)) } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt similarity index 72% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt index e9c61964..224a0711 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt @@ -15,7 +15,6 @@ package io.zenoh.jni import io.zenoh.ZenohLoad -import io.zenoh.bytes.ZBytes import java.lang.reflect.Type @PublishedApi @@ -25,13 +24,13 @@ internal object JNIZBytes { ZenohLoad } - fun serialize(any: Any, type: Type): ZBytes = serializeViaJNI(any, type) + fun serialize(any: Any, type: Type): ByteArray = serializeViaJNI(any, type) - fun deserialize(zBytes: ZBytes, type: Type): Any = deserializeViaJNI(zBytes, type) + fun deserialize(bytes: ByteArray, type: Type): Any = deserializeViaJNI(bytes, type) @JvmStatic - private external fun serializeViaJNI(any: Any, type: Type): ZBytes + private external fun serializeViaJNI(any: Any, type: Type): ByteArray @JvmStatic - private external fun deserializeViaJNI(zBytes: ZBytes, type: Type): Any + private external fun deserializeViaJNI(bytes: ByteArray, type: Type): Any } diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs index 627bb8b0..ae4ca8b8 100644 --- a/zenoh-jni/src/zbytes.rs +++ b/zenoh-jni/src/zbytes.rs @@ -171,15 +171,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeViaJNI( 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()) + Ok(byte_array.as_raw()) }() .unwrap_or_else(|err| { throw_exception!(env, err); @@ -296,17 +288,11 @@ fn serialize( pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeViaJNI( mut env: JNIEnv, _class: JClass, - zbytes: JObject, + bytes: JByteArray, 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 decoded_bytes: Vec = decode_byte_array(&env, bytes)?; let zbytes = ZBytes::from(decoded_bytes); let mut deserializer = ZDeserializer::new(&zbytes); let jtype = decode_token_type(&mut env, jtype)?; From 35f6a053bb98528fee62032d00139e2759ee401b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:32:14 +0200 Subject: [PATCH 035/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 8e4fcdd3..9a85452c 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -1,5 +1,5 @@ // -// Copyright (c) 2026 ZettaScale Technology +// 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 From 52cd138cd14a214b4bb7d20103530f25fec66a77 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:32:28 +0200 Subject: [PATCH 036/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 9a85452c..19274167 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -21,7 +21,8 @@ import java.io.InputStream import java.util.zip.ZipInputStream /** - * Static singleton class to load the Zenoh native library once and only once. + * Static singleton class to load the Zenoh native library once and only once, as well as the logger in function of the + * log level configuration. */ public actual object ZenohLoad { private const val ZENOH_LIB_NAME = "zenoh_jni" From 9b0d18298d942fd1a2e60e06ba06ff958bae5d9b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:32:38 +0200 Subject: [PATCH 037/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- .../src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 19274167..45d7ac8a 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -35,7 +35,13 @@ public actual object ZenohLoad { tryLoadingLibraryFromJarPackage(target).getOrThrow() } } - + /** + * Determine target + * + * Determines the [Target] corresponding to the machine on top of which the native code will run. + * + * @return A result with the target. + */ private fun determineTarget(): Result = runCatching { val osName = System.getProperty("os.name").lowercase() val osArch = System.getProperty("os.arch").lowercase() From f492c2487e75975d5ec7da0c09622db9511b8ab4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:35:45 +0200 Subject: [PATCH 038/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- .../src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 45d7ac8a..fb0e2341 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -82,6 +82,18 @@ public actual object ZenohLoad { return Result.success(target) } + /** + * Unzip library. + * + * The Zenoh libraries are stored within the JAR as compressed ZIP files. + * The location of the zipped files is expected to be under target/target.zip. + * It is expected that the zip file only contains the compressed library. + * + * The uncompressed library will be stored temporarily and deleted on exit. + * + * @param compressedLib Input stream pointing to the compressed library. + * @return A result with the uncompressed library file. + */ private fun unzipLibrary(compressedLib: InputStream): Result = runCatching { val zipInputStream = ZipInputStream(compressedLib) val buffer = ByteArray(1024) From edf32b6cd37ca6aab8a1366eab04d804e004f8b4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:35:52 +0200 Subject: [PATCH 039/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index fb0e2341..8ca14d30 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -35,6 +35,7 @@ public actual object ZenohLoad { tryLoadingLibraryFromJarPackage(target).getOrThrow() } } + /** * Determine target * From dd7a570a9bc9ea3fa03fc33c0be6a0017b95a68d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:35:59 +0200 Subject: [PATCH 040/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 8ca14d30..29afdb80 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -140,6 +140,13 @@ public actual object ZenohLoad { System.load(tempLib.absolutePath) } + /** + * Load library from jar package. + * + * Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar. + * + * @param target + */ private fun tryLoadingLibraryFromJarPackage(target: Target): Result = runCatching { val lib: Result = loadLibraryAsInputStream(target) lib.onSuccess { loadZenohJNI(it) }.onFailure { throw Exception("Unable to load Zenoh JNI: $it") } From f2780ac84bf526cefe8debe83539f099cb67374e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:36:08 +0200 Subject: [PATCH 041/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 29afdb80..c92d686e 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -152,6 +152,13 @@ public actual object ZenohLoad { lib.onSuccess { loadZenohJNI(it) }.onFailure { throw Exception("Unable to load Zenoh JNI: $it") } } + /** + * Load library from jar package. + * + * Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar. + * + * @param target + */ private fun tryLoadingLocalLibrary(): Result = runCatching { val lib = ClassLoader.getSystemClassLoader().findLibraryStream(ZENOH_LIB_NAME) ?: javaClass.classLoader.findLibraryStream( From 51726c8c7f4063c706672ffd7c8497bb69f0aa33 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:39:25 +0200 Subject: [PATCH 042/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index c92d686e..1023b76a 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -153,9 +153,7 @@ public actual object ZenohLoad { } /** - * Load library from jar package. - * - * Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar. + * Try loading local library. * * @param target */ From c4ec1d89c246a76edd03128593fd34f6641c405d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 11:39:32 +0200 Subject: [PATCH 043/126] Update zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt index 1023b76a..aa542844 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -155,7 +155,8 @@ public actual object ZenohLoad { /** * Try loading local library. * - * @param target + * This function aims to load the default library that is usually included when building the zenoh kotlin library + * locally. */ private fun tryLoadingLocalLibrary(): Result = runCatching { val lib = ClassLoader.getSystemClassLoader().findLibraryStream(ZENOH_LIB_NAME) From 6433a7d77c7f4c7651dbb2c9e4842c901e669de2 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 20:54:24 +0200 Subject: [PATCH 044/126] zenoh-jni-runtime api corrected --- .../commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt | 4 ++-- .../commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt | 4 ++-- .../src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt | 4 +--- .../src/commonMain/kotlin/io/zenoh/jni/JNISession.kt | 2 +- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt | 2 +- .../{commonMain => jvmMain}/kotlin/io/zenoh/jni/JNIZBytes.kt | 0 zenoh-jni/src/liveliness.rs | 4 ++-- 7 files changed, 9 insertions(+), 11 deletions(-) rename zenoh-jni-runtime/src/{commonMain => jvmMain}/kotlin/io/zenoh/jni/JNIZBytes.kt (100%) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt index eee392b6..62d822f6 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt @@ -36,8 +36,8 @@ public class JNIAdvancedPublisher(private val ptr: Long) { } @Throws(ZError::class) - fun declareMatchingListener(callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback): Long = - declareMatchingListenerViaJNI(ptr, callback, onClose) + fun declareMatchingListener(callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback): JNIMatchingListener = + JNIMatchingListener(declareMatchingListenerViaJNI(ptr, callback, onClose)) @Throws(ZError::class) fun declareBackgroundMatchingListener(callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback) = diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt index 13df29a8..084b00f9 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedSubscriber.kt @@ -31,7 +31,7 @@ public class JNIAdvancedSubscriber(private val ptr: Long) { history: Boolean, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, - ): Long = declareDetectPublishersSubscriberViaJNI(ptr, history, callback, onClose) + ): JNISubscriber = JNISubscriber(declareDetectPublishersSubscriberViaJNI(ptr, history, callback, onClose)) @Throws(ZError::class) fun declareBackgroundDetectPublishersSubscriber( @@ -44,7 +44,7 @@ public class JNIAdvancedSubscriber(private val ptr: Long) { fun declareSampleMissListener( callback: JNISampleMissListenerCallback, onClose: JNIOnCloseCallback, - ): Long = declareSampleMissListenerViaJNI(ptr, callback, onClose) + ): JNISampleMissListener = JNISampleMissListener(declareSampleMissListenerViaJNI(ptr, callback, onClose)) @Throws(ZError::class) fun declareBackgroundSampleMissListener( diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt index dcf14b6b..1f88d3ad 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt @@ -21,7 +21,5 @@ public class JNILivelinessToken(private val ptr: Long) { undeclareViaJNI(ptr) } - companion object { - private external fun undeclareViaJNI(ptr: Long) - } + private external fun undeclareViaJNI(ptr: Long) } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 841f6306..688b3eb0 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -22,7 +22,7 @@ import io.zenoh.jni.callbacks.JNIQueryableCallback import io.zenoh.jni.callbacks.JNISubscriberCallback /** Adapter class to handle communication with the Zenoh JNI code for a Session. */ -public class JNISession(public val sessionPtr: Long) { +public class JNISession(internal val sessionPtr: Long) { companion object { init { diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt index ad35d536..6460f7ce 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt @@ -14,7 +14,7 @@ package io.zenoh -public enum class Target { +internal enum class Target { WINDOWS_X86_64_MSVC, WINDOWS_AARCH64_MSVC, LINUX_X86_64, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt similarity index 100% rename from zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt rename to zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 37e0925b..8acdc551 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -127,9 +127,9 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessTokenViaJNI( #[no_mangle] #[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNILivelinessToken_00024Companion_undeclareViaJNI( +pub extern "C" fn Java_io_zenoh_jni_JNILivelinessToken_undeclareViaJNI( _env: JNIEnv, - _: JClass, + _: JObject, token_ptr: *const LivelinessToken, ) { unsafe { Arc::from_raw(token_ptr) }; From 0ba4a69c295714324140e3c9ace262d761b78b3b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 17 Apr 2026 21:25:16 +0200 Subject: [PATCH 045/126] JNIZBytes made public --- zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt index 224a0711..f9690eec 100644 --- a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt @@ -17,8 +17,7 @@ package io.zenoh.jni import io.zenoh.ZenohLoad import java.lang.reflect.Type -@PublishedApi -internal object JNIZBytes { +object JNIZBytes { init { ZenohLoad From 8bbfaa559d5ddd34a9f87aa12e77f318e9345dd2 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 18 Apr 2026 02:58:14 +0200 Subject: [PATCH 046/126] moved serializaion out of commonMain for correctness --- zenoh-java/build.gradle.kts | 9 ++++++++- .../kotlin/io/zenoh/ext/ZDeserializer.kt | 0 .../kotlin/io/zenoh/ext/ZSerializer.kt | 0 zenoh-jni-runtime/build.gradle.kts | 7 +++++++ .../kotlin/io/zenoh/jni/JNIZBytes.kt | 0 5 files changed, 15 insertions(+), 1 deletion(-) rename zenoh-java/src/{commonMain => javaMain}/kotlin/io/zenoh/ext/ZDeserializer.kt (100%) rename zenoh-java/src/{commonMain => javaMain}/kotlin/io/zenoh/ext/ZSerializer.kt (100%) rename zenoh-jni-runtime/src/{jvmMain => javaMain}/kotlin/io/zenoh/jni/JNIZBytes.kt (100%) diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index 81ea2335..fe36083a 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -67,19 +67,26 @@ kotlin { implementation("com.google.guava:guava:33.3.1-jre") } } + // javaMain is an intermediate source set between commonMain and both jvmMain/androidMain. + // ZSerializer and ZDeserializer use java.lang.reflect.Type (via Guava's TypeToken) — + // available on JVM and Android (ART), but absent on Kotlin/Native and Kotlin/JS targets. + // Placing them here (rather than commonMain) ensures those targets can be added in the + // future without compilation errors. + val javaMain by creating { dependsOn(commonMain) } val commonTest by getting { dependencies { implementation(kotlin("test")) } } if (androidEnabled) { + val androidMain by getting { dependsOn(javaMain) } val androidUnitTest by getting { dependencies { implementation(kotlin("test-junit")) } } } - val jvmMain by getting {} + val jvmMain by getting { dependsOn(javaMain) } val jvmTest by getting {} } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt b/zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZDeserializer.kt similarity index 100% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt rename to zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZDeserializer.kt diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt b/zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZSerializer.kt similarity index 100% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt rename to zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZSerializer.kt diff --git a/zenoh-jni-runtime/build.gradle.kts b/zenoh-jni-runtime/build.gradle.kts index dc997ce5..3fd84190 100644 --- a/zenoh-jni-runtime/build.gradle.kts +++ b/zenoh-jni-runtime/build.gradle.kts @@ -60,12 +60,18 @@ kotlin { @Suppress("Unused") sourceSets { val commonMain by getting {} + // javaMain is an intermediate source set between commonMain and both jvmMain/androidMain. + // It holds code that uses java.lang.reflect.Type — available on JVM and Android (ART), + // but absent on Kotlin/Native and Kotlin/JS targets. Placing such code here (rather than + // commonMain) ensures those targets can be added in the future without compilation errors. + val javaMain by creating { dependsOn(commonMain) } val commonTest by getting { dependencies { implementation(kotlin("test")) } } if (androidEnabled) { + val androidMain by getting { dependsOn(javaMain) } val androidUnitTest by getting { dependencies { implementation(kotlin("test-junit")) @@ -73,6 +79,7 @@ kotlin { } } val jvmMain by getting { + dependsOn(javaMain) if (isRemotePublication) { resources.srcDir("../jni-libs").include("*/**") } else { diff --git a/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-jni-runtime/src/javaMain/kotlin/io/zenoh/jni/JNIZBytes.kt similarity index 100% rename from zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/jni/JNIZBytes.kt rename to zenoh-jni-runtime/src/javaMain/kotlin/io/zenoh/jni/JNIZBytes.kt From 08cbd450f7d53d25192f5f6f4d3f536c320b93b9 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 18 Apr 2026 03:25:55 +0200 Subject: [PATCH 047/126] gradlew added --- README.md | 7 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 8 + gradlew | 251 +++++++++++++++++++++++ gradlew.bat | 94 +++++++++ 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat diff --git a/README.md b/README.md index affbaa42..1c214a6d 100644 --- a/README.md +++ b/README.md @@ -114,12 +114,17 @@ Basically: - Rust ([Installation guide](https://doc.rust-lang.org/cargo/getting-started/installation.html)) - Kotlin ([Installation guide](https://kotlinlang.org/docs/getting-started.html#backend)) -- Gradle ([Installation guide](https://gradle.org/install/)) and in case of targeting Android you'll also need: - Android SDK ([Installation guide](https://developer.android.com/about/versions/11/setup-sdk)) +## Gradle wrapper + +This repository ships a [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) (`./gradlew` / `gradlew.bat`), so no system-wide Gradle installation is required. The wrapper pins the build to **Gradle 8.12.1** and verifies the distribution checksum before use, ensuring a reproducible and tamper-evident build environment. + +Use `./gradlew` in place of `gradle` for all commands listed below. + ## JVM JVM To publish a library for a JVM project into Maven local, run diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..a4b76b9530d66f5e68d973ea569d8e19de379189 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-Vi3+ZOI=+qP}n zw(+!WcTd~4ZJX1!ZM&y!+uyt=&i!+~d(V%GjH;-NsEEv6nS1TERt|RHh!0>W4+4pp z1-*EzAM~i`+1f(VEHI8So`S`akPfPTfq*`l{Fz`hS%k#JS0cjT2mS0#QLGf=J?1`he3W*;m4)ce8*WFq1sdP=~$5RlH1EdWm|~dCvKOi4*I_96{^95p#B<(n!d?B z=o`0{t+&OMwKcxiBECznJcfH!fL(z3OvmxP#oWd48|mMjpE||zdiTBdWelj8&Qosv zZFp@&UgXuvJw5y=q6*28AtxZzo-UUpkRW%ne+Ylf!V-0+uQXBW=5S1o#6LXNtY5!I z%Rkz#(S8Pjz*P7bqB6L|M#Er{|QLae-Y{KA>`^} z@lPjeX>90X|34S-7}ZVXe{wEei1<{*e8T-Nbj8JmD4iwcE+Hg_zhkPVm#=@b$;)h6 z<<6y`nPa`f3I6`!28d@kdM{uJOgM%`EvlQ5B2bL)Sl=|y@YB3KeOzz=9cUW3clPAU z^sYc}xf9{4Oj?L5MOlYxR{+>w=vJjvbyO5}ptT(o6dR|ygO$)nVCvNGnq(6;bHlBd zl?w-|plD8spjDF03g5ip;W3Z z><0{BCq!Dw;h5~#1BuQilq*TwEu)qy50@+BE4bX28+7erX{BD4H)N+7U`AVEuREE8 z;X?~fyhF-x_sRfHIj~6f(+^@H)D=ngP;mwJjxhQUbUdzk8f94Ab%59-eRIq?ZKrwD z(BFI=)xrUlgu(b|hAysqK<}8bslmNNeD=#JW*}^~Nrswn^xw*nL@Tx!49bfJecV&KC2G4q5a!NSv)06A_5N3Y?veAz;Gv+@U3R% z)~UA8-0LvVE{}8LVDOHzp~2twReqf}ODIyXMM6=W>kL|OHcx9P%+aJGYi_Om)b!xe zF40Vntn0+VP>o<$AtP&JANjXBn7$}C@{+@3I@cqlwR2MdwGhVPxlTIcRVu@Ho-wO` z_~Or~IMG)A_`6-p)KPS@cT9mu9RGA>dVh5wY$NM9-^c@N=hcNaw4ITjm;iWSP^ZX| z)_XpaI61<+La+U&&%2a z0za$)-wZP@mwSELo#3!PGTt$uy0C(nTT@9NX*r3Ctw6J~7A(m#8fE)0RBd`TdKfAT zCf@$MAxjP`O(u9s@c0Fd@|}UQ6qp)O5Q5DPCeE6mSIh|Rj{$cAVIWsA=xPKVKxdhg zLzPZ`3CS+KIO;T}0Ip!fAUaNU>++ZJZRk@I(h<)RsJUhZ&Ru9*!4Ptn;gX^~4E8W^TSR&~3BAZc#HquXn)OW|TJ`CTahk+{qe`5+ixON^zA9IFd8)kc%*!AiLu z>`SFoZ5bW-%7}xZ>gpJcx_hpF$2l+533{gW{a7ce^B9sIdmLrI0)4yivZ^(Vh@-1q zFT!NQK$Iz^xu%|EOK=n>ug;(7J4OnS$;yWmq>A;hsD_0oAbLYhW^1Vdt9>;(JIYjf zdb+&f&D4@4AS?!*XpH>8egQvSVX`36jMd>$+RgI|pEg))^djhGSo&#lhS~9%NuWfX zDDH;3T*GzRT@5=7ibO>N-6_XPBYxno@mD_3I#rDD?iADxX`! zh*v8^i*JEMzyN#bGEBz7;UYXki*Xr(9xXax(_1qVW=Ml)kSuvK$coq2A(5ZGhs_pF z$*w}FbN6+QDseuB9=fdp_MTs)nQf!2SlROQ!gBJBCXD&@-VurqHj0wm@LWX-TDmS= z71M__vAok|@!qgi#H&H%Vg-((ZfxPAL8AI{x|VV!9)ZE}_l>iWk8UPTGHs*?u7RfP z5MC&=c6X;XlUzrz5q?(!eO@~* zoh2I*%J7dF!!_!vXoSIn5o|wj1#_>K*&CIn{qSaRc&iFVxt*^20ngCL;QonIS>I5^ zMw8HXm>W0PGd*}Ko)f|~dDd%;Wu_RWI_d;&2g6R3S63Uzjd7dn%Svu-OKpx*o|N>F zZg=-~qLb~VRLpv`k zWSdfHh@?dp=s_X`{yxOlxE$4iuyS;Z-x!*E6eqmEm*j2bE@=ZI0YZ5%Yj29!5+J$4h{s($nakA`xgbO8w zi=*r}PWz#lTL_DSAu1?f%-2OjD}NHXp4pXOsCW;DS@BC3h-q4_l`<))8WgzkdXg3! zs1WMt32kS2E#L0p_|x+x**TFV=gn`m9BWlzF{b%6j-odf4{7a4y4Uaef@YaeuPhU8 zHBvRqN^;$Jizy+ z=zW{E5<>2gp$pH{M@S*!sJVQU)b*J5*bX4h>5VJve#Q6ga}cQ&iL#=(u+KroWrxa%8&~p{WEUF0il=db;-$=A;&9M{Rq`ouZ5m%BHT6%st%saGsD6)fQgLN}x@d3q>FC;=f%O3Cyg=Ke@Gh`XW za@RajqOE9UB6eE=zhG%|dYS)IW)&y&Id2n7r)6p_)vlRP7NJL(x4UbhlcFXWT8?K=%s7;z?Vjts?y2+r|uk8Wt(DM*73^W%pAkZa1Jd zNoE)8FvQA>Z`eR5Z@Ig6kS5?0h;`Y&OL2D&xnnAUzQz{YSdh0k zB3exx%A2TyI)M*EM6htrxSlep!Kk(P(VP`$p0G~f$smld6W1r_Z+o?=IB@^weq>5VYsYZZR@` z&XJFxd5{|KPZmVOSxc@^%71C@;z}}WhbF9p!%yLj3j%YOlPL5s>7I3vj25 z@xmf=*z%Wb4;Va6SDk9cv|r*lhZ`(y_*M@>q;wrn)oQx%B(2A$9(74>;$zmQ!4fN; z>XurIk-7@wZys<+7XL@0Fhe-f%*=(weaQEdR9Eh6>Kl-EcI({qoZqyzziGwpg-GM#251sK_ z=3|kitS!j%;fpc@oWn65SEL73^N&t>Ix37xgs= zYG%eQDJc|rqHFia0!_sm7`@lvcv)gfy(+KXA@E{3t1DaZ$DijWAcA)E0@X?2ziJ{v z&KOYZ|DdkM{}t+@{@*6ge}m%xfjIxi%qh`=^2Rwz@w0cCvZ&Tc#UmCDbVwABrON^x zEBK43FO@weA8s7zggCOWhMvGGE`baZ62cC)VHyy!5Zbt%ieH+XN|OLbAFPZWyC6)p z4P3%8sq9HdS3=ih^0OOlqTPbKuzQ?lBEI{w^ReUO{V?@`ARsL|S*%yOS=Z%sF)>-y z(LAQdhgAcuF6LQjRYfdbD1g4o%tV4EiK&ElLB&^VZHbrV1K>tHTO{#XTo>)2UMm`2 z^t4s;vnMQgf-njU-RVBRw0P0-m#d-u`(kq7NL&2T)TjI_@iKuPAK-@oH(J8?%(e!0Ir$yG32@CGUPn5w4)+9@8c&pGx z+K3GKESI4*`tYlmMHt@br;jBWTei&(a=iYslc^c#RU3Q&sYp zSG){)V<(g7+8W!Wxeb5zJb4XE{I|&Y4UrFWr%LHkdQ;~XU zgy^dH-Z3lmY+0G~?DrC_S4@=>0oM8Isw%g(id10gWkoz2Q%7W$bFk@mIzTCcIB(K8 zc<5h&ZzCdT=9n-D>&a8vl+=ZF*`uTvQviG_bLde*k>{^)&0o*b05x$MO3gVLUx`xZ z43j+>!u?XV)Yp@MmG%Y`+COH2?nQcMrQ%k~6#O%PeD_WvFO~Kct za4XoCM_X!c5vhRkIdV=xUB3xI2NNStK*8_Zl!cFjOvp-AY=D;5{uXj}GV{LK1~IE2 z|KffUiBaStRr;10R~K2VVtf{TzM7FaPm;Y(zQjILn+tIPSrJh&EMf6evaBKIvi42-WYU9Vhj~3< zZSM-B;E`g_o8_XTM9IzEL=9Lb^SPhe(f(-`Yh=X6O7+6ALXnTcUFpI>ekl6v)ZQeNCg2 z^H|{SKXHU*%nBQ@I3It0m^h+6tvI@FS=MYS$ZpBaG7j#V@P2ZuYySbp@hA# ze(kc;P4i_-_UDP?%<6>%tTRih6VBgScKU^BV6Aoeg6Uh(W^#J^V$Xo^4#Ekp ztqQVK^g9gKMTHvV7nb64UU7p~!B?>Y0oFH5T7#BSW#YfSB@5PtE~#SCCg3p^o=NkMk$<8- z6PT*yIKGrvne7+y3}_!AC8NNeI?iTY(&nakN>>U-zT0wzZf-RuyZk^X9H-DT_*wk= z;&0}6LsGtfVa1q)CEUPlx#(ED@-?H<1_FrHU#z5^P3lEB|qsxEyn%FOpjx z3S?~gvoXy~L(Q{Jh6*i~=f%9kM1>RGjBzQh_SaIDfSU_9!<>*Pm>l)cJD@wlyxpBV z4Fmhc2q=R_wHCEK69<*wG%}mgD1=FHi4h!98B-*vMu4ZGW~%IrYSLGU{^TuseqVgV zLP<%wirIL`VLyJv9XG_p8w@Q4HzNt-o;U@Au{7%Ji;53!7V8Rv0^Lu^Vf*sL>R(;c zQG_ZuFl)Mh-xEIkGu}?_(HwkB2jS;HdPLSxVU&Jxy9*XRG~^HY(f0g8Q}iqnVmgjI zfd=``2&8GsycjR?M%(zMjn;tn9agcq;&rR!Hp z$B*gzHsQ~aXw8c|a(L^LW(|`yGc!qOnV(ZjU_Q-4z1&0;jG&vAKuNG=F|H?@m5^N@ zq{E!1n;)kNTJ>|Hb2ODt-7U~-MOIFo%9I)_@7fnX+eMMNh>)V$IXesJpBn|uo8f~#aOFytCT zf9&%MCLf8mp4kwHTcojWmM3LU=#|{3L>E}SKwOd?%{HogCZ_Z1BSA}P#O(%H$;z7XyJ^sjGX;j5 zrzp>|Ud;*&VAU3x#f{CKwY7Vc{%TKKqmB@oTHA9;>?!nvMA;8+Jh=cambHz#J18x~ zs!dF>$*AnsQ{{82r5Aw&^7eRCdvcgyxH?*DV5(I$qXh^zS>us*I66_MbL8y4d3ULj z{S(ipo+T3Ag!+5`NU2sc+@*m{_X|&p#O-SAqF&g_n7ObB82~$p%fXA5GLHMC+#qqL zdt`sJC&6C2)=juQ_!NeD>U8lDVpAOkW*khf7MCcs$A(wiIl#B9HM%~GtQ^}yBPjT@ z+E=|A!Z?A(rwzZ;T}o6pOVqHzTr*i;Wrc%&36kc@jXq~+w8kVrs;%=IFdACoLAcCAmhFNpbP8;s`zG|HC2Gv?I~w4ITy=g$`0qMQdkijLSOtX6xW%Z9Nw<;M- zMN`c7=$QxN00DiSjbVt9Mi6-pjv*j(_8PyV-il8Q-&TwBwH1gz1uoxs6~uU}PrgWB zIAE_I-a1EqlIaGQNbcp@iI8W1sm9fBBNOk(k&iLBe%MCo#?xI$%ZmGA?=)M9D=0t7 zc)Q0LnI)kCy{`jCGy9lYX%mUsDWwsY`;jE(;Us@gmWPqjmXL+Hu#^;k%eT>{nMtzj zsV`Iy6leTA8-PndszF;N^X@CJrTw5IIm!GPeu)H2#FQitR{1p;MasQVAG3*+=9FYK zw*k!HT(YQorfQj+1*mCV458(T5=fH`um$gS38hw(OqVMyunQ;rW5aPbF##A3fGH6h z@W)i9Uff?qz`YbK4c}JzQpuxuE3pcQO)%xBRZp{zJ^-*|oryTxJ-rR+MXJ)!f=+pp z10H|DdGd2exhi+hftcYbM0_}C0ZI-2vh+$fU1acsB-YXid7O|=9L!3e@$H*6?G*Zp z%qFB(sgl=FcC=E4CYGp4CN>=M8#5r!RU!u+FJVlH6=gI5xHVD&k;Ta*M28BsxfMV~ zLz+@6TxnfLhF@5=yQo^1&S}cmTN@m!7*c6z;}~*!hNBjuE>NLVl2EwN!F+)0$R1S! zR|lF%n!9fkZ@gPW|x|B={V6x3`=jS*$Pu0+5OWf?wnIy>Y1MbbGSncpKO0qE(qO=ts z!~@&!N`10S593pVQu4FzpOh!tvg}p%zCU(aV5=~K#bKi zHdJ1>tQSrhW%KOky;iW+O_n;`l9~omqM%sdxdLtI`TrJzN6BQz+7xOl*rM>xVI2~# z)7FJ^Dc{DC<%~VS?@WXzuOG$YPLC;>#vUJ^MmtbSL`_yXtNKa$Hk+l-c!aC7gn(Cg ze?YPYZ(2Jw{SF6MiO5(%_pTo7j@&DHNW`|lD`~{iH+_eSTS&OC*2WTT*a`?|9w1dh zh1nh@$a}T#WE5$7Od~NvSEU)T(W$p$s5fe^GpG+7fdJ9=enRT9$wEk+ZaB>G3$KQO zgq?-rZZnIv!p#>Ty~}c*Lb_jxJg$eGM*XwHUwuQ|o^}b3^T6Bxx{!?va8aC@-xK*H ztJBFvFfsSWu89%@b^l3-B~O!CXs)I6Y}y#0C0U0R0WG zybjroj$io0j}3%P7zADXOwHwafT#uu*zfM!oD$6aJx7+WL%t-@6^rD_a_M?S^>c;z zMK580bZXo1f*L$CuMeM4Mp!;P@}b~$cd(s5*q~FP+NHSq;nw3fbWyH)i2)-;gQl{S zZO!T}A}fC}vUdskGSq&{`oxt~0i?0xhr6I47_tBc`fqaSrMOzR4>0H^;A zF)hX1nfHs)%Zb-(YGX;=#2R6C{BG;k=?FfP?9{_uFLri~-~AJ;jw({4MU7e*d)?P@ zXX*GkNY9ItFjhwgAIWq7Y!ksbMzfqpG)IrqKx9q{zu%Mdl+{Dis#p9q`02pr1LG8R z@As?eG!>IoROgS!@J*to<27coFc1zpkh?w=)h9CbYe%^Q!Ui46Y*HO0mr% zEff-*$ndMNw}H2a5@BsGj5oFfd!T(F&0$<{GO!Qdd?McKkorh=5{EIjDTHU`So>8V zBA-fqVLb2;u7UhDV1xMI?y>fe3~4urv3%PX)lDw+HYa;HFkaLqi4c~VtCm&Ca+9C~ zge+67hp#R9`+Euq59WhHX&7~RlXn=--m8$iZ~~1C8cv^2(qO#X0?vl91gzUKBeR1J z^p4!!&7)3#@@X&2aF2-)1Ffcc^F8r|RtdL2X%HgN&XU-KH2SLCbpw?J5xJ*!F-ypZ zMG%AJ!Pr&}`LW?E!K~=(NJxuSVTRCGJ$2a*Ao=uUDSys!OFYu!Vs2IT;xQ6EubLIl z+?+nMGeQQhh~??0!s4iQ#gm3!BpMpnY?04kK375e((Uc7B3RMj;wE?BCoQGu=UlZt!EZ1Q*auI)dj3Jj{Ujgt zW5hd~-HWBLI_3HuO) zNrb^XzPsTIb=*a69wAAA3J6AAZZ1VsYbIG}a`=d6?PjM)3EPaDpW2YP$|GrBX{q*! z$KBHNif)OKMBCFP5>!1d=DK>8u+Upm-{hj5o|Wn$vh1&K!lVfDB&47lw$tJ?d5|=B z^(_9=(1T3Fte)z^>|3**n}mIX;mMN5v2F#l(q*CvU{Ga`@VMp#%rQkDBy7kYbmb-q z<5!4iuB#Q_lLZ8}h|hPODI^U6`gzLJre9u3k3c#%86IKI*^H-@I48Bi*@avYm4v!n0+v zWu{M{&F8#p9cx+gF0yTB_<2QUrjMPo9*7^-uP#~gGW~y3nfPAoV%amgr>PSyVAd@l)}8#X zR5zV6t*uKJZL}?NYvPVK6J0v4iVpwiN|>+t3aYiZSp;m0!(1`bHO}TEtWR1tY%BPB z(W!0DmXbZAsT$iC13p4f>u*ZAy@JoLAkJhzFf1#4;#1deO8#8d&89}en&z!W&A3++^1(;>0SB1*54d@y&9Pn;^IAf3GiXbfT`_>{R+Xv; zQvgL>+0#8-laO!j#-WB~(I>l0NCMt_;@Gp_f0#^c)t?&#Xh1-7RR0@zPyBz!U#0Av zT?}n({(p?p7!4S2ZBw)#KdCG)uPnZe+U|0{BW!m)9 zi_9$F?m<`2!`JNFv+w8MK_K)qJ^aO@7-Ig>cM4-r0bi=>?B_2mFNJ}aE3<+QCzRr*NA!QjHw# z`1OsvcoD0?%jq{*7b!l|L1+Tw0TTAM4XMq7*ntc-Ived>Sj_ZtS|uVdpfg1_I9knY z2{GM_j5sDC7(W&}#s{jqbybqJWyn?{PW*&cQIU|*v8YGOKKlGl@?c#TCnmnAkAzV- zmK={|1G90zz=YUvC}+fMqts0d4vgA%t6Jhjv?d;(Z}(Ep8fTZfHA9``fdUHkA+z3+ zhh{ohP%Bj?T~{i0sYCQ}uC#5BwN`skI7`|c%kqkyWIQ;!ysvA8H`b-t()n6>GJj6xlYDu~8qX{AFo$Cm3d|XFL=4uvc?Keb zzb0ZmMoXca6Mob>JqkNuoP>B2Z>D`Q(TvrG6m`j}-1rGP!g|qoL=$FVQYxJQjFn33lODt3Wb1j8VR zlR++vIT6^DtYxAv_hxupbLLN3e0%A%a+hWTKDV3!Fjr^cWJ{scsAdfhpI)`Bms^M6 zQG$waKgFr=c|p9Piug=fcJvZ1ThMnNhQvBAg-8~b1?6wL*WyqXhtj^g(Ke}mEfZVM zJuLNTUVh#WsE*a6uqiz`b#9ZYg3+2%=C(6AvZGc=u&<6??!slB1a9K)=VL zY9EL^mfyKnD zSJyYBc_>G;5RRnrNgzJz#Rkn3S1`mZgO`(r5;Hw6MveN(URf_XS-r58Cn80K)ArH4 z#Rrd~LG1W&@ttw85cjp8xV&>$b%nSXH_*W}7Ch2pg$$c0BdEo-HWRTZcxngIBJad> z;C>b{jIXjb_9Jis?NZJsdm^EG}e*pR&DAy0EaSGi3XWTa(>C%tz1n$u?5Fb z1qtl?;_yjYo)(gB^iQq?=jusF%kywm?CJP~zEHi0NbZ);$(H$w(Hy@{i>$wcVRD_X|w-~(0Z9BJyh zhNh;+eQ9BEIs;tPz%jSVnfCP!3L&9YtEP;svoj_bNzeGSQIAjd zBss@A;)R^WAu-37RQrM%{DfBNRx>v!G31Z}8-El9IOJlb_MSoMu2}GDYycNaf>uny z+8xykD-7ONCM!APry_Lw6-yT>5!tR}W;W`C)1>pxSs5o1z#j7%m=&=7O4hz+Lsqm` z*>{+xsabZPr&X=}G@obTb{nPTkccJX8w3CG7X+1+t{JcMabv~UNv+G?txRqXib~c^Mo}`q{$`;EBNJ;#F*{gvS12kV?AZ%O0SFB$^ zn+}!HbmEj}w{Vq(G)OGAzH}R~kS^;(-s&=ectz8vN!_)Yl$$U@HNTI-pV`LSj7Opu zTZ5zZ)-S_{GcEQPIQXLQ#oMS`HPu{`SQiAZ)m1at*Hy%3xma|>o`h%E%8BEbi9p0r zVjcsh<{NBKQ4eKlXU|}@XJ#@uQw*$4BxKn6#W~I4T<^f99~(=}a`&3(ur8R9t+|AQ zWkQx7l}wa48-jO@ft2h+7qn%SJtL%~890FG0s5g*kNbL3I&@brh&f6)TlM`K^(bhr zJWM6N6x3flOw$@|C@kPi7yP&SP?bzP-E|HSXQXG>7gk|R9BTj`e=4de9C6+H7H7n# z#GJeVs1mtHhLDmVO?LkYRQc`DVOJ_vdl8VUihO-j#t=0T3%Fc1f9F73ufJz*adn*p zc%&vi(4NqHu^R>sAT_0EDjVR8bc%wTz#$;%NU-kbDyL_dg0%TFafZwZ?5KZpcuaO54Z9hX zD$u>q!-9`U6-D`E#`W~fIfiIF5_m6{fvM)b1NG3xf4Auw;Go~Fu7cth#DlUn{@~yu z=B;RT*dp?bO}o%4x7k9v{r=Y@^YQ^UUm(Qmliw8brO^=NP+UOohLYiaEB3^DB56&V zK?4jV61B|1Uj_5fBKW;8LdwOFZKWp)g{B%7g1~DgO&N& z#lisxf?R~Z@?3E$Mms$$JK8oe@X`5m98V*aV6Ua}8Xs2#A!{x?IP|N(%nxsH?^c{& z@vY&R1QmQs83BW28qAmJfS7MYi=h(YK??@EhjL-t*5W!p z^gYX!Q6-vBqcv~ruw@oMaU&qp0Fb(dbVzm5xJN%0o_^@fWq$oa3X?9s%+b)x4w-q5Koe(@j6Ez7V@~NRFvd zfBH~)U5!ix3isg`6be__wBJp=1@yfsCMw1C@y+9WYD9_C%{Q~7^0AF2KFryfLlUP# zwrtJEcH)jm48!6tUcxiurAMaiD04C&tPe6DI0#aoqz#Bt0_7_*X*TsF7u*zv(iEfA z;$@?XVu~oX#1YXtceQL{dSneL&*nDug^OW$DSLF0M1Im|sSX8R26&)<0Fbh^*l6!5wfSu8MpMoh=2l z^^0Sr$UpZp*9oqa23fcCfm7`ya2<4wzJ`Axt7e4jJrRFVf?nY~2&tRL* zd;6_njcz01c>$IvN=?K}9ie%Z(BO@JG2J}fT#BJQ+f5LFSgup7i!xWRKw6)iITjZU z%l6hPZia>R!`aZjwCp}I zg)%20;}f+&@t;(%5;RHL>K_&7MH^S+7<|(SZH!u zznW|jz$uA`P9@ZWtJgv$EFp>)K&Gt+4C6#*khZQXS*S~6N%JDT$r`aJDs9|uXWdbg zBwho$phWx}x!qy8&}6y5Vr$G{yGSE*r$^r{}pw zVTZKvikRZ`J_IJrjc=X1uw?estdwm&bEahku&D04HD+0Bm~q#YGS6gp!KLf$A{%Qd z&&yX@Hp>~(wU{|(#U&Bf92+1i&Q*-S+=y=3pSZy$#8Uc$#7oiJUuO{cE6=tsPhwPe| zxQpK>`Dbka`V)$}e6_OXKLB%i76~4N*zA?X+PrhH<&)}prET;kel24kW%+9))G^JI zsq7L{P}^#QsZViX%KgxBvEugr>ZmFqe^oAg?{EI=&_O#e)F3V#rc z8$4}0Zr19qd3tE4#$3_f=Bbx9oV6VO!d3(R===i-7p=Vj`520w0D3W6lQfY48}!D* z&)lZMG;~er2qBoI2gsX+Ts-hnpS~NYRDtPd^FPzn!^&yxRy#CSz(b&E*tL|jIkq|l zf%>)7Dtu>jCf`-7R#*GhGn4FkYf;B$+9IxmqH|lf6$4irg{0ept__%)V*R_OK=T06 zyT_m-o@Kp6U{l5h>W1hGq*X#8*y@<;vsOFqEjTQXFEotR+{3}ODDnj;o0@!bB5x=N z394FojuGOtVKBlVRLtHp%EJv_G5q=AgF)SKyRN5=cGBjDWv4LDn$IL`*=~J7u&Dy5 zrMc83y+w^F&{?X(KOOAl-sWZDb{9X9#jrQtmrEXD?;h-}SYT7yM(X_6qksM=K_a;Z z3u0qT0TtaNvDER_8x*rxXw&C^|h{P1qxK|@pS7vdlZ#P z7PdB7MmC2}%sdzAxt>;WM1s0??`1983O4nFK|hVAbHcZ3x{PzytQLkCVk7hA!Lo` zEJH?4qw|}WH{dc4z%aB=0XqsFW?^p=X}4xnCJXK%c#ItOSjdSO`UXJyuc8bh^Cf}8 z@Ht|vXd^6{Fgai8*tmyRGmD_s_nv~r^Fy7j`Bu`6=G)5H$i7Q7lvQnmea&TGvJp9a|qOrUymZ$6G|Ly z#zOCg++$3iB$!6!>215A4!iryregKuUT344X)jQb3|9qY>c0LO{6Vby05n~VFzd?q zgGZv&FGlkiH*`fTurp>B8v&nSxNz)=5IF$=@rgND4d`!AaaX;_lK~)-U8la_Wa8i?NJC@BURO*sUW)E9oyv3RG^YGfN%BmxzjlT)bp*$<| zX3tt?EAy<&K+bhIuMs-g#=d1}N_?isY)6Ay$mDOKRh z4v1asEGWoAp=srraLW^h&_Uw|6O+r;wns=uwYm=JN4Q!quD8SQRSeEcGh|Eb5Jg8m zOT}u;N|x@aq)=&;wufCc^#)5U^VcZw;d_wwaoh9$p@Xrc{DD6GZUqZ ziC6OT^zSq@-lhbgR8B+e;7_Giv;DK5gn^$bs<6~SUadiosfewWDJu`XsBfOd1|p=q zE>m=zF}!lObA%ePey~gqU8S6h-^J2Y?>7)L2+%8kV}Gp=h`Xm_}rlm)SyUS=`=S7msKu zC|T!gPiI1rWGb1z$Md?0YJQ;%>uPLOXf1Z>N~`~JHJ!^@D5kSXQ4ugnFZ>^`zH8CAiZmp z6Ms|#2gcGsQ{{u7+Nb9sA?U>(0e$5V1|WVwY`Kn)rsnnZ4=1u=7u!4WexZD^IQ1Jk zfF#NLe>W$3m&C^ULjdw+5|)-BSHwpegdyt9NYC{3@QtMfd8GrIWDu`gd0nv-3LpGCh@wgBaG z176tikL!_NXM+Bv#7q^cyn9$XSeZR6#!B4JE@GVH zoobHZN_*RF#@_SVYKkQ_igme-Y5U}cV(hkR#k1c{bQNMji zU7aE`?dHyx=1`kOYZo_8U7?3-7vHOp`Qe%Z*i+FX!s?6huNp0iCEW-Z7E&jRWmUW_ z67j>)Ew!yq)hhG4o?^z}HWH-e=es#xJUhDRc4B51M4~E-l5VZ!&zQq`gWe`?}#b~7w1LH4Xa-UCT5LXkXQWheBa2YJYbyQ zl1pXR%b(KCXMO0OsXgl0P0Og<{(@&z1aokU-Pq`eQq*JYgt8xdFQ6S z6Z3IFSua8W&M#`~*L#r>Jfd6*BzJ?JFdBR#bDv$_0N!_5vnmo@!>vULcDm`MFU823 zpG9pqjqz^FE5zMDoGqhs5OMmC{Y3iVcl>F}5Rs24Y5B^mYQ;1T&ks@pIApHOdrzXF z-SdX}Hf{X;TaSxG_T$0~#RhqKISGKNK47}0*x&nRIPtmdwxc&QT3$8&!3fWu1eZ_P zJveQj^hJL#Sn!*4k`3}(d(aasl&7G0j0-*_2xtAnoX1@9+h zO#c>YQg60Z;o{Bi=3i7S`Ic+ZE>K{(u|#)9y}q*j8uKQ1^>+(BI}m%1v3$=4ojGBc zm+o1*!T&b}-lVvZqIUBc8V}QyFEgm#oyIuC{8WqUNV{Toz`oxhYpP!_p2oHHh5P@iB*NVo~2=GQm+8Yrkm2Xjc_VyHg1c0>+o~@>*Qzo zHVBJS>$$}$_4EniTI;b1WShX<5-p#TPB&!;lP!lBVBbLOOxh6FuYloD%m;n{r|;MU3!q4AVkua~fieeWu2 zQAQ$ue(IklX6+V;F1vCu-&V?I3d42FgWgsb_e^29ol}HYft?{SLf>DrmOp9o!t>I^ zY7fBCk+E8n_|apgM|-;^=#B?6RnFKlN`oR)`e$+;D=yO-(U^jV;rft^G_zl`n7qnM zL z*-Y4Phq+ZI1$j$F-f;`CD#|`-T~OM5Q>x}a>B~Gb3-+9i>Lfr|Ca6S^8g*{*?_5!x zH_N!SoRP=gX1?)q%>QTY!r77e2j9W(I!uAz{T`NdNmPBBUzi2{`XMB^zJGGwFWeA9 z{fk33#*9SO0)DjROug+(M)I-pKA!CX;IY(#gE!UxXVsa)X!UftIN98{pt#4MJHOhY zM$_l}-TJlxY?LS6Nuz1T<44m<4i^8k@D$zuCPrkmz@sdv+{ciyFJG2Zwy&%c7;atIeTdh!a(R^QXnu1Oq1b42*OQFWnyQ zWeQrdvP|w_idy53Wa<{QH^lFmEd+VlJkyiC>6B#s)F;w-{c;aKIm;Kp50HnA-o3lY z9B~F$gJ@yYE#g#X&3ADx&tO+P_@mnQTz9gv30_sTsaGXkfNYXY{$(>*PEN3QL>I!k zp)KibPhrfX3%Z$H6SY`rXGYS~143wZrG2;=FLj50+VM6soI~up_>fU(2Wl@{BRsMi zO%sL3x?2l1cXTF)k&moNsHfQrQ+wu(gBt{sk#CU=UhrvJIncy@tJX5klLjgMn>~h= zg|FR&;@eh|C7`>s_9c~0-{IAPV){l|Ts`i=)AW;d9&KPc3fMeoTS%8@V~D8*h;&(^>yjT84MM}=%#LS7shLAuuj(0VAYoozhWjq z4LEr?wUe2^WGwdTIgWBkDUJa>YP@5d9^Rs$kCXmMRxuF*YMVrn?0NFyPl}>`&dqZb z<5eqR=ZG3>n2{6v6BvJ`YBZeeTtB88TAY(x0a58EWyuf>+^|x8Qa6wA|1Nb_p|nA zWWa}|z8a)--Wj`LqyFk_a3gN2>5{Rl_wbW?#by7&i*^hRknK%jwIH6=dQ8*-_{*x0j^DUfMX0`|K@6C<|1cgZ~D(e5vBFFm;HTZF(!vT8=T$K+|F)x3kqzBV4-=p1V(lzi(s7jdu0>LD#N=$Lk#3HkG!a zIF<7>%B7sRNzJ66KrFV76J<2bdYhxll0y2^_rdG=I%AgW4~)1Nvz=$1UkE^J%BxLo z+lUci`UcU062os*=`-j4IfSQA{w@y|3}Vk?i;&SSdh8n+$iHA#%ERL{;EpXl6u&8@ zzg}?hkEOUOJt?ZL=pWZFJ19mI1@P=$U5*Im1e_8Z${JsM>Ov?nh8Z zP5QvI!{Jy@&BP48%P2{Jr_VgzW;P@7)M9n|lDT|Ep#}7C$&ud&6>C^5ZiwKIg2McPU(4jhM!BD@@L(Gd*Nu$ji(ljZ<{FIeW_1Mmf;76{LU z-ywN~=uNN)Xi6$<12A9y)K%X|(W0p|&>>4OXB?IiYr||WKDOJPxiSe01NSV-h24^L z_>m$;|C+q!Mj**-qQ$L-*++en(g|hw;M!^%_h-iDjFHLo-n3JpB;p?+o2;`*jpvJU zLY^lt)Un4joij^^)O(CKs@7E%*!w>!HA4Q?0}oBJ7Nr8NQ7QmY^4~jvf0-`%waOLn zdNjAPaC0_7c|RVhw)+71NWjRi!y>C+Bl;Z`NiL^zn2*0kmj5gyhCLCxts*cWCdRI| zjsd=sT5BVJc^$GxP~YF$-U{-?kW6r@^vHXB%{CqYzU@1>dzf#3SYedJG-Rm6^RB7s zGM5PR(yKPKR)>?~vpUIeTP7A1sc8-knnJk*9)3t^e%izbdm>Y=W{$wm(cy1RB-19i za#828DMBY+ps#7Y8^6t)=Ea@%Nkt)O6JCx|ybC;Ap}Z@Zw~*}3P>MZLPb4Enxz9Wf zssobT^(R@KuShj8>@!1M7tm|2%-pYYDxz-5`rCbaTCG5{;Uxm z*g=+H1X8{NUvFGzz~wXa%Eo};I;~`37*WrRU&K0dPSB$yk(Z*@K&+mFal^?c zurbqB-+|Kb5|sznT;?Pj!+kgFY1#Dr;_%A(GIQC{3ct|{*Bji%FNa6c-thbpBkA;U zURV!Dr&X{0J}iht#-Qp2=xzuh(fM>zRoiGrYl5ttw2#r34gC41CCOC31m~^UPTK@s z6;A@)7O7_%C)>bnAXerYuAHdE93>j2N}H${zEc6&SbZ|-fiG*-qtGuy-qDelH(|u$ zorf8_T6Zqe#Ub!+e3oSyrskt_HyW_^5lrWt#30l)tHk|j$@YyEkXUOV;6B51L;M@=NIWZXU;GrAa(LGxO%|im%7F<-6N;en0Cr zLH>l*y?pMwt`1*cH~LdBPFY_l;~`N!Clyfr;7w<^X;&(ZiVdF1S5e(+Q%60zgh)s4 zn2yj$+mE=miVERP(g8}G4<85^-5f@qxh2ec?n+$A_`?qN=iyT1?U@t?V6DM~BIlBB z>u~eXm-aE>R0sQy!-I4xtCNi!!qh?R1!kKf6BoH2GG{L4%PAz0{Sh6xpuyI%*~u)s z%rLuFl)uQUCBQAtMyN;%)zFMx4loh7uTfKeB2Xif`lN?2gq6NhWhfz0u5WP9J>=V2 zo{mLtSy&BA!mSzs&CrKWq^y40JF5a&GSXIi2= z{EYb59J4}VwikL4P=>+mc6{($FNE@e=VUwG+KV21;<@lrN`mnz5jYGASyvz7BOG_6(p^eTxD-4O#lROgon;R35=|nj#eHIfJBYPWG>H>`dHKCDZ3`R{-?HO0mE~(5_WYcFmp8sU?wr*UkAQiNDGc6T zA%}GOLXlOWqL?WwfHO8MB#8M8*~Y*gz;1rWWoVSXP&IbKxbQ8+s%4Jnt?kDsq7btI zCDr0PZ)b;B%!lu&CT#RJzm{l{2fq|BcY85`w~3LSK<><@(2EdzFLt9Y_`;WXL6x`0 zDoQ?=?I@Hbr;*VVll1Gmd8*%tiXggMK81a+T(5Gx6;eNb8=uYn z5BG-0g>pP21NPn>$ntBh>`*})Fl|38oC^9Qz>~MAazH%3Q~Qb!ALMf$srexgPZ2@&c~+hxRi1;}+)-06)!#Mq<6GhP z-Q?qmgo${aFBApb5p}$1OJKTClfi8%PpnczyVKkoHw7Ml9e7ikrF0d~UB}i3vizos zXW4DN$SiEV9{faLt5bHy2a>33K%7Td-n5C*N;f&ZqAg#2hIqEb(y<&f4u5BWJ>2^4 z414GosL=Aom#m&=x_v<0-fp1r%oVJ{T-(xnomNJ(Dryv zh?vj+%=II_nV+@NR+(!fZZVM&(W6{6%9cm+o+Z6}KqzLw{(>E86uA1`_K$HqINlb1 zKelh3-jr2I9V?ych`{hta9wQ2c9=MM`2cC{m6^MhlL2{DLv7C^j z$xXBCnDl_;l|bPGMX@*tV)B!c|4oZyftUlP*?$YU9C_eAsuVHJ58?)zpbr30P*C`T z7y#ao`uE-SOG(Pi+`$=e^mle~)pRrdwL5)N;o{gpW21of(QE#U6w%*C~`v-z0QqBML!!5EeYA5IQB0 z^l01c;L6E(iytN!LhL}wfwP7W9PNAkb+)Cst?qg#$n;z41O4&v+8-zPs+XNb-q zIeeBCh#ivnFLUCwfS;p{LC0O7tm+Sf9Jn)~b%uwP{%69;QC)Ok0t%*a5M+=;y8j=v z#!*pp$9@!x;UMIs4~hP#pnfVc!%-D<+wsG@R2+J&%73lK|2G!EQC)O05TCV=&3g)C!lT=czLpZ@Sa%TYuoE?v8T8`V;e$#Zf2_Nj6nvBgh1)2 GZ~q4|mN%#X literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..63532c5e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..057afac5 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..640d6868 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 743e696fb5f2da34c0017d511ffc4f06e637e093 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 18 Apr 2026 03:29:32 +0200 Subject: [PATCH 048/126] use gradlew in ci --- .github/workflows/ci.yml | 4 +--- .github/workflows/publish-android.yml | 6 ------ .github/workflows/publish-dokka.yml | 8 +------- .github/workflows/publish-jvm.yml | 6 ------ 4 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39e3832b..dd30cfe1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,11 +55,9 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: 8.12.1 - name: Gradle Test - run: gradle jvmTest --info + run: ./gradlew jvmTest --info markdown_lint: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish-android.yml index 77f9f611..380e5444 100644 --- a/.github/workflows/publish-android.yml +++ b/.github/workflows/publish-android.yml @@ -58,12 +58,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: 8.12.1 - - - name: Gradle Wrapper - run: | - gradle wrapper - name: Set pub mode env var # Note: This step is intended to allow publishing snapshot packages. diff --git a/.github/workflows/publish-dokka.yml b/.github/workflows/publish-dokka.yml index b5fe106d..4964557b 100644 --- a/.github/workflows/publish-dokka.yml +++ b/.github/workflows/publish-dokka.yml @@ -27,15 +27,9 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: 8.12.1 - - - name: Gradle Wrapper - run: | - gradle wrapper - name: Build doc - run: gradle dokkaGenerate + run: ./gradlew dokkaGenerate - name: Deploy doc if: ${{ inputs.live-run || false }} diff --git a/.github/workflows/publish-jvm.yml b/.github/workflows/publish-jvm.yml index 7caf1b48..ce0ba5e6 100644 --- a/.github/workflows/publish-jvm.yml +++ b/.github/workflows/publish-jvm.yml @@ -163,12 +163,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: 8.12.1 - - - name: Gradle Wrapper - run: | - gradle wrapper - name: Set pub mode env var # Note: This step is intended to allow publishing snapshot packages. From 4596466bf86301e7136880aebb4c16cae94c3a00 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 18 Apr 2026 14:59:27 +0200 Subject: [PATCH 049/126] build naming conflict fix --- zenoh-java/build.gradle.kts | 10 ++++++---- .../kotlin/io/zenoh/ext/ZDeserializer.kt | 0 .../kotlin/io/zenoh/ext/ZSerializer.kt | 0 zenoh-jni-runtime/build.gradle.kts | 10 ++++++---- .../kotlin/io/zenoh/jni/JNIZBytes.kt | 0 5 files changed, 12 insertions(+), 8 deletions(-) rename zenoh-java/src/{javaMain => jvmAndAndroidMain}/kotlin/io/zenoh/ext/ZDeserializer.kt (100%) rename zenoh-java/src/{javaMain => jvmAndAndroidMain}/kotlin/io/zenoh/ext/ZSerializer.kt (100%) rename zenoh-jni-runtime/src/{javaMain => jvmAndAndroidMain}/kotlin/io/zenoh/jni/JNIZBytes.kt (100%) diff --git a/zenoh-java/build.gradle.kts b/zenoh-java/build.gradle.kts index fe36083a..dfb36284 100644 --- a/zenoh-java/build.gradle.kts +++ b/zenoh-java/build.gradle.kts @@ -67,26 +67,28 @@ kotlin { implementation("com.google.guava:guava:33.3.1-jre") } } - // javaMain is an intermediate source set between commonMain and both jvmMain/androidMain. + // jvmAndAndroidMain is an intermediate source set between commonMain and both jvmMain/androidMain. // ZSerializer and ZDeserializer use java.lang.reflect.Type (via Guava's TypeToken) — // available on JVM and Android (ART), but absent on Kotlin/Native and Kotlin/JS targets. // Placing them here (rather than commonMain) ensures those targets can be added in the // future without compilation errors. - val javaMain by creating { dependsOn(commonMain) } + // Note: named jvmAndAndroidMain (not javaMain) to avoid conflicting with the DokkaSourceSetSpec + // that Dokka auto-registers for the Java compilation created by withJava(). + val jvmAndAndroidMain by creating { dependsOn(commonMain) } val commonTest by getting { dependencies { implementation(kotlin("test")) } } if (androidEnabled) { - val androidMain by getting { dependsOn(javaMain) } + val androidMain by getting { dependsOn(jvmAndAndroidMain) } val androidUnitTest by getting { dependencies { implementation(kotlin("test-junit")) } } } - val jvmMain by getting { dependsOn(javaMain) } + val jvmMain by getting { dependsOn(jvmAndAndroidMain) } val jvmTest by getting {} } diff --git a/zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZDeserializer.kt b/zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZDeserializer.kt similarity index 100% rename from zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZDeserializer.kt rename to zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZDeserializer.kt diff --git a/zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZSerializer.kt b/zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZSerializer.kt similarity index 100% rename from zenoh-java/src/javaMain/kotlin/io/zenoh/ext/ZSerializer.kt rename to zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZSerializer.kt diff --git a/zenoh-jni-runtime/build.gradle.kts b/zenoh-jni-runtime/build.gradle.kts index 3fd84190..7a4c7b15 100644 --- a/zenoh-jni-runtime/build.gradle.kts +++ b/zenoh-jni-runtime/build.gradle.kts @@ -60,18 +60,20 @@ kotlin { @Suppress("Unused") sourceSets { val commonMain by getting {} - // javaMain is an intermediate source set between commonMain and both jvmMain/androidMain. + // jvmAndAndroidMain is an intermediate source set between commonMain and both jvmMain/androidMain. // It holds code that uses java.lang.reflect.Type — available on JVM and Android (ART), // but absent on Kotlin/Native and Kotlin/JS targets. Placing such code here (rather than // commonMain) ensures those targets can be added in the future without compilation errors. - val javaMain by creating { dependsOn(commonMain) } + // Note: named jvmAndAndroidMain (not javaMain) to avoid conflicting with the DokkaSourceSetSpec + // that Dokka auto-registers for the Java compilation created by withJava(). + val jvmAndAndroidMain by creating { dependsOn(commonMain) } val commonTest by getting { dependencies { implementation(kotlin("test")) } } if (androidEnabled) { - val androidMain by getting { dependsOn(javaMain) } + val androidMain by getting { dependsOn(jvmAndAndroidMain) } val androidUnitTest by getting { dependencies { implementation(kotlin("test-junit")) @@ -79,7 +81,7 @@ kotlin { } } val jvmMain by getting { - dependsOn(javaMain) + dependsOn(jvmAndAndroidMain) if (isRemotePublication) { resources.srcDir("../jni-libs").include("*/**") } else { diff --git a/zenoh-jni-runtime/src/javaMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytes.kt similarity index 100% rename from zenoh-jni-runtime/src/javaMain/kotlin/io/zenoh/jni/JNIZBytes.kt rename to zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytes.kt From 0000b7aac4fdca6a1ae849984f263bb1592ad790 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 18 Apr 2026 18:31:35 +0200 Subject: [PATCH 050/126] kotlin serialization --- .../kotlin/io/zenoh/jni/JNIZBytesKotlin.kt | 49 ++ zenoh-jni/src/lib.rs | 2 + zenoh-jni/src/zbytes_kotlin.rs | 540 ++++++++++++++++++ 3 files changed, 591 insertions(+) create mode 100644 zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytesKotlin.kt create mode 100644 zenoh-jni/src/zbytes_kotlin.rs diff --git a/zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytesKotlin.kt b/zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytesKotlin.kt new file mode 100644 index 00000000..3570b9e6 --- /dev/null +++ b/zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytesKotlin.kt @@ -0,0 +1,49 @@ +// +// Copyright (c) 2026 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 kotlin.reflect.KType + +/** + * JNI bridge for Kotlin-type-aware serialization/deserialization. + * + * Uses [KType] (obtained via `typeOf()` with reified generics) instead of + * [java.lang.reflect.Type], making this bridge usable from commonMain Kotlin + * code and compatible with Kotlin-specific types (unsigned integers, Pair, Triple). + * + * Supported types: + * - Primitives: Boolean, Byte, Short, Int, Long, Float, Double + * - Unsigned (Kotlin-only): UByte, UShort, UInt, ULong + * - Text/Binary: String, ByteArray + * - Collections: List, Map (recursive) + * - Tuples: Pair, Triple + */ +object JNIZBytesKotlin { + + init { + ZenohLoad + } + + fun serialize(any: Any, kType: KType): ByteArray = serializeViaJNI(any, kType) + + fun deserialize(bytes: ByteArray, kType: KType): Any = deserializeViaJNI(bytes, kType) + + @JvmStatic + private external fun serializeViaJNI(any: Any, kType: KType): ByteArray + + @JvmStatic + private external fun deserializeViaJNI(bytes: ByteArray, kType: KType): Any +} diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index 93765edb..f8603617 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -31,6 +31,8 @@ mod subscriber; mod utils; #[cfg(feature = "zenoh-ext")] mod zbytes; +#[cfg(feature = "zenoh-ext")] +mod zbytes_kotlin; mod zenoh_id; // Test should be runned with `cargo test --no-default-features` diff --git a/zenoh-jni/src/zbytes_kotlin.rs b/zenoh-jni/src/zbytes_kotlin.rs new file mode 100644 index 00000000..6d1cc44c --- /dev/null +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -0,0 +1,540 @@ +// +// Copyright (c) 2026 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, +// + +//! JNI bridge for Kotlin-type-aware (de)serialization. +//! +//! Mirrors [zbytes.rs] but uses `kotlin.reflect.KType` instead of +//! `java.lang.reflect.Type`, enabling support for Kotlin-specific types: +//! unsigned integers (UByte, UShort, UInt, ULong) and tuples (Pair, Triple). +//! +//! Entry points are named for class `io.zenoh.jni.JNIZBytesKotlin` to avoid +//! JNI symbol conflicts with the existing `JNIZBytes` class (same method name, +//! different receiver type — JNI cannot overload on parameter type alone). + +use jni::{ + objects::{JByteArray, JClass, JList, JMap, JObject, 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 KotlinType { + Boolean, + String, + ByteArray, + Byte, + Short, + Int, + Long, + Float, + Double, + UByte, + UShort, + UInt, + ULong, + List(Box), + Map(Box, Box), + Pair(Box, Box), + Triple(Box, Box, Box), +} + +/// Extracts the KType of the Nth type argument from a generic KType. +/// +/// `ktype.getArguments()` → `List`. +/// `projection.getType()` → `KType?`. +/// Extracts the KType at `index` from the type arguments of `ktype` and +/// immediately decodes it to [KotlinType]. Returning [KotlinType] (a pure Rust +/// type) avoids the JObject lifetime chain that arises when returning JObject. +fn decode_ktype_arg(env: &mut JNIEnv, ktype: &JObject, index: i32) -> ZResult { + let args = env + .call_method(ktype, "getArguments", "()Ljava/util/List;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + + let projection = env + .call_method(&args, "get", "(I)Ljava/lang/Object;", &[JValue::Int(index)]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + + let arg_type = env + .call_method(&projection, "getType", "()Lkotlin/reflect/KType;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + + if arg_type.is_null() { + return Err(zerror!("KTypeProjection.type is null (star projection not supported)")); + } + decode_ktype(env, arg_type) +} + +/// Decodes a `kotlin.reflect.KType` JVM object into a [KotlinType] enum. +fn decode_ktype(env: &mut JNIEnv, ktype: JObject) -> ZResult { + let classifier = env + .call_method(&ktype, "getClassifier", "()Lkotlin/reflect/KClassifier;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + + if classifier.is_null() { + return Err(zerror!("KType has no classifier (star projection not supported)")); + } + + let name_obj = env + .call_method(&classifier, "getQualifiedName", "()Ljava/lang/String;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + + if name_obj.is_null() { + return Err(zerror!("KClass has no qualified name (anonymous/local class not supported)")); + } + + let qualified_name: std::string::String = env + .get_string(&JString::from(name_obj)) + .map_err(|err| zerror!(err))? + .into(); + + match qualified_name.as_str() { + "kotlin.Boolean" => Ok(KotlinType::Boolean), + "kotlin.String" => Ok(KotlinType::String), + "kotlin.ByteArray" => Ok(KotlinType::ByteArray), + "kotlin.Byte" => Ok(KotlinType::Byte), + "kotlin.Short" => Ok(KotlinType::Short), + "kotlin.Int" => Ok(KotlinType::Int), + "kotlin.Long" => Ok(KotlinType::Long), + "kotlin.Float" => Ok(KotlinType::Float), + "kotlin.Double" => Ok(KotlinType::Double), + "kotlin.UByte" => Ok(KotlinType::UByte), + "kotlin.UShort" => Ok(KotlinType::UShort), + "kotlin.UInt" => Ok(KotlinType::UInt), + "kotlin.ULong" => Ok(KotlinType::ULong), + "kotlin.collections.List" => { + Ok(KotlinType::List(Box::new(decode_ktype_arg(env, &ktype, 0)?))) + } + "kotlin.collections.Map" => { + let key = decode_ktype_arg(env, &ktype, 0)?; + let val = decode_ktype_arg(env, &ktype, 1)?; + Ok(KotlinType::Map(Box::new(key), Box::new(val))) + } + "kotlin.Pair" => { + let first = decode_ktype_arg(env, &ktype, 0)?; + let second = decode_ktype_arg(env, &ktype, 1)?; + Ok(KotlinType::Pair(Box::new(first), Box::new(second))) + } + "kotlin.Triple" => { + let first = decode_ktype_arg(env, &ktype, 0)?; + let second = decode_ktype_arg(env, &ktype, 1)?; + let third = decode_ktype_arg(env, &ktype, 2)?; + Ok(KotlinType::Triple(Box::new(first), Box::new(second), Box::new(third))) + } + _ => Err(zerror!("Unsupported Kotlin type: {}", qualified_name)), + } +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytesKotlin_serializeViaJNI( + mut env: JNIEnv, + _class: JClass, + any: JObject, + ktype: JObject, +) -> jobject { + || -> ZResult { + let kotlin_type = decode_ktype(&mut env, ktype)?; + let mut serializer = ZSerializer::new(); + serialize(&mut env, &mut serializer, any, &kotlin_type)?; + let zbytes = serializer.finish(); + let byte_array = bytes_to_java_array(&env, &zbytes).map_err(|err| zerror!(err))?; + Ok(byte_array.as_raw()) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + JObject::default().as_raw() + }) +} + +fn serialize( + env: &mut JNIEnv, + serializer: &mut ZSerializer, + any: JObject, + ktype: &KotlinType, +) -> ZResult<()> { + match ktype { + KotlinType::Boolean => { + let v = env + .call_method(&any, "booleanValue", "()Z", &[]) + .map_err(|err| zerror!(err))? + .z() + .map_err(|err| zerror!(err))?; + serializer.serialize(v); + } + KotlinType::Byte => { + let v = env + .call_method(&any, "byteValue", "()B", &[]) + .map_err(|err| zerror!(err))? + .b() + .map_err(|err| zerror!(err))?; + serializer.serialize(v); + } + KotlinType::Short => { + let v = env + .call_method(&any, "shortValue", "()S", &[]) + .map_err(|err| zerror!(err))? + .s() + .map_err(|err| zerror!(err))?; + serializer.serialize(v); + } + KotlinType::Int => { + let v = env + .call_method(&any, "intValue", "()I", &[]) + .map_err(|err| zerror!(err))? + .i() + .map_err(|err| zerror!(err))?; + serializer.serialize(v); + } + KotlinType::Long => { + let v = env + .call_method(&any, "longValue", "()J", &[]) + .map_err(|err| zerror!(err))? + .j() + .map_err(|err| zerror!(err))?; + serializer.serialize(v); + } + KotlinType::Float => { + let v = env + .call_method(&any, "floatValue", "()F", &[]) + .map_err(|err| zerror!(err))? + .f() + .map_err(|err| zerror!(err))?; + serializer.serialize(v); + } + KotlinType::Double => { + let v = env + .call_method(&any, "doubleValue", "()D", &[]) + .map_err(|err| zerror!(err))? + .d() + .map_err(|err| zerror!(err))?; + serializer.serialize(v); + } + KotlinType::String => { + let s: std::string::String = env + .get_string(&JString::from(any)) + .map_err(|err| zerror!(err))? + .into(); + serializer.serialize(s); + } + KotlinType::ByteArray => { + let bytes = decode_byte_array(env, JByteArray::from(any)).map_err(|err| zerror!(err))?; + serializer.serialize(bytes); + } + // Unsigned types: `toByte/Short/Int/Long()` returns the raw signed bits; + // we reinterpret as unsigned — wire encoding is identical (LE fixed-width). + KotlinType::UByte => { + let v = env + .call_method(&any, "toByte", "()B", &[]) + .map_err(|err| zerror!(err))? + .b() + .map_err(|err| zerror!(err))?; + serializer.serialize(v as u8); + } + KotlinType::UShort => { + let v = env + .call_method(&any, "toShort", "()S", &[]) + .map_err(|err| zerror!(err))? + .s() + .map_err(|err| zerror!(err))?; + serializer.serialize(v as u16); + } + KotlinType::UInt => { + let v = env + .call_method(&any, "toInt", "()I", &[]) + .map_err(|err| zerror!(err))? + .i() + .map_err(|err| zerror!(err))?; + serializer.serialize(v as u32); + } + KotlinType::ULong => { + let v = env + .call_method(&any, "toLong", "()J", &[]) + .map_err(|err| zerror!(err))? + .j() + .map_err(|err| zerror!(err))?; + serializer.serialize(v as u64); + } + KotlinType::List(inner) => { + let jlist = JList::from_env(env, &any).map_err(|err| zerror!(err))?; + let size = jlist.size(env).map_err(|err| zerror!(err))?; + serializer.serialize(VarInt(size as usize)); + let mut iter = jlist.iter(env).map_err(|err| zerror!(err))?; + while let Some(item) = iter.next(env).map_err(|err| zerror!(err))? { + serialize(env, serializer, item, inner)?; + } + } + KotlinType::Map(key_type, val_type) => { + let jmap = JMap::from_env(env, &any).map_err(|err| zerror!(err))?; + let size = env + .call_method(&jmap, "size", "()I", &[]) + .map_err(|err| zerror!(err))? + .i() + .map_err(|err| zerror!(err))?; + serializer.serialize(VarInt(size as usize)); + let mut iter = jmap.iter(env).map_err(|err| zerror!(err))?; + while let Some((k, v)) = iter.next(env).map_err(|err| zerror!(err))? { + serialize(env, serializer, k, key_type)?; + serialize(env, serializer, v, val_type)?; + } + } + KotlinType::Pair(first_type, second_type) => { + let first = env + .call_method(&any, "getFirst", "()Ljava/lang/Object;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + let second = env + .call_method(&any, "getSecond", "()Ljava/lang/Object;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + serialize(env, serializer, first, first_type)?; + serialize(env, serializer, second, second_type)?; + } + KotlinType::Triple(first_type, second_type, third_type) => { + let first = env + .call_method(&any, "getFirst", "()Ljava/lang/Object;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + let second = env + .call_method(&any, "getSecond", "()Ljava/lang/Object;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + let third = env + .call_method(&any, "getThird", "()Ljava/lang/Object;", &[]) + .map_err(|err| zerror!(err))? + .l() + .map_err(|err| zerror!(err))?; + serialize(env, serializer, first, first_type)?; + serialize(env, serializer, second, second_type)?; + serialize(env, serializer, third, third_type)?; + } + } + Ok(()) +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytesKotlin_deserializeViaJNI( + mut env: JNIEnv, + _class: JClass, + bytes: JByteArray, + ktype: JObject, +) -> jobject { + || -> ZResult { + let raw = decode_byte_array(&env, bytes)?; + let zbytes = ZBytes::from(raw); + let mut deserializer = ZDeserializer::new(&zbytes); + let kotlin_type = decode_ktype(&mut env, ktype)?; + let obj = deserialize(&mut env, &mut deserializer, &kotlin_type)?; + 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, + ktype: &KotlinType, +) -> ZResult { + match ktype { + KotlinType::Boolean => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("java/lang/Boolean", "(Z)V", &[JValue::Bool(v as u8)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::Byte => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("java/lang/Byte", "(B)V", &[JValue::Byte(v)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::Short => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("java/lang/Short", "(S)V", &[JValue::Short(v)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::Int => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("java/lang/Integer", "(I)V", &[JValue::Int(v)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::Long => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("java/lang/Long", "(J)V", &[JValue::Long(v)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::Float => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("java/lang/Float", "(F)V", &[JValue::Float(v)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::Double => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("java/lang/Double", "(D)V", &[JValue::Double(v)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::String => { + let s = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; + let jstr = env.new_string(&s).map_err(|err| zerror!(err))?; + Ok(jstr.into_raw()) + } + KotlinType::ByteArray => { + let bytes = deserializer + .deserialize::>() + .map_err(|err| zerror!(err))?; + let jbytes = env + .byte_array_from_slice(bytes.as_slice()) + .map_err(|err| zerror!(err))?; + Ok(jbytes.into_raw()) + } + KotlinType::UByte => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("kotlin/UByte", "(B)V", &[JValue::Byte(v as i8)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::UShort => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("kotlin/UShort", "(S)V", &[JValue::Short(v as i16)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::UInt => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("kotlin/UInt", "(I)V", &[JValue::Int(v as i32)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::ULong => { + let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let obj = env + .new_object("kotlin/ULong", "(J)V", &[JValue::Long(v as i64)]) + .map_err(|err| zerror!(err))?; + Ok(obj.as_raw()) + } + KotlinType::List(inner) => { + let 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..size { + let item = deserialize(env, deserializer, inner)?; + let item_obj = unsafe { JObject::from_raw(item) }; + jlist.add(env, &item_obj).map_err(|err| zerror!(err))?; + } + Ok(array_list.as_raw()) + } + KotlinType::Map(key_type, val_type) => { + let size = deserializer + .deserialize::>() + .map_err(|err| zerror!(err))? + .0; + let hash_map = env + .new_object("java/util/HashMap", "()V", &[]) + .map_err(|err| zerror!(err))?; + let jmap = JMap::from_env(env, &hash_map).map_err(|err| zerror!(err))?; + for _ in 0..size { + let k = deserialize(env, deserializer, key_type)?; + let k_obj = unsafe { JObject::from_raw(k) }; + let v = deserialize(env, deserializer, val_type)?; + let v_obj = unsafe { JObject::from_raw(v) }; + jmap.put(env, &k_obj, &v_obj).map_err(|err| zerror!(err))?; + } + Ok(hash_map.as_raw()) + } + KotlinType::Pair(first_type, second_type) => { + let first = deserialize(env, deserializer, first_type)?; + let second = deserialize(env, deserializer, second_type)?; + let first_obj = unsafe { JObject::from_raw(first) }; + let second_obj = unsafe { JObject::from_raw(second) }; + let pair = env + .new_object( + "kotlin/Pair", + "(Ljava/lang/Object;Ljava/lang/Object;)V", + &[JValue::Object(&first_obj), JValue::Object(&second_obj)], + ) + .map_err(|err| zerror!(err))?; + Ok(pair.as_raw()) + } + KotlinType::Triple(first_type, second_type, third_type) => { + let first = deserialize(env, deserializer, first_type)?; + let second = deserialize(env, deserializer, second_type)?; + let third = deserialize(env, deserializer, third_type)?; + let first_obj = unsafe { JObject::from_raw(first) }; + let second_obj = unsafe { JObject::from_raw(second) }; + let third_obj = unsafe { JObject::from_raw(third) }; + let triple = env + .new_object( + "kotlin/Triple", + "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V", + &[ + JValue::Object(&first_obj), + JValue::Object(&second_obj), + JValue::Object(&third_obj), + ], + ) + .map_err(|err| zerror!(err))?; + Ok(triple.as_raw()) + } + } +} From 969166ab357ff382ba32198f14a7dbfa5b69933e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 18 Apr 2026 21:58:52 +0200 Subject: [PATCH 051/126] serialiser iteroperability test --- zenoh-jni-runtime/build.gradle.kts | 3 + .../kotlin/io/zenoh/ZBytesInteropTests.kt | 308 ++++++++++++++++++ zenoh-jni/src/zbytes_kotlin.rs | 10 +- 3 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 zenoh-jni-runtime/src/jvmTest/kotlin/io/zenoh/ZBytesInteropTests.kt diff --git a/zenoh-jni-runtime/build.gradle.kts b/zenoh-jni-runtime/build.gradle.kts index 7a4c7b15..5e6d4def 100644 --- a/zenoh-jni-runtime/build.gradle.kts +++ b/zenoh-jni-runtime/build.gradle.kts @@ -91,6 +91,9 @@ kotlin { val jvmTest by getting { resources.srcDir("../zenoh-jni/target/$buildMode").include(arrayListOf("*.dylib", "*.so", "*.dll")) + dependencies { + implementation(kotlin("test-junit")) + } } } diff --git a/zenoh-jni-runtime/src/jvmTest/kotlin/io/zenoh/ZBytesInteropTests.kt b/zenoh-jni-runtime/src/jvmTest/kotlin/io/zenoh/ZBytesInteropTests.kt new file mode 100644 index 00000000..5e8f6bd5 --- /dev/null +++ b/zenoh-jni-runtime/src/jvmTest/kotlin/io/zenoh/ZBytesInteropTests.kt @@ -0,0 +1,308 @@ +// +// Copyright (c) 2026 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.jni.JNIZBytes +import io.zenoh.jni.JNIZBytesKotlin +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +/** + * Tests for Java/Kotlin serialization interoperability at the JNI bridge layer. + * + * Two groups: + * 1. Kotlin round-trips — serialize and deserialize via [JNIZBytesKotlin] (KType path), covering + * all supported KotlinType variants including unsigned integers and Pair/Triple. + * 2. Cross-path interop — verify that [JNIZBytes] (Java Type path) and [JNIZBytesKotlin] + * (KType path) produce identical wire bytes for the common types they share. + */ +class ZBytesInteropTests { + + // ------------------------------------------------------------------------- + // Group 1: Kotlin round-trips via JNIZBytesKotlin + // ------------------------------------------------------------------------- + + @Test + fun testBooleanKotlinRoundTrip() { + val input = true + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Boolean) + } + + @Test + fun testByteKotlinRoundTrip() { + val input: Byte = 42 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Byte) + } + + @Test + fun testShortKotlinRoundTrip() { + val input: Short = 1234 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Short) + } + + @Test + fun testIntKotlinRoundTrip() { + val input = 1234567 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Int) + } + + @Test + fun testLongKotlinRoundTrip() { + val input = 123456789012345L + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Long) + } + + @Test + fun testFloatKotlinRoundTrip() { + val input = 3.1415f + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Float, 0.0001f) + } + + @Test + fun testDoubleKotlinRoundTrip() { + val input = 2.718281828 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Double, 0.000000001) + } + + @Test + fun testStringKotlinRoundTrip() { + val input = "hello zenoh" + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as String) + } + + @Test + fun testByteArrayKotlinRoundTrip() { + val input = byteArrayOf(1, 2, 3, 4, 5) + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertContentEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as ByteArray) + } + + // Kotlin-only unsigned types + + @Test + fun testUByteKotlinRoundTrip() { + val input: UByte = 200u + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as UByte) + } + + @Test + fun testUShortKotlinRoundTrip() { + val input: UShort = 60000u + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as UShort) + } + + @Test + fun testUIntKotlinRoundTrip() { + val input: UInt = 3000000000u + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as UInt) + } + + @Test + fun testULongKotlinRoundTrip() { + val input: ULong = 10000000000000000000u + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as ULong) + } + + // Collections + + @Test + fun testListOfIntKotlinRoundTrip() { + val input = listOf(1, 2, 3, 4, 5) + val bytes = JNIZBytesKotlin.serialize(input, typeOf>()) + @Suppress("UNCHECKED_CAST") + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf>()) as List) + } + + @Test + fun testListOfStringKotlinRoundTrip() { + val input = listOf("a", "b", "c") + val bytes = JNIZBytesKotlin.serialize(input, typeOf>()) + @Suppress("UNCHECKED_CAST") + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf>()) as List) + } + + @Test + fun testMapOfStringToIntKotlinRoundTrip() { + val input = mapOf("one" to 1, "two" to 2, "three" to 3) + val bytes = JNIZBytesKotlin.serialize(input, typeOf>()) + @Suppress("UNCHECKED_CAST") + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf>()) as Map) + } + + // Kotlin-only compound types + + @Test + fun testPairKotlinRoundTrip() { + val input = Pair(42, "hello") + val bytes = JNIZBytesKotlin.serialize(input, typeOf>()) + @Suppress("UNCHECKED_CAST") + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf>()) as Pair) + } + + @Test + fun testTripleKotlinRoundTrip() { + val input = Triple("zenoh", 99, true) + val bytes = JNIZBytesKotlin.serialize(input, typeOf>()) + @Suppress("UNCHECKED_CAST") + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf>()) as Triple) + } + + // ------------------------------------------------------------------------- + // Group 2: Cross-path interop — Java Type path ↔ KType path + // + // For the Java path, java.lang.Class implements java.lang.reflect.Type. + // Boxed types give the qualified names zbytes.rs expects ("java.lang.Integer", etc.). + // ------------------------------------------------------------------------- + + @Test + fun testBooleanJavaToKotlinInterop() { + val input = true + val bytes = JNIZBytes.serialize(input, java.lang.Boolean::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Boolean) + } + + @Test + fun testBooleanKotlinToJavaInterop() { + val input = true + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.Boolean::class.java) as Boolean) + } + + @Test + fun testByteJavaToKotlinInterop() { + val input: Byte = 42 + val bytes = JNIZBytes.serialize(input, java.lang.Byte::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Byte) + } + + @Test + fun testByteKotlinToJavaInterop() { + val input: Byte = 42 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.Byte::class.java) as Byte) + } + + @Test + fun testShortJavaToKotlinInterop() { + val input: Short = 1234 + val bytes = JNIZBytes.serialize(input, java.lang.Short::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Short) + } + + @Test + fun testShortKotlinToJavaInterop() { + val input: Short = 1234 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.Short::class.java) as Short) + } + + @Test + fun testIntJavaToKotlinInterop() { + val input = 42 + val bytes = JNIZBytes.serialize(input, java.lang.Integer::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Int) + } + + @Test + fun testIntKotlinToJavaInterop() { + val input = 42 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.Integer::class.java) as Int) + } + + @Test + fun testLongJavaToKotlinInterop() { + val input = 123456789012345L + val bytes = JNIZBytes.serialize(input, java.lang.Long::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Long) + } + + @Test + fun testLongKotlinToJavaInterop() { + val input = 123456789012345L + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.Long::class.java) as Long) + } + + @Test + fun testFloatJavaToKotlinInterop() { + val input = 3.1415f + val bytes = JNIZBytes.serialize(input, java.lang.Float::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Float, 0.0001f) + } + + @Test + fun testFloatKotlinToJavaInterop() { + val input = 3.1415f + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.Float::class.java) as Float, 0.0001f) + } + + @Test + fun testDoubleJavaToKotlinInterop() { + val input = 2.718281828 + val bytes = JNIZBytes.serialize(input, java.lang.Double::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as Double, 0.000000001) + } + + @Test + fun testDoubleKotlinToJavaInterop() { + val input = 2.718281828 + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.Double::class.java) as Double, 0.000000001) + } + + @Test + fun testStringJavaToKotlinInterop() { + val input = "hello zenoh" + val bytes = JNIZBytes.serialize(input, java.lang.String::class.java) + assertEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as String) + } + + @Test + fun testStringKotlinToJavaInterop() { + val input = "hello zenoh" + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertEquals(input, JNIZBytes.deserialize(bytes, java.lang.String::class.java) as String) + } + + @Test + fun testByteArrayJavaToKotlinInterop() { + val input = byteArrayOf(10, 20, 30, 40, 50) + val bytes = JNIZBytes.serialize(input, ByteArray::class.java) + assertContentEquals(input, JNIZBytesKotlin.deserialize(bytes, typeOf()) as ByteArray) + } + + @Test + fun testByteArrayKotlinToJavaInterop() { + val input = byteArrayOf(10, 20, 30, 40, 50) + val bytes = JNIZBytesKotlin.serialize(input, typeOf()) + assertContentEquals(input, JNIZBytes.deserialize(bytes, ByteArray::class.java) as ByteArray) + } +} diff --git a/zenoh-jni/src/zbytes_kotlin.rs b/zenoh-jni/src/zbytes_kotlin.rs index 6d1cc44c..c2fd84ef 100644 --- a/zenoh-jni/src/zbytes_kotlin.rs +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -249,11 +249,9 @@ fn serialize( let bytes = decode_byte_array(env, JByteArray::from(any)).map_err(|err| zerror!(err))?; serializer.serialize(bytes); } - // Unsigned types: `toByte/Short/Int/Long()` returns the raw signed bits; - // we reinterpret as unsigned — wire encoding is identical (LE fixed-width). KotlinType::UByte => { let v = env - .call_method(&any, "toByte", "()B", &[]) + .get_field(&any, "data", "B") .map_err(|err| zerror!(err))? .b() .map_err(|err| zerror!(err))?; @@ -261,7 +259,7 @@ fn serialize( } KotlinType::UShort => { let v = env - .call_method(&any, "toShort", "()S", &[]) + .get_field(&any, "data", "S") .map_err(|err| zerror!(err))? .s() .map_err(|err| zerror!(err))?; @@ -269,7 +267,7 @@ fn serialize( } KotlinType::UInt => { let v = env - .call_method(&any, "toInt", "()I", &[]) + .get_field(&any, "data", "I") .map_err(|err| zerror!(err))? .i() .map_err(|err| zerror!(err))?; @@ -277,7 +275,7 @@ fn serialize( } KotlinType::ULong => { let v = env - .call_method(&any, "toLong", "()J", &[]) + .get_field(&any, "data", "J") .map_err(|err| zerror!(err))? .j() .map_err(|err| zerror!(err))?; From fc47b41f40974af89af6a3885821228ca9c66427 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 19 Apr 2026 16:28:25 +0200 Subject: [PATCH 052/126] cargo fmt --- zenoh-jni/src/zbytes_kotlin.rs | 78 +++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/zenoh-jni/src/zbytes_kotlin.rs b/zenoh-jni/src/zbytes_kotlin.rs index c2fd84ef..997bd51b 100644 --- a/zenoh-jni/src/zbytes_kotlin.rs +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -84,7 +84,9 @@ fn decode_ktype_arg(env: &mut JNIEnv, ktype: &JObject, index: i32) -> ZResult ZResult ZResult { let classifier = env - .call_method(&ktype, "getClassifier", "()Lkotlin/reflect/KClassifier;", &[]) + .call_method( + &ktype, + "getClassifier", + "()Lkotlin/reflect/KClassifier;", + &[], + ) .map_err(|err| zerror!(err))? .l() .map_err(|err| zerror!(err))?; if classifier.is_null() { - return Err(zerror!("KType has no classifier (star projection not supported)")); + return Err(zerror!( + "KType has no classifier (star projection not supported)" + )); } let name_obj = env @@ -108,7 +117,9 @@ fn decode_ktype(env: &mut JNIEnv, ktype: JObject) -> ZResult { .map_err(|err| zerror!(err))?; if name_obj.is_null() { - return Err(zerror!("KClass has no qualified name (anonymous/local class not supported)")); + return Err(zerror!( + "KClass has no qualified name (anonymous/local class not supported)" + )); } let qualified_name: std::string::String = env @@ -130,9 +141,9 @@ fn decode_ktype(env: &mut JNIEnv, ktype: JObject) -> ZResult { "kotlin.UShort" => Ok(KotlinType::UShort), "kotlin.UInt" => Ok(KotlinType::UInt), "kotlin.ULong" => Ok(KotlinType::ULong), - "kotlin.collections.List" => { - Ok(KotlinType::List(Box::new(decode_ktype_arg(env, &ktype, 0)?))) - } + "kotlin.collections.List" => Ok(KotlinType::List(Box::new(decode_ktype_arg( + env, &ktype, 0, + )?))), "kotlin.collections.Map" => { let key = decode_ktype_arg(env, &ktype, 0)?; let val = decode_ktype_arg(env, &ktype, 1)?; @@ -147,7 +158,11 @@ fn decode_ktype(env: &mut JNIEnv, ktype: JObject) -> ZResult { let first = decode_ktype_arg(env, &ktype, 0)?; let second = decode_ktype_arg(env, &ktype, 1)?; let third = decode_ktype_arg(env, &ktype, 2)?; - Ok(KotlinType::Triple(Box::new(first), Box::new(second), Box::new(third))) + Ok(KotlinType::Triple( + Box::new(first), + Box::new(second), + Box::new(third), + )) } _ => Err(zerror!("Unsupported Kotlin type: {}", qualified_name)), } @@ -246,7 +261,8 @@ fn serialize( serializer.serialize(s); } KotlinType::ByteArray => { - let bytes = decode_byte_array(env, JByteArray::from(any)).map_err(|err| zerror!(err))?; + let bytes = + decode_byte_array(env, JByteArray::from(any)).map_err(|err| zerror!(err))?; serializer.serialize(bytes); } KotlinType::UByte => { @@ -374,49 +390,63 @@ fn deserialize( ) -> ZResult { match ktype { KotlinType::Boolean => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("java/lang/Boolean", "(Z)V", &[JValue::Bool(v as u8)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::Byte => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("java/lang/Byte", "(B)V", &[JValue::Byte(v)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::Short => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("java/lang/Short", "(S)V", &[JValue::Short(v)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::Int => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("java/lang/Integer", "(I)V", &[JValue::Int(v)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::Long => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("java/lang/Long", "(J)V", &[JValue::Long(v)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::Float => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("java/lang/Float", "(F)V", &[JValue::Float(v)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::Double => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("java/lang/Double", "(D)V", &[JValue::Double(v)]) .map_err(|err| zerror!(err))?; @@ -439,28 +469,36 @@ fn deserialize( Ok(jbytes.into_raw()) } KotlinType::UByte => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("kotlin/UByte", "(B)V", &[JValue::Byte(v as i8)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::UShort => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("kotlin/UShort", "(S)V", &[JValue::Short(v as i16)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::UInt => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("kotlin/UInt", "(I)V", &[JValue::Int(v as i32)]) .map_err(|err| zerror!(err))?; Ok(obj.as_raw()) } KotlinType::ULong => { - let v = deserializer.deserialize::().map_err(|err| zerror!(err))?; + let v = deserializer + .deserialize::() + .map_err(|err| zerror!(err))?; let obj = env .new_object("kotlin/ULong", "(J)V", &[JValue::Long(v as i64)]) .map_err(|err| zerror!(err))?; From df09e82e6fee0d01264b159219a3192ac0ffd115 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 12:19:30 +0200 Subject: [PATCH 053/126] zenoh-flat crate --- zenoh-flat/Cargo.toml | 9 +++++++++ zenoh-flat/src/lib.rs | 2 ++ zenoh-jni/Cargo.lock | 5 +++++ zenoh-jni/Cargo.toml | 1 + 4 files changed, 17 insertions(+) create mode 100644 zenoh-flat/Cargo.toml create mode 100644 zenoh-flat/src/lib.rs diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml new file mode 100644 index 00000000..92a1374d --- /dev/null +++ b/zenoh-flat/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "zenoh-flat" +version = "1.9.0" +edition = "2021" +license = "EPL-2.0 OR Apache-2.0" +description = "Zenoh flat data interchange support." +repository = "https://github.com/eclipse-zenoh/zenoh" + +[dependencies] diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs new file mode 100644 index 00000000..8fa77d93 --- /dev/null +++ b/zenoh-flat/src/lib.rs @@ -0,0 +1,2 @@ +//! `zenoh-flat` is a placeholder Rust crate for Zenoh flat data support. + diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index bd456c8a..edd05747 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -3942,6 +3942,10 @@ dependencies = [ "zenoh-util", ] +[[package]] +name = "zenoh-flat" +version = "1.9.0" + [[package]] name = "zenoh-keyexpr" version = "1.9.0" @@ -4317,6 +4321,7 @@ dependencies = [ "uhlc", "zenoh", "zenoh-ext", + "zenoh-flat", ] [[package]] diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index 1afcceee..e30cd4e1 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -39,6 +39,7 @@ serde_yaml = "0.9.19" zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false, optional = true } tracing = { version = "0.1" , features = ["log"] } +zenoh-flat = { path = "../zenoh-flat" } [lib] name = "zenoh_jni" crate_type = ["staticlib", "dylib"] From f7aa4ed0efe506746013c885ff8be57dd24f35a4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 12:27:49 +0200 Subject: [PATCH 054/126] open_session moved --- zenoh-flat/Cargo.toml | 1 + zenoh-flat/src/lib.rs | 2 ++ zenoh-flat/src/session.rs | 19 +++++++++++++++++++ zenoh-jni/Cargo.lock | 3 +++ zenoh-jni/src/session.rs | 8 +++----- 5 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 zenoh-flat/src/session.rs diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 92a1374d..b25bf724 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -7,3 +7,4 @@ description = "Zenoh flat data interchange support." repository = "https://github.com/eclipse-zenoh/zenoh" [dependencies] +zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index 8fa77d93..68004bd8 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -1,2 +1,4 @@ //! `zenoh-flat` is a placeholder Rust crate for Zenoh flat data support. +pub mod session; + diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs new file mode 100644 index 00000000..9290c994 --- /dev/null +++ b/zenoh-flat/src/session.rs @@ -0,0 +1,19 @@ +// 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 zenoh::{config::Config, session::Session, Wait}; + +/// Open a Zenoh session using a borrowed configuration. +pub fn open_session(config: &Config) -> Result { + zenoh::open(config.clone()).wait() +} diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index edd05747..d35c62b8 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -3945,6 +3945,9 @@ dependencies = [ [[package]] name = "zenoh-flat" version = "1.9.0" +dependencies = [ + "zenoh", +] [[package]] name = "zenoh-keyexpr" diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 15a23a50..d1bff817 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -79,11 +79,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( /// /// If the config path provided is null then the default configuration is loaded. /// -unsafe fn open_session(config_ptr: *const Config) -> ZResult { - let config = OwnedObject::from_raw(config_ptr); - zenoh::open((*config).clone()) - .wait() - .map_err(|err: zenoh::Error| zerror!(err)) +fn open_session(config_ptr: *const Config) -> ZResult { + let config = unsafe { OwnedObject::from_raw(config_ptr) }; + zenoh_flat::session::open_session(&config).map_err(|err| zerror!(err)) } /// Open a Zenoh session with a JSON configuration. From d8b9f88b4941cc9eab389415bf192b73c6ce13d9 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 12:44:41 +0200 Subject: [PATCH 055/126] zerror in zenoh-flat --- zenoh-flat/src/errors.rs | 35 +++++++++++++++++++++++ zenoh-flat/src/lib.rs | 1 + zenoh-flat/src/session.rs | 7 +++-- zenoh-jni/src/config.rs | 2 +- zenoh-jni/src/errors.rs | 36 ++++++++---------------- zenoh-jni/src/ext/advanced_publisher.rs | 1 - zenoh-jni/src/ext/advanced_subscriber.rs | 1 - zenoh-jni/src/key_expr.rs | 2 +- zenoh-jni/src/lib.rs | 3 ++ zenoh-jni/src/liveliness.rs | 1 - zenoh-jni/src/logger.rs | 2 +- zenoh-jni/src/publisher.rs | 1 - zenoh-jni/src/querier.rs | 1 - zenoh-jni/src/query.rs | 1 - zenoh-jni/src/sample_callback.rs | 2 +- zenoh-jni/src/scouting.rs | 2 +- zenoh-jni/src/session.rs | 4 +-- zenoh-jni/src/utils.rs | 2 +- zenoh-jni/src/zbytes.rs | 1 - zenoh-jni/src/zbytes_kotlin.rs | 1 - zenoh-jni/src/zenoh_id.rs | 2 +- 21 files changed, 64 insertions(+), 44 deletions(-) create mode 100644 zenoh-flat/src/errors.rs diff --git a/zenoh-flat/src/errors.rs b/zenoh-flat/src/errors.rs new file mode 100644 index 00000000..7ac177ab --- /dev/null +++ b/zenoh-flat/src/errors.rs @@ -0,0 +1,35 @@ +// 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 std::fmt; + +#[macro_export] +macro_rules! zerror { + ($arg:expr) => { + $crate::errors::ZError($arg.to_string()) + }; + ($fmt:expr, $($arg:tt)*) => { + $crate::errors::ZError(format!($fmt, $($arg)*)) + }; +} + +pub(crate) type ZResult = core::result::Result; + +#[derive(Debug)] +pub struct ZError(pub String); + +impl fmt::Display for ZError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index 68004bd8..3569ad96 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -1,4 +1,5 @@ //! `zenoh-flat` is a placeholder Rust crate for Zenoh flat data support. +pub mod errors; pub mod session; diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 9290c994..7493fef4 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -11,9 +11,12 @@ // ZettaScale Zenoh Team, // +use crate::zerror; use zenoh::{config::Config, session::Session, Wait}; /// Open a Zenoh session using a borrowed configuration. -pub fn open_session(config: &Config) -> Result { - zenoh::open(config.clone()).wait() +pub fn open_session(config: &Config) -> crate::errors::ZResult { + zenoh::open(config.clone()) + .wait() + .map_err(|err| zerror!(err)) } diff --git a/zenoh-jni/src/config.rs b/zenoh-jni/src/config.rs index b64a78a4..a63fb8be 100644 --- a/zenoh-jni/src/config.rs +++ b/zenoh-jni/src/config.rs @@ -22,7 +22,7 @@ use jni::{ use zenoh::Config; use crate::owned_object::OwnedObject; -use crate::{errors::ZResult, zerror}; +use crate::errors::ZResult; use crate::{throw_exception, utils::decode_string}; /// Loads the default configuration, returning a raw pointer to it. diff --git a/zenoh-jni/src/errors.rs b/zenoh-jni/src/errors.rs index 23687d4c..4bd08aea 100644 --- a/zenoh-jni/src/errors.rs +++ b/zenoh-jni/src/errors.rs @@ -12,46 +12,32 @@ // ZettaScale Zenoh Team, // -use std::fmt; - use jni::JNIEnv; #[macro_export] macro_rules! throw_exception { ($env:expr, $err:expr) => {{ - let _ = $err.throw_on_jvm(&mut $env).map_err(|err| { + let _ = ::throw_on_jvm( + &$err, + &mut $env, + ) + .map_err(|err| { tracing::error!("Unable to throw exception: {}", err); }); }}; } -#[macro_export] -macro_rules! zerror { - ($arg:expr) => { - $crate::errors::ZError($arg.to_string()) - }; - ($fmt:expr, $($arg:tt)*) => { - $crate::errors::ZError(format!($fmt, $($arg)*)) - }; -} - +pub(crate) type ZError = zenoh_flat::errors::ZError; pub(crate) type ZResult = core::result::Result; -#[derive(Debug)] -pub(crate) struct ZError(pub String); - -impl fmt::Display for ZError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } +pub(crate) trait ThrowOnJvm { + fn throw_on_jvm(&self, env: &mut JNIEnv) -> ZResult<()>; } -impl ZError { - const KOTLIN_EXCEPTION_NAME: &'static str = "io/zenoh/exceptions/ZError"; - - pub fn throw_on_jvm(&self, env: &mut JNIEnv) -> ZResult<()> { +impl ThrowOnJvm for ZError { + fn throw_on_jvm(&self, env: &mut JNIEnv) -> ZResult<()> { let exception_class = env - .find_class(Self::KOTLIN_EXCEPTION_NAME) + .find_class("io/zenoh/exceptions/ZError") .map_err(|err| zerror!("Failed to retrieve exception class: {}", err))?; env.throw_new(exception_class, self.to_string()) .map_err(|err| zerror!("Failed to throw exception: {}", err)) diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index 51500880..b7ff2dae 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -31,7 +31,6 @@ use crate::throw_exception; use crate::{ errors::ZResult, utils::{decode_byte_array, decode_encoding}, - zerror, }; use jni::sys::jboolean; use std::ptr::null; diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs index 4fbc624a..93def10e 100644 --- a/zenoh-jni/src/ext/advanced_subscriber.rs +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -31,7 +31,6 @@ use zenoh::Wait; use crate::owned_object::OwnedObject; use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; -use crate::zerror; use std::ptr::null; use crate::throw_exception; diff --git a/zenoh-jni/src/key_expr.rs b/zenoh-jni/src/key_expr.rs index dedbe565..be5e4d1f 100644 --- a/zenoh-jni/src/key_expr.rs +++ b/zenoh-jni/src/key_expr.rs @@ -22,7 +22,7 @@ use zenoh::key_expr::KeyExpr; use crate::errors::ZResult; use crate::owned_object::OwnedObject; use crate::utils::decode_string; -use crate::{throw_exception, zerror}; +use crate::throw_exception; /// Validates the provided `key_expr` to be a valid key expression, returning it back /// in case of success or throwing an exception in case of failure. diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index f8603617..fe0c98c6 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -12,6 +12,9 @@ // ZettaScale Zenoh Team, // +#[macro_use] +extern crate zenoh_flat; + mod config; mod errors; #[cfg(feature = "zenoh-ext")] diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 8acdc551..283325f2 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -33,7 +33,6 @@ use crate::{ session::{on_reply_error, on_reply_success}, throw_exception, utils::{get_callback_global_ref, get_java_vm, load_on_close}, - zerror, }; #[no_mangle] diff --git a/zenoh-jni/src/logger.rs b/zenoh-jni/src/logger.rs index dd64548f..c91ae0d7 100644 --- a/zenoh-jni/src/logger.rs +++ b/zenoh-jni/src/logger.rs @@ -17,7 +17,7 @@ use jni::{ JNIEnv, }; -use crate::{errors::ZResult, throw_exception, zerror}; +use crate::{errors::ZResult, throw_exception}; /// Redirects the Rust logs either to logcat for Android systems or to the standard output (for non-Android systems). /// diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index e2e76339..3c30d18c 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -26,7 +26,6 @@ use crate::throw_exception; use crate::{ errors::ZResult, utils::{decode_byte_array, decode_encoding}, - zerror, }; /// Performs a PUT operation on a Zenoh publisher via JNI. diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 27bf7a63..835b2f39 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -31,7 +31,6 @@ use crate::{ decode_byte_array, decode_encoding, decode_string, get_callback_global_ref, get_java_vm, load_on_close, }, - zerror, }; /// Perform a Zenoh GET through a querier. diff --git a/zenoh-jni/src/query.rs b/zenoh-jni/src/query.rs index 9c843892..25e9f538 100644 --- a/zenoh-jni/src/query.rs +++ b/zenoh-jni/src/query.rs @@ -15,7 +15,6 @@ use std::sync::Arc; use crate::utils::{decode_byte_array, decode_encoding}; -use crate::zerror; use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception}; use jni::{ objects::{JByteArray, JClass, JString}, diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index fdf677b8..e105b66d 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -26,7 +26,7 @@ use zenoh::{ sample::Sample, }; -use crate::{errors::ZResult, utils::*, zerror}; +use crate::{errors::ZResult, utils::*}; pub(crate) trait SetJniSampleCallback: Sized + HasSampleCallbackSetter { unsafe fn set_jni_sample_callback( diff --git a/zenoh-jni/src/scouting.rs b/zenoh-jni/src/scouting.rs index cc45aee7..c5ec79d1 100644 --- a/zenoh-jni/src/scouting.rs +++ b/zenoh-jni/src/scouting.rs @@ -24,7 +24,7 @@ use zenoh::{scouting::Scout, Config}; use crate::owned_object::OwnedObject; use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; -use crate::{errors::ZResult, throw_exception, zerror}; +use crate::{errors::ZResult, throw_exception}; /// Start a scout. /// diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index d1bff817..07b94e25 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -41,7 +41,7 @@ use zenoh_ext::{ }; use crate::{ - errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, zerror, + errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, }; /// Open a Zenoh session via JNI. @@ -81,7 +81,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( /// fn open_session(config_ptr: *const Config) -> ZResult { let config = unsafe { OwnedObject::from_raw(config_ptr) }; - zenoh_flat::session::open_session(&config).map_err(|err| zerror!(err)) + zenoh_flat::session::open_session(&config) } /// Open a Zenoh session with a JSON configuration. diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index a2c6a9f4..7be1eded 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -14,7 +14,7 @@ use std::sync::Arc; -use crate::{errors::ZResult, throw_exception, zerror}; +use crate::{errors::ZResult, throw_exception}; use jni::{ objects::{JByteArray, JObject, JString}, sys::jint, diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs index ae4ca8b8..ae6d5405 100644 --- a/zenoh-jni/src/zbytes.rs +++ b/zenoh-jni/src/zbytes.rs @@ -24,7 +24,6 @@ use crate::{ errors::ZResult, throw_exception, utils::{bytes_to_java_array, decode_byte_array}, - zerror, }; enum JavaType { diff --git a/zenoh-jni/src/zbytes_kotlin.rs b/zenoh-jni/src/zbytes_kotlin.rs index 997bd51b..2cd30ca1 100644 --- a/zenoh-jni/src/zbytes_kotlin.rs +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -34,7 +34,6 @@ use crate::{ errors::ZResult, throw_exception, utils::{bytes_to_java_array, decode_byte_array}, - zerror, }; enum KotlinType { diff --git a/zenoh-jni/src/zenoh_id.rs b/zenoh-jni/src/zenoh_id.rs index 6647f86f..d0244f48 100644 --- a/zenoh-jni/src/zenoh_id.rs +++ b/zenoh-jni/src/zenoh_id.rs @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -use crate::{errors::ZResult, throw_exception, utils::decode_byte_array, zerror}; +use crate::{errors::ZResult, throw_exception, utils::decode_byte_array}; use jni::{ objects::{JByteArray, JClass, JString}, sys::jstring, From 9570d7c2a3851ca4265b0f05655040356117ace7 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 12:46:59 +0200 Subject: [PATCH 056/126] config from ptr in Java_io_zenoh_jni_JNISession_openSessionViaJNI --- zenoh-jni/src/session.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 07b94e25..c456c02b 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -64,7 +64,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( _class: JClass, config_ptr: *const Config, ) -> *const Session { - let session = open_session(config_ptr); + let config = unsafe { OwnedObject::from_raw(config_ptr) }; + let session = zenoh_flat::session::open_session(&config); match session { Ok(session) => Arc::into_raw(Arc::new(session)), Err(err) => { @@ -75,15 +76,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( } } -/// Open a Zenoh session with the configuration pointed out by `config_path`. -/// -/// If the config path provided is null then the default configuration is loaded. -/// -fn open_session(config_ptr: *const Config) -> ZResult { - let config = unsafe { OwnedObject::from_raw(config_ptr) }; - zenoh_flat::session::open_session(&config) -} - /// Open a Zenoh session with a JSON configuration. /// /// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute From 3db3e0c9a33f83e68a0025c0e0db090621327295 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 13:03:14 +0200 Subject: [PATCH 057/126] use config from zenoh-flat --- zenoh-flat/Cargo.toml | 1 + zenoh-flat/src/config.rs | 24 ++++++++++++++++++++++++ zenoh-flat/src/lib.rs | 1 + zenoh-jni/Cargo.lock | 1 + zenoh-jni/src/session.rs | 9 ++------- 5 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 zenoh-flat/src/config.rs diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index b25bf724..18e8528c 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -8,3 +8,4 @@ repository = "https://github.com/eclipse-zenoh/zenoh" [dependencies] zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } +json5 = "0.4.1" diff --git a/zenoh-flat/src/config.rs b/zenoh-flat/src/config.rs new file mode 100644 index 00000000..18f33175 --- /dev/null +++ b/zenoh-flat/src/config.rs @@ -0,0 +1,24 @@ +// 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 crate::zerror; +use zenoh::config::Config; + +/// Create a Zenoh configuration from a JSON string. +pub fn create_config_from_json(json: &str) -> crate::errors::ZResult { + let mut deserializer = json5::Deserializer::from_str(json).map_err(|err| zerror!(err))?; + Config::from_deserializer(&mut deserializer).map_err(|err| match err { + Ok(c) => zerror!("Invalid configuration: {}", c), + Err(e) => zerror!("JSON error: {}", e), + }) +} diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index 3569ad96..583f1bb0 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -1,5 +1,6 @@ //! `zenoh-flat` is a placeholder Rust crate for Zenoh flat data support. +pub mod config; pub mod errors; pub mod session; diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index d35c62b8..1f0c7377 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -3946,6 +3946,7 @@ dependencies = [ name = "zenoh-flat" version = "1.9.0" dependencies = [ + "json5", "zenoh", ] diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index c456c02b..3015f57c 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -111,13 +111,8 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( /// fn open_session_with_json_config(env: &mut JNIEnv, json_config: JString) -> ZResult { let json_config = decode_string(env, &json_config)?; - let mut deserializer = - json5::Deserializer::from_str(&json_config).map_err(|err| zerror!(err))?; - let config = Config::from_deserializer(&mut deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("JSON error: {}", e), - })?; - zenoh::open(config).wait().map_err(|err| zerror!(err)) + let config = zenoh_flat::config::create_config_from_json(&json_config)?; + zenoh_flat::session::open_session(&config) } /// Open a Zenoh session with a YAML configuration. From 65c9d5578d9548c406b1648b11d607677210f12d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 13:07:52 +0200 Subject: [PATCH 058/126] Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI refactor --- zenoh-jni/src/session.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 3015f57c..5053713f 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -96,23 +96,32 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( _class: JClass, json_config: JString, ) -> *const Session { - let session = open_session_with_json_config(&mut env, json_config); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), + let json_config = match decode_string(&mut env, &json_config) { + Ok(config) => config, + Err(err) => { + throw_exception!(env, err); + return null(); + } + }; + + let config = match zenoh_flat::config::create_config_from_json(&json_config) { + Ok(config) => config, + Err(err) => { + throw_exception!(env, err); + return null(); + } + }; + + let session = match zenoh_flat::session::open_session(&config) { + Ok(session) => session, Err(err) => { tracing::error!("Unable to open session: {}", err); throw_exception!(env, zerror!(err)); - null() + return null(); } - } -} + }; -/// Open a Zenoh session with the provided json configuration. -/// -fn open_session_with_json_config(env: &mut JNIEnv, json_config: JString) -> ZResult { - let json_config = decode_string(env, &json_config)?; - let config = zenoh_flat::config::create_config_from_json(&json_config)?; - zenoh_flat::session::open_session(&config) + Arc::into_raw(Arc::new(session)) } /// Open a Zenoh session with a YAML configuration. From 49504d65d0b93e0130ca90705c2eac78320f4f7b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 13:23:16 +0200 Subject: [PATCH 059/126] config from yaml in zenoh flat --- zenoh-flat/Cargo.toml | 1 + zenoh-flat/src/config.rs | 9 +++++++++ zenoh-jni/Cargo.lock | 1 + zenoh-jni/src/session.rs | 37 +++++++++++++++++++++---------------- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 18e8528c..1da9485f 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -9,3 +9,4 @@ repository = "https://github.com/eclipse-zenoh/zenoh" [dependencies] zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } json5 = "0.4.1" +serde_yaml = "0.9.19" diff --git a/zenoh-flat/src/config.rs b/zenoh-flat/src/config.rs index 18f33175..1ba58d93 100644 --- a/zenoh-flat/src/config.rs +++ b/zenoh-flat/src/config.rs @@ -22,3 +22,12 @@ pub fn create_config_from_json(json: &str) -> crate::errors::ZResult { Err(e) => zerror!("JSON error: {}", e), }) } + +/// Create a Zenoh configuration from a YAML string. +pub fn create_config_from_yaml(yaml: &str) -> crate::errors::ZResult { + let deserializer = serde_yaml::Deserializer::from_str(yaml); + Config::from_deserializer(deserializer).map_err(|err| match err { + Ok(c) => zerror!("Invalid configuration: {}", c), + Err(e) => zerror!("YAML error: {}", e), + }) +} diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index 1f0c7377..ed69c575 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -3947,6 +3947,7 @@ name = "zenoh-flat" version = "1.9.0" dependencies = [ "json5", + "serde_yaml", "zenoh", ] diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 5053713f..c8a5f226 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -144,27 +144,32 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI( _class: JClass, yaml_config: JString, ) -> *const Session { - let session = open_session_with_yaml_config(&mut env, yaml_config); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), + let yaml_config = match decode_string(&mut env, &yaml_config) { + Ok(config) => config, + Err(err) => { + throw_exception!(env, err); + return null(); + } + }; + + let config = match zenoh_flat::config::create_config_from_yaml(&yaml_config) { + Ok(config) => config, + Err(err) => { + throw_exception!(env, err); + return null(); + } + }; + + let session = match zenoh_flat::session::open_session(&config) { + Ok(session) => session, Err(err) => { tracing::error!("Unable to open session: {}", err); throw_exception!(env, zerror!(err)); - null() + return null(); } - } -} + }; -/// Open a Zenoh session with the provided yaml configuration. -/// -fn open_session_with_yaml_config(env: &mut JNIEnv, yaml_config: JString) -> ZResult { - let yaml_config = decode_string(env, &yaml_config)?; - let deserializer = serde_yaml::Deserializer::from_str(&yaml_config); - let config = Config::from_deserializer(deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("YAML error: {}", e), - })?; - zenoh::open(config).wait().map_err(|err| zerror!(err)) + Arc::into_raw(Arc::new(session)) } /// Closes a Zenoh session via JNI. From 172750f4a8915bbdfa0d3a918cd2496dc8d1f870 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 13:51:07 +0200 Subject: [PATCH 060/126] unused functions removed --- zenoh-flat/src/config.rs | 8 ++-- zenoh-jni/src/config.rs | 13 +----- zenoh-jni/src/session.rs | 88 ++++------------------------------------ 3 files changed, 14 insertions(+), 95 deletions(-) diff --git a/zenoh-flat/src/config.rs b/zenoh-flat/src/config.rs index 1ba58d93..35328830 100644 --- a/zenoh-flat/src/config.rs +++ b/zenoh-flat/src/config.rs @@ -14,8 +14,8 @@ use crate::zerror; use zenoh::config::Config; -/// Create a Zenoh configuration from a JSON string. -pub fn create_config_from_json(json: &str) -> crate::errors::ZResult { +/// Load a Zenoh configuration from a JSON string. +pub fn load_json_config(json: &str) -> crate::errors::ZResult { let mut deserializer = json5::Deserializer::from_str(json).map_err(|err| zerror!(err))?; Config::from_deserializer(&mut deserializer).map_err(|err| match err { Ok(c) => zerror!("Invalid configuration: {}", c), @@ -23,8 +23,8 @@ pub fn create_config_from_json(json: &str) -> crate::errors::ZResult { }) } -/// Create a Zenoh configuration from a YAML string. -pub fn create_config_from_yaml(yaml: &str) -> crate::errors::ZResult { +/// Load a Zenoh configuration from a YAML string. +pub fn load_yaml_config(yaml: &str) -> crate::errors::ZResult { let deserializer = serde_yaml::Deserializer::from_str(yaml); Config::from_deserializer(deserializer).map_err(|err| match err { Ok(c) => zerror!("Invalid configuration: {}", c), diff --git a/zenoh-jni/src/config.rs b/zenoh-jni/src/config.rs index a63fb8be..cfae24f4 100644 --- a/zenoh-jni/src/config.rs +++ b/zenoh-jni/src/config.rs @@ -79,12 +79,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadJsonConfigViaJN ) -> *const Config { || -> ZResult<*const Config> { let json_config = decode_string(&mut env, &json_config)?; - let mut deserializer = - json5::Deserializer::from_str(&json_config).map_err(|err| zerror!(err))?; - let config = Config::from_deserializer(&mut deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("JSON error: {}", e), - })?; + let config = zenoh_flat::config::load_json_config(&json_config)?; Ok(Arc::into_raw(Arc::new(config))) }() .unwrap_or_else(|err| { @@ -108,11 +103,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadYamlConfigViaJN ) -> *const Config { || -> ZResult<*const Config> { let yaml_config = decode_string(&mut env, &yaml_config)?; - let deserializer = serde_yaml::Deserializer::from_str(&yaml_config); - let config = Config::from_deserializer(deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("YAML error: {}", e), - })?; + let config = zenoh_flat::config::load_yaml_config(&yaml_config)?; Ok(Arc::into_raw(Arc::new(config))) }() .unwrap_or_else(|err| { diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index c8a5f226..b52e02e6 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -76,102 +76,30 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( } } -/// Open a Zenoh session with a JSON configuration. +/// Create a Zenoh configuration from a JSON string. /// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. +/// It returns an [Arc] raw pointer to the Zenoh [Config] object, which should be passed +/// to `openSessionViaJNI`. /// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. +/// If creation fails, an exception is thrown on the JVM and a null pointer is returned. /// /// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class (parameter required by the JNI interface but unused). /// - `json_config`: Configuration as a JSON string. /// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( - mut env: JNIEnv, - _class: JClass, - json_config: JString, -) -> *const Session { - let json_config = match decode_string(&mut env, &json_config) { - Ok(config) => config, - Err(err) => { - throw_exception!(env, err); - return null(); - } - }; - - let config = match zenoh_flat::config::create_config_from_json(&json_config) { - Ok(config) => config, - Err(err) => { - throw_exception!(env, err); - return null(); - } - }; - - let session = match zenoh_flat::session::open_session(&config) { - Ok(session) => session, - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - return null(); - } - }; - - Arc::into_raw(Arc::new(session)) -} - -/// Open a Zenoh session with a YAML configuration. +/// Create a Zenoh configuration from a YAML string. /// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. +/// It returns an [Arc] raw pointer to the Zenoh [Config] object, which should be passed +/// to `openSessionViaJNI`. /// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. +/// If creation fails, an exception is thrown on the JVM and a null pointer is returned. /// /// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class (parameter required by the JNI interface but unused). /// - `yaml_config`: Configuration as a YAML string. /// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI( - mut env: JNIEnv, - _class: JClass, - yaml_config: JString, -) -> *const Session { - let yaml_config = match decode_string(&mut env, &yaml_config) { - Ok(config) => config, - Err(err) => { - throw_exception!(env, err); - return null(); - } - }; - - let config = match zenoh_flat::config::create_config_from_yaml(&yaml_config) { - Ok(config) => config, - Err(err) => { - throw_exception!(env, err); - return null(); - } - }; - - let session = match zenoh_flat::session::open_session(&config) { - Ok(session) => session, - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - return null(); - } - }; - - Arc::into_raw(Arc::new(session)) -} - /// Closes a Zenoh session via JNI. /// /// # Parameters: From bc56917d3ab1de0a5eca35b29c02f6a675493a67 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 13:52:18 +0200 Subject: [PATCH 061/126] remained comment removed --- zenoh-jni/src/session.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index b52e02e6..5ef5f170 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -76,30 +76,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( } } -/// Create a Zenoh configuration from a JSON string. -/// -/// It returns an [Arc] raw pointer to the Zenoh [Config] object, which should be passed -/// to `openSessionViaJNI`. -/// -/// If creation fails, an exception is thrown on the JVM and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `json_config`: Configuration as a JSON string. -/// -/// Create a Zenoh configuration from a YAML string. -/// -/// It returns an [Arc] raw pointer to the Zenoh [Config] object, which should be passed -/// to `openSessionViaJNI`. -/// -/// If creation fails, an exception is thrown on the JVM and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `yaml_config`: Configuration as a YAML string. -/// /// Closes a Zenoh session via JNI. /// /// # Parameters: From a821b8d3228954bd33938df45aae5a178f85e188 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 14:02:10 +0200 Subject: [PATCH 062/126] session close in zenoh-flat --- zenoh-flat/src/session.rs | 7 +++++++ zenoh-jni/src/session.rs | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 7493fef4..f5e103c1 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -11,6 +11,8 @@ // ZettaScale Zenoh Team, // +use std::sync::Arc; + use crate::zerror; use zenoh::{config::Config, session::Session, Wait}; @@ -20,3 +22,8 @@ pub fn open_session(config: &Config) -> crate::errors::ZResult { .wait() .map_err(|err| zerror!(err)) } + +/// Close a Zenoh session taking ownership of the session object. +pub fn close_session(session: Arc) { + drop(session); +} diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 5ef5f170..c9a7dabe 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -96,7 +96,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( _class: JClass, session_ptr: *const Session, ) { - Arc::from_raw(session_ptr); + let session = Arc::from_raw(session_ptr); + zenoh_flat::session::close_session(session); } /// Declare a Zenoh publisher via JNI. From b990b7c8091a135f7f956ca14b0ebb5e1aa2d086 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 15:19:33 +0200 Subject: [PATCH 063/126] session close --- zenoh-flat/src/session.rs | 8 +++----- zenoh-jni/src/session.rs | 4 +++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index f5e103c1..c80ec7a2 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -11,8 +11,6 @@ // ZettaScale Zenoh Team, // -use std::sync::Arc; - use crate::zerror; use zenoh::{config::Config, session::Session, Wait}; @@ -23,7 +21,7 @@ pub fn open_session(config: &Config) -> crate::errors::ZResult { .map_err(|err| zerror!(err)) } -/// Close a Zenoh session taking ownership of the session object. -pub fn close_session(session: Arc) { - drop(session); +/// Close a Zenoh session using a reference to the session. +pub fn close_session(session: &Session) -> crate::errors::ZResult<()> { + session.close().wait().map_err(|err| zerror!(err)) } diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index c9a7dabe..766c4fb2 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -97,7 +97,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( session_ptr: *const Session, ) { let session = Arc::from_raw(session_ptr); - zenoh_flat::session::close_session(session); + if let Err(err) = zenoh_flat::session::close_session(&session) { + throw_exception!(env, err); + } } /// Declare a Zenoh publisher via JNI. From 30844196972edc317663f4487997c3b3f3da6d30 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 15:24:50 +0200 Subject: [PATCH 064/126] use --- zenoh-flat/src/session.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index c80ec7a2..4236d658 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -11,17 +11,17 @@ // ZettaScale Zenoh Team, // -use crate::zerror; +use crate::{errors::ZResult, zerror}; use zenoh::{config::Config, session::Session, Wait}; /// Open a Zenoh session using a borrowed configuration. -pub fn open_session(config: &Config) -> crate::errors::ZResult { +pub fn open_session(config: &Config) -> ZResult { zenoh::open(config.clone()) .wait() .map_err(|err| zerror!(err)) } /// Close a Zenoh session using a reference to the session. -pub fn close_session(session: &Session) -> crate::errors::ZResult<()> { +pub fn close_session(session: &Session) -> ZResult<()> { session.close().wait().map_err(|err| zerror!(err)) } From e0f380eb752c6fa0fa95dcc8360871e9c9f0baf5 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 15:31:20 +0200 Subject: [PATCH 065/126] declate_publisher in zenoh flat --- zenoh-flat/Cargo.lock | 2614 +++++++++++++++++++++++++++++++++++++ zenoh-flat/src/session.rs | 28 +- zenoh-jni/src/session.rs | 20 +- 3 files changed, 2650 insertions(+), 12 deletions(-) create mode 100644 zenoh-flat/Cargo.lock diff --git a/zenoh-flat/Cargo.lock b/zenoh-flat/Cargo.lock new file mode 100644 index 00000000..c7cd53e3 --- /dev/null +++ b/zenoh-flat/Cargo.lock @@ -0,0 +1,2614 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keyed-set" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d255a6b6ecd77bb93ce91de984d7039bff7503f500eb4851a1269732f22baf" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nonempty-collections" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e216d0e8cf9d54fa66e5780f6e1d5dc96d1c1b3c25aeba3b6758548bcbbd8b9d" +dependencies = [ + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "serde", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ringbuffer-spsc" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3e7aa0a681b232e7cd7f856a53b10603df88ca74b79a8d8088845185492e35" +dependencies = [ + "array-init", + "crossbeam", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "either", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "stabby" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976399a0c48ea769ef7f5dc303bb88240ab8d84008647a6b2303eced3dab3945" +dependencies = [ + "rustversion", + "stabby-abi", +] + +[[package]] +name = "stabby-abi" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b54832a9a1f92a0e55e74a5c0332744426edc515bb3fbad82f10b874a87f0d" +dependencies = [ + "rustc_version", + "rustversion", + "sha2-const-stable", + "stabby-macros", +] + +[[package]] +name = "stabby-macros" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a768b1e51e4dbfa4fa52ae5c01241c0a41e2938fdffbb84add0c8238092f9091" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "rand", + "syn 1.0.109", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "token-cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb48920ae769b58126c8c93269805011c793201f95fde28b479b81a9a531bbde" +dependencies = [ + "paste", + "portable-atomic", + "rustversion", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uhlc" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62a645e3e4e6c85b7abe49b086aa3204119431f42b6123b0070419fb6e9d24e" +dependencies = [ + "humantime", + "lazy_static", + "log", + "rand", + "serde", + "spin 0.10.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unzip-n" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5bb2756c16fb66f80cfbf5fb0e0c09a7001e739f453c9ec241b9c8b1556fda" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "validated_struct" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869a93e8a7286e339e1128630051d82babbcd75d585975af07b9f3327220e60e" +dependencies = [ + "json5", + "serde", + "serde_json", + "validated_struct_macros", +] + +[[package]] +name = "validated_struct_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c44ce98e7227a04eeb4cf9c784109a5c9710e54849ceb4f09f8597247897f1e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "unzip-n", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zenoh" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "ahash", + "arc-swap", + "async-trait", + "bytes", + "const_format", + "flate2", + "flume", + "futures", + "git-version", + "itertools", + "json5", + "lazy_static", + "nonempty-collections", + "once_cell", + "petgraph", + "phf", + "rand", + "rustc_version", + "serde", + "serde_json", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "uhlc", + "vec_map", + "zenoh-buffers", + "zenoh-codec", + "zenoh-collections", + "zenoh-config", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-link", + "zenoh-link-commons", + "zenoh-macros", + "zenoh-plugin-trait", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-transport", + "zenoh-util", +] + +[[package]] +name = "zenoh-buffers" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "zenoh-collections", +] + +[[package]] +name = "zenoh-codec" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "tracing", + "uhlc", + "zenoh-buffers", + "zenoh-protocol", +] + +[[package]] +name = "zenoh-collections" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "ahash", +] + +[[package]] +name = "zenoh-config" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "json5", + "nonempty-collections", + "num_cpus", + "secrecy", + "serde", + "serde_json", + "serde_with", + "serde_yaml", + "toml", + "tracing", + "uhlc", + "validated_struct", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-protocol", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-core" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "lazy_static", + "tokio", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-crypto" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "aes", + "hmac", + "rand", + "rand_chacha", + "sha3", + "zenoh-result", +] + +[[package]] +name = "zenoh-flat" +version = "1.9.0" +dependencies = [ + "json5", + "serde_yaml", + "zenoh", +] + +[[package]] +name = "zenoh-keyexpr" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "getrandom 0.2.17", + "hashbrown 0.16.1", + "keyed-set", + "rand", + "schemars 1.2.1", + "serde", + "token-cell", + "zenoh-result", +] + +[[package]] +name = "zenoh-link" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "zenoh-config", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-commons" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "async-trait", + "flume", + "futures", + "serde", + "socket2 0.5.10", + "time", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-core", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-util", +] + +[[package]] +name = "zenoh-macros" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "zenoh-keyexpr", +] + +[[package]] +name = "zenoh-plugin-trait" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "git-version", + "libloading", + "serde", + "stabby", + "tracing", + "zenoh-config", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-protocol" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "const_format", + "rand", + "serde", + "uhlc", + "zenoh-buffers", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-result" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "anyhow", +] + +[[package]] +name = "zenoh-runtime" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "lazy_static", + "ron", + "serde", + "tokio", + "tracing", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-sync" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "arc-swap", + "event-listener", + "futures", + "tokio", + "zenoh-buffers", + "zenoh-collections", + "zenoh-core", +] + +[[package]] +name = "zenoh-task" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "futures", + "tokio", + "tokio-util", + "tracing", + "zenoh-core", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-transport" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "async-trait", + "crossbeam-utils", + "flume", + "futures", + "lazy_static", + "lz4_flex", + "rand", + "ringbuffer-spsc", + "serde", + "sha3", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-config", + "zenoh-core", + "zenoh-crypto", + "zenoh-link", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-util", +] + +[[package]] +name = "zenoh-util" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +dependencies = [ + "async-trait", + "const_format", + "flume", + "home", + "humantime", + "lazy_static", + "libc", + "libloading", + "pnet_datalink", + "schemars 1.2.1", + "serde", + "serde_json", + "shellexpand", + "tokio", + "tracing", + "tracing-subscriber", + "winapi", + "zenoh-core", + "zenoh-result", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 4236d658..e65397c8 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -12,7 +12,14 @@ // use crate::{errors::ZResult, zerror}; -use zenoh::{config::Config, session::Session, Wait}; +use zenoh::{ + config::Config, + key_expr::KeyExpr, + pubsub::Publisher, + qos::{CongestionControl, Priority, Reliability}, + session::Session, + Wait, +}; /// Open a Zenoh session using a borrowed configuration. pub fn open_session(config: &Config) -> ZResult { @@ -21,6 +28,25 @@ pub fn open_session(config: &Config) -> ZResult { .map_err(|err| zerror!(err)) } +/// Declare a publisher through an existing Zenoh session. +pub fn declare_publisher( + session: &Session, + key_expr: KeyExpr<'static>, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + reliability: Reliability, +) -> ZResult> { + session + .declare_publisher(key_expr) + .congestion_control(congestion_control) + .priority(priority) + .express(express) + .reliability(reliability) + .wait() + .map_err(|err| zerror!(err)) +} + /// Close a Zenoh session using a reference to the session. pub fn close_session(session: &Session) -> ZResult<()> { session.close().wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 766c4fb2..768cd12e 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -146,17 +146,15 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; let reliability = decode_reliability(reliability)?; - let result = session - .declare_publisher(key_expr) - .congestion_control(congestion_control) - .priority(priority) - .express(is_express != 0) - .reliability(reliability) - .wait(); - match result { - Ok(publisher) => Ok(Arc::into_raw(Arc::new(publisher))), - Err(err) => Err(zerror!(err)), - } + let publisher = zenoh_flat::session::declare_publisher( + &session, + key_expr, + congestion_control, + priority, + is_express != 0, + reliability, + )?; + Ok(Arc::into_raw(Arc::new(publisher))) }() .unwrap_or_else(|err| { throw_exception!(env, err); From c3b900dce82a97aaa5ccaf074085de494618c092 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 15:41:24 +0200 Subject: [PATCH 066/126] put operation --- zenoh-flat/src/session.rs | 28 ++++++++++++++++++++++++++++ zenoh-jni/src/session.rs | 34 ++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index e65397c8..6fac4c9a 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -13,6 +13,7 @@ use crate::{errors::ZResult, zerror}; use zenoh::{ + bytes::Encoding, config::Config, key_expr::KeyExpr, pubsub::Publisher, @@ -47,6 +48,33 @@ pub fn declare_publisher( .map_err(|err| zerror!(err)) } +/// Perform a put operation through an existing Zenoh session. +pub fn put( + session: &Session, + key_expr: KeyExpr<'static>, + payload: Vec, + encoding: Encoding, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + reliability: Reliability, + attachment: Option>, +) -> ZResult<()> { + let mut put_builder = session + .put(&key_expr, payload) + .congestion_control(congestion_control) + .encoding(encoding) + .express(express) + .priority(priority) + .reliability(reliability); + + if let Some(attachment) = attachment { + put_builder = put_builder.attachment(attachment); + } + + put_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) +} + /// Close a Zenoh session using a reference to the session. pub fn close_session(session: &Session) -> ZResult<()> { session.close().wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 768cd12e..94167376 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -213,24 +213,26 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; let reliability = decode_reliability(reliability)?; + let key_expr_string = key_expr.to_string(); - let mut put_builder = session - .put(&key_expr, payload) - .congestion_control(congestion_control) - .encoding(encoding) - .express(is_express != 0) - .priority(priority) - .reliability(reliability); - - if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; - put_builder = put_builder.attachment(attachment) - } + let attachment = if !attachment.is_null() { + Some(decode_byte_array(&env, attachment)?) + } else { + None + }; - put_builder - .wait() - .map(|_| tracing::trace!("Put on '{key_expr}'")) - .map_err(|err| zerror!(err)) + zenoh_flat::session::put( + &session, + key_expr, + payload, + encoding, + congestion_control, + priority, + is_express != 0, + reliability, + attachment, + ) + .map(|_| tracing::trace!("Put on '{}'", key_expr_string)) }() .map_err(|err| throw_exception!(env, err)); } From b9e00884fd6d43c2df3e68a0df28cea31396db3e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 15:44:55 +0200 Subject: [PATCH 067/126] session delete --- zenoh-flat/src/session.rs | 24 ++++++++++++++++++++++++ zenoh-jni/src/session.rs | 31 ++++++++++++++++--------------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 6fac4c9a..00de9c0e 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -75,6 +75,30 @@ pub fn put( put_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) } +/// Perform a delete operation through an existing Zenoh session. +pub fn delete( + session: &Session, + key_expr: KeyExpr<'static>, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + reliability: Reliability, + attachment: Option>, +) -> ZResult<()> { + let mut delete_builder = session + .delete(&key_expr) + .congestion_control(congestion_control) + .express(express) + .priority(priority) + .reliability(reliability); + + if let Some(attachment) = attachment { + delete_builder = delete_builder.attachment(attachment); + } + + delete_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) +} + /// Close a Zenoh session using a reference to the session. pub fn close_session(session: &Session) -> ZResult<()> { session.close().wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 94167376..ba08cf37 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -281,23 +281,24 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; let reliability = decode_reliability(reliability)?; + let key_expr_string = key_expr.to_string(); - let mut delete_builder = session - .delete(&key_expr) - .congestion_control(congestion_control) - .express(is_express != 0) - .priority(priority) - .reliability(reliability); - - if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; - delete_builder = delete_builder.attachment(attachment) - } + let attachment = if !attachment.is_null() { + Some(decode_byte_array(&env, attachment)?) + } else { + None + }; - delete_builder - .wait() - .map(|_| tracing::trace!("Delete on '{key_expr}'")) - .map_err(|err| zerror!(err)) + zenoh_flat::session::delete( + &session, + key_expr, + congestion_control, + priority, + is_express != 0, + reliability, + attachment, + ) + .map(|_| tracing::trace!("Delete on '{}'", key_expr_string)) }() .map_err(|err| throw_exception!(env, err)); } From d4714de620734f17ce85cf150c7e59404bd5ea21 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 15:49:51 +0200 Subject: [PATCH 068/126] trace in zenoh-flat --- zenoh-flat/Cargo.lock | 1 + zenoh-flat/Cargo.toml | 1 + zenoh-flat/src/session.rs | 68 +++++++++++++++++++++++++++------------ zenoh-jni/Cargo.lock | 1 + zenoh-jni/src/session.rs | 4 --- 5 files changed, 50 insertions(+), 25 deletions(-) diff --git a/zenoh-flat/Cargo.lock b/zenoh-flat/Cargo.lock index c7cd53e3..8a4a5459 100644 --- a/zenoh-flat/Cargo.lock +++ b/zenoh-flat/Cargo.lock @@ -2378,6 +2378,7 @@ version = "1.9.0" dependencies = [ "json5", "serde_yaml", + "tracing", "zenoh", ] diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 1da9485f..4c7bc830 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -10,3 +10,4 @@ repository = "https://github.com/eclipse-zenoh/zenoh" zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } json5 = "0.4.1" serde_yaml = "0.9.19" +tracing = "0.1" diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 00de9c0e..5b2587df 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -12,6 +12,7 @@ // use crate::{errors::ZResult, zerror}; +use tracing::trace; use zenoh::{ bytes::Encoding, config::Config, @@ -38,14 +39,21 @@ pub fn declare_publisher( express: bool, reliability: Reliability, ) -> ZResult> { - session + let key_expr_string = key_expr.to_string(); + let result = session .declare_publisher(key_expr) .congestion_control(congestion_control) .priority(priority) .express(express) .reliability(reliability) .wait() - .map_err(|err| zerror!(err)) + .map_err(|err| zerror!(err)); + + if result.is_ok() { + trace!("Declared publisher on '{}'.", key_expr_string); + } + + result } /// Perform a put operation through an existing Zenoh session. @@ -60,19 +68,28 @@ pub fn put( reliability: Reliability, attachment: Option>, ) -> ZResult<()> { - let mut put_builder = session - .put(&key_expr, payload) - .congestion_control(congestion_control) - .encoding(encoding) - .express(express) - .priority(priority) - .reliability(reliability); + let key_expr_string = key_expr.to_string(); + let result = { + let mut put_builder = session + .put(&key_expr, payload) + .congestion_control(congestion_control) + .encoding(encoding) + .express(express) + .priority(priority) + .reliability(reliability); - if let Some(attachment) = attachment { - put_builder = put_builder.attachment(attachment); + if let Some(attachment) = attachment { + put_builder = put_builder.attachment(attachment); + } + + put_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) + }; + + if result.is_ok() { + trace!("Put on '{}'.", key_expr_string); } - put_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) + result } /// Perform a delete operation through an existing Zenoh session. @@ -85,18 +102,27 @@ pub fn delete( reliability: Reliability, attachment: Option>, ) -> ZResult<()> { - let mut delete_builder = session - .delete(&key_expr) - .congestion_control(congestion_control) - .express(express) - .priority(priority) - .reliability(reliability); + let key_expr_string = key_expr.to_string(); + let result = { + let mut delete_builder = session + .delete(&key_expr) + .congestion_control(congestion_control) + .express(express) + .priority(priority) + .reliability(reliability); + + if let Some(attachment) = attachment { + delete_builder = delete_builder.attachment(attachment); + } + + delete_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) + }; - if let Some(attachment) = attachment { - delete_builder = delete_builder.attachment(attachment); + if result.is_ok() { + trace!("Delete on '{}'.", key_expr_string); } - delete_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) + result } /// Close a Zenoh session using a reference to the session. diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index ed69c575..42d4ec42 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -3948,6 +3948,7 @@ version = "1.9.0" dependencies = [ "json5", "serde_yaml", + "tracing", "zenoh", ] diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index ba08cf37..99ddaf3d 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -213,7 +213,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; let reliability = decode_reliability(reliability)?; - let key_expr_string = key_expr.to_string(); let attachment = if !attachment.is_null() { Some(decode_byte_array(&env, attachment)?) @@ -232,7 +231,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( reliability, attachment, ) - .map(|_| tracing::trace!("Put on '{}'", key_expr_string)) }() .map_err(|err| throw_exception!(env, err)); } @@ -281,7 +279,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; let reliability = decode_reliability(reliability)?; - let key_expr_string = key_expr.to_string(); let attachment = if !attachment.is_null() { Some(decode_byte_array(&env, attachment)?) @@ -298,7 +295,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( reliability, attachment, ) - .map(|_| tracing::trace!("Delete on '{}'", key_expr_string)) }() .map_err(|err| throw_exception!(env, err)); } From 0ede86e44c135a951b0bd00287a79a24c4f934cf Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 15:54:36 +0200 Subject: [PATCH 069/126] systematic tracing in zenoh-flat --- zenoh-flat/src/session.rs | 109 ++++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 5b2587df..0529c268 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -12,7 +12,7 @@ // use crate::{errors::ZResult, zerror}; -use tracing::trace; +use tracing::{error, trace}; use zenoh::{ bytes::Encoding, config::Config, @@ -27,7 +27,14 @@ use zenoh::{ pub fn open_session(config: &Config) -> ZResult { zenoh::open(config.clone()) .wait() - .map_err(|err| zerror!(err)) + .map(|session| { + trace!("Opened Zenoh session."); + session + }) + .map_err(|err| { + error!("Unable to open session: {}", err); + zerror!(err) + }) } /// Declare a publisher through an existing Zenoh session. @@ -40,20 +47,21 @@ pub fn declare_publisher( reliability: Reliability, ) -> ZResult> { let key_expr_string = key_expr.to_string(); - let result = session + session .declare_publisher(key_expr) .congestion_control(congestion_control) .priority(priority) .express(express) .reliability(reliability) .wait() - .map_err(|err| zerror!(err)); - - if result.is_ok() { - trace!("Declared publisher on '{}'.", key_expr_string); - } - - result + .map(|publisher| { + trace!("Declared publisher on '{}'.", key_expr_string); + publisher + }) + .map_err(|err| { + error!("Unable to declare publisher on '{}': {}", key_expr_string, err); + zerror!(err) + }) } /// Perform a put operation through an existing Zenoh session. @@ -69,27 +77,27 @@ pub fn put( attachment: Option>, ) -> ZResult<()> { let key_expr_string = key_expr.to_string(); - let result = { - let mut put_builder = session - .put(&key_expr, payload) - .congestion_control(congestion_control) - .encoding(encoding) - .express(express) - .priority(priority) - .reliability(reliability); - - if let Some(attachment) = attachment { - put_builder = put_builder.attachment(attachment); - } - - put_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) - }; + let mut put_builder = session + .put(&key_expr, payload) + .congestion_control(congestion_control) + .encoding(encoding) + .express(express) + .priority(priority) + .reliability(reliability); - if result.is_ok() { - trace!("Put on '{}'.", key_expr_string); + if let Some(attachment) = attachment { + put_builder = put_builder.attachment(attachment); } - result + put_builder + .wait() + .map(|_| { + trace!("Put on '{}'.", key_expr_string); + }) + .map_err(|err| { + error!("Unable to put on '{}': {}", key_expr_string, err); + zerror!(err) + }) } /// Perform a delete operation through an existing Zenoh session. @@ -103,29 +111,38 @@ pub fn delete( attachment: Option>, ) -> ZResult<()> { let key_expr_string = key_expr.to_string(); - let result = { - let mut delete_builder = session - .delete(&key_expr) - .congestion_control(congestion_control) - .express(express) - .priority(priority) - .reliability(reliability); - - if let Some(attachment) = attachment { - delete_builder = delete_builder.attachment(attachment); - } - - delete_builder.wait().map_err(|err| zerror!(err)).map(|_| ()) - }; + let mut delete_builder = session + .delete(&key_expr) + .congestion_control(congestion_control) + .express(express) + .priority(priority) + .reliability(reliability); - if result.is_ok() { - trace!("Delete on '{}'.", key_expr_string); + if let Some(attachment) = attachment { + delete_builder = delete_builder.attachment(attachment); } - result + delete_builder + .wait() + .map(|_| { + trace!("Delete on '{}'.", key_expr_string); + }) + .map_err(|err| { + error!("Unable to delete on '{}': {}", key_expr_string, err); + zerror!(err) + }) } /// Close a Zenoh session using a reference to the session. pub fn close_session(session: &Session) -> ZResult<()> { - session.close().wait().map_err(|err| zerror!(err)) + session + .close() + .wait() + .map(|_| { + trace!("Closed Zenoh session."); + }) + .map_err(|err| { + error!("Unable to close session: {}", err); + zerror!(err) + }) } From efb60387b1283cbf3bc43bf51e8d45713dc2faa5 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 16:05:24 +0200 Subject: [PATCH 070/126] declare_subscriber --- zenoh-flat/src/session.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 0529c268..0800ae2a 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -17,7 +17,8 @@ use zenoh::{ bytes::Encoding, config::Config, key_expr::KeyExpr, - pubsub::Publisher, + pubsub::{Publisher, Subscriber}, + sample::Sample, qos::{CongestionControl, Priority, Reliability}, session::Session, Wait, @@ -64,6 +65,30 @@ pub fn declare_publisher( }) } +/// Declare a subscriber through an existing Zenoh session. +pub fn declare_subscriber( + session: &Session, + key_expr: KeyExpr<'static>, + callback: F, +) -> ZResult> +where + F: Fn(Sample) + Send + Sync + 'static, +{ + let key_expr_string = key_expr.to_string(); + session + .declare_subscriber(key_expr) + .callback(callback) + .wait() + .map(|subscriber| { + trace!("Declared subscriber on '{}'.", key_expr_string); + subscriber + }) + .map_err(|err| { + error!("Unable to declare subscriber on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + /// Perform a put operation through an existing Zenoh session. pub fn put( session: &Session, From a7fdb468c6e5b344c48b68d6355bbb3f1c728d98 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 16:06:24 +0200 Subject: [PATCH 071/126] impl syntax --- zenoh-flat/src/session.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 0800ae2a..912c6f79 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -66,14 +66,11 @@ pub fn declare_publisher( } /// Declare a subscriber through an existing Zenoh session. -pub fn declare_subscriber( +pub fn declare_subscriber( session: &Session, key_expr: KeyExpr<'static>, - callback: F, -) -> ZResult> -where - F: Fn(Sample) + Send + Sync + 'static, -{ + callback: impl Fn(Sample) + Send + Sync + 'static, +) -> ZResult> { let key_expr_string = key_expr.to_string(); session .declare_subscriber(key_expr) From 0a4906264eb7fee5919f6c0f04e0a3dcd6051e45 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 16:14:32 +0200 Subject: [PATCH 072/126] process callback method --- zenoh-jni/src/sample_callback.rs | 73 ++++++++++++++++++++++++++++++++ zenoh-jni/src/session.rs | 4 +- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index e105b66d..9e8cd677 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -28,6 +28,79 @@ use zenoh::{ use crate::{errors::ZResult, utils::*}; +pub(crate) unsafe fn process_kotlin_sample_callback( + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let on_close = load_on_close(&java_vm, on_close_global_ref); + + Ok(move |sample: Sample| { + on_close.noop(); + let _ = || -> ZResult<()> { + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + zerror!("Unable to attach thread for sample callback: {}", err) + })?; + let byte_array = bytes_to_java_array(&env, sample.payload()) + .map(|array| env.auto_local(array))?; + + let encoding_id: jint = sample.encoding().id() as jint; + let encoding_schema = match sample.encoding().schema() { + Some(schema) => slice_to_java_string(&env, schema)?, + None => JString::default(), + }; + let kind = sample.kind() as jint; + let (timestamp, is_valid) = sample + .timestamp() + .map(|timestamp| (timestamp.get_time().as_u64(), true)) + .unwrap_or((0, false)); + + let attachment_bytes = sample + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(&env, attachment), + ) + .map(|array| env.auto_local(array)) + .map_err(|err| zerror!("Error processing attachment: {}", err))?; + + let key_expr_str = env.auto_local( + env.new_string(sample.key_expr().to_string()) + .map_err(|err| zerror!("Error processing sample key expr: {}", err))?, + ); + + let express = sample.express(); + let priority = sample.priority() as jint; + let cc = sample.congestion_control() as jint; + + env.call_method( + &callback_global_ref, + "run", + "(Ljava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&key_expr_str), + JValue::from(&byte_array), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + JValue::from(kind), + JValue::from(timestamp as i64), + JValue::from(is_valid), + JValue::from(&attachment_bytes), + JValue::from(express), + JValue::from(priority), + JValue::from(cc), + ], + ) + .map_err(|err| zerror!(err))?; + Ok(()) + }() + .map_err(|err| tracing::error!("On sample callback error: {err}")); + }) +} + pub(crate) trait SetJniSampleCallback: Sized + HasSampleCallbackSetter { unsafe fn set_jni_sample_callback( self, diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 99ddaf3d..7082b7a8 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -30,7 +30,7 @@ use zenoh::{ }; use crate::owned_object::OwnedObject; -use crate::sample_callback::SetJniSampleCallback; +use crate::sample_callback::{process_kotlin_sample_callback, SetJniSampleCallback}; #[cfg(feature = "zenoh-ext")] use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] @@ -342,7 +342,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( let subscriber = session .declare_subscriber(key_expr.to_owned()) - .set_jni_sample_callback(&mut env, callback, on_close)? + .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) .wait() .map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; From d48905860a20569ace849504120010098341fcf0 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 16:18:24 +0200 Subject: [PATCH 073/126] process_kotlin_sample_callback everythere --- zenoh-jni/src/ext/advanced_subscriber.rs | 6 +- zenoh-jni/src/liveliness.rs | 4 +- zenoh-jni/src/sample_callback.rs | 114 +---------------------- zenoh-jni/src/session.rs | 4 +- 4 files changed, 8 insertions(+), 120 deletions(-) diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs index 93def10e..5cc85809 100644 --- a/zenoh-jni/src/ext/advanced_subscriber.rs +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -21,7 +21,7 @@ use zenoh::pubsub::Subscriber; use zenoh_ext::SampleMissListener; use zenoh_ext::{AdvancedSubscriber, Miss, SampleMissListenerBuilder}; -use crate::sample_callback::SetJniSampleCallback; +use crate::sample_callback::process_kotlin_sample_callback; use jni::objects::JObject; use crate::errors::ZResult; @@ -141,7 +141,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareDetectPu let detect_publishers_subscriber = advanced_subscriber .detect_publishers() .history(history != 0) - .set_jni_sample_callback(&mut env, callback, on_close)? + .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) .wait() .map_err(|err| zerror!("Unable to declare detect publishers subscriber: {}", err))?; @@ -197,7 +197,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareBackgrou advanced_subscriber .detect_publishers() .history(history != 0) - .set_jni_sample_callback(&mut env, callback, on_close)? + .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) .background() .wait() .map_err(|err| { diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 283325f2..395989a6 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -29,7 +29,7 @@ use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, owned_object::OwnedObject, - sample_callback::SetJniSampleCallback, + sample_callback::process_kotlin_sample_callback, session::{on_reply_error, on_reply_success}, throw_exception, utils::{get_callback_global_ref, get_java_vm, load_on_close}, @@ -155,7 +155,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessSubscribe .liveliness() .declare_subscriber(key_expr.to_owned()) .history(history != 0) - .set_jni_sample_callback(&mut env, callback, on_close)? + .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) .wait() .map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index 9e8cd677..cf113f1f 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -19,12 +19,7 @@ use jni::{ sys::jint, JNIEnv, }; -use zenoh::{ - handlers::{Callback, DefaultHandler}, - liveliness::LivelinessSubscriberBuilder, - pubsub::SubscriberBuilder, - sample::Sample, -}; +use zenoh::sample::Sample; use crate::{errors::ZResult, utils::*}; @@ -101,111 +96,4 @@ pub(crate) unsafe fn process_kotlin_sample_callback( }) } -pub(crate) trait SetJniSampleCallback: Sized + HasSampleCallbackSetter { - unsafe fn set_jni_sample_callback( - self, - env: &mut JNIEnv, - callback: JObject, - on_close: JObject, - ) -> ZResult { - let java_vm = Arc::new(get_java_vm(env)?); - let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); - - let builder = self.set_callback(move |sample: Sample| { - on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure - let _ = || -> ZResult<()> { - let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { - zerror!("Unable to attach thread for sample callback: {}", err) - })?; - let byte_array = bytes_to_java_array(&env, sample.payload()) - .map(|array| env.auto_local(array))?; - - let encoding_id: jint = sample.encoding().id() as jint; - let encoding_schema = match sample.encoding().schema() { - Some(schema) => slice_to_java_string(&env, schema)?, - None => JString::default(), - }; - let kind = sample.kind() as jint; - let (timestamp, is_valid) = sample - .timestamp() - .map(|timestamp| (timestamp.get_time().as_u64(), true)) - .unwrap_or((0, false)); - - let attachment_bytes = sample - .attachment() - .map_or_else( - || Ok(JByteArray::default()), - |attachment| bytes_to_java_array(&env, attachment), - ) - .map(|array| env.auto_local(array)) - .map_err(|err| zerror!("Error processing attachment: {}", err))?; - - let key_expr_str = env.auto_local( - env.new_string(sample.key_expr().to_string()) - .map_err(|err| zerror!("Error processing sample key expr: {}", err))?, - ); - - let express = sample.express(); - let priority = sample.priority() as jint; - let cc = sample.congestion_control() as jint; - - env.call_method( - &callback_global_ref, - "run", - "(Ljava/lang/String;[BILjava/lang/String;IJZ[BZII)V", - &[ - JValue::from(&key_expr_str), - JValue::from(&byte_array), - JValue::from(encoding_id), - JValue::from(&encoding_schema), - JValue::from(kind), - JValue::from(timestamp as i64), - JValue::from(is_valid), - JValue::from(&attachment_bytes), - JValue::from(express), - JValue::from(priority), - JValue::from(cc), - ], - ) - .map_err(|err| zerror!(err))?; - Ok(()) - }() - .map_err(|err| tracing::error!("On sample callback error: {err}")); - }); - Ok(builder) - } -} - -impl SetJniSampleCallback for T {} - -pub(crate) trait HasSampleCallbackSetter { - type BuilderWithCallback; - - fn set_callback(self, callback: F) -> Self::BuilderWithCallback - where - F: Fn(Sample) + Send + Sync + 'static; -} - -impl<'a, 'b> HasSampleCallbackSetter for SubscriberBuilder<'a, 'b, DefaultHandler> { - type BuilderWithCallback = SubscriberBuilder<'a, 'b, Callback>; - fn set_callback(self, callback: F) -> Self::BuilderWithCallback - where - F: Fn(Sample) + Send + Sync + 'static, - { - self.callback(callback) - } -} - -impl<'a, 'b> HasSampleCallbackSetter for LivelinessSubscriberBuilder<'a, 'b, DefaultHandler> { - type BuilderWithCallback = LivelinessSubscriberBuilder<'a, 'b, Callback>; - - fn set_callback(self, callback: F) -> Self::BuilderWithCallback - where - F: Fn(Sample) + Send + Sync + 'static, - { - self.callback(callback) - } -} diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 7082b7a8..cb381a9d 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -30,7 +30,7 @@ use zenoh::{ }; use crate::owned_object::OwnedObject; -use crate::sample_callback::{process_kotlin_sample_callback, SetJniSampleCallback}; +use crate::sample_callback::process_kotlin_sample_callback; #[cfg(feature = "zenoh-ext")] use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] @@ -1083,7 +1083,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV tracing::debug!("Declaring advanced subscriber on '{}'...", key_expr); let mut builder = session .declare_subscriber(key_expr.to_owned()) - .set_jni_sample_callback(&mut env, callback, on_close)? + .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) .advanced(); tracing::debug!("Advanced subscriber declared on '{}'.", key_expr); From cca52bc0e1b1f24dc0c9f6278acf8825d662b7e9 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 16:22:44 +0200 Subject: [PATCH 074/126] Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI --- zenoh-jni/src/session.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index cb381a9d..d232ad56 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -338,15 +338,15 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Subscriber<()>> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - tracing::debug!("Declaring subscriber on '{}'...", key_expr); + let callback = process_kotlin_sample_callback(&mut env, callback, on_close)?; - let subscriber = session - .declare_subscriber(key_expr.to_owned()) - .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) - .wait() - .map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; + let subscriber = zenoh_flat::session::declare_subscriber( + &session, + key_expr, + callback, + ) + .map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; - tracing::debug!("Subscriber declared on '{}'.", key_expr); Ok(Arc::into_raw(Arc::new(subscriber))) }() .unwrap_or_else(|err| { From 4c1c0618016051c4c8e327e45fd28c959bffe786 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 16:29:38 +0200 Subject: [PATCH 075/126] declate_querier in zenoh-flat --- zenoh-flat/src/session.rs | 36 ++++++++++++++++++++++++++++++++++++ zenoh-jni/src/session.rs | 25 ++++++++++++------------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 912c6f79..c750a447 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -13,11 +13,14 @@ use crate::{errors::ZResult, zerror}; use tracing::{error, trace}; +use std::time::Duration; + use zenoh::{ bytes::Encoding, config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, + query::{ConsolidationMode, QueryTarget, Querier, ReplyKeyExpr}, sample::Sample, qos::{CongestionControl, Priority, Reliability}, session::Session, @@ -86,6 +89,39 @@ pub fn declare_subscriber( }) } +/// Declare a querier through an existing Zenoh session. +pub fn declare_querier( + session: &Session, + key_expr: KeyExpr<'static>, + query_target: QueryTarget, + consolidation: ConsolidationMode, + congestion_control: CongestionControl, + express: bool, + priority: Priority, + timeout: Duration, + reply_key_expr: ReplyKeyExpr, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + session + .declare_querier(key_expr) + .congestion_control(congestion_control) + .consolidation(consolidation) + .express(express) + .target(query_target) + .priority(priority) + .timeout(timeout) + .accept_replies(reply_key_expr) + .wait() + .map(|querier| { + trace!("Declared querier on '{}'.", key_expr_string); + querier + }) + .map_err(|err| { + error!("Unable to declare querier on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + /// Perform a put operation through an existing Zenoh session. pub fn put( session: &Session, diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index d232ad56..1e8ee926 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -397,21 +397,20 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( let timeout = Duration::from_millis(timeout_ms as u64); let priority = decode_priority(priority)?; let reply_key_expr = decode_reply_key_expr(accept_replies)?; - tracing::debug!("Declaring querier on '{}'...", key_expr); - let querier = session - .declare_querier(key_expr.to_owned()) - .congestion_control(congestion_control) - .consolidation(consolidation) - .express(is_express != 0) - .target(query_target) - .priority(priority) - .timeout(timeout) - .accept_replies(reply_key_expr) - .wait() - .map_err(|err| zerror!(err))?; + let querier = zenoh_flat::session::declare_querier( + &session, + key_expr, + query_target, + consolidation, + congestion_control, + is_express != 0, + priority, + timeout, + reply_key_expr, + ) + .map_err(|err| zerror!("Unable to declare querier: {}", err))?; - tracing::debug!("Querier declared on '{}'.", key_expr); Ok(Arc::into_raw(Arc::new(querier))) }() .unwrap_or_else(|err| { From 1fa32f22f9f120a4b52a479553ee7803919f0260 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 16:40:55 +0200 Subject: [PATCH 076/126] declare queryable --- zenoh-flat/src/session.rs | 25 ++++++- zenoh-jni/src/sample_callback.rs | 118 +++++++++++++++++++++++++++++- zenoh-jni/src/session.rs | 121 ++----------------------------- 3 files changed, 144 insertions(+), 120 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index c750a447..65b10456 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -20,7 +20,7 @@ use zenoh::{ config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, - query::{ConsolidationMode, QueryTarget, Querier, ReplyKeyExpr}, + query::{ConsolidationMode, Query, QueryTarget, Queryable, Querier, ReplyKeyExpr}, sample::Sample, qos::{CongestionControl, Priority, Reliability}, session::Session, @@ -122,6 +122,29 @@ pub fn declare_querier( }) } +/// Declare a queryable through an existing Zenoh session. +pub fn declare_queryable( + session: &Session, + key_expr: KeyExpr<'static>, + callback: impl Fn(Query) + Send + Sync + 'static, + complete: bool, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + session + .declare_queryable(key_expr) + .callback(callback) + .complete(complete) + .wait() + .map(|queryable| { + trace!("Declared queryable on '{}'.", key_expr_string); + queryable + }) + .map_err(|err| { + error!("Unable to declare queryable on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + /// Perform a put operation through an existing Zenoh session. pub fn put( session: &Session, diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index cf113f1f..73c4e6f5 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -15,11 +15,11 @@ use std::sync::Arc; use jni::{ - objects::{JByteArray, JObject, JString, JValue}, - sys::jint, + objects::{GlobalRef, JByteArray, JObject, JString, JValue}, + sys::{jint, jlong}, JNIEnv, }; -use zenoh::sample::Sample; +use zenoh::{query::{Query, ReplyKeyExpr}, sample::Sample}; use crate::{errors::ZResult, utils::*}; @@ -96,4 +96,116 @@ pub(crate) unsafe fn process_kotlin_sample_callback( }) } +pub(crate) unsafe fn process_kotlin_query_callback( + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let on_close = load_on_close(&java_vm, on_close_global_ref); + + Ok(move |query: Query| { + on_close.noop(); + let env = match java_vm.attach_current_thread_as_daemon() { + Ok(env) => env, + Err(err) => { + tracing::error!("Unable to attach thread for queryable callback: {}", err); + return; + } + }; + + tracing::debug!("Receiving query through JNI: {}", query.to_string()); + match on_query(env, query, &callback_global_ref) { + Ok(_) => tracing::debug!("Queryable callback called successfully."), + Err(err) => tracing::error!("Error calling queryable callback: {}", err), + } + }) +} + +fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> ZResult<()> { + let selector_params_jstr = env + .new_string(query.parameters().to_string()) + .map(|value| env.auto_local(value)) + .map_err(|err| { + zerror!( + "Could not create a JString through JNI for the Query key expression. {}", + err + ) + })?; + + let (payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() { + let encoding = query.encoding().unwrap(); //If there is payload, there is encoding. + let encoding_id = encoding.id() as jint; + let encoding_schema = encoding + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(&env, schema), + ) + .map(|value| env.auto_local(value))?; + let byte_array = bytes_to_java_array(&env, payload).map(|value| env.auto_local(value))?; + (byte_array, encoding_id, encoding_schema) + } else { + ( + env.auto_local(JByteArray::default()), + 0, + env.auto_local(JString::default()), + ) + }; + + let attachment_bytes = query + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(&env, attachment), + ) + .map(|value| env.auto_local(value)) + .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; + + let key_expr_str = env + .new_string(query.key_expr().to_string()) + .map(|key_expr| env.auto_local(key_expr)) + .map_err(|err| { + zerror!( + "Could not create a JString through JNI for the Query key expression: {}.", + err + ) + })?; + + let accepts_replies: jint = match query.accepts_replies() { + ReplyKeyExpr::MatchingQuery => 0, + ReplyKeyExpr::Any => 1, + }; + + let query_ptr = Arc::into_raw(Arc::new(query)); + + let result = env + .call_method( + callback_global_ref, + "run", + "(Ljava/lang/String;Ljava/lang/String;[BILjava/lang/String;[BJI)V", + &[ + JValue::from(&key_expr_str), + JValue::from(&selector_params_jstr), + JValue::from(&payload), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + JValue::from(&attachment_bytes), + JValue::from(query_ptr as jlong), + JValue::from(accepts_replies), + ], + ) + .map(|_| ()) + .map_err(|err| { + unsafe { + Arc::from_raw(query_ptr); + }; + _ = env.exception_describe(); + zerror!(err) + }); + result +} + diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 1e8ee926..20f359b5 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -23,14 +23,14 @@ use zenoh::{ config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, - query::{Querier, Query, Queryable, ReplyError, ReplyKeyExpr, Selector}, + query::{Querier, Queryable, ReplyError, Selector}, sample::Sample, session::{EntityGlobalId, Session, ZenohId}, Wait, }; use crate::owned_object::OwnedObject; -use crate::sample_callback::process_kotlin_sample_callback; +use crate::sample_callback::{process_kotlin_query_callback, process_kotlin_sample_callback}; #[cfg(feature = "zenoh-ext")] use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] @@ -461,36 +461,13 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( ) -> *const Queryable<()> { let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Queryable<()>> { - let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let complete = complete != 0; - let on_close = load_on_close(&java_vm, on_close_global_ref); - tracing::debug!("Declaring queryable through JNI on {}", key_expr); - let builder = session - .declare_queryable(key_expr) - .callback(move |query: Query| { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - let env = match java_vm.attach_current_thread_as_daemon() { - Ok(env) => env, - Err(err) => { - tracing::error!("Unable to attach thread for queryable callback: {}", err); - return; - } - }; + let callback = process_kotlin_query_callback(&mut env, callback, on_close)?; - tracing::debug!("Receiving query through JNI: {}", query.to_string()); - match on_query(env, query, &callback_global_ref) { - Ok(_) => tracing::debug!("Queryable callback called successfully."), - Err(err) => tracing::error!("Error calling queryable callback: {}", err), - } - }) - .complete(complete); + let queryable = zenoh_flat::session::declare_queryable(&session, key_expr, callback, complete) + .map_err(|err| zerror!("Unable to declare queryable: {}", err))?; - let queryable = builder - .wait() - .map_err(|err| zerror!("Error declaring queryable: {}", err))?; Ok(Arc::into_raw(Arc::new(queryable))) }() .unwrap_or_else(|err| { @@ -499,94 +476,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( }) } -fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> ZResult<()> { - let selector_params_jstr = env - .new_string(query.parameters().to_string()) - .map(|value| env.auto_local(value)) - .map_err(|err| { - zerror!( - "Could not create a JString through JNI for the Query key expression. {}", - err - ) - })?; - - let (payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() { - let encoding = query.encoding().unwrap(); //If there is payload, there is encoding. - let encoding_id = encoding.id() as jint; - let encoding_schema = encoding - .schema() - .map_or_else( - || Ok(JString::default()), - |schema| slice_to_java_string(&env, schema), - ) - .map(|value| env.auto_local(value))?; - let byte_array = bytes_to_java_array(&env, payload).map(|value| env.auto_local(value))?; - (byte_array, encoding_id, encoding_schema) - } else { - ( - env.auto_local(JByteArray::default()), - 0, - env.auto_local(JString::default()), - ) - }; - - let attachment_bytes = query - .attachment() - .map_or_else( - || Ok(JByteArray::default()), - |attachment| bytes_to_java_array(&env, attachment), - ) - .map(|value| env.auto_local(value)) - .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; - - let key_expr_str = env - .new_string(query.key_expr().to_string()) - .map(|key_expr| env.auto_local(key_expr)) - .map_err(|err| { - zerror!( - "Could not create a JString through JNI for the Query key expression: {}.", - err - ) - })?; - - let accepts_replies: jint = match query.accepts_replies() { - ReplyKeyExpr::MatchingQuery => 0, - ReplyKeyExpr::Any => 1, - }; - - let query_ptr = Arc::into_raw(Arc::new(query)); - - let result = env - .call_method( - callback_global_ref, - "run", - "(Ljava/lang/String;Ljava/lang/String;[BILjava/lang/String;[BJI)V", - &[ - JValue::from(&key_expr_str), - JValue::from(&selector_params_jstr), - JValue::from(&payload), - JValue::from(encoding_id), - JValue::from(&encoding_schema), - JValue::from(&attachment_bytes), - JValue::from(query_ptr as jlong), - JValue::from(accepts_replies), - ], - ) - .map(|_| ()) - .map_err(|err| { - // The callback could not be invoked, therefore the created kotlin query object won't be - // used. Since `query_ptr` as well as `key_expr_ptr` was created within this function - // and remains unaltered, it is safe to reclaim ownership of the memory by converting - // the raw pointers back into an `Arc` and freeing the memory. - unsafe { - Arc::from_raw(query_ptr); - }; - _ = env.exception_describe(); - zerror!(err) - }); - result -} - /// Declare a [KeyExpr] through a [Session] via JNI. /// /// # Parameters: From 2d48599360cbe07774747966eeed112b1c4240fa Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 19:27:03 +0200 Subject: [PATCH 077/126] made zenoh_flat calls template-ready --- zenoh-flat/src/session.rs | 6 ++-- zenoh-jni/src/session.rs | 61 +++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 65b10456..a874c1c9 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -96,8 +96,8 @@ pub fn declare_querier( query_target: QueryTarget, consolidation: ConsolidationMode, congestion_control: CongestionControl, - express: bool, priority: Priority, + express: bool, timeout: Duration, reply_key_expr: ReplyKeyExpr, ) -> ZResult> { @@ -154,8 +154,8 @@ pub fn put( congestion_control: CongestionControl, priority: Priority, express: bool, - reliability: Reliability, attachment: Option>, + reliability: Reliability, ) -> ZResult<()> { let key_expr_string = key_expr.to_string(); let mut put_builder = session @@ -188,8 +188,8 @@ pub fn delete( congestion_control: CongestionControl, priority: Priority, express: bool, - reliability: Reliability, attachment: Option>, + reliability: Reliability, ) -> ZResult<()> { let key_expr_string = key_expr.to_string(); let mut delete_builder = session diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 20f359b5..68668b65 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -64,16 +64,15 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( _class: JClass, config_ptr: *const Config, ) -> *const Session { - let config = unsafe { OwnedObject::from_raw(config_ptr) }; - let session = zenoh_flat::session::open_session(&config); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - null() - } - } + let config = OwnedObject::from_raw(config_ptr); + || -> ZResult<*const Session> { + let session = zenoh_flat::session::open_session(&config)?; + Ok(Arc::into_raw(Arc::new(session))) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }) } /// Closes a Zenoh session via JNI. @@ -97,9 +96,10 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( session_ptr: *const Session, ) { let session = Arc::from_raw(session_ptr); - if let Err(err) = zenoh_flat::session::close_session(&session) { - throw_exception!(env, err); - } + let _ = || -> ZResult<()> { + zenoh_flat::session::close_session(&session) + }() + .map_err(|err| throw_exception!(env, err)); } /// Declare a Zenoh publisher via JNI. @@ -145,13 +145,14 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; + let is_express = is_express != 0; let reliability = decode_reliability(reliability)?; let publisher = zenoh_flat::session::declare_publisher( &session, key_expr, congestion_control, priority, - is_express != 0, + is_express, reliability, )?; Ok(Arc::into_raw(Arc::new(publisher))) @@ -212,13 +213,13 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; - let reliability = decode_reliability(reliability)?; - + let is_express = is_express != 0; let attachment = if !attachment.is_null() { Some(decode_byte_array(&env, attachment)?) } else { None }; + let reliability = decode_reliability(reliability)?; zenoh_flat::session::put( &session, @@ -227,9 +228,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( encoding, congestion_control, priority, - is_express != 0, - reliability, + is_express, attachment, + reliability, ) }() .map_err(|err| throw_exception!(env, err)); @@ -278,22 +279,22 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; - let reliability = decode_reliability(reliability)?; - + let is_express = is_express != 0; let attachment = if !attachment.is_null() { Some(decode_byte_array(&env, attachment)?) } else { None }; + let reliability = decode_reliability(reliability)?; zenoh_flat::session::delete( &session, key_expr, congestion_control, priority, - is_express != 0, - reliability, + is_express, attachment, + reliability, ) }() .map_err(|err| throw_exception!(env, err)); @@ -344,8 +345,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( &session, key_expr, callback, - ) - .map_err(|err| zerror!("Unable to declare subscriber: {}", err))?; + )?; Ok(Arc::into_raw(Arc::new(subscriber))) }() @@ -394,8 +394,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( let query_target = decode_query_target(target)?; let consolidation = decode_consolidation(consolidation)?; let congestion_control = decode_congestion_control(congestion_control)?; - let timeout = Duration::from_millis(timeout_ms as u64); let priority = decode_priority(priority)?; + let is_express = is_express != 0; + let timeout = Duration::from_millis(timeout_ms as u64); let reply_key_expr = decode_reply_key_expr(accept_replies)?; let querier = zenoh_flat::session::declare_querier( @@ -404,12 +405,11 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( query_target, consolidation, congestion_control, - is_express != 0, priority, + is_express, timeout, reply_key_expr, - ) - .map_err(|err| zerror!("Unable to declare querier: {}", err))?; + )?; Ok(Arc::into_raw(Arc::new(querier))) }() @@ -462,11 +462,10 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Queryable<()>> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let complete = complete != 0; let callback = process_kotlin_query_callback(&mut env, callback, on_close)?; + let complete = complete != 0; - let queryable = zenoh_flat::session::declare_queryable(&session, key_expr, callback, complete) - .map_err(|err| zerror!("Unable to declare queryable: {}", err))?; + let queryable = zenoh_flat::session::declare_queryable(&session, key_expr, callback, complete)?; Ok(Arc::into_raw(Arc::new(queryable))) }() From 605f7c5a88b618852c69a2b20d1aa191da93003c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 22 Apr 2026 20:15:07 +0200 Subject: [PATCH 078/126] common template for zenoh_flat wrapping --- zenoh-jni/src/session.rs | 51 +++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 68668b65..77592b26 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -95,11 +95,15 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( _class: JClass, session_ptr: *const Session, ) { - let session = Arc::from_raw(session_ptr); - let _ = || -> ZResult<()> { - zenoh_flat::session::close_session(&session) + || -> ZResult<()> { + let session = Arc::from_raw(session_ptr); + zenoh_flat::session::close_session(&session)?; + Ok(()) }() - .map_err(|err| throw_exception!(env, err)); + .unwrap_or_else(|err| { + throw_exception!(env, err); + () + }); } /// Declare a Zenoh publisher via JNI. @@ -140,8 +144,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( is_express: jboolean, reliability: jint, ) -> *const Publisher<'static> { - let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Publisher<'static>> { + let session = OwnedObject::from_raw(session_ptr); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; @@ -206,8 +210,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( attachment: JByteArray, reliability: jint, ) { - let session = OwnedObject::from_raw(session_ptr); - let _ = || -> ZResult<()> { + || -> ZResult<()> { + let session = OwnedObject::from_raw(session_ptr); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let payload = decode_byte_array(&env, payload)?; let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; @@ -231,9 +235,13 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( is_express, attachment, reliability, - ) + )?; + Ok(()) }() - .map_err(|err| throw_exception!(env, err)); + .unwrap_or_else(|err| { + throw_exception!(env, err); + () + }); } /// Performs a `delete` operation in the Zenoh session via JNI. @@ -274,8 +282,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( attachment: JByteArray, reliability: jint, ) { - let session = OwnedObject::from_raw(session_ptr); - let _ = || -> ZResult<()> { + || -> ZResult<()> { + let session = OwnedObject::from_raw(session_ptr); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; @@ -295,9 +303,13 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( is_express, attachment, reliability, - ) + )?; + Ok(()) }() - .map_err(|err| throw_exception!(env, err)); + .unwrap_or_else(|err| { + throw_exception!(env, err); + () + }); } /// Declare a Zenoh subscriber via JNI. @@ -336,8 +348,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( callback: JObject, on_close: JObject, ) -> *const Subscriber<()> { - let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Subscriber<()>> { + let session = OwnedObject::from_raw(session_ptr); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let callback = process_kotlin_sample_callback(&mut env, callback, on_close)?; @@ -346,7 +358,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( key_expr, callback, )?; - Ok(Arc::into_raw(Arc::new(subscriber))) }() .unwrap_or_else(|err| { @@ -388,8 +399,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( timeout_ms: jlong, accept_replies: jint, ) -> *const Querier<'static> { - let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Querier<'static>> { + let session = OwnedObject::from_raw(session_ptr); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let query_target = decode_query_target(target)?; let consolidation = decode_consolidation(consolidation)?; @@ -398,7 +409,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( let is_express = is_express != 0; let timeout = Duration::from_millis(timeout_ms as u64); let reply_key_expr = decode_reply_key_expr(accept_replies)?; - let querier = zenoh_flat::session::declare_querier( &session, key_expr, @@ -410,7 +420,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( timeout, reply_key_expr, )?; - Ok(Arc::into_raw(Arc::new(querier))) }() .unwrap_or_else(|err| { @@ -459,14 +468,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( on_close: JObject, complete: jboolean, ) -> *const Queryable<()> { - let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Queryable<()>> { + let session = OwnedObject::from_raw(session_ptr); let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; let callback = process_kotlin_query_callback(&mut env, callback, on_close)?; let complete = complete != 0; - let queryable = zenoh_flat::session::declare_queryable(&session, key_expr, callback, complete)?; - Ok(Arc::into_raw(Arc::new(queryable))) }() .unwrap_or_else(|err| { @@ -501,8 +508,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( session_ptr: *const Session, key_expr_str: JString, ) -> *const KeyExpr<'static> { - let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const KeyExpr<'static>> { + let session = OwnedObject::from_raw(session_ptr); let key_expr_str = decode_string(&mut env, &key_expr_str)?; let key_expr = session .declare_keyexpr(key_expr_str.to_owned()) From c0c3525151e2dfac165aa7dd93e6a3cc533c3835 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 20:47:43 +0200 Subject: [PATCH 079/126] initial prebindgen --- zenoh-flat/Cargo.lock | 196 +++++++++++++++++++++++++++++++++++--- zenoh-flat/Cargo.toml | 5 + zenoh-flat/build.rs | 3 + zenoh-flat/src/lib.rs | 3 + zenoh-flat/src/session.rs | 1 + zenoh-jni/Cargo.lock | 126 ++++++++++++++++++++++++ zenoh-jni/Cargo.toml | 6 ++ zenoh-jni/build.rs | 145 ++++++++++++++++++++++++++++ zenoh-jni/src/session.rs | 31 +----- 9 files changed, 474 insertions(+), 42 deletions(-) create mode 100644 zenoh-flat/build.rs create mode 100644 zenoh-jni/build.rs diff --git a/zenoh-flat/Cargo.lock b/zenoh-flat/Cargo.lock index 8a4a5459..ffb5009e 100644 --- a/zenoh-flat/Cargo.lock +++ b/zenoh-flat/Cargo.lock @@ -184,7 +184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", - "konst", + "konst 0.2.20", ] [[package]] @@ -198,6 +198,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -696,6 +705,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + [[package]] name = "indexmap" version = "1.9.3" @@ -800,12 +815,39 @@ dependencies = [ "konst_macro_rules", ] +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + [[package]] name = "konst_macro_rules" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1136,6 +1178,67 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prebindgen" +version = "0.4.1" +dependencies = [ + "if_rust_version", + "itertools", + "konst 0.3.17", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools", + "konst 0.3.17", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen-proc-macro" +version = "0.4.1" +dependencies = [ + "prebindgen 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "rand 0.9.4", + "serde", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1176,8 +1279,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1187,7 +1300,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1199,6 +1322,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -1230,6 +1362,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1271,6 +1415,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1600,7 +1755,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "rand", + "rand 0.8.6", "syn 1.0.109", ] @@ -1911,6 +2066,21 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "ucd-trie" version = "0.1.7" @@ -1926,7 +2096,7 @@ dependencies = [ "humantime", "lazy_static", "log", - "rand", + "rand 0.8.6", "serde", "spin 0.10.0", ] @@ -2267,7 +2437,7 @@ dependencies = [ "once_cell", "petgraph", "phf", - "rand", + "rand 0.8.6", "rustc_version", "serde", "serde_json", @@ -2366,8 +2536,8 @@ source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfc dependencies = [ "aes", "hmac", - "rand", - "rand_chacha", + "rand 0.8.6", + "rand_chacha 0.3.1", "sha3", "zenoh-result", ] @@ -2377,6 +2547,8 @@ name = "zenoh-flat" version = "1.9.0" dependencies = [ "json5", + "prebindgen 0.4.1", + "prebindgen-proc-macro", "serde_yaml", "tracing", "zenoh", @@ -2390,7 +2562,7 @@ dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", "keyed-set", - "rand", + "rand 0.8.6", "schemars 1.2.1", "serde", "token-cell", @@ -2465,7 +2637,7 @@ version = "1.9.0" source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" dependencies = [ "const_format", - "rand", + "rand 0.8.6", "serde", "uhlc", "zenoh-buffers", @@ -2534,7 +2706,7 @@ dependencies = [ "futures", "lazy_static", "lz4_flex", - "rand", + "rand 0.8.6", "ringbuffer-spsc", "serde", "sha3", diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 4c7bc830..7cdcaeaf 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -5,9 +5,14 @@ edition = "2021" license = "EPL-2.0 OR Apache-2.0" description = "Zenoh flat data interchange support." repository = "https://github.com/eclipse-zenoh/zenoh" +build = "build.rs" [dependencies] zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } json5 = "0.4.1" serde_yaml = "0.9.19" tracing = "0.1" +prebindgen-proc-macro = { path = "/Users/milyin/prebindgen/prebindgen-proc-macro" } + +[build-dependencies] +prebindgen = { path = "/Users/milyin/prebindgen/prebindgen" } diff --git a/zenoh-flat/build.rs b/zenoh-flat/build.rs new file mode 100644 index 00000000..cc8fe95d --- /dev/null +++ b/zenoh-flat/build.rs @@ -0,0 +1,3 @@ +fn main() { + prebindgen::init_prebindgen_out_dir(); +} diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index 583f1bb0..d7014e34 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -1,5 +1,8 @@ //! `zenoh-flat` is a placeholder Rust crate for Zenoh flat data support. +pub const PREBINDGEN_OUT_DIR: &str = prebindgen_proc_macro::prebindgen_out_dir!(); +pub const FEATURES: &str = prebindgen_proc_macro::features!(); + pub mod config; pub mod errors; pub mod session; diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index a874c1c9..5194c8d2 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -28,6 +28,7 @@ use zenoh::{ }; /// Open a Zenoh session using a borrowed configuration. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn open_session(config: &Config) -> ZResult { zenoh::open(config.clone()) .wait() diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index 42d4ec42..cf6c07ec 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -350,6 +350,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1094,6 +1103,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + [[package]] name = "indexmap" version = "1.9.3" @@ -1233,6 +1248,33 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1790,6 +1832,57 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prebindgen" +version = "0.4.1" +dependencies = [ + "if_rust_version", + "itertools", + "konst", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools", + "konst", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen-proc-macro" +version = "0.4.1" +dependencies = [ + "prebindgen 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "rand 0.9.2", + "serde", + "serde_json", + "syn 2.0.117", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2085,6 +2178,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3053,6 +3157,21 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "ucd-trie" version = "0.1.7" @@ -3947,6 +4066,8 @@ name = "zenoh-flat" version = "1.9.0" dependencies = [ "json5", + "prebindgen 0.4.1", + "prebindgen-proc-macro", "serde_yaml", "tracing", "zenoh", @@ -4321,8 +4442,13 @@ dependencies = [ "flume 0.10.14", "jni", "json5", + "konst", + "prebindgen 0.4.1", + "proc-macro2", + "quote", "rustc_version", "serde_yaml", + "syn 2.0.117", "tracing", "uhlc", "zenoh", diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index e30cd4e1..3a79427a 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -40,12 +40,18 @@ zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false, optional = true } tracing = { version = "0.1" , features = ["log"] } zenoh-flat = { path = "../zenoh-flat" } +konst = "0.3.0" [lib] name = "zenoh_jni" crate_type = ["staticlib", "dylib"] [build-dependencies] rustc_version = "0.4.0" +prebindgen = { path = "/Users/milyin/prebindgen/prebindgen" } +zenoh-flat = { path = "../zenoh-flat" } +syn = { version = "2", features = ["full", "extra-traits"] } +quote = "1" +proc-macro2 = "1" [profile.release] debug = false # If you want debug symbol in release mode, set the env variable: RUSTFLAGS=-g diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs new file mode 100644 index 00000000..e4cb9834 --- /dev/null +++ b/zenoh-jni/build.rs @@ -0,0 +1,145 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; + +fn main() { + let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); + + source + .items_all() + .map(|(item, loc)| (jni_convert(item, &loc), loc)) + .collect::() + .write("zenoh_flat_jni.rs"); +} + +fn jni_convert(item: syn::Item, loc: &prebindgen::SourceLocation) -> syn::Item { + let syn::Item::Fn(func) = item else { + return item; + }; + syn::Item::Fn(convert_fn(func, loc)) +} + +fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemFn { + let original_name = func.sig.ident.to_string(); + let jni_name = format_ident!( + "Java_io_zenoh_jni_JNISession_{}ViaJNI", + snake_to_camel(&original_name) + ); + let orig_ident = &func.sig.ident; + + let mut prelude: Vec = Vec::new(); + let mut jni_params: Vec = Vec::new(); + let mut call_args: Vec = Vec::new(); + + for input in &func.sig.inputs { + let syn::FnArg::Typed(pat_type) = input else { + panic!("receiver args not supported at {loc}"); + }; + let syn::Pat::Ident(pat_ident) = &*pat_type.pat else { + panic!("non-ident param pattern at {loc}"); + }; + let name = &pat_ident.ident; + + match &*pat_type.ty { + syn::Type::Reference(r) if r.mutability.is_none() => { + let elem = &*r.elem; + let ptr_ident = format_ident!("{}_ptr", name); + jni_params.push(quote! { #ptr_ident: *const #elem }); + prelude.push(quote! { + let #name = crate::owned_object::OwnedObject::from_raw(#ptr_ident); + }); + call_args.push(quote! { &#name }); + } + other => panic!( + "unsupported parameter type `{}` for `{}` at {loc}", + other.to_token_stream(), + name + ), + } + } + + let (ret_ty_jni, wrap_ok, on_err, closure_ret): ( + TokenStream, + TokenStream, + TokenStream, + TokenStream, + ) = match &func.sig.output { + syn::ReturnType::Type(_, ty) => { + let inner = extract_zresult_inner(ty) + .unwrap_or_else(|| panic!("return must be ZResult for `{original_name}`")); + ( + quote! { *const #inner }, + quote! { Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) }, + quote! { std::ptr::null() }, + quote! { crate::errors::ZResult<*const #inner> }, + ) + } + syn::ReturnType::Default => ( + quote! { () }, + quote! { Ok(()) }, + quote! { () }, + quote! { crate::errors::ZResult<()> }, + ), + }; + + let body = quote! { + { + #(#prelude)* + (|| -> #closure_ret { + let __result = zenoh_flat::session::#orig_ident( #(#call_args),* )?; + #wrap_ok + })() + .unwrap_or_else(|err| { + crate::throw_exception!(env, err); + #on_err + }) + } + }; + + let tokens = quote! { + #[no_mangle] + #[allow(non_snake_case)] + pub unsafe extern "C" fn #jni_name( + mut env: jni::JNIEnv, + _class: jni::objects::JClass, + #(#jni_params),* + ) -> #ret_ty_jni #body + }; + + syn::parse2(tokens).expect("generated JNI wrapper must parse") +} + +fn extract_zresult_inner(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { + return None; + }; + let seg = tp.path.segments.last()?; + if seg.ident != "ZResult" { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + let arg = args.args.first()?; + let syn::GenericArgument::Type(inner) = arg else { + return None; + }; + Some(inner.clone()) +} + +fn snake_to_camel(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut upper_next = false; + for (i, c) in s.chars().enumerate() { + if c == '_' { + upper_next = true; + } else if upper_next { + out.extend(c.to_uppercase()); + upper_next = false; + } else if i == 0 { + out.extend(c.to_lowercase()); + } else { + out.push(c); + } + } + out +} diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 77592b26..0ba7608e 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -44,36 +44,7 @@ use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, }; -/// Open a Zenoh session via JNI. -/// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. -/// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `config_ptr`: Pointer to the Zenoh config. If null, the default configuration will be loaded. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( - mut env: JNIEnv, - _class: JClass, - config_ptr: *const Config, -) -> *const Session { - let config = OwnedObject::from_raw(config_ptr); - || -> ZResult<*const Session> { - let session = zenoh_flat::session::open_session(&config)?; - Ok(Arc::into_raw(Arc::new(session))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} +include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); /// Closes a Zenoh session via JNI. /// From 100a8b3da2f4fccd9df1889985aa5ce3184371e5 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 20:49:28 +0200 Subject: [PATCH 080/126] use from crates.io --- zenoh-flat/Cargo.toml | 4 ++-- zenoh-jni/Cargo.lock | 26 +++++--------------------- zenoh-jni/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 7cdcaeaf..0bb69d05 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -12,7 +12,7 @@ zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", json5 = "0.4.1" serde_yaml = "0.9.19" tracing = "0.1" -prebindgen-proc-macro = { path = "/Users/milyin/prebindgen/prebindgen-proc-macro" } +prebindgen-proc-macro = "0.4.1" [build-dependencies] -prebindgen = { path = "/Users/milyin/prebindgen/prebindgen" } +prebindgen = "0.4.1" diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index cf6c07ec..2143f669 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -1832,24 +1832,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prebindgen" -version = "0.4.1" -dependencies = [ - "if_rust_version", - "itertools", - "konst", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "roxygen", - "serde", - "serde_json", - "syn 2.0.117", - "toml", -] - [[package]] name = "prebindgen" version = "0.4.1" @@ -1873,8 +1855,10 @@ dependencies = [ [[package]] name = "prebindgen-proc-macro" version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a68d8870ef9ba6f8abbe9d4bca5775e1d854d5fa6dac8c27dd1c79ecebe398" dependencies = [ - "prebindgen 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "prebindgen", "proc-macro2", "quote", "rand 0.9.2", @@ -4066,7 +4050,7 @@ name = "zenoh-flat" version = "1.9.0" dependencies = [ "json5", - "prebindgen 0.4.1", + "prebindgen", "prebindgen-proc-macro", "serde_yaml", "tracing", @@ -4443,7 +4427,7 @@ dependencies = [ "jni", "json5", "konst", - "prebindgen 0.4.1", + "prebindgen", "proc-macro2", "quote", "rustc_version", diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index 3a79427a..6ec42048 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -47,7 +47,7 @@ crate_type = ["staticlib", "dylib"] [build-dependencies] rustc_version = "0.4.0" -prebindgen = { path = "/Users/milyin/prebindgen/prebindgen" } +prebindgen = "0.4.1" zenoh-flat = { path = "../zenoh-flat" } syn = { version = "2", features = ["full", "extra-traits"] } quote = "1" From 1825abc39ae3b8e28b37fc32e6c0d5262582126d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 20:59:27 +0200 Subject: [PATCH 081/126] declare_publisher updated --- zenoh-flat/src/session.rs | 1 + zenoh-jni/build.rs | 123 ++++++++++++++++++++++++++++++++++---- zenoh-jni/src/session.rs | 61 ------------------- 3 files changed, 111 insertions(+), 74 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 5194c8d2..8632ba3b 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -43,6 +43,7 @@ pub fn open_session(config: &Config) -> ZResult { } /// Declare a publisher through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn declare_publisher( session: &Session, key_expr: KeyExpr<'static>, diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index e4cb9834..470dd510 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -38,10 +38,10 @@ fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemF panic!("non-ident param pattern at {loc}"); }; let name = &pat_ident.ident; + let ty = &*pat_type.ty; - match &*pat_type.ty { - syn::Type::Reference(r) if r.mutability.is_none() => { - let elem = &*r.elem; + match classify_arg(ty) { + ArgKind::OpaqueRef(elem) => { let ptr_ident = format_ident!("{}_ptr", name); jni_params.push(quote! { #ptr_ident: *const #elem }); prelude.push(quote! { @@ -49,9 +49,35 @@ fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemF }); call_args.push(quote! { &#name }); } - other => panic!( + ArgKind::KeyExpr => { + let ptr_ident = format_ident!("{}_ptr", name); + let str_ident = format_ident!("{}_str", name); + jni_params.push(quote! { + #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> + }); + jni_params.push(quote! { #str_ident: jni::objects::JString }); + prelude.push(quote! { + let #name = crate::key_expr::process_kotlin_key_expr( + &mut env, &#str_ident, #ptr_ident, + )?; + }); + call_args.push(quote! { #name }); + } + ArgKind::Enum(decoder) => { + jni_params.push(quote! { #name: jni::sys::jint }); + prelude.push(quote! { + let #name = crate::utils::#decoder(#name)?; + }); + call_args.push(quote! { #name }); + } + ArgKind::Bool => { + jni_params.push(quote! { #name: jni::sys::jboolean }); + prelude.push(quote! { let #name = #name != 0; }); + call_args.push(quote! { #name }); + } + ArgKind::Unsupported => panic!( "unsupported parameter type `{}` for `{}` at {loc}", - other.to_token_stream(), + ty.to_token_stream(), name ), } @@ -66,12 +92,21 @@ fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemF syn::ReturnType::Type(_, ty) => { let inner = extract_zresult_inner(ty) .unwrap_or_else(|| panic!("return must be ZResult for `{original_name}`")); - ( - quote! { *const #inner }, - quote! { Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) }, - quote! { std::ptr::null() }, - quote! { crate::errors::ZResult<*const #inner> }, - ) + if is_unit(&inner) { + ( + quote! { () }, + quote! { Ok(()) }, + quote! { () }, + quote! { crate::errors::ZResult<()> }, + ) + } else { + ( + quote! { *const #inner }, + quote! { Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) }, + quote! { std::ptr::null() }, + quote! { crate::errors::ZResult<*const #inner> }, + ) + } } syn::ReturnType::Default => ( quote! { () }, @@ -83,8 +118,8 @@ fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemF let body = quote! { { - #(#prelude)* (|| -> #closure_ret { + #(#prelude)* let __result = zenoh_flat::session::#orig_ident( #(#call_args),* )?; #wrap_ok })() @@ -97,7 +132,7 @@ fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemF let tokens = quote! { #[no_mangle] - #[allow(non_snake_case)] + #[allow(non_snake_case, unused_mut, unused_variables)] pub unsafe extern "C" fn #jni_name( mut env: jni::JNIEnv, _class: jni::objects::JClass, @@ -108,6 +143,68 @@ fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemF syn::parse2(tokens).expect("generated JNI wrapper must parse") } +enum ArgKind { + /// `&T` (excluding `&KeyExpr`) — handle arg via `OwnedObject::from_raw`. + OpaqueRef(syn::Type), + /// `KeyExpr<'static>` — two JNI args (ptr + JString) via `process_kotlin_key_expr`. + KeyExpr, + /// Enum type passed as `jint` ordinal, with matching `decode_*` helper. + Enum(syn::Ident), + /// `bool` → `jboolean`. + Bool, + Unsupported, +} + +fn classify_arg(ty: &syn::Type) -> ArgKind { + match ty { + syn::Type::Reference(r) if r.mutability.is_none() => { + if type_last_segment(&r.elem).map(|s| s == "KeyExpr").unwrap_or(false) { + ArgKind::KeyExpr + } else { + ArgKind::OpaqueRef((*r.elem).clone()) + } + } + syn::Type::Path(tp) => { + let Some(last) = tp.path.segments.last() else { + return ArgKind::Unsupported; + }; + let name = last.ident.to_string(); + if name == "bool" { + return ArgKind::Bool; + } + if name == "KeyExpr" { + return ArgKind::KeyExpr; + } + if let Some(decoder) = enum_decoder(&name) { + return ArgKind::Enum(format_ident!("{}", decoder)); + } + ArgKind::Unsupported + } + _ => ArgKind::Unsupported, + } +} + +fn enum_decoder(type_name: &str) -> Option<&'static str> { + Some(match type_name { + "CongestionControl" => "decode_congestion_control", + "Priority" => "decode_priority", + "Reliability" => "decode_reliability", + "QueryTarget" => "decode_query_target", + "ConsolidationMode" => "decode_consolidation", + "ReplyKeyExpr" => "decode_reply_key_expr", + _ => return None, + }) +} + +fn type_last_segment(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + tp.path.segments.last().map(|s| s.ident.to_string()) +} + +fn is_unit(ty: &syn::Type) -> bool { + matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) +} + fn extract_zresult_inner(ty: &syn::Type) -> Option { let syn::Type::Path(tp) = ty else { return None; diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 0ba7608e..3cd0e6a8 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -77,67 +77,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( }); } -/// Declare a Zenoh publisher via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the publisher, may be null. -/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the publisher. -/// It is only considered when the key_expr_ptr parameter is null, meaning the function is -/// receiving a key expression that was not declared. -/// - `session_ptr`: Raw pointer to the Zenoh [Session] to be used for the publisher. -/// - `congestion_control`: The [zenoh::publisher::CongestionControl] configuration as an ordinal. -/// - `priority`: The [zenoh::core::Priority] configuration as an ordinal. -/// - `is_express`: The express config of the publisher (see [zenoh::prelude::QoSBuilderTrait]). -/// - `reliability`: The reliability value as an ordinal. -/// -/// # Returns: -/// - A raw pointer to the declared Zenoh publisher or null in case of failure. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The ownership of the session is not transferred, and the session pointer remains valid -/// after this function call so it is safe to use it after this call. -/// - The function may throw an exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - congestion_control: jint, - priority: jint, - is_express: jboolean, - reliability: jint, -) -> *const Publisher<'static> { - || -> ZResult<*const Publisher<'static>> { - let session = OwnedObject::from_raw(session_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let is_express = is_express != 0; - let reliability = decode_reliability(reliability)?; - let publisher = zenoh_flat::session::declare_publisher( - &session, - key_expr, - congestion_control, - priority, - is_express, - reliability, - )?; - Ok(Arc::into_raw(Arc::new(publisher))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - /// Performs a `put` operation in the Zenoh session via JNI. /// /// Parameters: From 135456545777ecb7355742cfbdcff97c0f485d0e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 22:46:23 +0200 Subject: [PATCH 082/126] jni_converter in zenoh-flat --- zenoh-flat/Cargo.toml | 5 + zenoh-flat/src/jni_converter.rs | 408 ++++++++++++++++++++++++++++++++ zenoh-flat/src/lib.rs | 1 + zenoh-flat/src/session.rs | 1 + zenoh-jni/Cargo.lock | 21 +- zenoh-jni/Cargo.toml | 4 +- zenoh-jni/build.rs | 251 ++------------------ zenoh-jni/src/session.rs | 62 ----- 8 files changed, 450 insertions(+), 303 deletions(-) create mode 100644 zenoh-flat/src/jni_converter.rs diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 0bb69d05..19921f93 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -13,6 +13,11 @@ json5 = "0.4.1" serde_yaml = "0.9.19" tracing = "0.1" prebindgen-proc-macro = "0.4.1" +prebindgen = "0.4.1" +syn = { version = "2", features = ["full", "extra-traits"] } +quote = "1" +proc-macro2 = "1" +itertools = "0.12" [build-dependencies] prebindgen = "0.4.1" diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs new file mode 100644 index 00000000..b73333fb --- /dev/null +++ b/zenoh-flat/src/jni_converter.rs @@ -0,0 +1,408 @@ +//! JNI binding generator for functions marked with `#[prebindgen]`. +//! +//! This module mirrors the pattern of [`prebindgen::batching::FfiConverter`], but +//! instead of emitting `#[no_mangle] extern "C"` proxy functions, it emits +//! `Java__ViaJNI` wrappers that decode JNI arguments, call the +//! original Rust function, and wrap the result into a raw pointer (or throw a +//! JVM exception on error). +//! +//! # Pipeline +//! +//! ```ignore +//! use itertools::Itertools; +//! let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); +//! let converter = zenoh_flat::jni_converter::JniConverter::builder() +//! .class_prefix("Java_io_zenoh_jni_JNISession_") +//! .function_suffix("ViaJNI") +//! .source_module("zenoh_flat::session") +//! .owned_object("crate::owned_object::OwnedObject") +//! .zresult("crate::errors::ZResult") +//! .throw_exception("crate::throw_exception") +//! .key_expr_decoder("crate::key_expr::process_kotlin_key_expr") +//! .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") +//! .build(); +//! source +//! .items_all() +//! .batching(converter.into_closure()) +//! .collect::() +//! .write("zenoh_flat_jni.rs"); +//! ``` +//! +//! This crate is currently coupled to zenoh-jni's type layout (e.g. `KeyExpr`, +//! decoder helpers, `OwnedObject`) — those couplings are configurable through +//! the [`Builder`] so the converter itself stays data-driven. + +use std::collections::{HashMap, VecDeque}; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; + +use prebindgen::SourceLocation; + +/// Builder for [`JniConverter`]. +pub struct Builder { + class_prefix: String, + function_suffix: String, + source_module: syn::Path, + owned_object: syn::Path, + zresult: syn::Path, + throw_exception: syn::Path, + key_expr_decoder: Option, + enum_decoders: HashMap, +} + +impl Default for Builder { + fn default() -> Self { + Self { + class_prefix: String::new(), + function_suffix: String::new(), + source_module: syn::parse_str("crate").unwrap(), + owned_object: syn::parse_str("OwnedObject").unwrap(), + zresult: syn::parse_str("ZResult").unwrap(), + throw_exception: syn::parse_str("throw_exception").unwrap(), + key_expr_decoder: None, + enum_decoders: HashMap::new(), + } + } +} + +impl Builder { + /// JNI class prefix prepended to each function name, e.g. + /// `"Java_io_zenoh_jni_JNISession_"`. + pub fn class_prefix(mut self, prefix: impl Into) -> Self { + self.class_prefix = prefix.into(); + self + } + + /// Suffix appended to the camel-case function name, e.g. `"ViaJNI"`. + pub fn function_suffix(mut self, suffix: impl Into) -> Self { + self.function_suffix = suffix.into(); + self + } + + /// Fully-qualified path of the module that contains the original Rust + /// functions being wrapped, e.g. `"zenoh_flat::session"`. + pub fn source_module(mut self, path: impl AsRef) -> Self { + self.source_module = syn::parse_str(path.as_ref()).expect("invalid source_module path"); + self + } + + /// Path of the `OwnedObject` helper used to borrow Arc-pointers. + pub fn owned_object(mut self, path: impl AsRef) -> Self { + self.owned_object = syn::parse_str(path.as_ref()).expect("invalid owned_object path"); + self + } + + /// Path of the `ZResult` type used in the closure's return type. + pub fn zresult(mut self, path: impl AsRef) -> Self { + self.zresult = syn::parse_str(path.as_ref()).expect("invalid zresult path"); + self + } + + /// Path of the `throw_exception!` macro (will be called as `!`). + pub fn throw_exception(mut self, path: impl AsRef) -> Self { + self.throw_exception = + syn::parse_str(path.as_ref()).expect("invalid throw_exception path"); + self + } + + /// Path of the function that decodes a `KeyExpr` from a `(ptr, JString)` + /// pair, e.g. `"crate::key_expr::process_kotlin_key_expr"`. + pub fn key_expr_decoder(mut self, path: impl AsRef) -> Self { + self.key_expr_decoder = + Some(syn::parse_str(path.as_ref()).expect("invalid key_expr_decoder path")); + self + } + + /// Register a decoder for an enum type. `type_name` is matched against the + /// last segment of the parameter's type path. + pub fn enum_decoder( + mut self, + type_name: impl Into, + decoder: impl AsRef, + ) -> Self { + let path: syn::Path = + syn::parse_str(decoder.as_ref()).expect("invalid enum_decoder path"); + self.enum_decoders.insert(type_name.into(), path); + self + } + + pub fn build(self) -> JniConverter { + JniConverter { + cfg: self, + pending: VecDeque::new(), + } + } +} + +/// Converter that transforms `#[prebindgen]`-marked Rust functions into JNI +/// `Java_*` wrappers. +/// +/// Intended for use with `itertools::batching`: +/// +/// ```ignore +/// source.items_all().batching(converter.into_closure()) +/// ``` +pub struct JniConverter { + cfg: Builder, + pending: VecDeque<(syn::Item, SourceLocation)>, +} + +impl JniConverter { + pub fn builder() -> Builder { + Builder::default() + } + + /// Pull one item from `iter`, convert it, and return it. Non-function + /// items are passed through unchanged. Returns `None` once `iter` is + /// exhausted and no buffered items remain. + pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> + where + I: Iterator, + { + if let Some(buf) = self.pending.pop_front() { + return Some(buf); + } + let (item, loc) = iter.next()?; + Some((self.convert(item, &loc), loc)) + } + + /// Closure suitable for `itertools::batching`. + pub fn into_closure( + mut self, + ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + where + I: Iterator, + { + move |iter| self.call(iter) + } + + fn convert(&self, item: syn::Item, loc: &SourceLocation) -> syn::Item { + match item { + syn::Item::Fn(func) => syn::Item::Fn(self.convert_fn(func, loc)), + other => other, + } + } + + fn convert_fn(&self, func: syn::ItemFn, loc: &SourceLocation) -> syn::ItemFn { + let original_name = func.sig.ident.to_string(); + let camel = snake_to_camel(&original_name); + let jni_name = format_ident!("{}{}{}", self.cfg.class_prefix, camel, self.cfg.function_suffix); + let orig_ident = &func.sig.ident; + let source_module = &self.cfg.source_module; + let owned_object = &self.cfg.owned_object; + let zresult = &self.cfg.zresult; + let throw_exception = &self.cfg.throw_exception; + + let mut prelude: Vec = Vec::new(); + let mut jni_params: Vec = Vec::new(); + let mut call_args: Vec = Vec::new(); + + for input in &func.sig.inputs { + let syn::FnArg::Typed(pat_type) = input else { + panic!("receiver args not supported at {loc}"); + }; + let syn::Pat::Ident(pat_ident) = &*pat_type.pat else { + panic!("non-ident param pattern at {loc}"); + }; + let name = &pat_ident.ident; + let ty = &*pat_type.ty; + + match self.classify_arg(ty) { + ArgKind::OpaqueRef(elem) => { + let ptr_ident = format_ident!("{}_ptr", name); + jni_params.push(quote! { #ptr_ident: *const #elem }); + prelude.push(quote! { + let #name = #owned_object::from_raw(#ptr_ident); + }); + call_args.push(quote! { &#name }); + } + ArgKind::KeyExpr => { + let decoder = self + .cfg + .key_expr_decoder + .as_ref() + .expect("key_expr_decoder not configured"); + let ptr_ident = format_ident!("{}_ptr", name); + let str_ident = format_ident!("{}_str", name); + jni_params.push(quote! { + #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> + }); + jni_params.push(quote! { #str_ident: jni::objects::JString }); + prelude.push(quote! { + let #name = #decoder(&mut env, &#str_ident, #ptr_ident)?; + }); + call_args.push(quote! { #name }); + } + ArgKind::Enum(decoder) => { + jni_params.push(quote! { #name: jni::sys::jint }); + prelude.push(quote! { + let #name = #decoder(#name)?; + }); + call_args.push(quote! { #name }); + } + ArgKind::Bool => { + jni_params.push(quote! { #name: jni::sys::jboolean }); + prelude.push(quote! { let #name = #name != 0; }); + call_args.push(quote! { #name }); + } + ArgKind::Duration => { + jni_params.push(quote! { #name: jni::sys::jlong }); + prelude.push(quote! { + let #name = std::time::Duration::from_millis(#name as u64); + }); + call_args.push(quote! { #name }); + } + ArgKind::Unsupported => panic!( + "unsupported parameter type `{}` for `{}` at {loc}", + ty.to_token_stream(), + name + ), + } + } + + let (ret_ty_jni, wrap_ok, on_err, closure_ret): ( + TokenStream, + TokenStream, + TokenStream, + TokenStream, + ) = match &func.sig.output { + syn::ReturnType::Type(_, ty) => { + let inner = extract_zresult_inner(ty).unwrap_or_else(|| { + panic!("return must be ZResult for `{original_name}` at {loc}") + }); + if is_unit(&inner) { + ( + quote! { () }, + quote! { Ok(()) }, + quote! { () }, + quote! { #zresult<()> }, + ) + } else { + ( + quote! { *const #inner }, + quote! { Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) }, + quote! { std::ptr::null() }, + quote! { #zresult<*const #inner> }, + ) + } + } + syn::ReturnType::Default => ( + quote! { () }, + quote! { Ok(()) }, + quote! { () }, + quote! { #zresult<()> }, + ), + }; + + let body = quote! { + { + (|| -> #closure_ret { + #(#prelude)* + let __result = #source_module::#orig_ident( #(#call_args),* )?; + #wrap_ok + })() + .unwrap_or_else(|err| { + #throw_exception!(env, err); + #on_err + }) + } + }; + + let tokens = quote! { + #[no_mangle] + #[allow(non_snake_case, unused_mut, unused_variables)] + pub unsafe extern "C" fn #jni_name( + mut env: jni::JNIEnv, + _class: jni::objects::JClass, + #(#jni_params),* + ) -> #ret_ty_jni #body + }; + + syn::parse2(tokens).expect("generated JNI wrapper must parse") + } + + fn classify_arg(&self, ty: &syn::Type) -> ArgKind { + match ty { + syn::Type::Reference(r) if r.mutability.is_none() => { + if type_last_segment(&r.elem).map(|s| s == "KeyExpr").unwrap_or(false) { + ArgKind::KeyExpr + } else { + ArgKind::OpaqueRef((*r.elem).clone()) + } + } + syn::Type::Path(tp) => { + let Some(last) = tp.path.segments.last() else { + return ArgKind::Unsupported; + }; + let name = last.ident.to_string(); + if name == "bool" { + return ArgKind::Bool; + } + if name == "KeyExpr" { + return ArgKind::KeyExpr; + } + if name == "Duration" { + return ArgKind::Duration; + } + if let Some(decoder) = self.cfg.enum_decoders.get(&name) { + return ArgKind::Enum(decoder.clone()); + } + ArgKind::Unsupported + } + _ => ArgKind::Unsupported, + } + } +} + +enum ArgKind { + OpaqueRef(syn::Type), + KeyExpr, + Enum(syn::Path), + Bool, + Duration, + Unsupported, +} + +fn type_last_segment(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + tp.path.segments.last().map(|s| s.ident.to_string()) +} + +fn is_unit(ty: &syn::Type) -> bool { + matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) +} + +fn extract_zresult_inner(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + let seg = tp.path.segments.last()?; + if seg.ident != "ZResult" { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + let arg = args.args.first()?; + let syn::GenericArgument::Type(inner) = arg else { + return None; + }; + Some(inner.clone()) +} + +fn snake_to_camel(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut upper_next = false; + for (i, c) in s.chars().enumerate() { + if c == '_' { + upper_next = true; + } else if upper_next { + out.extend(c.to_uppercase()); + upper_next = false; + } else if i == 0 { + out.extend(c.to_lowercase()); + } else { + out.push(c); + } + } + out +} diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index d7014e34..0b6a6f60 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -5,5 +5,6 @@ pub const FEATURES: &str = prebindgen_proc_macro::features!(); pub mod config; pub mod errors; +pub mod jni_converter; pub mod session; diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 8632ba3b..3f350b3d 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -92,6 +92,7 @@ pub fn declare_subscriber( } /// Declare a querier through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn declare_querier( session: &Session, key_expr: KeyExpr<'static>, diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index 2143f669..ef7fffde 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -1150,6 +1150,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1839,7 +1848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" dependencies = [ "if_rust_version", - "itertools", + "itertools 0.14.0", "konst", "prettyplease", "proc-macro2", @@ -3914,7 +3923,7 @@ dependencies = [ "flume 0.11.1", "futures", "git-version", - "itertools", + "itertools 0.14.0", "json5", "lazy_static", "nonempty-collections", @@ -4049,10 +4058,14 @@ dependencies = [ name = "zenoh-flat" version = "1.9.0" dependencies = [ + "itertools 0.12.1", "json5", "prebindgen", "prebindgen-proc-macro", + "proc-macro2", + "quote", "serde_yaml", + "syn 2.0.117", "tracing", "zenoh", ] @@ -4424,15 +4437,13 @@ dependencies = [ "async-std", "clap", "flume 0.10.14", + "itertools 0.12.1", "jni", "json5", "konst", "prebindgen", - "proc-macro2", - "quote", "rustc_version", "serde_yaml", - "syn 2.0.117", "tracing", "uhlc", "zenoh", diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index 6ec42048..e5d7fd68 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -49,9 +49,7 @@ crate_type = ["staticlib", "dylib"] rustc_version = "0.4.0" prebindgen = "0.4.1" zenoh-flat = { path = "../zenoh-flat" } -syn = { version = "2", features = ["full", "extra-traits"] } -quote = "1" -proc-macro2 = "1" +itertools = "0.12" [profile.release] debug = false # If you want debug symbol in release mode, set the env variable: RUSTFLAGS=-g diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 470dd510..6d08818a 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,242 +1,27 @@ -use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; +use itertools::Itertools; fn main() { let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); + let converter = zenoh_flat::jni_converter::JniConverter::builder() + .class_prefix("Java_io_zenoh_jni_JNISession_") + .function_suffix("ViaJNI") + .source_module("zenoh_flat::session") + .owned_object("crate::owned_object::OwnedObject") + .zresult("crate::errors::ZResult") + .throw_exception("crate::throw_exception") + .key_expr_decoder("crate::key_expr::process_kotlin_key_expr") + .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") + .enum_decoder("Priority", "crate::utils::decode_priority") + .enum_decoder("Reliability", "crate::utils::decode_reliability") + .enum_decoder("QueryTarget", "crate::utils::decode_query_target") + .enum_decoder("ConsolidationMode", "crate::utils::decode_consolidation") + .enum_decoder("ReplyKeyExpr", "crate::utils::decode_reply_key_expr") + .build(); + source .items_all() - .map(|(item, loc)| (jni_convert(item, &loc), loc)) + .batching(converter.into_closure()) .collect::() .write("zenoh_flat_jni.rs"); } - -fn jni_convert(item: syn::Item, loc: &prebindgen::SourceLocation) -> syn::Item { - let syn::Item::Fn(func) = item else { - return item; - }; - syn::Item::Fn(convert_fn(func, loc)) -} - -fn convert_fn(func: syn::ItemFn, loc: &prebindgen::SourceLocation) -> syn::ItemFn { - let original_name = func.sig.ident.to_string(); - let jni_name = format_ident!( - "Java_io_zenoh_jni_JNISession_{}ViaJNI", - snake_to_camel(&original_name) - ); - let orig_ident = &func.sig.ident; - - let mut prelude: Vec = Vec::new(); - let mut jni_params: Vec = Vec::new(); - let mut call_args: Vec = Vec::new(); - - for input in &func.sig.inputs { - let syn::FnArg::Typed(pat_type) = input else { - panic!("receiver args not supported at {loc}"); - }; - let syn::Pat::Ident(pat_ident) = &*pat_type.pat else { - panic!("non-ident param pattern at {loc}"); - }; - let name = &pat_ident.ident; - let ty = &*pat_type.ty; - - match classify_arg(ty) { - ArgKind::OpaqueRef(elem) => { - let ptr_ident = format_ident!("{}_ptr", name); - jni_params.push(quote! { #ptr_ident: *const #elem }); - prelude.push(quote! { - let #name = crate::owned_object::OwnedObject::from_raw(#ptr_ident); - }); - call_args.push(quote! { &#name }); - } - ArgKind::KeyExpr => { - let ptr_ident = format_ident!("{}_ptr", name); - let str_ident = format_ident!("{}_str", name); - jni_params.push(quote! { - #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> - }); - jni_params.push(quote! { #str_ident: jni::objects::JString }); - prelude.push(quote! { - let #name = crate::key_expr::process_kotlin_key_expr( - &mut env, &#str_ident, #ptr_ident, - )?; - }); - call_args.push(quote! { #name }); - } - ArgKind::Enum(decoder) => { - jni_params.push(quote! { #name: jni::sys::jint }); - prelude.push(quote! { - let #name = crate::utils::#decoder(#name)?; - }); - call_args.push(quote! { #name }); - } - ArgKind::Bool => { - jni_params.push(quote! { #name: jni::sys::jboolean }); - prelude.push(quote! { let #name = #name != 0; }); - call_args.push(quote! { #name }); - } - ArgKind::Unsupported => panic!( - "unsupported parameter type `{}` for `{}` at {loc}", - ty.to_token_stream(), - name - ), - } - } - - let (ret_ty_jni, wrap_ok, on_err, closure_ret): ( - TokenStream, - TokenStream, - TokenStream, - TokenStream, - ) = match &func.sig.output { - syn::ReturnType::Type(_, ty) => { - let inner = extract_zresult_inner(ty) - .unwrap_or_else(|| panic!("return must be ZResult for `{original_name}`")); - if is_unit(&inner) { - ( - quote! { () }, - quote! { Ok(()) }, - quote! { () }, - quote! { crate::errors::ZResult<()> }, - ) - } else { - ( - quote! { *const #inner }, - quote! { Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) }, - quote! { std::ptr::null() }, - quote! { crate::errors::ZResult<*const #inner> }, - ) - } - } - syn::ReturnType::Default => ( - quote! { () }, - quote! { Ok(()) }, - quote! { () }, - quote! { crate::errors::ZResult<()> }, - ), - }; - - let body = quote! { - { - (|| -> #closure_ret { - #(#prelude)* - let __result = zenoh_flat::session::#orig_ident( #(#call_args),* )?; - #wrap_ok - })() - .unwrap_or_else(|err| { - crate::throw_exception!(env, err); - #on_err - }) - } - }; - - let tokens = quote! { - #[no_mangle] - #[allow(non_snake_case, unused_mut, unused_variables)] - pub unsafe extern "C" fn #jni_name( - mut env: jni::JNIEnv, - _class: jni::objects::JClass, - #(#jni_params),* - ) -> #ret_ty_jni #body - }; - - syn::parse2(tokens).expect("generated JNI wrapper must parse") -} - -enum ArgKind { - /// `&T` (excluding `&KeyExpr`) — handle arg via `OwnedObject::from_raw`. - OpaqueRef(syn::Type), - /// `KeyExpr<'static>` — two JNI args (ptr + JString) via `process_kotlin_key_expr`. - KeyExpr, - /// Enum type passed as `jint` ordinal, with matching `decode_*` helper. - Enum(syn::Ident), - /// `bool` → `jboolean`. - Bool, - Unsupported, -} - -fn classify_arg(ty: &syn::Type) -> ArgKind { - match ty { - syn::Type::Reference(r) if r.mutability.is_none() => { - if type_last_segment(&r.elem).map(|s| s == "KeyExpr").unwrap_or(false) { - ArgKind::KeyExpr - } else { - ArgKind::OpaqueRef((*r.elem).clone()) - } - } - syn::Type::Path(tp) => { - let Some(last) = tp.path.segments.last() else { - return ArgKind::Unsupported; - }; - let name = last.ident.to_string(); - if name == "bool" { - return ArgKind::Bool; - } - if name == "KeyExpr" { - return ArgKind::KeyExpr; - } - if let Some(decoder) = enum_decoder(&name) { - return ArgKind::Enum(format_ident!("{}", decoder)); - } - ArgKind::Unsupported - } - _ => ArgKind::Unsupported, - } -} - -fn enum_decoder(type_name: &str) -> Option<&'static str> { - Some(match type_name { - "CongestionControl" => "decode_congestion_control", - "Priority" => "decode_priority", - "Reliability" => "decode_reliability", - "QueryTarget" => "decode_query_target", - "ConsolidationMode" => "decode_consolidation", - "ReplyKeyExpr" => "decode_reply_key_expr", - _ => return None, - }) -} - -fn type_last_segment(ty: &syn::Type) -> Option { - let syn::Type::Path(tp) = ty else { return None }; - tp.path.segments.last().map(|s| s.ident.to_string()) -} - -fn is_unit(ty: &syn::Type) -> bool { - matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) -} - -fn extract_zresult_inner(ty: &syn::Type) -> Option { - let syn::Type::Path(tp) = ty else { - return None; - }; - let seg = tp.path.segments.last()?; - if seg.ident != "ZResult" { - return None; - } - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return None; - }; - let arg = args.args.first()?; - let syn::GenericArgument::Type(inner) = arg else { - return None; - }; - Some(inner.clone()) -} - -fn snake_to_camel(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - let mut upper_next = false; - for (i, c) in s.chars().enumerate() { - if c == '_' { - upper_next = true; - } else if upper_next { - out.extend(c.to_uppercase()); - upper_next = false; - } else if i == 0 { - out.extend(c.to_lowercase()); - } else { - out.push(c); - } - } - out -} diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 3cd0e6a8..ed81ffa6 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -276,68 +276,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( }) } -/// Declare a Zenoh querier via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: A raw pointer to the [KeyExpr] to be used for the querier. May be null in case of using an -/// undeclared key expression. -/// - `key_expr_str`: String representation of the key expression to be used to declare the querier. -/// It won't be considered in case a key_expr_ptr to a declared key expression is provided. -/// - `target`: The ordinal value of the query target enum value. -/// - `consolidation`: The ordinal value of the consolidation enum value. -/// - `congestion_control`: The ordinal value of the congestion control enum value. -/// - `priority`: The ordinal value of the priority enum value. -/// - `is_express`: The boolean express value of the QoS provided. -/// - `timeout_ms`: The timeout in milliseconds. -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - target: jint, - consolidation: jint, - congestion_control: jint, - priority: jint, - is_express: jboolean, - timeout_ms: jlong, - accept_replies: jint, -) -> *const Querier<'static> { - || -> ZResult<*const Querier<'static>> { - let session = OwnedObject::from_raw(session_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let query_target = decode_query_target(target)?; - let consolidation = decode_consolidation(consolidation)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let is_express = is_express != 0; - let timeout = Duration::from_millis(timeout_ms as u64); - let reply_key_expr = decode_reply_key_expr(accept_replies)?; - let querier = zenoh_flat::session::declare_querier( - &session, - key_expr, - query_target, - consolidation, - congestion_control, - priority, - is_express, - timeout, - reply_key_expr, - )?; - Ok(Arc::into_raw(Arc::new(querier))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - /// Declare a Zenoh queryable via JNI. /// /// This function is meant to be called from Java/Kotlin code through JNI. From cba0dc334a6b76c45b36bff8e70735afe292ee58 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 22:54:45 +0200 Subject: [PATCH 083/126] delete operation wrapped --- zenoh-flat/src/jni_converter.rs | 61 ++++++++++++++++++++++++++++++ zenoh-flat/src/session.rs | 1 + zenoh-jni/build.rs | 1 + zenoh-jni/src/session.rs | 67 --------------------------------- 4 files changed, 63 insertions(+), 67 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index b73333fb..56f8ac51 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -48,6 +48,7 @@ pub struct Builder { zresult: syn::Path, throw_exception: syn::Path, key_expr_decoder: Option, + byte_array_decoder: Option, enum_decoders: HashMap, } @@ -61,6 +62,7 @@ impl Default for Builder { zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), key_expr_decoder: None, + byte_array_decoder: None, enum_decoders: HashMap::new(), } } @@ -114,6 +116,15 @@ impl Builder { self } + /// Path of the function that decodes a `JByteArray` into `Vec`, e.g. + /// `"crate::utils::decode_byte_array"`. Used for both `Vec` and + /// `Option>` parameters. + pub fn byte_array_decoder(mut self, path: impl AsRef) -> Self { + self.byte_array_decoder = + Some(syn::parse_str(path.as_ref()).expect("invalid byte_array_decoder path")); + self + } + /// Register a decoder for an enum type. `type_name` is matched against the /// last segment of the parameter's type path. pub fn enum_decoder( @@ -253,6 +264,22 @@ impl JniConverter { }); call_args.push(quote! { #name }); } + ArgKind::OptionVecU8 => { + let decoder = self + .cfg + .byte_array_decoder + .as_ref() + .expect("byte_array_decoder not configured"); + jni_params.push(quote! { #name: jni::objects::JByteArray }); + prelude.push(quote! { + let #name = if !#name.is_null() { + Some(#decoder(&env, #name)?) + } else { + None + }; + }); + call_args.push(quote! { #name }); + } ArgKind::Unsupported => panic!( "unsupported parameter type `{}` for `{}` at {loc}", ty.to_token_stream(), @@ -345,6 +372,9 @@ impl JniConverter { if name == "Duration" { return ArgKind::Duration; } + if name == "Option" && is_option_of_vec_u8(last) { + return ArgKind::OptionVecU8; + } if let Some(decoder) = self.cfg.enum_decoders.get(&name) { return ArgKind::Enum(decoder.clone()); } @@ -361,6 +391,8 @@ enum ArgKind { Enum(syn::Path), Bool, Duration, + /// `Option>` → `JByteArray` decoded via `byte_array_decoder`. + OptionVecU8, Unsupported, } @@ -369,6 +401,35 @@ fn type_last_segment(ty: &syn::Type) -> Option { tp.path.segments.last().map(|s| s.ident.to_string()) } +/// Check whether an `Option<...>` path segment wraps exactly `Vec`. +fn is_option_of_vec_u8(seg: &syn::PathSegment) -> bool { + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return false; + }; + let Some(syn::GenericArgument::Type(inner)) = args.args.first() else { + return false; + }; + let syn::Type::Path(inner_path) = inner else { + return false; + }; + let Some(inner_seg) = inner_path.path.segments.last() else { + return false; + }; + if inner_seg.ident != "Vec" { + return false; + } + let syn::PathArguments::AngleBracketed(vec_args) = &inner_seg.arguments else { + return false; + }; + let Some(syn::GenericArgument::Type(elem)) = vec_args.args.first() else { + return false; + }; + matches!( + elem, + syn::Type::Path(tp) if tp.path.is_ident("u8") + ) +} + fn is_unit(ty: &syn::Type) -> bool { matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) } diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 3f350b3d..3698c7df 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -185,6 +185,7 @@ pub fn put( } /// Perform a delete operation through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn delete( session: &Session, key_expr: KeyExpr<'static>, diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 6d08818a..b58bb2c8 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -11,6 +11,7 @@ fn main() { .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") .key_expr_decoder("crate::key_expr::process_kotlin_key_expr") + .byte_array_decoder("crate::utils::decode_byte_array") .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") .enum_decoder("Priority", "crate::utils::decode_priority") .enum_decoder("Reliability", "crate::utils::decode_reliability") diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index ed81ffa6..d5cd80f7 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -154,73 +154,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( }); } -/// Performs a `delete` operation in the Zenoh session via JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the operation, may be null. -/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the operation. -/// It is only considered when the key_expr_ptr parameter is null, meaning the function is -/// receiving a key expression that was not declared. -/// - `session_ptr`: Raw pointer to the [Session] to be used for the operation. -/// - `congestion_control`: The [CongestionControl] mechanism specified. -/// - `priority`: The [Priority] mechanism specified. -/// - `is_express`: The express flag. -/// - `attachment`: Optional attachment encoded into a byte array. May be null. -/// - `reliability`: The reliability value as an ordinal. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw a JNI exception or a Session exception in case of failure, which -/// should be handled by the Java/Kotlin caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - congestion_control: jint, - priority: jint, - is_express: jboolean, - attachment: JByteArray, - reliability: jint, -) { - || -> ZResult<()> { - let session = OwnedObject::from_raw(session_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let is_express = is_express != 0; - let attachment = if !attachment.is_null() { - Some(decode_byte_array(&env, attachment)?) - } else { - None - }; - let reliability = decode_reliability(reliability)?; - - zenoh_flat::session::delete( - &session, - key_expr, - congestion_control, - priority, - is_express, - attachment, - reliability, - )?; - Ok(()) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - () - }); -} /// Declare a Zenoh subscriber via JNI. /// From d7901ceb1b96ef43461b639f3b248cb52dde71a8 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 23:05:04 +0200 Subject: [PATCH 084/126] put is wrapped --- zenoh-flat/src/jni_converter.rs | 58 ++++++++++++++++++++++++- zenoh-flat/src/session.rs | 1 + zenoh-jni/build.rs | 1 + zenoh-jni/src/session.rs | 76 --------------------------------- 4 files changed, 58 insertions(+), 78 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 56f8ac51..31b7b6e6 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -49,6 +49,7 @@ pub struct Builder { throw_exception: syn::Path, key_expr_decoder: Option, byte_array_decoder: Option, + encoding_decoder: Option, enum_decoders: HashMap, } @@ -63,6 +64,7 @@ impl Default for Builder { throw_exception: syn::parse_str("throw_exception").unwrap(), key_expr_decoder: None, byte_array_decoder: None, + encoding_decoder: None, enum_decoders: HashMap::new(), } } @@ -125,6 +127,16 @@ impl Builder { self } + /// Path of the function that decodes an `Encoding` from a `(jint id, + /// &JString schema)` pair, e.g. `"crate::utils::decode_encoding"`. + /// The generated JNI signature splits the single `Encoding` parameter + /// into `_id: jint` + `_schema: JString`. + pub fn encoding_decoder(mut self, path: impl AsRef) -> Self { + self.encoding_decoder = + Some(syn::parse_str(path.as_ref()).expect("invalid encoding_decoder path")); + self + } + /// Register a decoder for an enum type. `type_name` is matched against the /// last segment of the parameter's type path. pub fn enum_decoder( @@ -280,6 +292,33 @@ impl JniConverter { }); call_args.push(quote! { #name }); } + ArgKind::VecU8 => { + let decoder = self + .cfg + .byte_array_decoder + .as_ref() + .expect("byte_array_decoder not configured"); + jni_params.push(quote! { #name: jni::objects::JByteArray }); + prelude.push(quote! { + let #name = #decoder(&env, #name)?; + }); + call_args.push(quote! { #name }); + } + ArgKind::Encoding => { + let decoder = self + .cfg + .encoding_decoder + .as_ref() + .expect("encoding_decoder not configured"); + let id_ident = format_ident!("{}_id", name); + let schema_ident = format_ident!("{}_schema", name); + jni_params.push(quote! { #id_ident: jni::sys::jint }); + jni_params.push(quote! { #schema_ident: jni::objects::JString }); + prelude.push(quote! { + let #name = #decoder(&mut env, #id_ident, &#schema_ident)?; + }); + call_args.push(quote! { #name }); + } ArgKind::Unsupported => panic!( "unsupported parameter type `{}` for `{}` at {loc}", ty.to_token_stream(), @@ -372,9 +411,15 @@ impl JniConverter { if name == "Duration" { return ArgKind::Duration; } + if name == "Encoding" { + return ArgKind::Encoding; + } if name == "Option" && is_option_of_vec_u8(last) { return ArgKind::OptionVecU8; } + if name == "Vec" && is_vec_of_u8(last) { + return ArgKind::VecU8; + } if let Some(decoder) = self.cfg.enum_decoders.get(&name) { return ArgKind::Enum(decoder.clone()); } @@ -393,6 +438,10 @@ enum ArgKind { Duration, /// `Option>` → `JByteArray` decoded via `byte_array_decoder`. OptionVecU8, + /// `Vec` → `JByteArray` decoded via `byte_array_decoder`. + VecU8, + /// `Encoding` → `(jint id, JString schema)` pair via `encoding_decoder`. + Encoding, Unsupported, } @@ -418,10 +467,15 @@ fn is_option_of_vec_u8(seg: &syn::PathSegment) -> bool { if inner_seg.ident != "Vec" { return false; } - let syn::PathArguments::AngleBracketed(vec_args) = &inner_seg.arguments else { + is_vec_of_u8(inner_seg) +} + +/// Check whether a `Vec<...>` path segment has element type `u8`. +fn is_vec_of_u8(seg: &syn::PathSegment) -> bool { + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return false; }; - let Some(syn::GenericArgument::Type(elem)) = vec_args.args.first() else { + let Some(syn::GenericArgument::Type(elem)) = args.args.first() else { return false; }; matches!( diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 3698c7df..4c497bf0 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -149,6 +149,7 @@ pub fn declare_queryable( } /// Perform a put operation through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn put( session: &Session, key_expr: KeyExpr<'static>, diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index b58bb2c8..d40b71b7 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -12,6 +12,7 @@ fn main() { .throw_exception("crate::throw_exception") .key_expr_decoder("crate::key_expr::process_kotlin_key_expr") .byte_array_decoder("crate::utils::decode_byte_array") + .encoding_decoder("crate::utils::decode_encoding") .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") .enum_decoder("Priority", "crate::utils::decode_priority") .enum_decoder("Reliability", "crate::utils::decode_reliability") diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index d5cd80f7..7e67af86 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -77,82 +77,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( }); } -/// Performs a `put` operation in the Zenoh session via JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the operation, may be null. -/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the operation. -/// It is only considered when the key_expr_ptr parameter is null, meaning the function is -/// receiving a key expression that was not declared. -/// - `session_ptr`: Raw pointer to the [Session] to be used for the operation. -/// - `payload`: The payload to send through the network. -/// - `encoding_id`: The encoding id of the payload. -/// - `encoding_schema`: Optional encoding schema, may be null. -/// - `congestion_control`: The [CongestionControl] mechanism specified. -/// - `priority`: The [Priority] mechanism specified. -/// - `is_express`: The express flag. -/// - `attachment`: Optional attachment encoded into a byte array. May be null. -/// - `reliability`: The reliability value as an ordinal. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw an exception in case of failure, which should be handled by the Java/Kotlin caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - payload: JByteArray, - encoding_id: jint, - encoding_schema: JString, - congestion_control: jint, - priority: jint, - is_express: jboolean, - attachment: JByteArray, - reliability: jint, -) { - || -> ZResult<()> { - let session = OwnedObject::from_raw(session_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let payload = decode_byte_array(&env, payload)?; - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let is_express = is_express != 0; - let attachment = if !attachment.is_null() { - Some(decode_byte_array(&env, attachment)?) - } else { - None - }; - let reliability = decode_reliability(reliability)?; - - zenoh_flat::session::put( - &session, - key_expr, - payload, - encoding, - congestion_control, - priority, - is_express, - attachment, - reliability, - )?; - Ok(()) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - () - }); -} /// Declare a Zenoh subscriber via JNI. From 25befc766b99006cef3c7c8c480c8ab03a6c6439 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 23:12:07 +0200 Subject: [PATCH 085/126] close converted --- zenoh-flat/src/jni_converter.rs | 34 +++++++++++++++++++++++++++++---- zenoh-flat/src/session.rs | 1 + zenoh-jni/build.rs | 1 + zenoh-jni/src/session.rs | 30 ----------------------------- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 31b7b6e6..31ff566a 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -32,7 +32,7 @@ //! decoder helpers, `OwnedObject`) — those couplings are configurable through //! the [`Builder`] so the converter itself stays data-driven. -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; @@ -51,6 +51,10 @@ pub struct Builder { byte_array_decoder: Option, encoding_decoder: Option, enum_decoders: HashMap, + /// Names of source functions whose first `&T` argument must be consumed + /// (transferred out) via `Arc::from_raw` instead of borrowed via + /// `OwnedObject::from_raw`. Typically used for `close_*` functions. + consume_fns: HashSet, } impl Default for Builder { @@ -66,6 +70,7 @@ impl Default for Builder { byte_array_decoder: None, encoding_decoder: None, enum_decoders: HashMap::new(), + consume_fns: HashSet::new(), } } } @@ -150,6 +155,17 @@ impl Builder { self } + /// Mark a source function as consuming its first `&T` argument: the + /// generated wrapper will take ownership of the pointer via + /// `Arc::from_raw` (dropping the Arc at end of scope), rather than + /// borrowing it through `OwnedObject::from_raw`. + /// + /// Typically used for `close_*` functions that invalidate the handle. + pub fn consume_fn(mut self, fn_name: impl Into) -> Self { + self.consume_fns.insert(fn_name.into()); + self + } + pub fn build(self) -> JniConverter { JniConverter { cfg: self, @@ -216,10 +232,12 @@ impl JniConverter { let owned_object = &self.cfg.owned_object; let zresult = &self.cfg.zresult; let throw_exception = &self.cfg.throw_exception; + let consume_first = self.cfg.consume_fns.contains(&original_name); let mut prelude: Vec = Vec::new(); let mut jni_params: Vec = Vec::new(); let mut call_args: Vec = Vec::new(); + let mut seen_first_opaque = false; for input in &func.sig.inputs { let syn::FnArg::Typed(pat_type) = input else { @@ -235,9 +253,17 @@ impl JniConverter { ArgKind::OpaqueRef(elem) => { let ptr_ident = format_ident!("{}_ptr", name); jni_params.push(quote! { #ptr_ident: *const #elem }); - prelude.push(quote! { - let #name = #owned_object::from_raw(#ptr_ident); - }); + let is_first_opaque = !seen_first_opaque; + seen_first_opaque = true; + if consume_first && is_first_opaque { + prelude.push(quote! { + let #name = std::sync::Arc::from_raw(#ptr_ident); + }); + } else { + prelude.push(quote! { + let #name = #owned_object::from_raw(#ptr_ident); + }); + } call_args.push(quote! { &#name }); } ArgKind::KeyExpr => { diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 4c497bf0..3aff376b 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -220,6 +220,7 @@ pub fn delete( } /// Close a Zenoh session using a reference to the session. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn close_session(session: &Session) -> ZResult<()> { session .close() diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index d40b71b7..7f8cfaff 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -19,6 +19,7 @@ fn main() { .enum_decoder("QueryTarget", "crate::utils::decode_query_target") .enum_decoder("ConsolidationMode", "crate::utils::decode_consolidation") .enum_decoder("ReplyKeyExpr", "crate::utils::decode_reply_key_expr") + .consume_fn("close_session") .build(); source diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 7e67af86..9dd1c01a 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -46,36 +46,6 @@ use crate::{ include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Closes a Zenoh session via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `session_ptr`: The raw pointer to the Zenoh session. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// - After the session is closed, the provided pointer is no more valid. -/// -#[no_mangle] -#[allow(non_snake_case, unused)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, -) { - || -> ZResult<()> { - let session = Arc::from_raw(session_ptr); - zenoh_flat::session::close_session(&session)?; - Ok(()) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - () - }); -} From e3a2aec2dbabee9b03bc49c1f043a6ca95b53c8d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 23:24:19 +0200 Subject: [PATCH 086/126] get in flat --- zenoh-flat/Cargo.lock | 41 +++++++-------- zenoh-flat/src/session.rs | 60 ++++++++++++++++++++- zenoh-jni/src/session.rs | 107 ++++++++++++++++++++------------------ 3 files changed, 135 insertions(+), 73 deletions(-) diff --git a/zenoh-flat/Cargo.lock b/zenoh-flat/Cargo.lock index ffb5009e..339332b0 100644 --- a/zenoh-flat/Cargo.lock +++ b/zenoh-flat/Cargo.lock @@ -752,6 +752,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1178,24 +1187,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prebindgen" -version = "0.4.1" -dependencies = [ - "if_rust_version", - "itertools", - "konst 0.3.17", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "roxygen", - "serde", - "serde_json", - "syn 2.0.117", - "toml", -] - [[package]] name = "prebindgen" version = "0.4.1" @@ -1203,7 +1194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" dependencies = [ "if_rust_version", - "itertools", + "itertools 0.14.0", "konst 0.3.17", "prettyplease", "proc-macro2", @@ -1219,8 +1210,10 @@ dependencies = [ [[package]] name = "prebindgen-proc-macro" version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a68d8870ef9ba6f8abbe9d4bca5775e1d854d5fa6dac8c27dd1c79ecebe398" dependencies = [ - "prebindgen 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "prebindgen", "proc-macro2", "quote", "rand 0.9.4", @@ -2430,7 +2423,7 @@ dependencies = [ "flume", "futures", "git-version", - "itertools", + "itertools 0.14.0", "json5", "lazy_static", "nonempty-collections", @@ -2546,10 +2539,14 @@ dependencies = [ name = "zenoh-flat" version = "1.9.0" dependencies = [ + "itertools 0.12.1", "json5", - "prebindgen 0.4.1", + "prebindgen", "prebindgen-proc-macro", + "proc-macro2", + "quote", "serde_yaml", + "syn 2.0.117", "tracing", "zenoh", ] diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 3aff376b..a90e6480 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -20,7 +20,7 @@ use zenoh::{ config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, - query::{ConsolidationMode, Query, QueryTarget, Queryable, Querier, ReplyKeyExpr}, + query::{ConsolidationMode, Query, QueryTarget, Queryable, Querier, Reply, ReplyKeyExpr, Selector}, sample::Sample, qos::{CongestionControl, Priority, Reliability}, session::Session, @@ -148,6 +148,64 @@ pub fn declare_queryable( }) } +/// Perform a get (query) through an existing Zenoh session. +/// +/// Each [`Reply`] received from the network is delivered to the `callback`. +/// `selector_params` is appended to the `key_expr` to form the query selector +/// (pass `String::new()` for no parameters). `payload` and `encoding` are +/// coupled: if a payload is given, encoding is attached; if no payload is +/// given, encoding is ignored. +pub fn get( + session: &Session, + key_expr: KeyExpr<'static>, + selector_params: String, + callback: impl Fn(Reply) + Send + Sync + 'static, + query_target: QueryTarget, + consolidation: ConsolidationMode, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + timeout: Duration, + reply_key_expr: ReplyKeyExpr, + payload: Option>, + encoding: Option, + attachment: Option>, +) -> ZResult<()> { + let key_expr_string = key_expr.to_string(); + let selector = Selector::owned(&key_expr, selector_params); + let mut get_builder = session + .get(selector) + .callback(callback) + .target(query_target) + .consolidation(consolidation) + .congestion_control(congestion_control) + .priority(priority) + .express(express) + .timeout(timeout) + .accept_replies(reply_key_expr); + + if let Some(payload) = payload { + if let Some(encoding) = encoding { + get_builder = get_builder.encoding(encoding); + } + get_builder = get_builder.payload(payload); + } + + if let Some(attachment) = attachment { + get_builder = get_builder.attachment::>(attachment); + } + + get_builder + .wait() + .map(|_| { + trace!("Performing get on '{}'.", key_expr_string); + }) + .map_err(|err| { + error!("Unable to perform get on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + /// Perform a put operation through an existing Zenoh session. #[prebindgen_proc_macro::prebindgen("jni")] pub fn put( diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 9dd1c01a..9c069c1e 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -23,7 +23,7 @@ use zenoh::{ config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, - query::{Querier, Queryable, ReplyError, Selector}, + query::{Querier, Queryable, ReplyError}, sample::Sample, session::{EntityGlobalId, Session, ZenohId}, Wait, @@ -322,57 +322,64 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( } else { decode_string(&mut env, &selector_params)? }; - let selector = Selector::owned(&key_expr, selector_params); - let mut get_builder = session - .get(selector) - .congestion_control(congestion_control) - .priority(priority) - .express(is_express != 0) - .callback(move |reply| { - || -> ZResult<()> { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - tracing::debug!("Receiving reply through JNI: {:?}", reply); - let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { - zerror!("Unable to attach thread for GET query callback: {}.", err) - })?; - - match reply.result() { - Ok(sample) => on_reply_success( - &mut env, - reply.replier_id(), - sample, - &callback_global_ref, - ), - Err(error) => on_reply_error( - &mut env, - reply.replier_id(), - error, - &callback_global_ref, - ), - } - }() - .unwrap_or_else(|err| tracing::error!("Error on get callback: {err}")); - }) - .target(query_target) - .timeout(timeout) - .consolidation(consolidation) - .accept_replies(reply_key_expr); - - if !payload.is_null() { - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; - get_builder = get_builder.encoding(encoding); - get_builder = get_builder.payload(decode_byte_array(&env, payload)?); - } - if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; - get_builder = get_builder.attachment::>(attachment); - } + let reply_callback = move |reply: zenoh::query::Reply| { + || -> ZResult<()> { + on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure + tracing::debug!("Receiving reply through JNI: {:?}", reply); + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + zerror!("Unable to attach thread for GET query callback: {}.", err) + })?; + match reply.result() { + Ok(sample) => on_reply_success( + &mut env, + reply.replier_id(), + sample, + &callback_global_ref, + ), + Err(error) => on_reply_error( + &mut env, + reply.replier_id(), + error, + &callback_global_ref, + ), + } + }() + .unwrap_or_else(|err| tracing::error!("Error on get callback: {err}")); + }; - get_builder - .wait() - .map(|_| tracing::trace!("Performing get on '{key_expr}'.",)) - .map_err(|err| zerror!(err)) + let payload = if !payload.is_null() { + Some(decode_byte_array(&env, payload)?) + } else { + None + }; + let encoding = if payload.is_some() { + Some(decode_encoding(&mut env, encoding_id, &encoding_schema)?) + } else { + None + }; + let attachment = if !attachment.is_null() { + Some(decode_byte_array(&env, attachment)?) + } else { + None + }; + + zenoh_flat::session::get( + &session, + key_expr, + selector_params, + reply_callback, + query_target, + consolidation, + congestion_control, + priority, + is_express != 0, + timeout, + reply_key_expr, + payload, + encoding, + attachment, + ) }() .map_err(|err| throw_exception!(env, err)); } From a1a765f3ae68bcd0efb442331accbf5724fd9de7 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 23:31:21 +0200 Subject: [PATCH 087/126] reply callback function --- zenoh-jni/src/sample_callback.rs | 38 +++++++++++++++++++++++++++++++- zenoh-jni/src/session.rs | 34 ++++------------------------ 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index 73c4e6f5..04ae030a 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -19,7 +19,7 @@ use jni::{ sys::{jint, jlong}, JNIEnv, }; -use zenoh::{query::{Query, ReplyKeyExpr}, sample::Sample}; +use zenoh::{query::{Query, Reply, ReplyKeyExpr}, sample::Sample}; use crate::{errors::ZResult, utils::*}; @@ -96,6 +96,42 @@ pub(crate) unsafe fn process_kotlin_sample_callback( }) } +pub(crate) unsafe fn process_kotlin_reply_callback( + env: &mut JNIEnv, + callback: JObject, + on_close: JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let on_close = load_on_close(&java_vm, on_close_global_ref); + + Ok(move |reply: Reply| { + || -> ZResult<()> { + on_close.noop(); + tracing::debug!("Receiving reply through JNI: {:?}", reply); + let mut env = java_vm + .attach_current_thread_as_daemon() + .map_err(|err| zerror!("Unable to attach thread for GET query callback: {}", err))?; + match reply.result() { + Ok(sample) => crate::session::on_reply_success( + &mut env, + reply.replier_id(), + sample, + &callback_global_ref, + ), + Err(error) => crate::session::on_reply_error( + &mut env, + reply.replier_id(), + error, + &callback_global_ref, + ), + } + }() + .unwrap_or_else(|err| tracing::error!("Error on get callback: {err}")); + }) +} + pub(crate) unsafe fn process_kotlin_query_callback( env: &mut JNIEnv, callback: JObject, diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 9c069c1e..553369f8 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -30,7 +30,9 @@ use zenoh::{ }; use crate::owned_object::OwnedObject; -use crate::sample_callback::{process_kotlin_query_callback, process_kotlin_sample_callback}; +use crate::sample_callback::{ + process_kotlin_query_callback, process_kotlin_reply_callback, process_kotlin_sample_callback, +}; #[cfg(feature = "zenoh-ext")] use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] @@ -307,47 +309,19 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( let session = OwnedObject::from_raw(session_ptr); let _ = || -> ZResult<()> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let reply_callback = process_kotlin_reply_callback(&mut env, callback, on_close)?; let query_target = decode_query_target(target)?; let consolidation = decode_consolidation(consolidation)?; let timeout = Duration::from_millis(timeout_ms as u64); let congestion_control = decode_congestion_control(congestion_control)?; let priority = decode_priority(priority)?; let reply_key_expr = decode_reply_key_expr(accept_replies)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); let selector_params = if selector_params.is_null() { String::new() } else { decode_string(&mut env, &selector_params)? }; - let reply_callback = move |reply: zenoh::query::Reply| { - || -> ZResult<()> { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - tracing::debug!("Receiving reply through JNI: {:?}", reply); - let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { - zerror!("Unable to attach thread for GET query callback: {}.", err) - })?; - match reply.result() { - Ok(sample) => on_reply_success( - &mut env, - reply.replier_id(), - sample, - &callback_global_ref, - ), - Err(error) => on_reply_error( - &mut env, - reply.replier_id(), - error, - &callback_global_ref, - ), - } - }() - .unwrap_or_else(|err| tracing::error!("Error on get callback: {err}")); - }; - let payload = if !payload.is_null() { Some(decode_byte_array(&env, payload)?) } else { From 603e0b1a9065abb27da81b1748976e50dda856ae Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 23:39:36 +0200 Subject: [PATCH 088/126] declare subscriber in prebindgen --- zenoh-flat/src/jni_converter.rs | 61 +++++++++++++++++++++++++++++++++ zenoh-flat/src/session.rs | 1 + zenoh-jni/build.rs | 3 ++ zenoh-jni/src/session.rs | 53 ---------------------------- 4 files changed, 65 insertions(+), 53 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 31ff566a..cb53efeb 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -51,6 +51,10 @@ pub struct Builder { byte_array_decoder: Option, encoding_decoder: Option, enum_decoders: HashMap, + /// Map from callback element type name (e.g. `"Sample"`) to the decoder + /// that builds an `impl Fn(T) + Send + Sync + 'static` closure from a + /// `(callback: JObject, on_close: JObject)` pair. + callback_decoders: HashMap, /// Names of source functions whose first `&T` argument must be consumed /// (transferred out) via `Arc::from_raw` instead of borrowed via /// `OwnedObject::from_raw`. Typically used for `close_*` functions. @@ -70,6 +74,7 @@ impl Default for Builder { byte_array_decoder: None, encoding_decoder: None, enum_decoders: HashMap::new(), + callback_decoders: HashMap::new(), consume_fns: HashSet::new(), } } @@ -155,6 +160,24 @@ impl Builder { self } + /// Register a decoder for an `impl Fn(T) + Send + Sync + 'static` callback + /// parameter. `element_type_name` is the last path segment of `T` + /// (e.g. `"Sample"`, `"Query"`, `"Reply"`). The decoder must have the + /// signature + /// `fn(&mut JNIEnv, JObject, JObject) -> ZResult`. + /// The generated JNI signature expands the single callback parameter into + /// two JNI args: `: JObject, _on_close: JObject`. + pub fn callback_decoder( + mut self, + element_type_name: impl Into, + decoder: impl AsRef, + ) -> Self { + let path: syn::Path = + syn::parse_str(decoder.as_ref()).expect("invalid callback_decoder path"); + self.callback_decoders.insert(element_type_name.into(), path); + self + } + /// Mark a source function as consuming its first `&T` argument: the /// generated wrapper will take ownership of the pointer via /// `Arc::from_raw` (dropping the Arc at end of scope), rather than @@ -330,6 +353,15 @@ impl JniConverter { }); call_args.push(quote! { #name }); } + ArgKind::Callback(decoder) => { + let on_close_ident = format_ident!("{}_on_close", name); + jni_params.push(quote! { #name: jni::objects::JObject }); + jni_params.push(quote! { #on_close_ident: jni::objects::JObject }); + prelude.push(quote! { + let #name = #decoder(&mut env, #name, #on_close_ident)?; + }); + call_args.push(quote! { #name }); + } ArgKind::Encoding => { let decoder = self .cfg @@ -423,6 +455,14 @@ impl JniConverter { ArgKind::OpaqueRef((*r.elem).clone()) } } + syn::Type::ImplTrait(it) => { + if let Some(elem) = extract_fn_single_arg_type_name(&it.bounds) { + if let Some(decoder) = self.cfg.callback_decoders.get(&elem) { + return ArgKind::Callback(decoder.clone()); + } + } + ArgKind::Unsupported + } syn::Type::Path(tp) => { let Some(last) = tp.path.segments.last() else { return ArgKind::Unsupported; @@ -468,6 +508,9 @@ enum ArgKind { VecU8, /// `Encoding` → `(jint id, JString schema)` pair via `encoding_decoder`. Encoding, + /// `impl Fn(T) + Send + Sync + 'static` → `(JObject callback, JObject on_close)` + /// pair decoded via a callback decoder registered for `T`. + Callback(syn::Path), Unsupported, } @@ -476,6 +519,24 @@ fn type_last_segment(ty: &syn::Type) -> Option { tp.path.segments.last().map(|s| s.ident.to_string()) } +/// Look through the trait bounds of an `impl Fn(T) + ...` for a `Fn`-family +/// trait and return the last-segment name of its single argument type `T`. +fn extract_fn_single_arg_type_name( + bounds: &syn::punctuated::Punctuated, +) -> Option { + for bound in bounds { + let syn::TypeParamBound::Trait(tb) = bound else { continue }; + let seg = tb.path.segments.last()?; + if !matches!(seg.ident.to_string().as_str(), "Fn" | "FnMut" | "FnOnce") { + continue; + } + let syn::PathArguments::Parenthesized(p) = &seg.arguments else { continue }; + let first = p.inputs.first()?; + return type_last_segment(first); + } + None +} + /// Check whether an `Option<...>` path segment wraps exactly `Vec`. fn is_option_of_vec_u8(seg: &syn::PathSegment) -> bool { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index a90e6480..9d8717b4 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -71,6 +71,7 @@ pub fn declare_publisher( } /// Declare a subscriber through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn declare_subscriber( session: &Session, key_expr: KeyExpr<'static>, diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 7f8cfaff..b03f77e5 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -19,6 +19,9 @@ fn main() { .enum_decoder("QueryTarget", "crate::utils::decode_query_target") .enum_decoder("ConsolidationMode", "crate::utils::decode_consolidation") .enum_decoder("ReplyKeyExpr", "crate::utils::decode_reply_key_expr") + .callback_decoder("Sample", "crate::sample_callback::process_kotlin_sample_callback") + .callback_decoder("Query", "crate::sample_callback::process_kotlin_query_callback") + .callback_decoder("Reply", "crate::sample_callback::process_kotlin_reply_callback") .consume_fn("close_session") .build(); diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 553369f8..1d0448de 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -51,59 +51,6 @@ include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Declare a Zenoh subscriber via JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: The key expression pointer for the subscriber. May be null in case of using an -/// undeclared key expression. -/// - `key_expr_str`: String representation of the key expression to be used to declare the subscriber. -/// It won't be considered in case a key_expr_ptr to a declared key expression is provided. -/// - `session_ptr`: The raw pointer to the Zenoh session. -/// - `callback`: The callback function as an instance of the `JNISubscriberCallback` interface in Java/Kotlin. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the subscriber. -/// -/// Returns: -/// - A raw pointer to the declared Zenoh subscriber. In case of failure, an exception is thrown and null is returned. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The callback function passed as `callback` must be a valid instance of the `JNISubscriberCallback` interface -/// in Java/Kotlin, matching the specified signature. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - callback: JObject, - on_close: JObject, -) -> *const Subscriber<()> { - || -> ZResult<*const Subscriber<()>> { - let session = OwnedObject::from_raw(session_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let callback = process_kotlin_sample_callback(&mut env, callback, on_close)?; - - let subscriber = zenoh_flat::session::declare_subscriber( - &session, - key_expr, - callback, - )?; - Ok(Arc::into_raw(Arc::new(subscriber))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} /// Declare a Zenoh queryable via JNI. /// From f0e59b890d181a479626ee345d84b7b9e6d7ab17 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 23:46:29 +0200 Subject: [PATCH 089/126] declare keyexpr in flat --- zenoh-flat/src/jni_converter.rs | 27 +++++++++++++++++++ zenoh-flat/src/session.rs | 24 +++++++++++++++++ zenoh-jni/build.rs | 1 + zenoh-jni/src/session.rs | 46 --------------------------------- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index cb53efeb..92217a8a 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -48,6 +48,7 @@ pub struct Builder { zresult: syn::Path, throw_exception: syn::Path, key_expr_decoder: Option, + string_decoder: Option, byte_array_decoder: Option, encoding_decoder: Option, enum_decoders: HashMap, @@ -71,6 +72,7 @@ impl Default for Builder { zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), key_expr_decoder: None, + string_decoder: None, byte_array_decoder: None, encoding_decoder: None, enum_decoders: HashMap::new(), @@ -128,6 +130,14 @@ impl Builder { self } + /// Path of the function that decodes a `JString` into `String`, e.g. + /// `"crate::utils::decode_string"`. + pub fn string_decoder(mut self, path: impl AsRef) -> Self { + self.string_decoder = + Some(syn::parse_str(path.as_ref()).expect("invalid string_decoder path")); + self + } + /// Path of the function that decodes a `JByteArray` into `Vec`, e.g. /// `"crate::utils::decode_byte_array"`. Used for both `Vec` and /// `Option>` parameters. @@ -306,6 +316,18 @@ impl JniConverter { }); call_args.push(quote! { #name }); } + ArgKind::String => { + let decoder = self + .cfg + .string_decoder + .as_ref() + .expect("string_decoder not configured"); + jni_params.push(quote! { #name: jni::objects::JString }); + prelude.push(quote! { + let #name = #decoder(&mut env, &#name)?; + }); + call_args.push(quote! { #name }); + } ArgKind::Enum(decoder) => { jni_params.push(quote! { #name: jni::sys::jint }); prelude.push(quote! { @@ -471,6 +493,9 @@ impl JniConverter { if name == "bool" { return ArgKind::Bool; } + if name == "String" { + return ArgKind::String; + } if name == "KeyExpr" { return ArgKind::KeyExpr; } @@ -499,6 +524,8 @@ impl JniConverter { enum ArgKind { OpaqueRef(syn::Type), KeyExpr, + /// `String` → `JString` decoded via `string_decoder`. + String, Enum(syn::Path), Bool, Duration, diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 9d8717b4..edd8142b 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -70,6 +70,30 @@ pub fn declare_publisher( }) } +/// Declare a key expression through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_key_expr(session: &Session, key_expr: String) -> ZResult> { + let key_expr_clone = key_expr.clone(); + session + .declare_keyexpr(key_expr) + .wait() + .map(|ke| { + trace!("Declared key expression '{}'.", key_expr_clone); + ke + }) + .map_err(|err| { + error!( + "Unable to declare key expression '{}': {}", + key_expr_clone, err + ); + zerror!( + "Unable to declare key expression '{}': {}", + key_expr_clone, + err + ) + }) +} + /// Declare a subscriber through an existing Zenoh session. #[prebindgen_proc_macro::prebindgen("jni")] pub fn declare_subscriber( diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index b03f77e5..5e8d4365 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -11,6 +11,7 @@ fn main() { .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") .key_expr_decoder("crate::key_expr::process_kotlin_key_expr") + .string_decoder("crate::utils::decode_string") .byte_array_decoder("crate::utils::decode_byte_array") .encoding_decoder("crate::utils::decode_encoding") .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 1d0448de..115540c3 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -106,52 +106,6 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( }) } -/// Declare a [KeyExpr] through a [Session] via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `session_ptr`: A raw pointer to the Zenoh [Session] from which to declare the key expression. -/// - `key_expr_str`: A Java String with the intended key expression. -/// -/// # Returns: -/// - A raw pointer to the declared key expression. In case of failure, an exception is thrown and null is returned. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw an exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_str: JString, -) -> *const KeyExpr<'static> { - || -> ZResult<*const KeyExpr<'static>> { - let session = OwnedObject::from_raw(session_ptr); - let key_expr_str = decode_string(&mut env, &key_expr_str)?; - let key_expr = session - .declare_keyexpr(key_expr_str.to_owned()) - .wait() - .map_err(|err| { - zerror!( - "Unable to declare key expression '{}': {}", - key_expr_str, - err - ) - })?; - Ok(Arc::into_raw(Arc::new(key_expr))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} /// Undeclare a [KeyExpr] through a [Session] via JNI. /// From 1963761b7629ceab276abcd16f9282253a816821 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 23 Apr 2026 23:52:20 +0200 Subject: [PATCH 090/126] declare_queryable in flat --- zenoh-flat/src/session.rs | 1 + zenoh-jni/src/session.rs | 57 +-------------------------------------- 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index edd8142b..f3a2831d 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -151,6 +151,7 @@ pub fn declare_querier( } /// Declare a queryable through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn declare_queryable( session: &Session, key_expr: KeyExpr<'static>, diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 115540c3..b026f29d 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -30,9 +30,7 @@ use zenoh::{ }; use crate::owned_object::OwnedObject; -use crate::sample_callback::{ - process_kotlin_query_callback, process_kotlin_reply_callback, process_kotlin_sample_callback, -}; +use crate::sample_callback::{process_kotlin_reply_callback, process_kotlin_sample_callback}; #[cfg(feature = "zenoh-ext")] use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] @@ -52,59 +50,6 @@ include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Declare a Zenoh queryable via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: A raw pointer to the [KeyExpr] to be used for the queryable. May be null in case of using an -/// undeclared key expression. -/// - `key_expr_str`: String representation of the key expression to be used to declare the queryable. -/// It won't be considered in case a key_expr_ptr to a declared key expression is provided. -/// - `session_ptr`: A raw pointer to the Zenoh [Session] to be used to declare the queryable. -/// - `callback`: The callback function as an instance of the `JNIQueryableCallback` interface in Java/Kotlin. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the queryable. -/// - `complete`: The completeness of the queryable. -/// -/// Returns: -/// - A raw pointer to the declared Zenoh queryable. In case of failure, an exception is thrown and null is returned. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The callback function passed as `callback` must be a valid instance of the `JNIQueryableCallback` interface -/// in Java/Kotlin, matching the specified signature. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - callback: JObject, - on_close: JObject, - complete: jboolean, -) -> *const Queryable<()> { - || -> ZResult<*const Queryable<()>> { - let session = OwnedObject::from_raw(session_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let callback = process_kotlin_query_callback(&mut env, callback, on_close)?; - let complete = complete != 0; - let queryable = zenoh_flat::session::declare_queryable(&session, key_expr, callback, complete)?; - Ok(Arc::into_raw(Arc::new(queryable))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} /// Undeclare a [KeyExpr] through a [Session] via JNI. From 60ebd4d6fd926e1be1cbc54c25ddc4e1698ba803 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 00:01:40 +0200 Subject: [PATCH 091/126] undeclre keyexpr converted --- zenoh-flat/src/jni_converter.rs | 89 ++++++++++++++++++++++----------- zenoh-flat/src/session.rs | 26 ++++++++++ zenoh-jni/build.rs | 3 +- zenoh-jni/src/session.rs | 44 +--------------- 4 files changed, 88 insertions(+), 74 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 92217a8a..7b6306df 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -56,10 +56,10 @@ pub struct Builder { /// that builds an `impl Fn(T) + Send + Sync + 'static` closure from a /// `(callback: JObject, on_close: JObject)` pair. callback_decoders: HashMap, - /// Names of source functions whose first `&T` argument must be consumed - /// (transferred out) via `Arc::from_raw` instead of borrowed via - /// `OwnedObject::from_raw`. Typically used for `close_*` functions. - consume_fns: HashSet, + /// Per-function set of argument names that must be consumed (taken from + /// the raw pointer via `Arc::from_raw`) instead of borrowed. Used for + /// close/undeclare-style functions that invalidate their handle. + consume_args: HashMap>, } impl Default for Builder { @@ -77,7 +77,7 @@ impl Default for Builder { encoding_decoder: None, enum_decoders: HashMap::new(), callback_decoders: HashMap::new(), - consume_fns: HashSet::new(), + consume_args: HashMap::new(), } } } @@ -188,14 +188,24 @@ impl Builder { self } - /// Mark a source function as consuming its first `&T` argument: the - /// generated wrapper will take ownership of the pointer via + /// Mark a specific argument of a source function as consuming: the + /// generated wrapper will take ownership of the raw pointer via /// `Arc::from_raw` (dropping the Arc at end of scope), rather than - /// borrowing it through `OwnedObject::from_raw`. + /// borrowing it through `OwnedObject::from_raw`. Applies to both + /// `OpaqueRef` (`&T`) and `KeyExpr` arguments — for the latter, the + /// string-fallback argument is omitted, leaving just the pointer. /// - /// Typically used for `close_*` functions that invalidate the handle. - pub fn consume_fn(mut self, fn_name: impl Into) -> Self { - self.consume_fns.insert(fn_name.into()); + /// Typically used for `close_*` / `undeclare_*` functions that invalidate + /// the handle. + pub fn consume_arg( + mut self, + fn_name: impl Into, + arg_name: impl Into, + ) -> Self { + self.consume_args + .entry(fn_name.into()) + .or_default() + .insert(arg_name.into()); self } @@ -265,12 +275,16 @@ impl JniConverter { let owned_object = &self.cfg.owned_object; let zresult = &self.cfg.zresult; let throw_exception = &self.cfg.throw_exception; - let consume_first = self.cfg.consume_fns.contains(&original_name); + let empty_consume_set: HashSet = HashSet::new(); + let consume_set = self + .cfg + .consume_args + .get(&original_name) + .unwrap_or(&empty_consume_set); let mut prelude: Vec = Vec::new(); let mut jni_params: Vec = Vec::new(); let mut call_args: Vec = Vec::new(); - let mut seen_first_opaque = false; for input in &func.sig.inputs { let syn::FnArg::Typed(pat_type) = input else { @@ -286,9 +300,7 @@ impl JniConverter { ArgKind::OpaqueRef(elem) => { let ptr_ident = format_ident!("{}_ptr", name); jni_params.push(quote! { #ptr_ident: *const #elem }); - let is_first_opaque = !seen_first_opaque; - seen_first_opaque = true; - if consume_first && is_first_opaque { + if consume_set.contains(&name.to_string()) { prelude.push(quote! { let #name = std::sync::Arc::from_raw(#ptr_ident); }); @@ -300,21 +312,38 @@ impl JniConverter { call_args.push(quote! { &#name }); } ArgKind::KeyExpr => { - let decoder = self - .cfg - .key_expr_decoder - .as_ref() - .expect("key_expr_decoder not configured"); let ptr_ident = format_ident!("{}_ptr", name); - let str_ident = format_ident!("{}_str", name); - jni_params.push(quote! { - #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> - }); - jni_params.push(quote! { #str_ident: jni::objects::JString }); - prelude.push(quote! { - let #name = #decoder(&mut env, &#str_ident, #ptr_ident)?; - }); - call_args.push(quote! { #name }); + if consume_set.contains(&name.to_string()) { + // Consume path: the declared KeyExpr is required (no + // string fallback). Arc::from_raw decrements the + // refcount at end of scope, freeing the handle once + // no other references remain. A cloned inner KeyExpr + // is passed to the callee by value. + let arc_ident = format_ident!("__{}_arc", name); + jni_params.push(quote! { + #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> + }); + prelude.push(quote! { + let #arc_ident = std::sync::Arc::from_raw(#ptr_ident); + let #name = (*#arc_ident).clone(); + }); + call_args.push(quote! { #name }); + } else { + let decoder = self + .cfg + .key_expr_decoder + .as_ref() + .expect("key_expr_decoder not configured"); + let str_ident = format_ident!("{}_str", name); + jni_params.push(quote! { + #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> + }); + jni_params.push(quote! { #str_ident: jni::objects::JString }); + prelude.push(quote! { + let #name = #decoder(&mut env, &#str_ident, #ptr_ident)?; + }); + call_args.push(quote! { #name }); + } } ArgKind::String => { let decoder = self diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index f3a2831d..0efc83eb 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -94,6 +94,32 @@ pub fn declare_key_expr(session: &Session, key_expr: String) -> ZResult) -> ZResult<()> { + let key_expr_string = key_expr.to_string(); + session + .undeclare(key_expr) + .wait() + .map(|_| { + trace!("Undeclared key expression '{}'.", key_expr_string); + }) + .map_err(|err| { + error!( + "Unable to undeclare key expression '{}': {}", + key_expr_string, err + ); + zerror!( + "Unable to undeclare key expression '{}': {}", + key_expr_string, + err + ) + }) +} + /// Declare a subscriber through an existing Zenoh session. #[prebindgen_proc_macro::prebindgen("jni")] pub fn declare_subscriber( diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 5e8d4365..787ed1f3 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -23,7 +23,8 @@ fn main() { .callback_decoder("Sample", "crate::sample_callback::process_kotlin_sample_callback") .callback_decoder("Query", "crate::sample_callback::process_kotlin_query_callback") .callback_decoder("Reply", "crate::sample_callback::process_kotlin_reply_callback") - .consume_fn("close_session") + .consume_arg("close_session", "session") + .consume_arg("undeclare_key_expr", "key_expr") .build(); source diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index b026f29d..2dc9b743 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -use std::{ops::Deref, ptr::null, sync::Arc, time::Duration}; +use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ objects::{GlobalRef, JByteArray, JClass, JList, JObject, JString, JValue}, @@ -52,48 +52,6 @@ include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Undeclare a [KeyExpr] through a [Session] via JNI. -/// -/// The key expression must have been previously declared on the specified session, otherwise an -/// exception is thrown. -/// -/// This functions frees the key expression pointer provided. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `session_ptr`: A raw pointer to the Zenoh [Session] from which to undeclare the key expression. -/// - `key_expr_ptr`: A raw pointer to the [KeyExpr] to undeclare. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session and keyexpr pointers are valid and have not been modified or freed. -/// - The session pointer remains valid after this function call. -/// - The key expression pointer is voided after this function call. -/// - The function may throw an exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: *const KeyExpr<'static>, -) { - let session = OwnedObject::from_raw(session_ptr); - let key_expr = Arc::from_raw(key_expr_ptr); - let key_expr_clone = key_expr.deref().clone(); - match session.undeclare(key_expr_clone).wait() { - Ok(_) => {} - Err(err) => { - throw_exception!( - env, - zerror!("Unable to declare key expression '{}': {}", key_expr, err) - ); - } - } - // `key_expr` is intentionally left to be freed by Rust -} /// Performs a `get` operation in the Zenoh session via JNI with Value. /// From 089770ed01a825b272e860e507b39ca39faf991c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 00:19:24 +0200 Subject: [PATCH 092/126] id functions in flat --- zenoh-flat/src/jni_converter.rs | 150 +++++++++++++++++++++++++ zenoh-flat/src/session.rs | 42 +++++-- zenoh-jni/build.rs | 12 ++ zenoh-jni/src/session.rs | 188 +------------------------------- zenoh-jni/src/zenoh_id.rs | 26 ++++- 5 files changed, 222 insertions(+), 196 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 7b6306df..a1a94085 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -60,6 +60,22 @@ pub struct Builder { /// the raw pointer via `Arc::from_raw`) instead of borrowed. Used for /// close/undeclare-style functions that invalidate their handle. consume_args: HashMap>, + /// Return-type wrappers keyed by the last-segment name of `T` in + /// `ZResult`. Applies when `T` is a plain (non-`Vec`) type. + return_wrappers: HashMap, + /// Return-type wrappers keyed by the element type name of `Vec` in + /// `ZResult>`. + return_wrappers_vec: HashMap, +} + +/// Describes how to render a `ZResult` return value into a JNI-compatible +/// output value. Registered via [`Builder::return_wrapper`] / +/// [`Builder::return_wrapper_vec`]. +#[derive(Clone)] +pub(crate) struct ReturnWrapper { + jni_type: syn::Type, + wrap_fn: syn::Path, + default_expr: syn::Expr, } impl Default for Builder { @@ -78,6 +94,8 @@ impl Default for Builder { enum_decoders: HashMap::new(), callback_decoders: HashMap::new(), consume_args: HashMap::new(), + return_wrappers: HashMap::new(), + return_wrappers_vec: HashMap::new(), } } } @@ -188,6 +206,37 @@ impl Builder { self } + /// Register a return-type wrapper for `ZResult` where `T`'s + /// last-segment name equals `type_name`. `jni_type` is the generated + /// `extern "C"` return type. `wrap_fn` must have signature + /// `fn(&mut JNIEnv, T) -> ZResult`. `default_expr` is the value + /// returned on error (before the exception is thrown on the JVM side). + pub fn return_wrapper( + mut self, + type_name: impl Into, + jni_type: impl AsRef, + wrap_fn: impl AsRef, + default_expr: impl AsRef, + ) -> Self { + self.return_wrappers + .insert(type_name.into(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); + self + } + + /// Like [`Builder::return_wrapper`] but applies when `T` is `Vec` + /// with `E`'s last-segment name equal to `element_type_name`. + pub fn return_wrapper_vec( + mut self, + element_type_name: impl Into, + jni_type: impl AsRef, + wrap_fn: impl AsRef, + default_expr: impl AsRef, + ) -> Self { + self.return_wrappers_vec + .insert(element_type_name.into(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); + self + } + /// Mark a specific argument of a source function as consuming: the /// generated wrapper will take ownership of the raw pointer via /// `Arc::from_raw` (dropping the Arc at end of scope), rather than @@ -428,6 +477,37 @@ impl JniConverter { }); call_args.push(quote! { #name }); } + ArgKind::OptionEncoding => { + let decoder = self + .cfg + .encoding_decoder + .as_ref() + .expect("encoding_decoder not configured"); + let id_ident = format_ident!("{}_id", name); + let schema_ident = format_ident!("{}_schema", name); + jni_params.push(quote! { #id_ident: jni::sys::jint }); + jni_params.push(quote! { #schema_ident: jni::objects::JString }); + prelude.push(quote! { + let #name = Some(#decoder(&mut env, #id_ident, &#schema_ident)?); + }); + call_args.push(quote! { #name }); + } + ArgKind::OptionString => { + let decoder = self + .cfg + .string_decoder + .as_ref() + .expect("string_decoder not configured"); + jni_params.push(quote! { #name: jni::objects::JString }); + prelude.push(quote! { + let #name = if !#name.is_null() { + Some(#decoder(&mut env, &#name)?) + } else { + None + }; + }); + call_args.push(quote! { #name }); + } ArgKind::Unsupported => panic!( "unsupported parameter type `{}` for `{}` at {loc}", ty.to_token_stream(), @@ -453,6 +533,18 @@ impl JniConverter { quote! { () }, quote! { #zresult<()> }, ) + } else if let Some(wrapper) = self.lookup_return_wrapper(&inner) { + let ReturnWrapper { + jni_type, + wrap_fn, + default_expr, + } = wrapper; + ( + quote! { #jni_type }, + quote! { #wrap_fn(&mut env, __result) }, + quote! { #default_expr }, + quote! { #zresult<#jni_type> }, + ) } else { ( quote! { *const #inner }, @@ -497,6 +589,23 @@ impl JniConverter { syn::parse2(tokens).expect("generated JNI wrapper must parse") } + fn lookup_return_wrapper(&self, ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + let seg = tp.path.segments.last()?; + let name = seg.ident.to_string(); + if name == "Vec" { + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + let syn::GenericArgument::Type(elem) = args.args.first()? else { + return None; + }; + let elem_name = type_last_segment(elem)?; + return self.cfg.return_wrappers_vec.get(&elem_name).cloned(); + } + self.cfg.return_wrappers.get(&name).cloned() + } + fn classify_arg(&self, ty: &syn::Type) -> ArgKind { match ty { syn::Type::Reference(r) if r.mutability.is_none() => { @@ -537,6 +646,16 @@ impl JniConverter { if name == "Option" && is_option_of_vec_u8(last) { return ArgKind::OptionVecU8; } + if name == "Option" { + if let Some(inner) = option_inner_type_name(last) { + if inner == "String" { + return ArgKind::OptionString; + } + if inner == "Encoding" { + return ArgKind::OptionEncoding; + } + } + } if name == "Vec" && is_vec_of_u8(last) { return ArgKind::VecU8; } @@ -564,12 +683,31 @@ enum ArgKind { VecU8, /// `Encoding` → `(jint id, JString schema)` pair via `encoding_decoder`. Encoding, + /// `Option` → `(jint id, JString schema)` pair via `encoding_decoder`, + /// wrapped in `Some(_)`. Semantic gating on payload presence is the + /// callee's responsibility. + OptionEncoding, + /// `Option` → nullable `JString`. + OptionString, /// `impl Fn(T) + Send + Sync + 'static` → `(JObject callback, JObject on_close)` /// pair decoded via a callback decoder registered for `T`. Callback(syn::Path), Unsupported, } +fn parse_return_wrapper( + jni_type: impl AsRef, + wrap_fn: impl AsRef, + default_expr: impl AsRef, +) -> ReturnWrapper { + ReturnWrapper { + jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid return wrapper jni_type"), + wrap_fn: syn::parse_str(wrap_fn.as_ref()).expect("invalid return wrapper wrap_fn path"), + default_expr: syn::parse_str(default_expr.as_ref()) + .expect("invalid return wrapper default_expr"), + } +} + fn type_last_segment(ty: &syn::Type) -> Option { let syn::Type::Path(tp) = ty else { return None }; tp.path.segments.last().map(|s| s.ident.to_string()) @@ -593,6 +731,18 @@ fn extract_fn_single_arg_type_name( None } +/// Return the last-segment name of the single generic argument of an +/// `Option<...>` path segment, if any (e.g. `Option` → `Some("String")`). +fn option_inner_type_name(seg: &syn::PathSegment) -> Option { + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + let syn::GenericArgument::Type(inner) = args.args.first()? else { + return None; + }; + type_last_segment(inner) +} + /// Check whether an `Option<...>` path segment wraps exactly `Vec`. fn is_option_of_vec_u8(seg: &syn::PathSegment) -> bool { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 0efc83eb..1ce3115f 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -23,7 +23,7 @@ use zenoh::{ query::{ConsolidationMode, Query, QueryTarget, Queryable, Querier, Reply, ReplyKeyExpr, Selector}, sample::Sample, qos::{CongestionControl, Priority, Reliability}, - session::Session, + session::{Session, ZenohId}, Wait, }; @@ -204,27 +204,31 @@ pub fn declare_queryable( /// /// Each [`Reply`] received from the network is delivered to the `callback`. /// `selector_params` is appended to the `key_expr` to form the query selector -/// (pass `String::new()` for no parameters). `payload` and `encoding` are -/// coupled: if a payload is given, encoding is attached; if no payload is -/// given, encoding is ignored. +/// (pass `None` for no parameters). `payload` and `encoding` are coupled: +/// if a payload is given, encoding is attached; if no payload is given, +/// encoding is ignored. +/// +/// Parameter order matches the JNI calling convention so the wrapper can be +/// generated by `zenoh_flat::jni_converter` without reordering. +#[prebindgen_proc_macro::prebindgen("jni")] pub fn get( session: &Session, key_expr: KeyExpr<'static>, - selector_params: String, + selector_params: Option, callback: impl Fn(Reply) + Send + Sync + 'static, + timeout: Duration, query_target: QueryTarget, consolidation: ConsolidationMode, + attachment: Option>, + payload: Option>, + encoding: Option, congestion_control: CongestionControl, priority: Priority, express: bool, - timeout: Duration, reply_key_expr: ReplyKeyExpr, - payload: Option>, - encoding: Option, - attachment: Option>, ) -> ZResult<()> { let key_expr_string = key_expr.to_string(); - let selector = Selector::owned(&key_expr, selector_params); + let selector = Selector::owned(&key_expr, selector_params.unwrap_or_default()); let mut get_builder = session .get(selector) .callback(callback) @@ -329,6 +333,24 @@ pub fn delete( }) } +/// Return the Zenoh ID of the session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn get_zid(session: &Session) -> ZResult { + Ok(session.info().zid().wait()) +} + +/// Return the Zenoh IDs of the peers connected to this session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn get_peers_zid(session: &Session) -> ZResult> { + Ok(session.info().peers_zid().wait().collect()) +} + +/// Return the Zenoh IDs of the routers connected to this session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn get_routers_zid(session: &Session) -> ZResult> { + Ok(session.info().routers_zid().wait().collect()) +} + /// Close a Zenoh session using a reference to the session. #[prebindgen_proc_macro::prebindgen("jni")] pub fn close_session(session: &Session) -> ZResult<()> { diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 787ed1f3..4eefb9fe 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -25,6 +25,18 @@ fn main() { .callback_decoder("Reply", "crate::sample_callback::process_kotlin_reply_callback") .consume_arg("close_session", "session") .consume_arg("undeclare_key_expr", "key_expr") + .return_wrapper( + "ZenohId", + "jni::sys::jbyteArray", + "crate::zenoh_id::zenoh_id_to_byte_array", + "jni::objects::JByteArray::default().as_raw()", + ) + .return_wrapper_vec( + "ZenohId", + "jni::sys::jobject", + "crate::zenoh_id::zenoh_ids_to_java_list", + "jni::objects::JObject::default().as_raw()", + ) .build(); source diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 2dc9b743..f6beac78 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -15,8 +15,8 @@ use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ - objects::{GlobalRef, JByteArray, JClass, JList, JObject, JString, JValue}, - sys::{jboolean, jbyteArray, jint, jlong, jobject}, + objects::{GlobalRef, JByteArray, JClass, JObject, JString, JValue}, + sys::{jboolean, jint, jlong}, JNIEnv, }; use zenoh::{ @@ -25,12 +25,12 @@ use zenoh::{ pubsub::{Publisher, Subscriber}, query::{Querier, Queryable, ReplyError}, sample::Sample, - session::{EntityGlobalId, Session, ZenohId}, + session::{EntityGlobalId, Session}, Wait, }; use crate::owned_object::OwnedObject; -use crate::sample_callback::{process_kotlin_reply_callback, process_kotlin_sample_callback}; +use crate::sample_callback::process_kotlin_sample_callback; #[cfg(feature = "zenoh-ext")] use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] @@ -53,114 +53,6 @@ include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Performs a `get` operation in the Zenoh session via JNI with Value. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to a declared [KeyExpr] to be used for the query. May be null in case -/// of using a non declared key expression, in which case the `key_expr_str` parameter will be used instead. -/// - `key_expr_str`: String representation of the key expression to be used to declare the query. It is not -/// considered if a `key_expr_ptr` is provided. -/// - `selector_params`: Optional parameters of the selector. -/// - `session_ptr`: A raw pointer to the Zenoh [Session]. -/// - `callback`: A Java/Kotlin callback to be called upon receiving a reply. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called when no more replies will be received. -/// - `timeout_ms`: The timeout in milliseconds. -/// - `target`: The query target as the ordinal of the enum. -/// - `consolidation`: The consolidation mode as the ordinal of the enum. -/// - `attachment`: An optional attachment encoded into a byte array. -/// - `payload`: Optional payload for the query. -/// - `encoding_id`: The encoding of the payload. -/// - `encoding_schema`: The encoding schema of the payload, may be null. -/// - `congestion_control`: The ordinal value of the congestion control enum value. -/// - `priority`: The ordinal value of the priority enum value. -/// - `is_express`: The boolean express value of the QoS provided. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// -/// Throws: -/// - An exception in case of failure handling the query. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - selector_params: /*nullable*/ JString, - callback: JObject, - on_close: JObject, - timeout_ms: jlong, - target: jint, - consolidation: jint, - attachment: /*nullable*/ JByteArray, - payload: /*nullable*/ JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, - congestion_control: jint, - priority: jint, - is_express: jboolean, - accept_replies: jint, -) { - let session = OwnedObject::from_raw(session_ptr); - let _ = || -> ZResult<()> { - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let reply_callback = process_kotlin_reply_callback(&mut env, callback, on_close)?; - let query_target = decode_query_target(target)?; - let consolidation = decode_consolidation(consolidation)?; - let timeout = Duration::from_millis(timeout_ms as u64); - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let reply_key_expr = decode_reply_key_expr(accept_replies)?; - let selector_params = if selector_params.is_null() { - String::new() - } else { - decode_string(&mut env, &selector_params)? - }; - - let payload = if !payload.is_null() { - Some(decode_byte_array(&env, payload)?) - } else { - None - }; - let encoding = if payload.is_some() { - Some(decode_encoding(&mut env, encoding_id, &encoding_schema)?) - } else { - None - }; - let attachment = if !attachment.is_null() { - Some(decode_byte_array(&env, attachment)?) - } else { - None - }; - - zenoh_flat::session::get( - &session, - key_expr, - selector_params, - reply_callback, - query_target, - consolidation, - congestion_control, - priority, - is_express != 0, - timeout, - reply_key_expr, - payload, - encoding, - attachment, - ) - }() - .map_err(|err| throw_exception!(env, err)); -} pub(crate) fn on_reply_success( env: &mut JNIEnv, @@ -309,78 +201,6 @@ pub(crate) fn on_reply_error( result } -/// Returns a list of zenoh ids as byte arrays corresponding to the peers connected to the session provided. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getPeersZidViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, -) -> jobject { - let session = OwnedObject::from_raw(session_ptr); - { - let peers_zid = session.info().peers_zid().wait(); - let ids = peers_zid.collect::>(); - ids_to_java_list(&mut env, ids).map_err(|err| zerror!(err)) - } - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::default().as_raw() - }) -} - -/// Returns a list of zenoh ids as byte arrays corresponding to the routers connected to the session provided. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, -) -> jobject { - let session = OwnedObject::from_raw(session_ptr); - { - let peers_zid = session.info().routers_zid().wait(); - let ids = peers_zid.collect::>(); - ids_to_java_list(&mut env, ids).map_err(|err| zerror!(err)) - } - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::default().as_raw() - }) -} - -/// Returns the Zenoh ID as a byte array of the session. -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getZidViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, -) -> jbyteArray { - let session = OwnedObject::from_raw(session_ptr); - { - let zid = session.info().zid().wait(); - env.byte_array_from_slice(&zid.to_le_bytes()) - .map(|x| x.as_raw()) - .map_err(|err| zerror!(err)) - } - .unwrap_or_else(|err| { - throw_exception!(env, err); - JByteArray::default().as_raw() - }) -} - -fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result { - let array_list = env.new_object("java/util/ArrayList", "()V", &[])?; - let jlist = JList::from_env(env, &array_list)?; - for id in ids { - let value = &mut env.byte_array_from_slice(&id.to_le_bytes())?; - jlist.add(env, value)?; - } - Ok(array_list.as_raw()) -} /// Declare an advanced Zenoh subscriber via JNI. /// diff --git a/zenoh-jni/src/zenoh_id.rs b/zenoh-jni/src/zenoh_id.rs index d0244f48..765b4421 100644 --- a/zenoh-jni/src/zenoh_id.rs +++ b/zenoh-jni/src/zenoh_id.rs @@ -14,12 +14,34 @@ use crate::{errors::ZResult, throw_exception, utils::decode_byte_array}; use jni::{ - objects::{JByteArray, JClass, JString}, - sys::jstring, + objects::{JByteArray, JClass, JList, JString}, + sys::{jbyteArray, jobject, jstring}, JNIEnv, }; use zenoh::session::ZenohId; +/// Encode a single [`ZenohId`] as a Java `byte[]`. +pub(crate) fn zenoh_id_to_byte_array(env: &JNIEnv<'_>, zid: ZenohId) -> ZResult { + env.byte_array_from_slice(&zid.to_le_bytes()) + .map(|x| x.as_raw()) + .map_err(|err| zerror!(err)) +} + +/// Encode a `Vec` as a Java `ArrayList`. +pub(crate) fn zenoh_ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> ZResult { + 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 id in ids { + let value = &mut env + .byte_array_from_slice(&id.to_le_bytes()) + .map_err(|err| zerror!(err))?; + jlist.add(env, value).map_err(|err| zerror!(err))?; + } + Ok(array_list.as_raw()) +} + /// Returns the string representation of a ZenohID. #[no_mangle] #[allow(non_snake_case)] From 376cdbe3a0230b3f9b52b855cb9f9d0898b43b89 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 00:25:51 +0200 Subject: [PATCH 093/126] on_reply functions moved --- zenoh-jni/src/liveliness.rs | 3 +- zenoh-jni/src/querier.rs | 2 +- zenoh-jni/src/sample_callback.rs | 153 +++++++++++++++++++++++++++++- zenoh-jni/src/session.rs | 155 +------------------------------ 4 files changed, 155 insertions(+), 158 deletions(-) diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 395989a6..de1aeed3 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -29,8 +29,7 @@ use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, owned_object::OwnedObject, - sample_callback::process_kotlin_sample_callback, - session::{on_reply_error, on_reply_success}, + sample_callback::{on_reply_error, on_reply_success, process_kotlin_sample_callback}, throw_exception, utils::{get_callback_global_ref, get_java_vm, load_on_close}, }; diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 835b2f39..52f26c31 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -25,7 +25,7 @@ use crate::{ errors::ZResult, key_expr::process_kotlin_key_expr, owned_object::OwnedObject, - session::{on_reply_error, on_reply_success}, + sample_callback::{on_reply_error, on_reply_success}, throw_exception, utils::{ decode_byte_array, decode_encoding, decode_string, get_callback_global_ref, get_java_vm, diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index 04ae030a..7fad23fa 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -19,10 +19,157 @@ use jni::{ sys::{jint, jlong}, JNIEnv, }; -use zenoh::{query::{Query, Reply, ReplyKeyExpr}, sample::Sample}; +use zenoh::{query::{Query, Reply, ReplyError, ReplyKeyExpr}, sample::Sample, session::EntityGlobalId}; use crate::{errors::ZResult, utils::*}; +pub(crate) fn on_reply_success( + env: &mut JNIEnv, + replier_id: Option, + sample: &Sample, + callback_global_ref: &GlobalRef, +) -> ZResult<()> { + let zenoh_id = replier_id + .map_or_else( + || Ok(JByteArray::default()), + |replier_id| { + env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) + .map_err(|err| zerror!(err)) + }, + ) + .map(|value| env.auto_local(value))?; + let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); + + let byte_array = + bytes_to_java_array(env, sample.payload()).map(|value| env.auto_local(value))?; + let encoding: jint = sample.encoding().id() as jint; + let encoding_schema = sample + .encoding() + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(env, schema), + ) + .map(|value| env.auto_local(value))?; + let kind = sample.kind() as jint; + + let (timestamp, is_valid) = sample + .timestamp() + .map(|timestamp| (timestamp.get_time().as_u64(), true)) + .unwrap_or((0, false)); + + let attachment_bytes = sample + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(env, attachment), + ) + .map(|value| env.auto_local(value)) + .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; + + let key_expr_str = env + .new_string(sample.key_expr().to_string()) + .map(|value| env.auto_local(value)) + .map_err(|err| { + zerror!( + "Could not create a JString through JNI for the Sample key expression. {}", + err + ) + })?; + + let express = sample.express(); + let priority = sample.priority() as jint; + let cc = sample.congestion_control() as jint; + + let result = match env.call_method( + callback_global_ref, + "run", + "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&zenoh_id), + JValue::from(eid), + JValue::from(true), + JValue::from(&key_expr_str), + JValue::from(&byte_array), + JValue::from(encoding), + JValue::from(&encoding_schema), + JValue::from(kind), + JValue::from(timestamp as i64), + JValue::from(is_valid), + JValue::from(&attachment_bytes), + JValue::from(express), + JValue::from(priority), + JValue::from(cc), + ], + ) { + Ok(_) => Ok(()), + Err(err) => { + _ = env.exception_describe(); + Err(zerror!("On GET callback error: {}", err)) + } + }; + result +} + +pub(crate) fn on_reply_error( + env: &mut JNIEnv, + replier_id: Option, + reply_error: &ReplyError, + callback_global_ref: &GlobalRef, +) -> ZResult<()> { + let zenoh_id = replier_id + .map_or_else( + || Ok(JByteArray::default()), + |replier_id| { + env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) + .map_err(|err| zerror!(err)) + }, + ) + .map(|value| env.auto_local(value))?; + let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); + + let payload = + bytes_to_java_array(env, reply_error.payload()).map(|value| env.auto_local(value))?; + let encoding_id: jint = reply_error.encoding().id() as jint; + let encoding_schema = reply_error + .encoding() + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(env, schema), + ) + .map(|value| env.auto_local(value))?; + let result = match env.call_method( + callback_global_ref, + "run", + "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&zenoh_id), + JValue::from(eid), + JValue::from(false), + JValue::from(&JString::default()), + JValue::from(&payload), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + // The remaining parameters aren't used in case of replying error, so we set them to default. + JValue::from(0 as jint), + JValue::from(0_i64), + JValue::from(false), + JValue::from(&JByteArray::default()), + JValue::from(false), + JValue::from(0 as jint), + JValue::from(0 as jint), + ], + ) { + Ok(_) => Ok(()), + Err(err) => { + _ = env.exception_describe(); + Err(zerror!("On GET callback error: {}", err)) + } + }; + result +} + pub(crate) unsafe fn process_kotlin_sample_callback( env: &mut JNIEnv, callback: JObject, @@ -114,13 +261,13 @@ pub(crate) unsafe fn process_kotlin_reply_callback( .attach_current_thread_as_daemon() .map_err(|err| zerror!("Unable to attach thread for GET query callback: {}", err))?; match reply.result() { - Ok(sample) => crate::session::on_reply_success( + Ok(sample) => crate::sample_callback::on_reply_success( &mut env, reply.replier_id(), sample, &callback_global_ref, ), - Err(error) => crate::session::on_reply_error( + Err(error) => crate::sample_callback::on_reply_error( &mut env, reply.replier_id(), error, diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index f6beac78..b450e66d 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -15,7 +15,7 @@ use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ - objects::{GlobalRef, JByteArray, JClass, JObject, JString, JValue}, + objects::{JClass, JObject, JString}, sys::{jboolean, jint, jlong}, JNIEnv, }; @@ -23,9 +23,8 @@ use zenoh::{ config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, - query::{Querier, Queryable, ReplyError}, - sample::Sample, - session::{EntityGlobalId, Session}, + query::{Querier, Queryable}, + session::Session, Wait, }; @@ -54,154 +53,6 @@ include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -pub(crate) fn on_reply_success( - env: &mut JNIEnv, - replier_id: Option, - sample: &Sample, - callback_global_ref: &GlobalRef, -) -> ZResult<()> { - let zenoh_id = replier_id - .map_or_else( - || Ok(JByteArray::default()), - |replier_id| { - env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) - .map_err(|err| zerror!(err)) - }, - ) - .map(|value| env.auto_local(value))?; - let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); - - let byte_array = - bytes_to_java_array(env, sample.payload()).map(|value| env.auto_local(value))?; - let encoding: jint = sample.encoding().id() as jint; - let encoding_schema = sample - .encoding() - .schema() - .map_or_else( - || Ok(JString::default()), - |schema| slice_to_java_string(env, schema), - ) - .map(|value| env.auto_local(value))?; - let kind = sample.kind() as jint; - - let (timestamp, is_valid) = sample - .timestamp() - .map(|timestamp| (timestamp.get_time().as_u64(), true)) - .unwrap_or((0, false)); - - let attachment_bytes = sample - .attachment() - .map_or_else( - || Ok(JByteArray::default()), - |attachment| bytes_to_java_array(env, attachment), - ) - .map(|value| env.auto_local(value)) - .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; - - let key_expr_str = env - .new_string(sample.key_expr().to_string()) - .map(|value| env.auto_local(value)) - .map_err(|err| { - zerror!( - "Could not create a JString through JNI for the Sample key expression. {}", - err - ) - })?; - - let express = sample.express(); - let priority = sample.priority() as jint; - let cc = sample.congestion_control() as jint; - - let result = match env.call_method( - callback_global_ref, - "run", - "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", - &[ - JValue::from(&zenoh_id), - JValue::from(eid), - JValue::from(true), - JValue::from(&key_expr_str), - JValue::from(&byte_array), - JValue::from(encoding), - JValue::from(&encoding_schema), - JValue::from(kind), - JValue::from(timestamp as i64), - JValue::from(is_valid), - JValue::from(&attachment_bytes), - JValue::from(express), - JValue::from(priority), - JValue::from(cc), - ], - ) { - Ok(_) => Ok(()), - Err(err) => { - _ = env.exception_describe(); - Err(zerror!("On GET callback error: {}", err)) - } - }; - result -} - -pub(crate) fn on_reply_error( - env: &mut JNIEnv, - replier_id: Option, - reply_error: &ReplyError, - callback_global_ref: &GlobalRef, -) -> ZResult<()> { - let zenoh_id = replier_id - .map_or_else( - || Ok(JByteArray::default()), - |replier_id| { - env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) - .map_err(|err| zerror!(err)) - }, - ) - .map(|value| env.auto_local(value))?; - let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); - - let payload = - bytes_to_java_array(env, reply_error.payload()).map(|value| env.auto_local(value))?; - let encoding_id: jint = reply_error.encoding().id() as jint; - let encoding_schema = reply_error - .encoding() - .schema() - .map_or_else( - || Ok(JString::default()), - |schema| slice_to_java_string(env, schema), - ) - .map(|value| env.auto_local(value))?; - let result = match env.call_method( - callback_global_ref, - "run", - "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", - &[ - JValue::from(&zenoh_id), - JValue::from(eid), - JValue::from(false), - JValue::from(&JString::default()), - JValue::from(&payload), - JValue::from(encoding_id), - JValue::from(&encoding_schema), - // The remaining parameters aren't used in case of replying error, so we set them to default. - JValue::from(0 as jint), - JValue::from(0_i64), - JValue::from(false), - JValue::from(&JByteArray::default()), - JValue::from(false), - JValue::from(0 as jint), - JValue::from(0 as jint), - ], - ) { - Ok(_) => Ok(()), - Err(err) => { - _ = env.exception_describe(); - Err(zerror!("On GET callback error: {}", err)) - } - }; - result -} - - /// Declare an advanced Zenoh subscriber via JNI. /// /// # Parameters: From 56b1ea1cdff764d1987f2de9077463330d5c8a3a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 00:26:55 +0200 Subject: [PATCH 094/126] cargo fmt --- zenoh-jni/build.rs | 20 ++++++++++++++++---- zenoh-jni/src/config.rs | 2 +- zenoh-jni/src/errors.rs | 12 +++++------- zenoh-jni/src/ext/advanced_subscriber.rs | 8 ++++++-- zenoh-jni/src/key_expr.rs | 2 +- zenoh-jni/src/liveliness.rs | 4 +++- zenoh-jni/src/sample_callback.rs | 24 +++++++++++++----------- zenoh-jni/src/session.rs | 16 ++++------------ 8 files changed, 49 insertions(+), 39 deletions(-) diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 4eefb9fe..bc866797 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -14,15 +14,27 @@ fn main() { .string_decoder("crate::utils::decode_string") .byte_array_decoder("crate::utils::decode_byte_array") .encoding_decoder("crate::utils::decode_encoding") - .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") + .enum_decoder( + "CongestionControl", + "crate::utils::decode_congestion_control", + ) .enum_decoder("Priority", "crate::utils::decode_priority") .enum_decoder("Reliability", "crate::utils::decode_reliability") .enum_decoder("QueryTarget", "crate::utils::decode_query_target") .enum_decoder("ConsolidationMode", "crate::utils::decode_consolidation") .enum_decoder("ReplyKeyExpr", "crate::utils::decode_reply_key_expr") - .callback_decoder("Sample", "crate::sample_callback::process_kotlin_sample_callback") - .callback_decoder("Query", "crate::sample_callback::process_kotlin_query_callback") - .callback_decoder("Reply", "crate::sample_callback::process_kotlin_reply_callback") + .callback_decoder( + "Sample", + "crate::sample_callback::process_kotlin_sample_callback", + ) + .callback_decoder( + "Query", + "crate::sample_callback::process_kotlin_query_callback", + ) + .callback_decoder( + "Reply", + "crate::sample_callback::process_kotlin_reply_callback", + ) .consume_arg("close_session", "session") .consume_arg("undeclare_key_expr", "key_expr") .return_wrapper( diff --git a/zenoh-jni/src/config.rs b/zenoh-jni/src/config.rs index cfae24f4..54c966ad 100644 --- a/zenoh-jni/src/config.rs +++ b/zenoh-jni/src/config.rs @@ -21,8 +21,8 @@ use jni::{ }; use zenoh::Config; -use crate::owned_object::OwnedObject; use crate::errors::ZResult; +use crate::owned_object::OwnedObject; use crate::{throw_exception, utils::decode_string}; /// Loads the default configuration, returning a raw pointer to it. diff --git a/zenoh-jni/src/errors.rs b/zenoh-jni/src/errors.rs index 4bd08aea..09dfdd32 100644 --- a/zenoh-jni/src/errors.rs +++ b/zenoh-jni/src/errors.rs @@ -17,13 +17,11 @@ use jni::JNIEnv; #[macro_export] macro_rules! throw_exception { ($env:expr, $err:expr) => {{ - let _ = ::throw_on_jvm( - &$err, - &mut $env, - ) - .map_err(|err| { - tracing::error!("Unable to throw exception: {}", err); - }); + let _ = + ::throw_on_jvm(&$err, &mut $env) + .map_err(|err| { + tracing::error!("Unable to throw exception: {}", err); + }); }}; } diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs index 5cc85809..513944fb 100644 --- a/zenoh-jni/src/ext/advanced_subscriber.rs +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -141,7 +141,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareDetectPu let detect_publishers_subscriber = advanced_subscriber .detect_publishers() .history(history != 0) - .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) + .callback(process_kotlin_sample_callback( + &mut env, callback, on_close, + )?) .wait() .map_err(|err| zerror!("Unable to declare detect publishers subscriber: {}", err))?; @@ -197,7 +199,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareBackgrou advanced_subscriber .detect_publishers() .history(history != 0) - .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) + .callback(process_kotlin_sample_callback( + &mut env, callback, on_close, + )?) .background() .wait() .map_err(|err| { diff --git a/zenoh-jni/src/key_expr.rs b/zenoh-jni/src/key_expr.rs index be5e4d1f..bb58f604 100644 --- a/zenoh-jni/src/key_expr.rs +++ b/zenoh-jni/src/key_expr.rs @@ -21,8 +21,8 @@ use zenoh::key_expr::KeyExpr; use crate::errors::ZResult; use crate::owned_object::OwnedObject; -use crate::utils::decode_string; use crate::throw_exception; +use crate::utils::decode_string; /// Validates the provided `key_expr` to be a valid key expression, returning it back /// in case of success or throwing an exception in case of failure. diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index de1aeed3..32bc376d 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -154,7 +154,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessSubscribe .liveliness() .declare_subscriber(key_expr.to_owned()) .history(history != 0) - .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) + .callback(process_kotlin_sample_callback( + &mut env, callback, on_close, + )?) .wait() .map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index 7fad23fa..cd536992 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -19,7 +19,11 @@ use jni::{ sys::{jint, jlong}, JNIEnv, }; -use zenoh::{query::{Query, Reply, ReplyError, ReplyKeyExpr}, sample::Sample, session::EntityGlobalId}; +use zenoh::{ + query::{Query, Reply, ReplyError, ReplyKeyExpr}, + sample::Sample, + session::EntityGlobalId, +}; use crate::{errors::ZResult, utils::*}; @@ -183,11 +187,11 @@ pub(crate) unsafe fn process_kotlin_sample_callback( Ok(move |sample: Sample| { on_close.noop(); let _ = || -> ZResult<()> { - let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { - zerror!("Unable to attach thread for sample callback: {}", err) - })?; - let byte_array = bytes_to_java_array(&env, sample.payload()) - .map(|array| env.auto_local(array))?; + let mut env = java_vm + .attach_current_thread_as_daemon() + .map_err(|err| zerror!("Unable to attach thread for sample callback: {}", err))?; + let byte_array = + bytes_to_java_array(&env, sample.payload()).map(|array| env.auto_local(array))?; let encoding_id: jint = sample.encoding().id() as jint; let encoding_schema = match sample.encoding().schema() { @@ -257,9 +261,9 @@ pub(crate) unsafe fn process_kotlin_reply_callback( || -> ZResult<()> { on_close.noop(); tracing::debug!("Receiving reply through JNI: {:?}", reply); - let mut env = java_vm - .attach_current_thread_as_daemon() - .map_err(|err| zerror!("Unable to attach thread for GET query callback: {}", err))?; + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + zerror!("Unable to attach thread for GET query callback: {}", err) + })?; match reply.result() { Ok(sample) => crate::sample_callback::on_reply_success( &mut env, @@ -390,5 +394,3 @@ fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> Z }); result } - - diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index b450e66d..c422e213 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -39,20 +39,10 @@ use zenoh_ext::{ RepliesConfig, }; -use crate::{ - errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, -}; +use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*}; include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); - - - - - - - - /// Declare an advanced Zenoh subscriber via JNI. /// /// # Parameters: @@ -108,7 +98,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV tracing::debug!("Declaring advanced subscriber on '{}'...", key_expr); let mut builder = session .declare_subscriber(key_expr.to_owned()) - .callback(process_kotlin_sample_callback(&mut env, callback, on_close)?) + .callback(process_kotlin_sample_callback( + &mut env, callback, on_close, + )?) .advanced(); tracing::debug!("Advanced subscriber declared on '{}'.", key_expr); From 203f29846bf269784ce736eb0d7bd3ee5d042bb9 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 00:37:30 +0200 Subject: [PATCH 095/126] declare_advanced_subscriber in zenoh_flat --- zenoh-flat/Cargo.toml | 5 +++ zenoh-flat/src/session.rs | 49 ++++++++++++++++++++++++++++++ zenoh-jni/Cargo.lock | 1 + zenoh-jni/Cargo.toml | 1 + zenoh-jni/src/session.rs | 64 ++++++++++++++++++--------------------- 5 files changed, 85 insertions(+), 35 deletions(-) diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 19921f93..1b427055 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -7,8 +7,13 @@ description = "Zenoh flat data interchange support." repository = "https://github.com/eclipse-zenoh/zenoh" build = "build.rs" +[features] +default = [] +zenoh-ext = ["dep:zenoh-ext"] + [dependencies] zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } +zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false, optional = true } json5 = "0.4.1" serde_yaml = "0.9.19" tracing = "0.1" diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 1ce3115f..afc4fe46 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -27,6 +27,11 @@ use zenoh::{ Wait, }; +#[cfg(feature = "zenoh-ext")] +use zenoh_ext::{ + AdvancedSubscriber, AdvancedSubscriberBuilderExt, HistoryConfig, RecoveryConfig, +}; + /// Open a Zenoh session using a borrowed configuration. #[prebindgen_proc_macro::prebindgen("jni")] pub fn open_session(config: &Config) -> ZResult { @@ -365,3 +370,47 @@ pub fn close_session(session: &Session) -> ZResult<()> { zerror!(err) }) } + +/// Declare an advanced subscriber through an existing Zenoh session. +/// +/// Builds on top of the regular subscriber chain and applies the supplied +/// advanced configuration. The JNI wrapper is expected to assemble the +/// `HistoryConfig` / `RecoveryConfig` from primitive arguments and pass them +/// here. +#[cfg(feature = "zenoh-ext")] +pub fn declare_advanced_subscriber( + session: &Session, + key_expr: KeyExpr<'static>, + callback: impl Fn(Sample) + Send + Sync + 'static, + history: Option, + recovery: Option, + subscriber_detection: bool, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + let mut builder = session + .declare_subscriber(key_expr) + .callback(callback) + .advanced(); + if let Some(history) = history { + builder = builder.history(history); + } + if let Some(recovery) = recovery { + builder = builder.recovery(recovery); + } + if subscriber_detection { + builder = builder.subscriber_detection(); + } + builder + .wait() + .map(|subscriber| { + trace!("Declared advanced subscriber on '{}'.", key_expr_string); + subscriber + }) + .map_err(|err| { + error!( + "Unable to declare advanced subscriber on '{}': {}", + key_expr_string, err + ); + zerror!(err) + }) +} diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index ef7fffde..ef58dad5 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -4068,6 +4068,7 @@ dependencies = [ "syn 2.0.117", "tracing", "zenoh", + "zenoh-ext", ] [[package]] diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index e5d7fd68..b80180f4 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -25,6 +25,7 @@ name = "zenoh_jni" [features] default = ["zenoh/default", "zenoh-ext"] +zenoh-ext = ["dep:zenoh-ext", "zenoh-flat/zenoh-ext"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index c422e213..8a961010 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -34,9 +34,8 @@ use crate::sample_callback::process_kotlin_sample_callback; use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] use zenoh_ext::{ - AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, - AdvancedSubscriberBuilderExt, CacheConfig, HistoryConfig, MissDetectionConfig, RecoveryConfig, - RepliesConfig, + AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, CacheConfig, HistoryConfig, + MissDetectionConfig, RecoveryConfig, RepliesConfig, }; use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*}; @@ -95,21 +94,14 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const AdvancedSubscriber<()>> { let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - tracing::debug!("Declaring advanced subscriber on '{}'...", key_expr); - let mut builder = session - .declare_subscriber(key_expr.to_owned()) - .callback(process_kotlin_sample_callback( - &mut env, callback, on_close, - )?) - .advanced(); - tracing::debug!("Advanced subscriber declared on '{}'.", key_expr); + let callback = process_kotlin_sample_callback(&mut env, callback, on_close)?; - if history_config_enabled != 0 { - let mut history = match history_detect_late_publishers != 0 { - true => HistoryConfig::default().detect_late_publishers(), - false => HistoryConfig::default(), + let history = if history_config_enabled != 0 { + let mut history = if history_detect_late_publishers != 0 { + HistoryConfig::default().detect_late_publishers() + } else { + HistoryConfig::default() }; - if history_max_samples > 0 { history = history.max_samples( history_max_samples @@ -117,36 +109,38 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberV .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, ); } - if history_max_age_seconds > 0.0 { history = history.max_age(history_max_age_seconds); } + Some(history) + } else { + None + }; - builder = builder.history(history); - } - - if recovery_config_enabled != 0 { - let recovery = if recovery_config_is_heartbeat != 0 { - RecoveryConfig::default().heartbeat() + let recovery = if recovery_config_enabled != 0 { + if recovery_config_is_heartbeat != 0 { + Some(RecoveryConfig::default().heartbeat()) } else { let dur = Duration::from_millis( recovery_query_period_ms .try_into() .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, ); - RecoveryConfig::default().periodic_queries(dur) - }; - builder = builder.recovery(recovery); - } - - if subscriber_detection != 0 { - builder = builder.subscriber_detection(); - } + Some(RecoveryConfig::default().periodic_queries(dur)) + } + } else { + None + }; - builder - .wait() - .map(|s| Arc::into_raw(Arc::new(s))) - .map_err(|err| zerror!("Unable to declare advanced subscriber: {}", err)) + let subscriber = zenoh_flat::session::declare_advanced_subscriber( + &session, + key_expr, + callback, + history, + recovery, + subscriber_detection != 0, + )?; + Ok(Arc::into_raw(Arc::new(subscriber))) }() .unwrap_or_else(|err| { throw_exception!(env, err); From ddc733da63c2bfcfa713840a0e803c7f88814c49 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 01:09:08 +0200 Subject: [PATCH 096/126] declareAdvancedSubscriber generated --- zenoh-flat/src/jni_converter.rs | 53 ++++++++ zenoh-flat/src/session.rs | 1 + .../kotlin/io/zenoh/jni/JNISession.kt | 37 +++--- .../kotlin/io/zenoh/jni/ext/HistoryConfig.kt | 28 +++++ .../kotlin/io/zenoh/jni/ext/RecoveryConfig.kt | 28 +++++ zenoh-jni/Cargo.toml | 2 +- zenoh-jni/build.rs | 8 ++ zenoh-jni/src/ext/advanced_subscriber.rs | 64 +++++++++- zenoh-jni/src/ext/mod.rs | 2 +- zenoh-jni/src/lib.rs | 2 +- zenoh-jni/src/session.rs | 115 +----------------- 11 files changed, 207 insertions(+), 133 deletions(-) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index a1a94085..d23a942c 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -56,6 +56,11 @@ pub struct Builder { /// that builds an `impl Fn(T) + Send + Sync + 'static` closure from a /// `(callback: JObject, on_close: JObject)` pair. callback_decoders: HashMap, + /// Decoders for struct parameters passed across JNI as a plain `JObject` + /// (e.g. a Kotlin `data class`). Keyed by the last-segment name of the + /// parameter's type (e.g. `"HistoryConfig"`). The decoder must have + /// signature `fn(&mut JNIEnv, &JObject) -> ZResult`. + struct_decoders: HashMap, /// Per-function set of argument names that must be consumed (taken from /// the raw pointer via `Arc::from_raw`) instead of borrowed. Used for /// close/undeclare-style functions that invalidate their handle. @@ -93,6 +98,7 @@ impl Default for Builder { encoding_decoder: None, enum_decoders: HashMap::new(), callback_decoders: HashMap::new(), + struct_decoders: HashMap::new(), consume_args: HashMap::new(), return_wrappers: HashMap::new(), return_wrappers_vec: HashMap::new(), @@ -188,6 +194,23 @@ impl Builder { self } + /// Register a decoder for a struct parameter passed across JNI as a + /// plain `JObject` (typically a Kotlin data class). `type_name` matches + /// the last segment of the parameter's type path (e.g. `"HistoryConfig"`). + /// The decoder must have signature + /// `fn(&mut JNIEnv, &JObject) -> ZResult`. Used both for the plain + /// form (`HistoryConfig`) and the `Option` form (nullable JObject). + pub fn struct_decoder( + mut self, + type_name: impl Into, + decoder: impl AsRef, + ) -> Self { + let path: syn::Path = + syn::parse_str(decoder.as_ref()).expect("invalid struct_decoder path"); + self.struct_decoders.insert(type_name.into(), path); + self + } + /// Register a decoder for an `impl Fn(T) + Send + Sync + 'static` callback /// parameter. `element_type_name` is the last path segment of `T` /// (e.g. `"Sample"`, `"Query"`, `"Reply"`). The decoder must have the @@ -508,6 +531,24 @@ impl JniConverter { }); call_args.push(quote! { #name }); } + ArgKind::StructFromJObject(decoder) => { + jni_params.push(quote! { #name: jni::objects::JObject }); + prelude.push(quote! { + let #name = #decoder(&mut env, &#name)?; + }); + call_args.push(quote! { #name }); + } + ArgKind::OptionStructFromJObject(decoder) => { + jni_params.push(quote! { #name: jni::objects::JObject }); + prelude.push(quote! { + let #name = if !#name.is_null() { + Some(#decoder(&mut env, &#name)?) + } else { + None + }; + }); + call_args.push(quote! { #name }); + } ArgKind::Unsupported => panic!( "unsupported parameter type `{}` for `{}` at {loc}", ty.to_token_stream(), @@ -654,6 +695,9 @@ impl JniConverter { if inner == "Encoding" { return ArgKind::OptionEncoding; } + if let Some(decoder) = self.cfg.struct_decoders.get(&inner) { + return ArgKind::OptionStructFromJObject(decoder.clone()); + } } } if name == "Vec" && is_vec_of_u8(last) { @@ -662,6 +706,9 @@ impl JniConverter { if let Some(decoder) = self.cfg.enum_decoders.get(&name) { return ArgKind::Enum(decoder.clone()); } + if let Some(decoder) = self.cfg.struct_decoders.get(&name) { + return ArgKind::StructFromJObject(decoder.clone()); + } ArgKind::Unsupported } _ => ArgKind::Unsupported, @@ -689,6 +736,12 @@ enum ArgKind { OptionEncoding, /// `Option` → nullable `JString`. OptionString, + /// Struct type registered via `struct_decoder` → single `JObject` arg + /// decoded via the registered decoder. + StructFromJObject(syn::Path), + /// `Option` where `T` is registered via `struct_decoder` → nullable + /// `JObject`, `None` when the JObject is null. + OptionStructFromJObject(syn::Path), /// `impl Fn(T) + Send + Sync + 'static` → `(JObject callback, JObject on_close)` /// pair decoded via a callback decoder registered for `T`. Callback(syn::Path), diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index afc4fe46..742f8966 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -378,6 +378,7 @@ pub fn close_session(session: &Session) -> ZResult<()> { /// `HistoryConfig` / `RecoveryConfig` from primitive arguments and pass them /// here. #[cfg(feature = "zenoh-ext")] +#[prebindgen_proc_macro::prebindgen("jni")] pub fn declare_advanced_subscriber( session: &Session, key_expr: KeyExpr<'static>, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 688b3eb0..30b81eef 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -20,6 +20,8 @@ import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNIOnCloseCallback import io.zenoh.jni.callbacks.JNIQueryableCallback import io.zenoh.jni.callbacks.JNISubscriberCallback +import io.zenoh.jni.ext.HistoryConfig +import io.zenoh.jni.ext.RecoveryConfig /** Adapter class to handle communication with the Zenoh JNI code for a Session. */ public class JNISession(internal val sessionPtr: Long) { @@ -254,33 +256,34 @@ public class JNISession(internal val sessionPtr: Long) { fun declareAdvancedSubscriber( jniKeyExpr: JNIKeyExpr?, keyExprStr: String, - historyConfigEnabled: Boolean, - historyDetectLatePublishers: Boolean, - historyMaxSamples: Long, - historyMaxAgeSeconds: Double, - recoveryConfigEnabled: Boolean, - recoveryConfigIsHeartbeat: Boolean, - recoveryQueryPeriodMs: Long, - subscriberDetection: Boolean, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, - ): JNIAdvancedSubscriber = JNIAdvancedSubscriber(declareAdvancedSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprStr, historyConfigEnabled, historyDetectLatePublishers, historyMaxSamples, historyMaxAgeSeconds, recoveryConfigEnabled, recoveryConfigIsHeartbeat, recoveryQueryPeriodMs, subscriberDetection, callback, onClose)) + history: HistoryConfig?, + recovery: RecoveryConfig?, + subscriberDetection: Boolean, + ): JNIAdvancedSubscriber = JNIAdvancedSubscriber( + declareAdvancedSubscriberViaJNI( + sessionPtr, + jniKeyExpr?.ptr ?: 0, + keyExprStr, + callback, + onClose, + history, + recovery, + subscriberDetection, + ) + ) @Throws(ZError::class) private external fun declareAdvancedSubscriberViaJNI( sessionPtr: Long, keyExprPtr: Long, keyExprStr: String, - historyConfigEnabled: Boolean, - historyDetectLatePublishers: Boolean, - historyMaxSamples: Long, - historyMaxAgeSeconds: Double, - recoveryConfigEnabled: Boolean, - recoveryConfigIsHeartbeat: Boolean, - recoveryQueryPeriodMs: Long, - subscriberDetection: Boolean, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, + history: HistoryConfig?, + recovery: RecoveryConfig?, + subscriberDetection: Boolean, ): Long @Throws(ZError::class) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt new file mode 100644 index 00000000..38f3a008 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt @@ -0,0 +1,28 @@ +// +// Copyright (c) 2026 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.ext + +/** + * History configuration for an advanced subscriber. + * + * Marshaled across JNI as a plain object; the native side reads fields by + * name via `env.get_field(...)`. `maxSamples <= 0` and `maxAgeSeconds <= 0.0` + * mean unlimited. + */ +data class HistoryConfig( + val detectLatePublishers: Boolean, + val maxSamples: Long, + val maxAgeSeconds: Double, +) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt new file mode 100644 index 00000000..f082d929 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt @@ -0,0 +1,28 @@ +// +// Copyright (c) 2026 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.ext + +/** + * Recovery configuration for an advanced subscriber. + * + * Marshaled across JNI as a plain object; the native side reads fields by + * name via `env.get_field(...)`. When [isHeartbeat] is `true`, [periodMs] is + * ignored; otherwise [periodMs] is the `periodic_queries` period in + * milliseconds. + */ +data class RecoveryConfig( + val isHeartbeat: Boolean, + val periodMs: Long, +) diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index b80180f4..acddd84c 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -49,7 +49,7 @@ crate_type = ["staticlib", "dylib"] [build-dependencies] rustc_version = "0.4.0" prebindgen = "0.4.1" -zenoh-flat = { path = "../zenoh-flat" } +zenoh-flat = { path = "../zenoh-flat", features = ["zenoh-ext"] } itertools = "0.12" [profile.release] diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index bc866797..e79b6ee7 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -35,6 +35,14 @@ fn main() { "Reply", "crate::sample_callback::process_kotlin_reply_callback", ) + .struct_decoder( + "HistoryConfig", + "crate::ext::advanced_subscriber::decode_history_config", + ) + .struct_decoder( + "RecoveryConfig", + "crate::ext::advanced_subscriber::decode_recovery_config", + ) .consume_arg("close_session", "session") .consume_arg("undeclare_key_expr", "key_expr") .return_wrapper( diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs index 513944fb..deec67e9 100644 --- a/zenoh-jni/src/ext/advanced_subscriber.rs +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -13,13 +13,75 @@ // use std::sync::Arc; +use std::time::Duration; use jni::sys::jboolean; use jni::{objects::JClass, JNIEnv}; use zenoh::handlers::{Callback, DefaultHandler}; use zenoh::pubsub::Subscriber; use zenoh_ext::SampleMissListener; -use zenoh_ext::{AdvancedSubscriber, Miss, SampleMissListenerBuilder}; +use zenoh_ext::{ + AdvancedSubscriber, HistoryConfig, Miss, RecoveryConfig, SampleMissListenerBuilder, +}; + +/// Decode a Kotlin `io.zenoh.jni.ext.HistoryConfig` data-class instance into +/// a zenoh-ext [`HistoryConfig`]. Matches field names `detectLatePublishers`, +/// `maxSamples`, `maxAgeSeconds`. +pub(crate) fn decode_history_config(env: &mut JNIEnv, obj: &JObject) -> ZResult { + let detect_late_publishers = env + .get_field(obj, "detectLatePublishers", "Z") + .and_then(|v| v.z()) + .map_err(|err| zerror!("HistoryConfig.detectLatePublishers: {}", err))?; + let max_samples = env + .get_field(obj, "maxSamples", "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!("HistoryConfig.maxSamples: {}", err))?; + let max_age_seconds = env + .get_field(obj, "maxAgeSeconds", "D") + .and_then(|v| v.d()) + .map_err(|err| zerror!("HistoryConfig.maxAgeSeconds: {}", err))?; + + let mut cfg = HistoryConfig::default(); + if detect_late_publishers { + cfg = cfg.detect_late_publishers(); + } + if max_samples > 0 { + let n: usize = max_samples + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?; + cfg = cfg.max_samples(n); + } + if max_age_seconds > 0.0 { + cfg = cfg.max_age(max_age_seconds); + } + Ok(cfg) +} + +/// Decode a Kotlin `io.zenoh.jni.ext.RecoveryConfig` data-class instance into +/// a zenoh-ext [`RecoveryConfig`]. Matches field names `isHeartbeat`, +/// `periodMs`. When `isHeartbeat` is `false`, `periodMs` is the +/// `periodic_queries` period in milliseconds. +pub(crate) fn decode_recovery_config(env: &mut JNIEnv, obj: &JObject) -> ZResult { + let is_heartbeat = env + .get_field(obj, "isHeartbeat", "Z") + .and_then(|v| v.z()) + .map_err(|err| zerror!("RecoveryConfig.isHeartbeat: {}", err))?; + let period_ms = env + .get_field(obj, "periodMs", "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!("RecoveryConfig.periodMs: {}", err))?; + + if is_heartbeat { + Ok(RecoveryConfig::default().heartbeat()) + } else { + let dur = Duration::from_millis( + period_ms + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, + ); + Ok(RecoveryConfig::default().periodic_queries(dur)) + } +} use crate::sample_callback::process_kotlin_sample_callback; use jni::objects::JObject; diff --git a/zenoh-jni/src/ext/mod.rs b/zenoh-jni/src/ext/mod.rs index 8a4a30b9..061aaeb2 100644 --- a/zenoh-jni/src/ext/mod.rs +++ b/zenoh-jni/src/ext/mod.rs @@ -13,6 +13,6 @@ // mod advanced_publisher; -mod advanced_subscriber; +pub(crate) mod advanced_subscriber; mod matching_listener; mod sample_miss_listener; diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index fe0c98c6..77725468 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -18,7 +18,7 @@ extern crate zenoh_flat; mod config; mod errors; #[cfg(feature = "zenoh-ext")] -mod ext; +pub(crate) mod ext; mod key_expr; mod liveliness; mod logger; diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 8a961010..e5e8ff48 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -15,7 +15,7 @@ use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ - objects::{JClass, JObject, JString}, + objects::{JClass, JString}, sys::{jboolean, jint, jlong}, JNIEnv, }; @@ -29,125 +29,16 @@ use zenoh::{ }; use crate::owned_object::OwnedObject; -use crate::sample_callback::process_kotlin_sample_callback; -#[cfg(feature = "zenoh-ext")] -use jni::sys::jdouble; #[cfg(feature = "zenoh-ext")] use zenoh_ext::{ - AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, CacheConfig, HistoryConfig, - MissDetectionConfig, RecoveryConfig, RepliesConfig, + AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, CacheConfig, + MissDetectionConfig, RepliesConfig, }; use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*}; include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Declare an advanced Zenoh subscriber via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr], may be null. -/// - `key_expr_str`: String representation of the [KeyExpr]. -/// - `history_config_enabled`: Whether history config is enabled. -/// - `history_detect_late_publishers`: Whether to detect late publishers in history. -/// - `history_max_samples`: Max samples in history (<=0 means unlimited). -/// - `history_max_age_seconds`: Max age in seconds for history (<=0.0 means unlimited). -/// - `recovery_config_enabled`: Whether recovery config is enabled. -/// - `recovery_config_is_heartbeat`: Whether to use heartbeat recovery. -/// - `recovery_query_period_ms`: Query period for periodic recovery in milliseconds. -/// - `subscriber_detection`: Allow this subscriber to be detected through liveliness. -/// - `session_ptr`: The raw pointer to the Zenoh session. -/// - `callback`: The callback function as an instance of the `JNISubscriberCallback` interface. -/// - `on_close`: A `JNIOnCloseCallback` to be called upon closing the subscriber. -/// -/// Returns: -/// - A raw pointer to the declared [AdvancedSubscriber]. In case of failure, an exception is thrown and null is returned. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// -#[cfg(feature = "zenoh-ext")] -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedSubscriberViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - // HistoryConfig - history_config_enabled: jboolean, - history_detect_late_publishers: jboolean, - history_max_samples: jlong, - history_max_age_seconds: jdouble, - // RecoveryConfig - recovery_config_enabled: jboolean, - recovery_config_is_heartbeat: jboolean, - recovery_query_period_ms: jlong, - - subscriber_detection: jboolean, - - callback: JObject, - on_close: JObject, -) -> *const AdvancedSubscriber<()> { - let session = OwnedObject::from_raw(session_ptr); - || -> ZResult<*const AdvancedSubscriber<()>> { - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let callback = process_kotlin_sample_callback(&mut env, callback, on_close)?; - - let history = if history_config_enabled != 0 { - let mut history = if history_detect_late_publishers != 0 { - HistoryConfig::default().detect_late_publishers() - } else { - HistoryConfig::default() - }; - if history_max_samples > 0 { - history = history.max_samples( - history_max_samples - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, - ); - } - if history_max_age_seconds > 0.0 { - history = history.max_age(history_max_age_seconds); - } - Some(history) - } else { - None - }; - - let recovery = if recovery_config_enabled != 0 { - if recovery_config_is_heartbeat != 0 { - Some(RecoveryConfig::default().heartbeat()) - } else { - let dur = Duration::from_millis( - recovery_query_period_ms - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, - ); - Some(RecoveryConfig::default().periodic_queries(dur)) - } - } else { - None - }; - - let subscriber = zenoh_flat::session::declare_advanced_subscriber( - &session, - key_expr, - callback, - history, - recovery, - subscriber_detection != 0, - )?; - Ok(Arc::into_raw(Arc::new(subscriber))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - /// Declare an advanced Zenoh publisher via JNI. /// /// # Parameters: From 13ac2c5d4a27176084959abb9dbec7ba01f2870f Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 01:27:59 +0200 Subject: [PATCH 097/126] declare advancved publisher generated --- zenoh-flat/src/session.rs | 54 ++++++- .../kotlin/io/zenoh/jni/JNISession.kt | 39 ++--- .../kotlin/io/zenoh/jni/ext/CacheConfig.kt | 33 ++++ .../io/zenoh/jni/ext/MissDetectionConfig.kt | 30 ++++ zenoh-jni/build.rs | 8 + zenoh-jni/src/ext/advanced_publisher.rs | 83 +++++++++- zenoh-jni/src/ext/mod.rs | 2 +- zenoh-jni/src/session.rs | 148 +----------------- 8 files changed, 228 insertions(+), 169 deletions(-) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 742f8966..2b5fdf94 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -29,7 +29,8 @@ use zenoh::{ #[cfg(feature = "zenoh-ext")] use zenoh_ext::{ - AdvancedSubscriber, AdvancedSubscriberBuilderExt, HistoryConfig, RecoveryConfig, + AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, AdvancedSubscriberBuilderExt, + CacheConfig, HistoryConfig, MissDetectionConfig, RecoveryConfig, }; /// Open a Zenoh session using a borrowed configuration. @@ -415,3 +416,54 @@ pub fn declare_advanced_subscriber( zerror!(err) }) } + +/// Declare an advanced publisher through an existing Zenoh session. +/// +/// Builds on top of the regular publisher chain and applies the supplied +/// advanced configuration. The JNI wrapper is expected to assemble the +/// `CacheConfig` / `MissDetectionConfig` from primitive arguments and pass +/// them here. +#[cfg(feature = "zenoh-ext")] +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_advanced_publisher( + session: &Session, + key_expr: KeyExpr<'static>, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + reliability: Reliability, + cache: Option, + sample_miss_detection: Option, + publisher_detection: bool, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + let mut builder = session + .declare_publisher(key_expr) + .congestion_control(congestion_control) + .priority(priority) + .express(express) + .reliability(reliability) + .advanced(); + if let Some(cache) = cache { + builder = builder.cache(cache); + } + if let Some(miss_detection) = sample_miss_detection { + builder = builder.sample_miss_detection(miss_detection); + } + if publisher_detection { + builder = builder.publisher_detection(); + } + builder + .wait() + .map(|publisher| { + trace!("Declared advanced publisher on '{}'.", key_expr_string); + publisher + }) + .map_err(|err| { + error!( + "Unable to declare advanced publisher on '{}': {}", + key_expr_string, err + ); + zerror!(err) + }) +} diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 30b81eef..e8776bc6 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -20,7 +20,9 @@ import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNIOnCloseCallback import io.zenoh.jni.callbacks.JNIQueryableCallback import io.zenoh.jni.callbacks.JNISubscriberCallback +import io.zenoh.jni.ext.CacheConfig import io.zenoh.jni.ext.HistoryConfig +import io.zenoh.jni.ext.MissDetectionConfig import io.zenoh.jni.ext.RecoveryConfig /** Adapter class to handle communication with the Zenoh JNI code for a Session. */ @@ -294,17 +296,23 @@ public class JNISession(internal val sessionPtr: Long) { priority: Int, isExpress: Boolean, reliability: Int, - cacheEnabled: Boolean, - cacheMaxSamples: Long, - cacheRepliesPriority: Int, - cacheRepliesCongestionControl: Int, - cacheRepliesIsExpress: Boolean, - sampleMissDetectionEnabled: Boolean, - sampleMissDetectionEnableHeartbeat: Boolean, - sampleMissDetectionHeartbeatMs: Long, - sampleMissDetectionHeartbeatIsSporadic: Boolean, + cache: CacheConfig?, + sampleMissDetection: MissDetectionConfig?, publisherDetection: Boolean, - ): JNIAdvancedPublisher = JNIAdvancedPublisher(declareAdvancedPublisherViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprStr, congestionControl, priority, isExpress, reliability, cacheEnabled, cacheMaxSamples, cacheRepliesPriority, cacheRepliesCongestionControl, cacheRepliesIsExpress, sampleMissDetectionEnabled, sampleMissDetectionEnableHeartbeat, sampleMissDetectionHeartbeatMs, sampleMissDetectionHeartbeatIsSporadic, publisherDetection)) + ): JNIAdvancedPublisher = JNIAdvancedPublisher( + declareAdvancedPublisherViaJNI( + sessionPtr, + jniKeyExpr?.ptr ?: 0, + keyExprStr, + congestionControl, + priority, + isExpress, + reliability, + cache, + sampleMissDetection, + publisherDetection, + ) + ) @Throws(ZError::class) private external fun declareAdvancedPublisherViaJNI( @@ -315,15 +323,8 @@ public class JNISession(internal val sessionPtr: Long) { priority: Int, isExpress: Boolean, reliability: Int, - cacheEnabled: Boolean, - cacheMaxSamples: Long, - cacheRepliesPriority: Int, - cacheRepliesCongestionControl: Int, - cacheRepliesIsExpress: Boolean, - sampleMissDetectionEnabled: Boolean, - sampleMissDetectionEnableHeartbeat: Boolean, - sampleMissDetectionHeartbeatMs: Long, - sampleMissDetectionHeartbeatIsSporadic: Boolean, + cache: CacheConfig?, + sampleMissDetection: MissDetectionConfig?, publisherDetection: Boolean, ): Long diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt new file mode 100644 index 00000000..a76735a0 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt @@ -0,0 +1,33 @@ +// +// Copyright (c) 2026 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.ext + +/** + * Cache configuration for an advanced publisher. + * + * Marshaled across JNI as a plain object; the native side reads fields by + * name via `env.get_field(...)`. The nested `RepliesConfig` is represented + * inline (flat fields `repliesPriority`, `repliesCongestionControl`, + * `repliesIsExpress`) to keep the JNI field-lookup simple. + * + * `repliesPriority` and `repliesCongestionControl` are the ordinals of the + * corresponding zenoh enums. + */ +data class CacheConfig( + val maxSamples: Long, + val repliesPriority: Int, + val repliesCongestionControl: Int, + val repliesIsExpress: Boolean, +) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt new file mode 100644 index 00000000..2af1479b --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt @@ -0,0 +1,30 @@ +// +// Copyright (c) 2026 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.ext + +/** + * Miss detection configuration for an advanced publisher. + * + * Marshaled across JNI as a plain object; the native side reads fields by + * name via `env.get_field(...)`. When [enableHeartbeat] is `true`, the + * heartbeat is configured with [periodMs] — sporadic if [isSporadic], regular + * otherwise. When [enableHeartbeat] is `false`, the other fields are ignored + * and `MissDetectionConfig::default()` is used. + */ +data class MissDetectionConfig( + val enableHeartbeat: Boolean, + val isSporadic: Boolean, + val periodMs: Long, +) diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index e79b6ee7..ac5057b1 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -43,6 +43,14 @@ fn main() { "RecoveryConfig", "crate::ext::advanced_subscriber::decode_recovery_config", ) + .struct_decoder( + "CacheConfig", + "crate::ext::advanced_publisher::decode_cache_config", + ) + .struct_decoder( + "MissDetectionConfig", + "crate::ext::advanced_publisher::decode_miss_detection_config", + ) .consume_arg("close_session", "session") .consume_arg("undeclare_key_expr", "key_expr") .return_wrapper( diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index b7ff2dae..d2ed2e9d 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -13,6 +13,7 @@ // use std::sync::Arc; +use std::time::Duration; use jni::objects::JValue; use jni::{ @@ -22,12 +23,90 @@ use jni::{ }; use zenoh::handlers::{Callback, DefaultHandler}; use zenoh::Wait; -use zenoh_ext::AdvancedPublisher; +use zenoh_ext::{AdvancedPublisher, CacheConfig, MissDetectionConfig, RepliesConfig}; use crate::owned_object::OwnedObject; -use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; +use crate::utils::{ + decode_congestion_control, decode_priority, get_callback_global_ref, get_java_vm, load_on_close, +}; use crate::throw_exception; + +/// Decode a Kotlin `io.zenoh.jni.ext.CacheConfig` data-class instance into a +/// zenoh-ext [`CacheConfig`] (with nested [`RepliesConfig`] built from the +/// flat `repliesPriority` / `repliesCongestionControl` / `repliesIsExpress` +/// fields). +pub(crate) fn decode_cache_config(env: &mut JNIEnv, obj: &JObject) -> ZResult { + let max_samples = env + .get_field(obj, "maxSamples", "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!("CacheConfig.maxSamples: {}", err))?; + let replies_priority = env + .get_field(obj, "repliesPriority", "I") + .and_then(|v| v.i()) + .map_err(|err| zerror!("CacheConfig.repliesPriority: {}", err))?; + let replies_cc = env + .get_field(obj, "repliesCongestionControl", "I") + .and_then(|v| v.i()) + .map_err(|err| zerror!("CacheConfig.repliesCongestionControl: {}", err))?; + let replies_express = env + .get_field(obj, "repliesIsExpress", "Z") + .and_then(|v| v.z()) + .map_err(|err| zerror!("CacheConfig.repliesIsExpress: {}", err))?; + + let replies = RepliesConfig::default() + .priority(decode_priority(replies_priority)?) + .congestion_control(decode_congestion_control(replies_cc)?) + .express(replies_express); + let cfg = CacheConfig::default() + .max_samples( + max_samples + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, + ) + .replies_config(replies); + Ok(cfg) +} + +/// Decode a Kotlin `io.zenoh.jni.ext.MissDetectionConfig` data-class instance +/// into a zenoh-ext [`MissDetectionConfig`]. Field semantics: +/// - `enableHeartbeat`: whether to call `.heartbeat(...)` or `.sporadic_heartbeat(...)` +/// on the config (otherwise `MissDetectionConfig::default()` is left as-is). +/// - `isSporadic`: picks between `sporadic_heartbeat` and `heartbeat`. +/// - `periodMs`: heartbeat period in milliseconds (used only when +/// `enableHeartbeat` is `true`). +pub(crate) fn decode_miss_detection_config( + env: &mut JNIEnv, + obj: &JObject, +) -> ZResult { + let enable_heartbeat = env + .get_field(obj, "enableHeartbeat", "Z") + .and_then(|v| v.z()) + .map_err(|err| zerror!("MissDetectionConfig.enableHeartbeat: {}", err))?; + let is_sporadic = env + .get_field(obj, "isSporadic", "Z") + .and_then(|v| v.z()) + .map_err(|err| zerror!("MissDetectionConfig.isSporadic: {}", err))?; + let period_ms = env + .get_field(obj, "periodMs", "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!("MissDetectionConfig.periodMs: {}", err))?; + + let mut cfg = MissDetectionConfig::default(); + if enable_heartbeat { + let dur = Duration::from_millis( + period_ms + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, + ); + cfg = if is_sporadic { + cfg.sporadic_heartbeat(dur) + } else { + cfg.heartbeat(dur) + }; + } + Ok(cfg) +} use crate::{ errors::ZResult, utils::{decode_byte_array, decode_encoding}, diff --git a/zenoh-jni/src/ext/mod.rs b/zenoh-jni/src/ext/mod.rs index 061aaeb2..e709c6f7 100644 --- a/zenoh-jni/src/ext/mod.rs +++ b/zenoh-jni/src/ext/mod.rs @@ -12,7 +12,7 @@ // ZettaScale Zenoh Team, // -mod advanced_publisher; +pub(crate) mod advanced_publisher; pub(crate) mod advanced_subscriber; mod matching_listener; mod sample_miss_listener; diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index e5e8ff48..26b584f3 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -12,160 +12,16 @@ // ZettaScale Zenoh Team, // -use std::{ptr::null, sync::Arc, time::Duration}; - -use jni::{ - objects::{JClass, JString}, - sys::{jboolean, jint, jlong}, - JNIEnv, -}; +// Types referenced by the generated `zenoh_flat_jni.rs` below must be in scope. use zenoh::{ config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, query::{Querier, Queryable}, session::Session, - Wait, }; - -use crate::owned_object::OwnedObject; #[cfg(feature = "zenoh-ext")] -use zenoh_ext::{ - AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, CacheConfig, - MissDetectionConfig, RepliesConfig, -}; - -use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*}; +use zenoh_ext::{AdvancedPublisher, AdvancedSubscriber}; include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Declare an advanced Zenoh publisher via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the publisher, may be null. -/// - `key_expr_str`: String representation of the [KeyExpr]. -/// - `session_ptr`: Raw pointer to the Zenoh [Session] to be used for the publisher. -/// - `congestion_control`: The [CongestionControl] configuration as an ordinal. -/// - `priority`: The [Priority] configuration as an ordinal. -/// - `is_express`: The express config of the publisher. -/// - `reliability`: The reliability value as an ordinal. -/// - `cache_enabled`: If true, attach a cache to the [AdvancedPublisher]. -/// - `cache_max_samples`: How many samples to keep for each resource. -/// - `cache_replies_priority`: The [Priority] for cache replies as an ordinal. -/// - `cache_replies_congestion_control`: The [CongestionControl] for cache replies as an ordinal. -/// - `cache_replies_is_express`: The express config for cache replies. -/// - `sample_miss_detection_enabled`: Enables sample miss detection functionality. -/// - `sample_miss_detection_enable_heartbeat`: Use heartbeat for miss detection. -/// - `sample_miss_detection_heartbeat_ms`: Heartbeat period in milliseconds. -/// - `sample_miss_detection_heartbeat_is_sporadic`: Whether heartbeat is sporadic. -/// - `publisher_detection`: Enables publisher detection. -/// -/// # Returns: -/// - A raw pointer to the declared [AdvancedPublisher] or null in case of failure. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// -#[cfg(feature = "zenoh-ext")] -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareAdvancedPublisherViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - congestion_control: jint, - priority: jint, - is_express: jboolean, - reliability: jint, - // CacheConfig - cache_enabled: jboolean, - cache_max_samples: jlong, - cache_replies_priority: jint, - cache_replies_congestion_control: jint, - cache_replies_is_express: jboolean, - // MissDetectionConfig - sample_miss_detection_enabled: jboolean, - sample_miss_detection_enable_heartbeat: jboolean, - sample_miss_detection_heartbeat_ms: jlong, - sample_miss_detection_heartbeat_is_sporadic: jboolean, - - publisher_detection: jboolean, -) -> *const AdvancedPublisher<'static> { - let session = OwnedObject::from_raw(session_ptr); - let publisher_ptr = || -> ZResult<*const AdvancedPublisher<'static>> { - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let reliability = decode_reliability(reliability)?; - let mut builder = session - .declare_publisher(key_expr) - .congestion_control(congestion_control) - .priority(priority) - .express(is_express != 0) - .reliability(reliability) - .advanced(); - - // fill CacheConfig - if cache_enabled != 0 { - let cache_congestion_control = - decode_congestion_control(cache_replies_congestion_control)?; - - let cache_priority = decode_priority(cache_replies_priority)?; - - let replies_config = RepliesConfig::default() - .priority(cache_priority) - .congestion_control(cache_congestion_control) - .express(cache_replies_is_express != 0); - - let cache_config = CacheConfig::default() - .max_samples( - cache_max_samples - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, - ) - .replies_config(replies_config); - - builder = builder.cache(cache_config); - } - - // fill MissDetectionConfig - if sample_miss_detection_enabled != 0 { - let miss_detection_config = { - let mut result = MissDetectionConfig::default(); - if sample_miss_detection_enable_heartbeat != 0 { - let duration = Duration::from_millis( - sample_miss_detection_heartbeat_ms - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, - ); - - result = match sample_miss_detection_heartbeat_is_sporadic != 0 { - true => result.sporadic_heartbeat(duration), - false => result.heartbeat(duration), - }; - } - result - }; - builder = builder.sample_miss_detection(miss_detection_config); - } - - if publisher_detection != 0 { - builder = builder.publisher_detection(); - } - - let result = builder.wait(); - match result { - Ok(publisher) => Ok(Arc::into_raw(Arc::new(publisher))), - Err(err) => Err(zerror!(err)), - } - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }); - publisher_ptr -} From a256d270081e067ff445d330aa8f234517981b76 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 14:49:06 +0200 Subject: [PATCH 098/126] pass structs --- zenoh-flat/src/jni_converter.rs | 67 ++--------------- .../src/commonMain/kotlin/io/zenoh/Session.kt | 11 +-- .../kotlin/io/zenoh/bytes/Encoding.kt | 5 ++ .../kotlin/io/zenoh/pubsub/Publisher.kt | 4 +- .../kotlin/io/zenoh/query/Querier.kt | 6 +- .../commonMain/kotlin/io/zenoh/query/Query.kt | 5 +- .../io/zenoh/jni/JNIAdvancedPublisher.kt | 6 +- .../kotlin/io/zenoh/jni/JNIEncoding.kt | 26 +++++++ .../kotlin/io/zenoh/jni/JNIKeyExpr.kt | 27 ++++++- .../kotlin/io/zenoh/jni/JNIPublisher.kt | 6 +- .../kotlin/io/zenoh/jni/JNIQuerier.kt | 11 +-- .../kotlin/io/zenoh/jni/JNIQuery.kt | 23 +++--- .../kotlin/io/zenoh/jni/JNISession.kt | 75 +++++++------------ zenoh-jni/build.rs | 4 +- zenoh-jni/src/ext/advanced_publisher.rs | 10 +-- zenoh-jni/src/key_expr.rs | 21 ++++++ zenoh-jni/src/liveliness.rs | 22 +++--- zenoh-jni/src/publisher.rs | 10 +-- zenoh-jni/src/querier.rs | 21 +++--- zenoh-jni/src/query.rs | 29 +++---- zenoh-jni/src/utils.rs | 15 ++++ 21 files changed, 197 insertions(+), 207 deletions(-) create mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIEncoding.kt diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index d23a942c..c733708e 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -50,7 +50,6 @@ pub struct Builder { key_expr_decoder: Option, string_decoder: Option, byte_array_decoder: Option, - encoding_decoder: Option, enum_decoders: HashMap, /// Map from callback element type name (e.g. `"Sample"`) to the decoder /// that builds an `impl Fn(T) + Send + Sync + 'static` closure from a @@ -95,7 +94,6 @@ impl Default for Builder { key_expr_decoder: None, string_decoder: None, byte_array_decoder: None, - encoding_decoder: None, enum_decoders: HashMap::new(), callback_decoders: HashMap::new(), struct_decoders: HashMap::new(), @@ -171,16 +169,6 @@ impl Builder { self } - /// Path of the function that decodes an `Encoding` from a `(jint id, - /// &JString schema)` pair, e.g. `"crate::utils::decode_encoding"`. - /// The generated JNI signature splits the single `Encoding` parameter - /// into `_id: jint` + `_schema: JString`. - pub fn encoding_decoder(mut self, path: impl AsRef) -> Self { - self.encoding_decoder = - Some(syn::parse_str(path.as_ref()).expect("invalid encoding_decoder path")); - self - } - /// Register a decoder for an enum type. `type_name` is matched against the /// last segment of the parameter's type path. pub fn enum_decoder( @@ -384,13 +372,13 @@ impl JniConverter { call_args.push(quote! { &#name }); } ArgKind::KeyExpr => { - let ptr_ident = format_ident!("{}_ptr", name); if consume_set.contains(&name.to_string()) { // Consume path: the declared KeyExpr is required (no // string fallback). Arc::from_raw decrements the // refcount at end of scope, freeing the handle once // no other references remain. A cloned inner KeyExpr // is passed to the callee by value. + let ptr_ident = format_ident!("{}_ptr", name); let arc_ident = format_ident!("__{}_arc", name); jni_params.push(quote! { #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> @@ -401,18 +389,17 @@ impl JniConverter { }); call_args.push(quote! { #name }); } else { + // Non-consume path: single `JObject` holder + // (io.zenoh.jni.JNIKeyExpr) decoded via the + // configured `key_expr_decoder`. let decoder = self .cfg .key_expr_decoder .as_ref() .expect("key_expr_decoder not configured"); - let str_ident = format_ident!("{}_str", name); - jni_params.push(quote! { - #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> - }); - jni_params.push(quote! { #str_ident: jni::objects::JString }); + jni_params.push(quote! { #name: jni::objects::JObject }); prelude.push(quote! { - let #name = #decoder(&mut env, &#str_ident, #ptr_ident)?; + let #name = #decoder(&mut env, &#name)?; }); call_args.push(quote! { #name }); } @@ -485,36 +472,6 @@ impl JniConverter { }); call_args.push(quote! { #name }); } - ArgKind::Encoding => { - let decoder = self - .cfg - .encoding_decoder - .as_ref() - .expect("encoding_decoder not configured"); - let id_ident = format_ident!("{}_id", name); - let schema_ident = format_ident!("{}_schema", name); - jni_params.push(quote! { #id_ident: jni::sys::jint }); - jni_params.push(quote! { #schema_ident: jni::objects::JString }); - prelude.push(quote! { - let #name = #decoder(&mut env, #id_ident, &#schema_ident)?; - }); - call_args.push(quote! { #name }); - } - ArgKind::OptionEncoding => { - let decoder = self - .cfg - .encoding_decoder - .as_ref() - .expect("encoding_decoder not configured"); - let id_ident = format_ident!("{}_id", name); - let schema_ident = format_ident!("{}_schema", name); - jni_params.push(quote! { #id_ident: jni::sys::jint }); - jni_params.push(quote! { #schema_ident: jni::objects::JString }); - prelude.push(quote! { - let #name = Some(#decoder(&mut env, #id_ident, &#schema_ident)?); - }); - call_args.push(quote! { #name }); - } ArgKind::OptionString => { let decoder = self .cfg @@ -681,9 +638,6 @@ impl JniConverter { if name == "Duration" { return ArgKind::Duration; } - if name == "Encoding" { - return ArgKind::Encoding; - } if name == "Option" && is_option_of_vec_u8(last) { return ArgKind::OptionVecU8; } @@ -692,9 +646,6 @@ impl JniConverter { if inner == "String" { return ArgKind::OptionString; } - if inner == "Encoding" { - return ArgKind::OptionEncoding; - } if let Some(decoder) = self.cfg.struct_decoders.get(&inner) { return ArgKind::OptionStructFromJObject(decoder.clone()); } @@ -728,12 +679,6 @@ enum ArgKind { OptionVecU8, /// `Vec` → `JByteArray` decoded via `byte_array_decoder`. VecU8, - /// `Encoding` → `(jint id, JString schema)` pair via `encoding_decoder`. - Encoding, - /// `Option` → `(jint id, JString schema)` pair via `encoding_decoder`, - /// wrapped in `Some(_)`. Semantic gating on payload presence is the - /// callee's responsibility. - OptionEncoding, /// `Option` → nullable `JString`. OptionString, /// Struct type registered via `struct_decoder` → single `JObject` arg diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index e60e6b4f..e19b102c 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -788,8 +788,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { options.consolidation.ordinal, options.attachment?.into()?.bytes, options.payload?.into()?.bytes, - options.encoding?.id ?: Encoding.defaultEncoding().id, - options.encoding?.schema, + options.encoding?.toJni(), options.qos.congestionControl.value, options.qos.priority.value, options.qos.express, @@ -838,8 +837,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { options.consolidation.ordinal, options.attachment?.into()?.bytes, options.payload?.into()?.bytes, - options.encoding?.id ?: Encoding.defaultEncoding().id, - options.encoding?.schema, + options.encoding?.toJni(), options.qos.congestionControl.value, options.qos.priority.value, options.qos.express, @@ -851,13 +849,12 @@ class Session private constructor(private val config: Config) : AutoCloseable { @Throws(ZError::class) internal fun resolvePut(keyExpr: KeyExpr, payload: IntoZBytes, putOptions: PutOptions) { jniSession?.run { - val encoding = putOptions.encoding ?: Encoding.defaultEncoding() + val encoding = (putOptions.encoding ?: Encoding.defaultEncoding()).toJni() put( keyExpr.jniKeyExpr, keyExpr.keyExpr, payload.into().bytes, - encoding.id, - encoding.schema, + encoding, putOptions.congestionControl.value, putOptions.priority.value, putOptions.express, diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt index a4f77815..35995ff9 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt @@ -14,6 +14,8 @@ package io.zenoh.bytes +import io.zenoh.jni.JNIEncoding + /** * Default encoding values used by Zenoh. * @@ -499,4 +501,7 @@ class Encoding private constructor( override fun hashCode(): Int { return id.hashCode() } + + /** Project this public [Encoding] into a JNI-boundary holder. */ + internal fun toJni(): JNIEncoding = JNIEncoding(id, schema) } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt index 9ec8641a..fee4ca14 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt @@ -79,14 +79,14 @@ class Publisher internal constructor( /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @Throws(ZError::class) fun put(payload: IntoZBytes) { - jniPublisher?.put(payload.into().bytes, encoding.id, encoding.schema, null) ?: throw publisherNotValid + jniPublisher?.put(payload.into().bytes, encoding.toJni(), null) ?: throw publisherNotValid } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @Throws(ZError::class) fun put(payload: IntoZBytes, options: PutOptions) { val enc = options.encoding ?: this.encoding - jniPublisher?.put(payload.into().bytes, enc.id, enc.schema, options.attachment?.into()?.bytes) ?: throw publisherNotValid + jniPublisher?.put(payload.into().bytes, enc.toJni(), options.attachment?.into()?.bytes) ?: throw publisherNotValid } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt index faca37a8..b7be5c16 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt @@ -179,8 +179,7 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v fun() {}, options.attachment?.into()?.bytes, options.payload?.into()?.bytes, - options.encoding?.id ?: Encoding.defaultEncoding().id, - options.encoding?.schema + options.encoding?.toJni() ) } @@ -214,8 +213,7 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v handler::onClose, options.attachment?.into()?.bytes, options.payload?.into()?.bytes, - options.encoding?.id ?: Encoding.defaultEncoding().id, - options.encoding?.schema + options.encoding?.toJni() ) return handler.receiver() } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt index c2128efa..8902732a 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt @@ -71,8 +71,7 @@ class Query internal constructor( keyExpr.jniKeyExpr, keyExpr.keyExpr, zbytes.bytes, - encoding?.id ?: Encoding.defaultEncoding().id, - encoding?.schema, + (encoding ?: Encoding.defaultEncoding()).toJni(), timestampEnabled, if (timestampEnabled) timestamp!!.ntpValue() else 0, options.attachment?.into()?.bytes, @@ -130,7 +129,7 @@ class Query internal constructor( fun replyErr(message: IntoZBytes, options: ReplyErrOptions = ReplyErrOptions()) { val encoding = options.encoding jniQuery?.apply { - replyError(message.into().bytes, encoding?.id ?: Encoding.defaultEncoding().id, encoding?.schema) + replyError(message.into().bytes, (encoding ?: Encoding.defaultEncoding()).toJni()) jniQuery = null } ?: throw (ZError("Query is invalid")) } diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt index 62d822f6..619a8ab5 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIAdvancedPublisher.kt @@ -26,8 +26,8 @@ import io.zenoh.jni.callbacks.JNIOnCloseCallback public class JNIAdvancedPublisher(private val ptr: Long) { @Throws(ZError::class) - fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { - putViaJNI(ptr, payload, encodingId, encodingSchema, attachment) + fun put(payload: ByteArray, encoding: JNIEncoding, attachment: ByteArray?) { + putViaJNI(ptr, payload, encoding, attachment) } @Throws(ZError::class) @@ -52,7 +52,7 @@ public class JNIAdvancedPublisher(private val ptr: Long) { @Throws(ZError::class) private external fun putViaJNI( - ptr: Long, payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray? + ptr: Long, payload: ByteArray, encoding: JNIEncoding, attachment: ByteArray? ) @Throws(ZError::class) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIEncoding.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIEncoding.kt new file mode 100644 index 00000000..f4b7a6f3 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIEncoding.kt @@ -0,0 +1,26 @@ +// +// Copyright (c) 2026 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 + +/** + * JNI-boundary holder for a Zenoh encoding. + * + * Marshaled across JNI as a single object; the native side reads [id] and + * [schema] via `env.get_field(...)`. Higher layers (e.g. `zenoh-java`'s + * `io.zenoh.bytes.Encoding`) construct this wrapper at the JNI call + * boundary. Keeping it here preserves the layering rule that + * `zenoh-jni-runtime` must not depend on `zenoh-java`. + */ +data class JNIEncoding(val id: Int, val schema: String?) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt index c13395c2..4afb60da 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt @@ -17,14 +17,37 @@ package io.zenoh.jni import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError -/** Adapter for native Zenoh key expressions. */ -public class JNIKeyExpr(internal val ptr: Long) { +/** + * Adapter for native Zenoh key expressions. + * + * Carries both the native-handle pointer and the source string so it can be + * passed across JNI as a single `JObject`. `ptr == 0L` means "not declared on + * a session — use [str] at the native side"; `ptr != 0L` means "declared — + * use the pointer and ignore [str]". + */ +public class JNIKeyExpr(internal val ptr: Long, internal val str: String) { companion object { init { ZenohLoad } + /** + * Build a JNI holder for an undeclared key expression (string-only). + * Used at JNI call boundaries where the caller may not have a + * declared [JNIKeyExpr] and needs to pass the raw string instead. + */ + fun undeclared(keyExpr: String): JNIKeyExpr = JNIKeyExpr(0L, keyExpr) + + /** + * Build a JNI holder from an optional declared expression and a + * source string. If [declared] is non-null its pointer is preserved + * (and the string is passed along for diagnostics / fallback); + * otherwise the result represents the undeclared [keyExpr]. + */ + fun of(declared: JNIKeyExpr?, keyExpr: String): JNIKeyExpr = + declared ?: JNIKeyExpr(0L, keyExpr) + @Throws(ZError::class) fun tryFrom(keyExpr: String): String = tryFromViaJNI(keyExpr) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt index 7f800597..f220d369 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt @@ -24,8 +24,8 @@ import io.zenoh.exceptions.ZError public class JNIPublisher(private val ptr: Long) { @Throws(ZError::class) - fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { - putViaJNI(ptr, payload, encodingId, encodingSchema, attachment) + fun put(payload: ByteArray, encoding: JNIEncoding, attachment: ByteArray?) { + putViaJNI(ptr, payload, encoding, attachment) } @Throws(ZError::class) @@ -39,7 +39,7 @@ public class JNIPublisher(private val ptr: Long) { @Throws(ZError::class) private external fun putViaJNI( - ptr: Long, valuePayload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray? + ptr: Long, valuePayload: ByteArray, encoding: JNIEncoding, attachment: ByteArray? ) @Throws(ZError::class) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt index 7757c086..408b4307 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -30,24 +30,21 @@ public class JNIQuerier(private val ptr: Long) { onClose: JNIOnCloseCallback, attachmentBytes: ByteArray?, payload: ByteArray?, - encodingId: Int, - encodingSchema: String?, + encoding: JNIEncoding?, ) { - getViaJNI(ptr, jniKeyExpr?.ptr ?: 0, keyExprString, parameters, callback, onClose, attachmentBytes, payload, encodingId, encodingSchema) + getViaJNI(ptr, JNIKeyExpr.of(jniKeyExpr, keyExprString), parameters, callback, onClose, attachmentBytes, payload, encoding) } @Throws(ZError::class) private external fun getViaJNI( querierPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, parameters: String?, callback: JNIGetCallback, onClose: JNIOnCloseCallback, attachmentBytes: ByteArray?, payload: ByteArray?, - encodingId: Int, - encodingSchema: String?, + encoding: JNIEncoding?, ) private external fun freePtrViaJNI(ptr: Long) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt index 78bd5dc3..372da155 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -28,19 +28,18 @@ public class JNIQuery(private val ptr: Long) { jniKeyExpr: JNIKeyExpr?, keyExprString: String, payload: ByteArray, - encodingId: Int, - encodingSchema: String?, + encoding: JNIEncoding, timestampEnabled: Boolean, timestampNtp64: Long, attachment: ByteArray?, qosExpress: Boolean, ) { - replySuccessViaJNI(ptr, jniKeyExpr?.ptr ?: 0, keyExprString, payload, encodingId, encodingSchema, timestampEnabled, timestampNtp64, attachment, qosExpress) + replySuccessViaJNI(ptr, JNIKeyExpr.of(jniKeyExpr, keyExprString), payload, encoding, timestampEnabled, timestampNtp64, attachment, qosExpress) } @Throws(ZError::class) - fun replyError(errorPayload: ByteArray, encodingId: Int, encodingSchema: String?) { - replyErrorViaJNI(ptr, errorPayload, encodingId, encodingSchema) + fun replyError(errorPayload: ByteArray, encoding: JNIEncoding) { + replyErrorViaJNI(ptr, errorPayload, encoding) } @Throws(ZError::class) @@ -52,7 +51,7 @@ public class JNIQuery(private val ptr: Long) { attachment: ByteArray?, qosExpress: Boolean, ) { - replyDeleteViaJNI(ptr, jniKeyExpr?.ptr ?: 0, keyExprString, timestampEnabled, timestampNtp64, attachment, qosExpress) + replyDeleteViaJNI(ptr, JNIKeyExpr.of(jniKeyExpr, keyExprString), timestampEnabled, timestampNtp64, attachment, qosExpress) } fun close() { @@ -62,11 +61,9 @@ public class JNIQuery(private val ptr: Long) { @Throws(ZError::class) private external fun replySuccessViaJNI( queryPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, valuePayload: ByteArray, - valueEncodingId: Int, - valueEncodingSchema: String?, + valueEncoding: JNIEncoding, timestampEnabled: Boolean, timestampNtp64: Long, attachment: ByteArray?, @@ -77,15 +74,13 @@ public class JNIQuery(private val ptr: Long) { private external fun replyErrorViaJNI( queryPtr: Long, errorValuePayload: ByteArray, - errorValueEncoding: Int, - encodingSchema: String?, + errorValueEncoding: JNIEncoding, ) @Throws(ZError::class) private external fun replyDeleteViaJNI( queryPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, timestampEnabled: Boolean, timestampNtp64: Long, attachment: ByteArray?, diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index e8776bc6..8ff133a7 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -55,13 +55,12 @@ public class JNISession(internal val sessionPtr: Long) { priority: Int, express: Boolean, reliability: Int - ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, congestionControl, priority, express, reliability)) + ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), congestionControl, priority, express, reliability)) @Throws(ZError::class) private external fun declarePublisherViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, congestionControl: Int, priority: Int, express: Boolean, @@ -74,13 +73,12 @@ public class JNISession(internal val sessionPtr: Long) { keyExprString: String, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, - ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, onClose)) + ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, onClose)) @Throws(ZError::class) private external fun declareSubscriberViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, ): Long @@ -92,13 +90,12 @@ public class JNISession(internal val sessionPtr: Long) { callback: JNIQueryableCallback, onClose: JNIOnCloseCallback, complete: Boolean - ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, onClose, complete)) + ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, onClose, complete)) @Throws(ZError::class) private external fun declareQueryableViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, callback: JNIQueryableCallback, onClose: JNIOnCloseCallback, complete: Boolean @@ -115,13 +112,12 @@ public class JNISession(internal val sessionPtr: Long) { express: Boolean, timeoutMs: Long, acceptReplies: Int - ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) + ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) @Throws(ZError::class) private external fun declareQuerierViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, target: Int, consolidation: Int, congestionControl: Int, @@ -132,7 +128,7 @@ public class JNISession(internal val sessionPtr: Long) { ): Long @Throws(ZError::class) - fun declareKeyExpr(keyExpr: String): JNIKeyExpr = JNIKeyExpr(declareKeyExprViaJNI(sessionPtr, keyExpr)) + fun declareKeyExpr(keyExpr: String): JNIKeyExpr = JNIKeyExpr(declareKeyExprViaJNI(sessionPtr, keyExpr), keyExpr) @Throws(ZError::class) private external fun declareKeyExprViaJNI(sessionPtr: Long, keyExpr: String): Long @@ -155,19 +151,17 @@ public class JNISession(internal val sessionPtr: Long) { consolidation: Int, attachmentBytes: ByteArray?, payload: ByteArray?, - encodingId: Int, - encodingSchema: String?, + encoding: JNIEncoding?, congestionControl: Int, priority: Int, express: Boolean, acceptReplies: Int, - ) = getViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, selectorParams, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encodingId, encodingSchema, congestionControl, priority, express, acceptReplies) + ) = getViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), selectorParams, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encoding, congestionControl, priority, express, acceptReplies) @Throws(ZError::class) private external fun getViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, selectorParams: String?, callback: JNIGetCallback, onClose: JNIOnCloseCallback, @@ -176,8 +170,7 @@ public class JNISession(internal val sessionPtr: Long) { consolidation: Int, attachmentBytes: ByteArray?, payload: ByteArray?, - encodingId: Int, - encodingSchema: String?, + encoding: JNIEncoding?, congestionControl: Int, priority: Int, express: Boolean, @@ -189,23 +182,20 @@ public class JNISession(internal val sessionPtr: Long) { jniKeyExpr: JNIKeyExpr?, keyExprString: String, valuePayload: ByteArray, - valueEncoding: Int, - valueEncodingSchema: String?, + valueEncoding: JNIEncoding, congestionControl: Int, priority: Int, express: Boolean, attachmentBytes: ByteArray?, reliability: Int - ) = putViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, valuePayload, valueEncoding, valueEncodingSchema, congestionControl, priority, express, attachmentBytes, reliability) + ) = putViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), valuePayload, valueEncoding, congestionControl, priority, express, attachmentBytes, reliability) @Throws(ZError::class) private external fun putViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, valuePayload: ByteArray, - valueEncoding: Int, - valueEncodingSchema: String?, + valueEncoding: JNIEncoding, congestionControl: Int, priority: Int, express: Boolean, @@ -222,13 +212,12 @@ public class JNISession(internal val sessionPtr: Long) { express: Boolean, attachmentBytes: ByteArray?, reliability: Int - ) = deleteViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, congestionControl, priority, express, attachmentBytes, reliability) + ) = deleteViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), congestionControl, priority, express, attachmentBytes, reliability) @Throws(ZError::class) private external fun deleteViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, congestionControl: Int, priority: Int, express: Boolean, @@ -266,8 +255,7 @@ public class JNISession(internal val sessionPtr: Long) { ): JNIAdvancedSubscriber = JNIAdvancedSubscriber( declareAdvancedSubscriberViaJNI( sessionPtr, - jniKeyExpr?.ptr ?: 0, - keyExprStr, + JNIKeyExpr.of(jniKeyExpr, keyExprStr), callback, onClose, history, @@ -279,8 +267,7 @@ public class JNISession(internal val sessionPtr: Long) { @Throws(ZError::class) private external fun declareAdvancedSubscriberViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprStr: String, + keyExpr: JNIKeyExpr, callback: JNISubscriberCallback, onClose: JNIOnCloseCallback, history: HistoryConfig?, @@ -302,8 +289,7 @@ public class JNISession(internal val sessionPtr: Long) { ): JNIAdvancedPublisher = JNIAdvancedPublisher( declareAdvancedPublisherViaJNI( sessionPtr, - jniKeyExpr?.ptr ?: 0, - keyExprStr, + JNIKeyExpr.of(jniKeyExpr, keyExprStr), congestionControl, priority, isExpress, @@ -317,8 +303,7 @@ public class JNISession(internal val sessionPtr: Long) { @Throws(ZError::class) private external fun declareAdvancedPublisherViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprStr: String, + keyExpr: JNIKeyExpr, congestionControl: Int, priority: Int, isExpress: Boolean, @@ -330,10 +315,10 @@ public class JNISession(internal val sessionPtr: Long) { @Throws(ZError::class) fun declareLivelinessToken(jniKeyExpr: JNIKeyExpr?, keyExprString: String): JNILivelinessToken = - JNILivelinessToken(declareLivelinessTokenViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString)) + JNILivelinessToken(declareLivelinessTokenViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString))) @Throws(ZError::class) - private external fun declareLivelinessTokenViaJNI(sessionPtr: Long, keyExprPtr: Long, keyExprString: String): Long + private external fun declareLivelinessTokenViaJNI(sessionPtr: Long, keyExpr: JNIKeyExpr): Long @Throws(ZError::class) fun declareLivelinessSubscriber( @@ -342,13 +327,12 @@ public class JNISession(internal val sessionPtr: Long) { callback: JNISubscriberCallback, history: Boolean, onClose: JNIOnCloseCallback, - ): JNISubscriber = JNISubscriber(declareLivelinessSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, history, onClose)) + ): JNISubscriber = JNISubscriber(declareLivelinessSubscriberViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, history, onClose)) @Throws(ZError::class) private external fun declareLivelinessSubscriberViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, callback: JNISubscriberCallback, history: Boolean, onClose: JNIOnCloseCallback, @@ -361,13 +345,12 @@ public class JNISession(internal val sessionPtr: Long) { callback: JNIGetCallback, timeoutMs: Long, onClose: JNIOnCloseCallback, - ) = livelinessGetViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, timeoutMs, onClose) + ) = livelinessGetViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, timeoutMs, onClose) @Throws(ZError::class) private external fun livelinessGetViaJNI( sessionPtr: Long, - keyExprPtr: Long, - keyExprString: String, + keyExpr: JNIKeyExpr, callback: JNIGetCallback, timeoutMs: Long, onClose: JNIOnCloseCallback, diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index ac5057b1..e46e0892 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -10,10 +10,9 @@ fn main() { .owned_object("crate::owned_object::OwnedObject") .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") - .key_expr_decoder("crate::key_expr::process_kotlin_key_expr") + .key_expr_decoder("crate::key_expr::decode_jni_key_expr") .string_decoder("crate::utils::decode_string") .byte_array_decoder("crate::utils::decode_byte_array") - .encoding_decoder("crate::utils::decode_encoding") .enum_decoder( "CongestionControl", "crate::utils::decode_congestion_control", @@ -35,6 +34,7 @@ fn main() { "Reply", "crate::sample_callback::process_kotlin_reply_callback", ) + .struct_decoder("Encoding", "crate::utils::decode_jni_encoding") .struct_decoder( "HistoryConfig", "crate::ext::advanced_subscriber::decode_history_config", diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index d2ed2e9d..572b6a12 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -17,8 +17,7 @@ use std::time::Duration; use jni::objects::JValue; use jni::{ - objects::{JByteArray, JClass, JString}, - sys::jint, + objects::{JByteArray, JClass}, JNIEnv, }; use zenoh::handlers::{Callback, DefaultHandler}; @@ -109,7 +108,7 @@ pub(crate) fn decode_miss_detection_config( } use crate::{ errors::ZResult, - utils::{decode_byte_array, decode_encoding}, + utils::{decode_byte_array, decode_jni_encoding}, }; use jni::sys::jboolean; use std::ptr::null; @@ -341,15 +340,14 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_putViaJNI( _class: JClass, publisher_ptr: *const AdvancedPublisher<'static>, payload: JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, attachment: /*nullable*/ JByteArray, ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { let payload = decode_byte_array(&env, payload)?; let mut publication = publisher.put(payload); - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let encoding = decode_jni_encoding(&mut env, &encoding)?; publication = publication.encoding(encoding); if !attachment.is_null() { let attachment = decode_byte_array(&env, attachment)?; diff --git a/zenoh-jni/src/key_expr.rs b/zenoh-jni/src/key_expr.rs index bb58f604..2b52c9ac 100644 --- a/zenoh-jni/src/key_expr.rs +++ b/zenoh-jni/src/key_expr.rs @@ -329,3 +329,24 @@ pub(crate) unsafe fn process_kotlin_key_expr( Ok((*key_expr).clone()) } } + +/// Decode a Kotlin `io.zenoh.jni.JNIKeyExpr` holder into a +/// [`KeyExpr<'static>`]. The holder carries `ptr: Long` and `str: String`; +/// `ptr != 0` means the KeyExpr was declared on a session and the pointer is +/// an `Arc::into_raw(Arc::new(KeyExpr))`; `ptr == 0` means to build the +/// expression from the string. +pub(crate) unsafe fn decode_jni_key_expr( + env: &mut JNIEnv, + obj: &jni::objects::JObject, +) -> ZResult> { + let ptr = env + .get_field(obj, "ptr", "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!("JNIKeyExpr.ptr: {}", err))?; + let str_obj = env + .get_field(obj, "str", "Ljava/lang/String;") + .and_then(|v| v.l()) + .map_err(|err| zerror!("JNIKeyExpr.str: {}", err))?; + let str_js: JString = str_obj.into(); + process_kotlin_key_expr(env, &str_js, ptr as *const KeyExpr<'static>) +} diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 32bc376d..4c53e0d1 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -15,19 +15,18 @@ use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ - objects::{JClass, JObject, JString}, + objects::{JClass, JObject}, sys::{jboolean, jlong}, JNIEnv, }; use zenoh::{ - internal::runtime::ZRuntime, key_expr::KeyExpr, liveliness::LivelinessToken, - pubsub::Subscriber, Session, Wait, + internal::runtime::ZRuntime, liveliness::LivelinessToken, pubsub::Subscriber, Session, Wait, }; use crate::{ errors::ZResult, - key_expr::process_kotlin_key_expr, + key_expr::decode_jni_key_expr, owned_object::OwnedObject, sample_callback::{on_reply_error, on_reply_success, process_kotlin_sample_callback}, throw_exception, @@ -40,15 +39,14 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_livelinessGetViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, callback: JObject, timeout_ms: jlong, on_close: JObject, ) { let session = unsafe { OwnedObject::from_raw(session_ptr) }; let _ = || -> ZResult<()> { - let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; + let key_expr = unsafe { decode_jni_key_expr(&mut env, &key_expr) }?; let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; @@ -103,12 +101,11 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessTokenViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, ) -> *const LivelinessToken { let session = unsafe { OwnedObject::from_raw(session_ptr) }; || -> ZResult<*const LivelinessToken> { - let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; + let key_expr = unsafe { decode_jni_key_expr(&mut env, &key_expr) }?; tracing::trace!("Declaring liveliness token on '{key_expr}'."); let token = session .liveliness() @@ -139,15 +136,14 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessSubscribe mut env: JNIEnv, _class: JClass, session_ptr: *const Session, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, callback: JObject, history: jboolean, on_close: JObject, ) -> *const Subscriber<()> { let session = OwnedObject::from_raw(session_ptr); || -> ZResult<*const Subscriber<()>> { - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; tracing::debug!("Declaring liveliness subscriber on '{}'...", key_expr); let subscriber = session diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index 3c30d18c..3717f1d0 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -15,8 +15,7 @@ use std::sync::Arc; use jni::{ - objects::{JByteArray, JClass, JString}, - sys::jint, + objects::{JByteArray, JClass, JObject}, JNIEnv, }; use zenoh::{pubsub::Publisher, Wait}; @@ -25,7 +24,7 @@ use crate::owned_object::OwnedObject; use crate::throw_exception; use crate::{ errors::ZResult, - utils::{decode_byte_array, decode_encoding}, + utils::{decode_byte_array, decode_jni_encoding}, }; /// Performs a PUT operation on a Zenoh publisher via JNI. @@ -52,15 +51,14 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( _class: JClass, publisher_ptr: *const Publisher<'static>, payload: JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, attachment: /*nullable*/ JByteArray, ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { let payload = decode_byte_array(&env, payload)?; let mut publication = publisher.put(payload); - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let encoding = decode_jni_encoding(&mut env, &encoding)?; publication = publication.encoding(encoding); if !attachment.is_null() { let attachment = decode_byte_array(&env, attachment)?; diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 52f26c31..82d3205b 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -16,19 +16,18 @@ use std::sync::Arc; use jni::{ objects::{JByteArray, JClass, JObject, JString}, - sys::jint, JNIEnv, }; -use zenoh::{key_expr::KeyExpr, query::Querier, Wait}; +use zenoh::{query::Querier, Wait}; use crate::{ errors::ZResult, - key_expr::process_kotlin_key_expr, + key_expr::decode_jni_key_expr, owned_object::OwnedObject, sample_callback::{on_reply_error, on_reply_success}, throw_exception, utils::{ - decode_byte_array, decode_encoding, decode_string, get_callback_global_ref, get_java_vm, + decode_byte_array, decode_jni_encoding, decode_string, get_callback_global_ref, get_java_vm, load_on_close, }, }; @@ -59,19 +58,17 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( mut env: JNIEnv, _class: JClass, querier_ptr: *const Querier, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, selector_params: /*nullable*/ JString, callback: JObject, on_close: JObject, attachment: /*nullable*/ JByteArray, payload: /*nullable*/ JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, ) { let querier = OwnedObject::from_raw(querier_ptr); let _ = || -> ZResult<()> { - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; let java_vm = Arc::new(get_java_vm(&mut env)?); let callback_global_ref = get_callback_global_ref(&mut env, callback)?; let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; @@ -102,8 +99,10 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( }; if !payload.is_null() { - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; - get_builder = get_builder.encoding(encoding); + if !encoding.is_null() { + let encoding = decode_jni_encoding(&mut env, &encoding)?; + get_builder = get_builder.encoding(encoding); + } get_builder = get_builder.payload(decode_byte_array(&env, payload)?); } diff --git a/zenoh-jni/src/query.rs b/zenoh-jni/src/query.rs index 25e9f538..3db348cf 100644 --- a/zenoh-jni/src/query.rs +++ b/zenoh-jni/src/query.rs @@ -14,16 +14,15 @@ use std::sync::Arc; -use crate::utils::{decode_byte_array, decode_encoding}; -use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception}; +use crate::utils::{decode_byte_array, decode_jni_encoding}; +use crate::{errors::ZResult, key_expr::decode_jni_key_expr, throw_exception}; use jni::{ - objects::{JByteArray, JClass, JString}, - sys::{jboolean, jint, jlong}, + objects::{JByteArray, JClass, JObject}, + sys::{jboolean, jlong}, JNIEnv, }; use uhlc::ID; use zenoh::{ - key_expr::KeyExpr, query::Query, time::{Timestamp, NTP64}, Wait, @@ -60,11 +59,9 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( mut env: JNIEnv, _class: JClass, query_ptr: *const Query, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, payload: JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, timestamp_enabled: jboolean, timestamp_ntp_64: jlong, attachment: /*nullable*/ JByteArray, @@ -72,10 +69,10 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( ) { let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; let payload = decode_byte_array(&env, payload)?; let mut reply_builder = query.reply(key_expr, payload); - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let encoding = decode_jni_encoding(&mut env, &encoding)?; reply_builder = reply_builder.encoding(encoding); if timestamp_enabled != 0 { let ts = Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand()); @@ -114,12 +111,11 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( _class: JClass, query_ptr: *const Query, payload: JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, ) { let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let encoding = decode_jni_encoding(&mut env, &encoding)?; query .reply_err(decode_byte_array(&env, payload)?) .encoding(encoding) @@ -157,8 +153,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( mut env: JNIEnv, _class: JClass, query_ptr: *const Query, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, timestamp_enabled: jboolean, timestamp_ntp_64: jlong, attachment: /*nullable*/ JByteArray, @@ -166,7 +161,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( ) { let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; let mut reply_builder = query.reply_del(key_expr); if timestamp_enabled != 0 { let ts = Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand()); diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index 7be1eded..e6453ac6 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -53,6 +53,21 @@ pub(crate) fn decode_encoding( Ok(Encoding::new(encoding_id, schema)) } +/// Decode a Kotlin `io.zenoh.jni.JNIEncoding` holder into a zenoh [`Encoding`]. +/// Fields: `id: Int`, `schema: String?`. +pub(crate) fn decode_jni_encoding(env: &mut JNIEnv, obj: &JObject) -> ZResult { + let id = env + .get_field(obj, "id", "I") + .and_then(|v| v.i()) + .map_err(|err| zerror!("JNIEncoding.id: {}", err))?; + let schema_obj = env + .get_field(obj, "schema", "Ljava/lang/String;") + .and_then(|v| v.l()) + .map_err(|err| zerror!("JNIEncoding.schema: {}", err))?; + let schema_js: JString = schema_obj.into(); + decode_encoding(env, id, &schema_js) +} + pub(crate) fn get_java_vm(env: &mut JNIEnv) -> ZResult { env.get_java_vm() .map_err(|err| zerror!("Unable to retrieve JVM reference: {}", err)) From 71adc305fa60a556847f43640b6fc2468e22c66e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 15:06:41 +0200 Subject: [PATCH 099/126] keyexpr in struct decoder --- zenoh-flat/src/jni_converter.rs | 20 +++++--------------- zenoh-jni/build.rs | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index c733708e..a9a766eb 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -18,7 +18,7 @@ //! .owned_object("crate::owned_object::OwnedObject") //! .zresult("crate::errors::ZResult") //! .throw_exception("crate::throw_exception") -//! .key_expr_decoder("crate::key_expr::process_kotlin_key_expr") +//! .struct_decoder("KeyExpr", "crate::key_expr::decode_jni_key_expr") //! .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") //! .build(); //! source @@ -47,7 +47,6 @@ pub struct Builder { owned_object: syn::Path, zresult: syn::Path, throw_exception: syn::Path, - key_expr_decoder: Option, string_decoder: Option, byte_array_decoder: Option, enum_decoders: HashMap, @@ -91,7 +90,6 @@ impl Default for Builder { owned_object: syn::parse_str("OwnedObject").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), - key_expr_decoder: None, string_decoder: None, byte_array_decoder: None, enum_decoders: HashMap::new(), @@ -144,14 +142,6 @@ impl Builder { self } - /// Path of the function that decodes a `KeyExpr` from a `(ptr, JString)` - /// pair, e.g. `"crate::key_expr::process_kotlin_key_expr"`. - pub fn key_expr_decoder(mut self, path: impl AsRef) -> Self { - self.key_expr_decoder = - Some(syn::parse_str(path.as_ref()).expect("invalid key_expr_decoder path")); - self - } - /// Path of the function that decodes a `JString` into `String`, e.g. /// `"crate::utils::decode_string"`. pub fn string_decoder(mut self, path: impl AsRef) -> Self { @@ -391,12 +381,12 @@ impl JniConverter { } else { // Non-consume path: single `JObject` holder // (io.zenoh.jni.JNIKeyExpr) decoded via the - // configured `key_expr_decoder`. + // `KeyExpr` entry in `struct_decoders`. let decoder = self .cfg - .key_expr_decoder - .as_ref() - .expect("key_expr_decoder not configured"); + .struct_decoders + .get("KeyExpr") + .expect("struct_decoder(\"KeyExpr\", ...) not configured"); jni_params.push(quote! { #name: jni::objects::JObject }); prelude.push(quote! { let #name = #decoder(&mut env, &#name)?; diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index e46e0892..eac4ed92 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -10,7 +10,6 @@ fn main() { .owned_object("crate::owned_object::OwnedObject") .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") - .key_expr_decoder("crate::key_expr::decode_jni_key_expr") .string_decoder("crate::utils::decode_string") .byte_array_decoder("crate::utils::decode_byte_array") .enum_decoder( @@ -34,6 +33,7 @@ fn main() { "Reply", "crate::sample_callback::process_kotlin_reply_callback", ) + .struct_decoder("KeyExpr", "crate::key_expr::decode_jni_key_expr") .struct_decoder("Encoding", "crate::utils::decode_jni_encoding") .struct_decoder( "HistoryConfig", From 2956be5beac32cd03871480e31841da0c275083f Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 20:17:18 +0200 Subject: [PATCH 100/126] generate kotlin prototypes --- .gitignore | 3 + zenoh-flat/Cargo.lock | 73 ++- zenoh-flat/src/jni_converter.rs | 504 +++++++++++++++++- zenoh-jni-runtime/build.gradle.kts | 4 +- .../kotlin/io/zenoh/jni/JNISession.kt | 145 +---- zenoh-jni/build.rs | 37 +- 6 files changed, 589 insertions(+), 177 deletions(-) diff --git a/.gitignore b/.gitignore index 53d109f6..9784c16c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ /build/ /.gradle/ */build/ + +# Generated Kotlin prototypes emitted by zenoh-jni's build.rs +/zenoh-jni/generated-kotlin/ diff --git a/zenoh-flat/Cargo.lock b/zenoh-flat/Cargo.lock index 339332b0..400db158 100644 --- a/zenoh-flat/Cargo.lock +++ b/zenoh-flat/Cargo.lock @@ -100,6 +100,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.11.1" @@ -863,6 +872,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + [[package]] name = "libc" version = "0.2.185" @@ -2412,7 +2427,7 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zenoh" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "ahash", "arc-swap", @@ -2462,7 +2477,7 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "zenoh-collections", ] @@ -2470,7 +2485,7 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "tracing", "uhlc", @@ -2481,7 +2496,7 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "ahash", ] @@ -2489,7 +2504,7 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "json5", "nonempty-collections", @@ -2514,7 +2529,7 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "lazy_static", "tokio", @@ -2525,7 +2540,7 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "aes", "hmac", @@ -2535,6 +2550,25 @@ dependencies = [ "zenoh-result", ] +[[package]] +name = "zenoh-ext" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "async-trait", + "bincode", + "flume", + "futures", + "leb128", + "serde", + "tokio", + "tracing", + "uhlc", + "zenoh", + "zenoh-macros", + "zenoh-util", +] + [[package]] name = "zenoh-flat" version = "1.9.0" @@ -2549,12 +2583,13 @@ dependencies = [ "syn 2.0.117", "tracing", "zenoh", + "zenoh-ext", ] [[package]] name = "zenoh-keyexpr" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -2569,7 +2604,7 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -2580,7 +2615,7 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "async-trait", "flume", @@ -2603,7 +2638,7 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "proc-macro2", "quote", @@ -2614,7 +2649,7 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "git-version", "libloading", @@ -2631,7 +2666,7 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "const_format", "rand 0.8.6", @@ -2646,7 +2681,7 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "anyhow", ] @@ -2654,7 +2689,7 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "lazy_static", "ron", @@ -2668,7 +2703,7 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "arc-swap", "event-listener", @@ -2682,7 +2717,7 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "futures", "tokio", @@ -2695,7 +2730,7 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "async-trait", "crossbeam-utils", @@ -2728,7 +2763,7 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#fe7a78ebcfcb0e2c57c5d00a89082a1a9c730869" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" dependencies = [ "async-trait", "const_format", diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index a9a766eb..80a2d72d 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -32,7 +32,8 @@ //! decoder helpers, `OwnedObject`) — those couplings are configurable through //! the [`Builder`] so the converter itself stays data-driven. -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; +use std::path::PathBuf; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; @@ -69,6 +70,39 @@ pub struct Builder { /// Return-type wrappers keyed by the element type name of `Vec` in /// `ZResult>`. return_wrappers_vec: HashMap, + /// Kotlin output config — if `None`, no Kotlin file is emitted. + kotlin: Option, +} + +/// Settings for generating a companion Kotlin file with `external fun` +/// prototypes. Enabled via [`Builder::kotlin_output`]. Per-type Kotlin names +/// are stored alongside each Rust decoder registration (see +/// [`Builder::struct_decoder`], [`Builder::callback_decoder`], and the return +/// wrappers), so one call registers both sides. +pub(crate) struct KotlinConfig { + output_path: PathBuf, + package: String, + class_name: String, + /// FQN of the `@Throws(::class)` exception — `None` disables the + /// annotation. + throws_class_fqn: Option, + /// FQN of a singleton referenced inside the generated `init { ... }` block + /// to force native-library loading — `None` disables the `init`. + init_load_fqn: Option, + /// FQN of the on-close callback type (typically + /// `io.zenoh.jni.callbacks.JNIOnCloseCallback`). Used for the synthetic + /// `OnClose` parameter injected for every callback arg. + on_close_callback_fqn: String, + /// Per-source-type Kotlin names (FQN or bare) for struct-decoded args. + struct_kotlin_types: HashMap, + /// Per-element-type Kotlin names for callback args (e.g. `Sample` → + /// `io.zenoh.jni.callbacks.JNISubscriberCallback`). + callback_kotlin_types: HashMap, + /// Per-return-type Kotlin names for `ZResult` with a return_wrapper. + return_kotlin_types: HashMap, + /// Per-element-type Kotlin names for `ZResult>` with a + /// return_wrapper_vec. + return_kotlin_types_vec: HashMap, } /// Describes how to render a `ZResult` return value into a JNI-compatible @@ -98,6 +132,7 @@ impl Default for Builder { consume_args: HashMap::new(), return_wrappers: HashMap::new(), return_wrappers_vec: HashMap::new(), + kotlin: None, } } } @@ -178,14 +213,24 @@ impl Builder { /// The decoder must have signature /// `fn(&mut JNIEnv, &JObject) -> ZResult`. Used both for the plain /// form (`HistoryConfig`) and the `Option` form (nullable JObject). + /// + /// `kotlin_type` is the Kotlin type name — FQN if out-of-package (import + /// is auto-derived) or bare for same-package / built-in. Only read when + /// Kotlin output is enabled via [`Builder::kotlin_output`]. pub fn struct_decoder( mut self, type_name: impl Into, decoder: impl AsRef, + kotlin_type: impl Into, ) -> Self { let path: syn::Path = syn::parse_str(decoder.as_ref()).expect("invalid struct_decoder path"); - self.struct_decoders.insert(type_name.into(), path); + let name = type_name.into(); + let kt = kotlin_type.into(); + self.struct_decoders.insert(name.clone(), path); + if let Some(k) = self.kotlin.as_mut() { + k.struct_kotlin_types.insert(name, kt); + } self } @@ -196,14 +241,23 @@ impl Builder { /// `fn(&mut JNIEnv, JObject, JObject) -> ZResult`. /// The generated JNI signature expands the single callback parameter into /// two JNI args: `: JObject, _on_close: JObject`. + /// + /// `kotlin_type` is the Kotlin callback type name — FQN if out-of-package, + /// bare otherwise. Only read when Kotlin output is enabled. pub fn callback_decoder( mut self, element_type_name: impl Into, decoder: impl AsRef, + kotlin_type: impl Into, ) -> Self { let path: syn::Path = syn::parse_str(decoder.as_ref()).expect("invalid callback_decoder path"); - self.callback_decoders.insert(element_type_name.into(), path); + let name = element_type_name.into(); + let kt = kotlin_type.into(); + self.callback_decoders.insert(name.clone(), path); + if let Some(k) = self.kotlin.as_mut() { + k.callback_kotlin_types.insert(name, kt); + } self } @@ -212,15 +266,24 @@ impl Builder { /// `extern "C"` return type. `wrap_fn` must have signature /// `fn(&mut JNIEnv, T) -> ZResult`. `default_expr` is the value /// returned on error (before the exception is thrown on the JVM side). + /// + /// `kotlin_type` is the Kotlin return type name (FQN or bare). Only read + /// when Kotlin output is enabled. pub fn return_wrapper( mut self, type_name: impl Into, jni_type: impl AsRef, wrap_fn: impl AsRef, default_expr: impl AsRef, + kotlin_type: impl Into, ) -> Self { + let name = type_name.into(); + let kt = kotlin_type.into(); self.return_wrappers - .insert(type_name.into(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); + .insert(name.clone(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); + if let Some(k) = self.kotlin.as_mut() { + k.return_kotlin_types.insert(name, kt); + } self } @@ -232,9 +295,15 @@ impl Builder { jni_type: impl AsRef, wrap_fn: impl AsRef, default_expr: impl AsRef, + kotlin_type: impl Into, ) -> Self { + let name = element_type_name.into(); + let kt = kotlin_type.into(); self.return_wrappers_vec - .insert(element_type_name.into(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); + .insert(name.clone(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); + if let Some(k) = self.kotlin.as_mut() { + k.return_kotlin_types_vec.insert(name, kt); + } self } @@ -259,10 +328,74 @@ impl Builder { self } + /// Enable Kotlin-side prototype generation. `path` is where the `.kt` + /// file will be written when [`JniConverter::write_kotlin`] is called. + /// Calling this method is what turns on Kotlin output; all other + /// `kotlin_*` methods are optional refinements. + pub fn kotlin_output(mut self, path: impl Into) -> Self { + self.kotlin.get_or_insert_with(KotlinConfig::default).output_path = path.into(); + self + } + + /// Kotlin `package` of the generated file (e.g. `"io.zenoh.jni"`). + pub fn kotlin_package(mut self, pkg: impl Into) -> Self { + self.kotlin.get_or_insert_with(KotlinConfig::default).package = pkg.into(); + self + } + + /// Name of the generated Kotlin `object` (e.g. `"JNISessionNative"`). Must + /// agree with the JNI class_prefix — i.e. `class_prefix` should end with + /// `"__"`. + pub fn kotlin_class(mut self, name: impl Into) -> Self { + self.kotlin.get_or_insert_with(KotlinConfig::default).class_name = name.into(); + self + } + + /// FQN of the exception type to annotate every `external fun` with via + /// `@Throws(::class)`. Unset ⇒ no annotation. + pub fn kotlin_throws(mut self, fqn: impl Into) -> Self { + self.kotlin.get_or_insert_with(KotlinConfig::default).throws_class_fqn = Some(fqn.into()); + self + } + + /// FQN of a singleton referenced from the generated `init { ... }` block + /// (typically `io.zenoh.ZenohLoad`) to force native-library loading. + /// Unset ⇒ no `init` block. + pub fn kotlin_init(mut self, fqn: impl Into) -> Self { + self.kotlin.get_or_insert_with(KotlinConfig::default).init_load_fqn = Some(fqn.into()); + self + } + + /// FQN of the Kotlin on-close callback type used for the synthetic + /// `OnClose` parameter injected for each callback argument. + pub fn kotlin_on_close(mut self, fqn: impl Into) -> Self { + self.kotlin.get_or_insert_with(KotlinConfig::default).on_close_callback_fqn = fqn.into(); + self + } + pub fn build(self) -> JniConverter { JniConverter { cfg: self, pending: VecDeque::new(), + kotlin_funs: Vec::new(), + kotlin_used_fqns: BTreeSet::new(), + } + } +} + +impl Default for KotlinConfig { + fn default() -> Self { + Self { + output_path: PathBuf::new(), + package: String::new(), + class_name: String::new(), + throws_class_fqn: None, + init_load_fqn: None, + on_close_callback_fqn: String::new(), + struct_kotlin_types: HashMap::new(), + callback_kotlin_types: HashMap::new(), + return_kotlin_types: HashMap::new(), + return_kotlin_types_vec: HashMap::new(), } } } @@ -278,6 +411,14 @@ impl Builder { pub struct JniConverter { cfg: Builder, pending: VecDeque<(syn::Item, SourceLocation)>, + /// Accumulated Kotlin `external fun ...` blocks, one per wrapped function. + /// Populated by `convert_fn` when Kotlin output is enabled, consumed by + /// [`JniConverter::write_kotlin`]. + kotlin_funs: Vec, + /// Set of Kotlin FQNs referenced by the emitted externals. Used to derive + /// the final `import` block (same-package and bare names are filtered + /// out). + kotlin_used_fqns: BTreeSet, } impl JniConverter { @@ -309,32 +450,116 @@ impl JniConverter { move |iter| self.call(iter) } - fn convert(&self, item: syn::Item, loc: &SourceLocation) -> syn::Item { + /// Borrowing closure suitable for `itertools::batching`. Unlike + /// [`JniConverter::into_closure`], this does not consume `self`, so the + /// converter survives the pipeline and [`JniConverter::write_kotlin`] can + /// be called after the pipeline completes. + pub fn as_closure<'a, I>( + &'a mut self, + ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a + where + I: Iterator, + { + move |iter| self.call(iter) + } + + /// Write the accumulated Kotlin `external fun ...` prototypes to the + /// configured output path. No-op when Kotlin output was not enabled via + /// [`Builder::kotlin_output`]. The output file is overwritten if it + /// already exists; parent directories are created as needed. + pub fn write_kotlin(&self) -> std::io::Result<()> { + let Some(kt) = self.cfg.kotlin.as_ref() else { + return Ok(()); + }; + if kt.output_path.as_os_str().is_empty() { + return Ok(()); + } + let contents = self.render_kotlin(kt); + if let Some(parent) = kt.output_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&kt.output_path, contents) + } + + fn render_kotlin(&self, kt: &KotlinConfig) -> String { + // Fold init-block FQN into the used set (deferred to emission time so + // it appears in imports only when the init block is emitted). + let mut used = self.kotlin_used_fqns.clone(); + if let Some(fqn) = kt.init_load_fqn.as_ref() { + if fqn.contains('.') { + used.insert(fqn.clone()); + } + } + + let mut imports: Vec = used + .into_iter() + .filter(|fqn| { + let pkg = fqn.rsplit_once('.').map(|(p, _)| p).unwrap_or(""); + !pkg.is_empty() && pkg != kt.package + }) + .collect(); + imports.sort(); + imports.dedup(); + + let mut out = String::new(); + out.push_str("// Auto-generated by JniConverter — do not edit by hand.\n"); + if !kt.package.is_empty() { + out.push_str(&format!("package {}\n\n", kt.package)); + } + for imp in &imports { + out.push_str(&format!("import {}\n", imp)); + } + if !imports.is_empty() { + out.push('\n'); + } + out.push_str(&format!("internal object {} {{\n", kt.class_name)); + if let Some(fqn) = kt.init_load_fqn.as_ref() { + let short = fqn.rsplit('.').next().unwrap_or(fqn); + out.push_str(&format!(" init {{ {} }}\n\n", short)); + } + for (i, block) in self.kotlin_funs.iter().enumerate() { + if i > 0 { + out.push('\n'); + } + out.push_str(block); + out.push('\n'); + } + out.push_str("}\n"); + out + } + + fn convert(&mut self, item: syn::Item, loc: &SourceLocation) -> syn::Item { match item { syn::Item::Fn(func) => syn::Item::Fn(self.convert_fn(func, loc)), other => other, } } - fn convert_fn(&self, func: syn::ItemFn, loc: &SourceLocation) -> syn::ItemFn { + fn convert_fn(&mut self, func: syn::ItemFn, loc: &SourceLocation) -> syn::ItemFn { let original_name = func.sig.ident.to_string(); let camel = snake_to_camel(&original_name); let jni_name = format_ident!("{}{}{}", self.cfg.class_prefix, camel, self.cfg.function_suffix); let orig_ident = &func.sig.ident; - let source_module = &self.cfg.source_module; - let owned_object = &self.cfg.owned_object; - let zresult = &self.cfg.zresult; - let throw_exception = &self.cfg.throw_exception; + let source_module = self.cfg.source_module.clone(); + let owned_object = self.cfg.owned_object.clone(); + let zresult = self.cfg.zresult.clone(); + let throw_exception = self.cfg.throw_exception.clone(); let empty_consume_set: HashSet = HashSet::new(); - let consume_set = self + let consume_set: HashSet = self .cfg .consume_args .get(&original_name) - .unwrap_or(&empty_consume_set); + .cloned() + .unwrap_or(empty_consume_set); let mut prelude: Vec = Vec::new(); let mut jni_params: Vec = Vec::new(); let mut call_args: Vec = Vec::new(); + // Kotlin param strings accumulated in parallel with `jni_params`. Only + // populated when Kotlin output is enabled. + let mut kotlin_params: Vec = Vec::new(); + let mut local_kotlin_fqns: BTreeSet = BTreeSet::new(); + let kt_cfg: Option<&KotlinConfig> = self.cfg.kotlin.as_ref(); for input in &func.sig.inputs { let syn::FnArg::Typed(pat_type) = input else { @@ -360,9 +585,16 @@ impl JniConverter { }); } call_args.push(quote! { &#name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: Long", + kotlin_param_name(&name.to_string(), /* ptr */ true) + )); + } } ArgKind::KeyExpr => { - if consume_set.contains(&name.to_string()) { + let consumed = consume_set.contains(&name.to_string()); + if consumed { // Consume path: the declared KeyExpr is required (no // string fallback). Arc::from_raw decrements the // refcount at end of scope, freeing the handle once @@ -393,6 +625,23 @@ impl JniConverter { }); call_args.push(quote! { #name }); } + if let Some(kt) = kt_cfg { + if consumed { + kotlin_params.push(format!( + "{}: Long", + kotlin_param_name(&name.to_string(), true) + )); + } else { + let fqn = kt.struct_kotlin_types.get("KeyExpr").cloned() + .expect("struct_decoder(\"KeyExpr\", ...) Kotlin type not configured"); + let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}", + kotlin_param_name(&name.to_string(), false), + short + )); + } + } } ArgKind::String => { let decoder = self @@ -405,6 +654,12 @@ impl JniConverter { let #name = #decoder(&mut env, &#name)?; }); call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: String", + kotlin_param_name(&name.to_string(), false) + )); + } } ArgKind::Enum(decoder) => { jni_params.push(quote! { #name: jni::sys::jint }); @@ -412,11 +667,23 @@ impl JniConverter { let #name = #decoder(#name)?; }); call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: Int", + kotlin_param_name(&name.to_string(), false) + )); + } } ArgKind::Bool => { jni_params.push(quote! { #name: jni::sys::jboolean }); prelude.push(quote! { let #name = #name != 0; }); call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: Boolean", + kotlin_param_name(&name.to_string(), false) + )); + } } ArgKind::Duration => { jni_params.push(quote! { #name: jni::sys::jlong }); @@ -424,6 +691,12 @@ impl JniConverter { let #name = std::time::Duration::from_millis(#name as u64); }); call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: Long", + kotlin_param_name(&name.to_string(), false) + )); + } } ArgKind::OptionVecU8 => { let decoder = self @@ -440,6 +713,12 @@ impl JniConverter { }; }); call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: ByteArray?", + kotlin_param_name(&name.to_string(), false) + )); + } } ArgKind::VecU8 => { let decoder = self @@ -452,8 +731,14 @@ impl JniConverter { let #name = #decoder(&env, #name)?; }); call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: ByteArray", + kotlin_param_name(&name.to_string(), false) + )); + } } - ArgKind::Callback(decoder) => { + ArgKind::Callback { decoder, element_type_name } => { let on_close_ident = format_ident!("{}_on_close", name); jni_params.push(quote! { #name: jni::objects::JObject }); jni_params.push(quote! { #on_close_ident: jni::objects::JObject }); @@ -461,6 +746,25 @@ impl JniConverter { let #name = #decoder(&mut env, #name, #on_close_ident)?; }); call_args.push(quote! { #name }); + if let Some(kt) = kt_cfg { + let cb_fqn = kt.callback_kotlin_types.get(&element_type_name).cloned() + .unwrap_or_else(|| panic!( + "callback_decoder({:?}, ...) Kotlin type not configured", + element_type_name + )); + let cb_short = kotlin_register_fqn(&cb_fqn, &mut local_kotlin_fqns); + let oc_short = kotlin_register_fqn(&kt.on_close_callback_fqn, &mut local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}", + kotlin_param_name(&name.to_string(), false), + cb_short + )); + kotlin_params.push(format!( + "{}OnClose: {}", + kotlin_param_name(&name.to_string(), false), + oc_short + )); + } } ArgKind::OptionString => { let decoder = self @@ -477,15 +781,34 @@ impl JniConverter { }; }); call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: String?", + kotlin_param_name(&name.to_string(), false) + )); + } } - ArgKind::StructFromJObject(decoder) => { + ArgKind::StructFromJObject { decoder, type_name } => { jni_params.push(quote! { #name: jni::objects::JObject }); prelude.push(quote! { let #name = #decoder(&mut env, &#name)?; }); call_args.push(quote! { #name }); + if let Some(kt) = kt_cfg { + let fqn = kt.struct_kotlin_types.get(&type_name).cloned() + .unwrap_or_else(|| panic!( + "struct_decoder({:?}, ...) Kotlin type not configured", + type_name + )); + let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}", + kotlin_param_name(&name.to_string(), false), + short + )); + } } - ArgKind::OptionStructFromJObject(decoder) => { + ArgKind::OptionStructFromJObject { decoder, type_name } => { jni_params.push(quote! { #name: jni::objects::JObject }); prelude.push(quote! { let #name = if !#name.is_null() { @@ -495,6 +818,19 @@ impl JniConverter { }; }); call_args.push(quote! { #name }); + if let Some(kt) = kt_cfg { + let fqn = kt.struct_kotlin_types.get(&type_name).cloned() + .unwrap_or_else(|| panic!( + "struct_decoder({:?}, ...) Kotlin type not configured", + type_name + )); + let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}?", + kotlin_param_name(&name.to_string(), false), + short + )); + } } ArgKind::Unsupported => panic!( "unsupported parameter type `{}` for `{}` at {loc}", @@ -504,6 +840,9 @@ impl JniConverter { } } + // Kotlin return type (None = Unit). Computed in parallel with the Rust + // return type so a single classification drives both outputs. + let mut kotlin_ret: Option = None; let (ret_ty_jni, wrap_ok, on_err, closure_ret): ( TokenStream, TokenStream, @@ -522,6 +861,13 @@ impl JniConverter { quote! { #zresult<()> }, ) } else if let Some(wrapper) = self.lookup_return_wrapper(&inner) { + if let Some(kt) = kt_cfg { + let fqn = self + .lookup_kotlin_return_type(&inner, kt) + .expect("return_wrapper(...) Kotlin type not configured"); + let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); + kotlin_ret = Some(short); + } let ReturnWrapper { jni_type, wrap_fn, @@ -534,6 +880,9 @@ impl JniConverter { quote! { #zresult<#jni_type> }, ) } else { + if kt_cfg.is_some() { + kotlin_ret = Some("Long".to_string()); + } ( quote! { *const #inner }, quote! { Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) }, @@ -574,9 +923,77 @@ impl JniConverter { ) -> #ret_ty_jni #body }; + // Assemble and stash the Kotlin `external fun ...` block so + // `write_kotlin()` can emit it later. + if self.cfg.kotlin.is_some() { + let kt_fn_name = format!("{}{}", camel, self.cfg.function_suffix); + let ret_suffix = match &kotlin_ret { + Some(r) => format!(": {}", r), + None => String::new(), + }; + let params_joined = if kotlin_params.is_empty() { + String::new() + } else { + format!("\n {},\n ", kotlin_params.join(",\n ")) + }; + let throws_line = if self + .cfg + .kotlin + .as_ref() + .and_then(|k| k.throws_class_fqn.as_ref()) + .is_some() + { + // Short name already registered via kotlin_register_fqn for + // the whole file (see write_kotlin); re-derive here. + let fqn = self + .cfg + .kotlin + .as_ref() + .unwrap() + .throws_class_fqn + .as_ref() + .unwrap(); + let short = fqn.rsplit('.').next().unwrap_or(fqn); + format!("@Throws({}::class)\n ", short) + } else { + String::new() + }; + let block = format!( + " @JvmStatic\n {}external fun {}({}){}", + throws_line, kt_fn_name, params_joined, ret_suffix + ); + self.kotlin_funs.push(block); + self.kotlin_used_fqns.extend(local_kotlin_fqns); + if let Some(kt) = self.cfg.kotlin.as_ref() { + if let Some(fqn) = kt.throws_class_fqn.as_ref() { + if fqn.contains('.') { + self.kotlin_used_fqns.insert(fqn.clone()); + } + } + } + } + syn::parse2(tokens).expect("generated JNI wrapper must parse") } + /// Resolve the Kotlin return-type FQN for a `ZResult` inner type `T`. + fn lookup_kotlin_return_type(&self, inner: &syn::Type, kt: &KotlinConfig) -> Option { + let syn::Type::Path(tp) = inner else { return None }; + let seg = tp.path.segments.last()?; + let name = seg.ident.to_string(); + if name == "Vec" { + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + let syn::GenericArgument::Type(elem) = args.args.first()? else { + return None; + }; + let elem_name = type_last_segment(elem)?; + return kt.return_kotlin_types_vec.get(&elem_name).cloned(); + } + kt.return_kotlin_types.get(&name).cloned() + } + fn lookup_return_wrapper(&self, ty: &syn::Type) -> Option { let syn::Type::Path(tp) = ty else { return None }; let seg = tp.path.segments.last()?; @@ -606,7 +1023,10 @@ impl JniConverter { syn::Type::ImplTrait(it) => { if let Some(elem) = extract_fn_single_arg_type_name(&it.bounds) { if let Some(decoder) = self.cfg.callback_decoders.get(&elem) { - return ArgKind::Callback(decoder.clone()); + return ArgKind::Callback { + decoder: decoder.clone(), + element_type_name: elem, + }; } } ArgKind::Unsupported @@ -637,7 +1057,10 @@ impl JniConverter { return ArgKind::OptionString; } if let Some(decoder) = self.cfg.struct_decoders.get(&inner) { - return ArgKind::OptionStructFromJObject(decoder.clone()); + return ArgKind::OptionStructFromJObject { + decoder: decoder.clone(), + type_name: inner, + }; } } } @@ -648,7 +1071,10 @@ impl JniConverter { return ArgKind::Enum(decoder.clone()); } if let Some(decoder) = self.cfg.struct_decoders.get(&name) { - return ArgKind::StructFromJObject(decoder.clone()); + return ArgKind::StructFromJObject { + decoder: decoder.clone(), + type_name: name, + }; } ArgKind::Unsupported } @@ -673,13 +1099,22 @@ enum ArgKind { OptionString, /// Struct type registered via `struct_decoder` → single `JObject` arg /// decoded via the registered decoder. - StructFromJObject(syn::Path), + StructFromJObject { + decoder: syn::Path, + type_name: String, + }, /// `Option` where `T` is registered via `struct_decoder` → nullable /// `JObject`, `None` when the JObject is null. - OptionStructFromJObject(syn::Path), + OptionStructFromJObject { + decoder: syn::Path, + type_name: String, + }, /// `impl Fn(T) + Send + Sync + 'static` → `(JObject callback, JObject on_close)` /// pair decoded via a callback decoder registered for `T`. - Callback(syn::Path), + Callback { + decoder: syn::Path, + element_type_name: String, + }, Unsupported, } @@ -802,3 +1237,26 @@ fn snake_to_camel(s: &str) -> String { } out } + +/// Map a Rust snake_case arg name to its Kotlin camelCase form, appending +/// `"Ptr"` for raw-pointer slots (OpaqueRef / consumed KeyExpr). +fn kotlin_param_name(rust_name: &str, is_pointer: bool) -> String { + let base = snake_to_camel(rust_name); + if is_pointer { + format!("{}Ptr", base) + } else { + base + } +} + +/// Record `fqn` in `used` if it looks fully-qualified (contains `.`) and +/// return the short name used at the emission site. Bare names are returned +/// as-is and not added to the import set. +fn kotlin_register_fqn(fqn: &str, used: &mut BTreeSet) -> String { + if fqn.contains('.') { + used.insert(fqn.to_string()); + fqn.rsplit('.').next().unwrap_or(fqn).to_string() + } else { + fqn.to_string() + } +} diff --git a/zenoh-jni-runtime/build.gradle.kts b/zenoh-jni-runtime/build.gradle.kts index 5e6d4def..716137b8 100644 --- a/zenoh-jni-runtime/build.gradle.kts +++ b/zenoh-jni-runtime/build.gradle.kts @@ -59,7 +59,9 @@ kotlin { @Suppress("Unused") sourceSets { - val commonMain by getting {} + val commonMain by getting { + kotlin.srcDir("$rootDir/zenoh-jni/generated-kotlin") + } // jvmAndAndroidMain is an intermediate source set between commonMain and both jvmMain/androidMain. // It holds code that uses java.lang.reflect.Type — available on JVM and Android (ART), // but absent on Kotlin/Native and Kotlin/JS targets. Placing such code here (rather than diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 8ff133a7..7baff37d 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -16,6 +16,22 @@ package io.zenoh.jni import io.zenoh.ZenohLoad import io.zenoh.exceptions.ZError +import io.zenoh.jni.JNISessionNative.closeSessionViaJNI +import io.zenoh.jni.JNISessionNative.declareAdvancedPublisherViaJNI +import io.zenoh.jni.JNISessionNative.declareAdvancedSubscriberViaJNI +import io.zenoh.jni.JNISessionNative.declareKeyExprViaJNI +import io.zenoh.jni.JNISessionNative.declarePublisherViaJNI +import io.zenoh.jni.JNISessionNative.declareQuerierViaJNI +import io.zenoh.jni.JNISessionNative.declareQueryableViaJNI +import io.zenoh.jni.JNISessionNative.declareSubscriberViaJNI +import io.zenoh.jni.JNISessionNative.deleteViaJNI +import io.zenoh.jni.JNISessionNative.getPeersZidViaJNI +import io.zenoh.jni.JNISessionNative.getRoutersZidViaJNI +import io.zenoh.jni.JNISessionNative.getViaJNI +import io.zenoh.jni.JNISessionNative.getZidViaJNI +import io.zenoh.jni.JNISessionNative.openSessionViaJNI +import io.zenoh.jni.JNISessionNative.putViaJNI +import io.zenoh.jni.JNISessionNative.undeclareKeyExprViaJNI import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNIOnCloseCallback import io.zenoh.jni.callbacks.JNIQueryableCallback @@ -38,15 +54,8 @@ public class JNISession(internal val sessionPtr: Long) { val sessionPtr = openSessionViaJNI(config.ptr) return JNISession(sessionPtr) } - - @JvmStatic - @Throws(ZError::class) - private external fun openSessionViaJNI(configPtr: Long): Long } - @Throws(ZError::class) - private external fun closeSessionViaJNI(ptr: Long) - @Throws(ZError::class) fun declarePublisher( jniKeyExpr: JNIKeyExpr?, @@ -57,16 +66,6 @@ public class JNISession(internal val sessionPtr: Long) { reliability: Int ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), congestionControl, priority, express, reliability)) - @Throws(ZError::class) - private external fun declarePublisherViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - congestionControl: Int, - priority: Int, - express: Boolean, - reliability: Int - ): Long - @Throws(ZError::class) fun declareSubscriber( jniKeyExpr: JNIKeyExpr?, @@ -75,14 +74,6 @@ public class JNISession(internal val sessionPtr: Long) { onClose: JNIOnCloseCallback, ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, onClose)) - @Throws(ZError::class) - private external fun declareSubscriberViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - callback: JNISubscriberCallback, - onClose: JNIOnCloseCallback, - ): Long - @Throws(ZError::class) fun declareQueryable( jniKeyExpr: JNIKeyExpr?, @@ -92,15 +83,6 @@ public class JNISession(internal val sessionPtr: Long) { complete: Boolean ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, onClose, complete)) - @Throws(ZError::class) - private external fun declareQueryableViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - callback: JNIQueryableCallback, - onClose: JNIOnCloseCallback, - complete: Boolean - ): Long - @Throws(ZError::class) fun declareQuerier( jniKeyExpr: JNIKeyExpr?, @@ -114,31 +96,12 @@ public class JNISession(internal val sessionPtr: Long) { acceptReplies: Int ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) - @Throws(ZError::class) - private external fun declareQuerierViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - target: Int, - consolidation: Int, - congestionControl: Int, - priority: Int, - express: Boolean, - timeoutMs: Long, - acceptReplies: Int - ): Long - @Throws(ZError::class) fun declareKeyExpr(keyExpr: String): JNIKeyExpr = JNIKeyExpr(declareKeyExprViaJNI(sessionPtr, keyExpr), keyExpr) - @Throws(ZError::class) - private external fun declareKeyExprViaJNI(sessionPtr: Long, keyExpr: String): Long - @Throws(ZError::class) fun undeclareKeyExpr(jniKeyExpr: JNIKeyExpr) = undeclareKeyExprViaJNI(sessionPtr, jniKeyExpr.ptr) - @Throws(ZError::class) - private external fun undeclareKeyExprViaJNI(sessionPtr: Long, keyExprPtr: Long) - @Throws(ZError::class) fun get( jniKeyExpr: JNIKeyExpr?, @@ -158,25 +121,6 @@ public class JNISession(internal val sessionPtr: Long) { acceptReplies: Int, ) = getViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), selectorParams, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encoding, congestionControl, priority, express, acceptReplies) - @Throws(ZError::class) - private external fun getViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - selectorParams: String?, - callback: JNIGetCallback, - onClose: JNIOnCloseCallback, - timeoutMs: Long, - target: Int, - consolidation: Int, - attachmentBytes: ByteArray?, - payload: ByteArray?, - encoding: JNIEncoding?, - congestionControl: Int, - priority: Int, - express: Boolean, - acceptReplies: Int, - ) - @Throws(ZError::class) fun put( jniKeyExpr: JNIKeyExpr?, @@ -190,19 +134,6 @@ public class JNISession(internal val sessionPtr: Long) { reliability: Int ) = putViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), valuePayload, valueEncoding, congestionControl, priority, express, attachmentBytes, reliability) - @Throws(ZError::class) - private external fun putViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - valuePayload: ByteArray, - valueEncoding: JNIEncoding, - congestionControl: Int, - priority: Int, - express: Boolean, - attachmentBytes: ByteArray?, - reliability: Int - ) - @Throws(ZError::class) fun delete( jniKeyExpr: JNIKeyExpr?, @@ -214,35 +145,15 @@ public class JNISession(internal val sessionPtr: Long) { reliability: Int ) = deleteViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), congestionControl, priority, express, attachmentBytes, reliability) - @Throws(ZError::class) - private external fun deleteViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - congestionControl: Int, - priority: Int, - express: Boolean, - attachmentBytes: ByteArray?, - reliability: Int - ) - @Throws(ZError::class) fun getZid(): ByteArray = getZidViaJNI(sessionPtr) - @Throws(ZError::class) - private external fun getZidViaJNI(ptr: Long): ByteArray - @Throws(ZError::class) fun getPeersZid(): List = getPeersZidViaJNI(sessionPtr) - @Throws(ZError::class) - private external fun getPeersZidViaJNI(ptr: Long): List - @Throws(ZError::class) fun getRoutersZid(): List = getRoutersZidViaJNI(sessionPtr) - @Throws(ZError::class) - private external fun getRoutersZidViaJNI(ptr: Long): List - @Throws(ZError::class) fun declareAdvancedSubscriber( jniKeyExpr: JNIKeyExpr?, @@ -264,17 +175,6 @@ public class JNISession(internal val sessionPtr: Long) { ) ) - @Throws(ZError::class) - private external fun declareAdvancedSubscriberViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - callback: JNISubscriberCallback, - onClose: JNIOnCloseCallback, - history: HistoryConfig?, - recovery: RecoveryConfig?, - subscriberDetection: Boolean, - ): Long - @Throws(ZError::class) fun declareAdvancedPublisher( jniKeyExpr: JNIKeyExpr?, @@ -300,19 +200,6 @@ public class JNISession(internal val sessionPtr: Long) { ) ) - @Throws(ZError::class) - private external fun declareAdvancedPublisherViaJNI( - sessionPtr: Long, - keyExpr: JNIKeyExpr, - congestionControl: Int, - priority: Int, - isExpress: Boolean, - reliability: Int, - cache: CacheConfig?, - sampleMissDetection: MissDetectionConfig?, - publisherDetection: Boolean, - ): Long - @Throws(ZError::class) fun declareLivelinessToken(jniKeyExpr: JNIKeyExpr?, keyExprString: String): JNILivelinessToken = JNILivelinessToken(declareLivelinessTokenViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString))) diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index eac4ed92..e9a4e5b6 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -3,8 +3,8 @@ use itertools::Itertools; fn main() { let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); - let converter = zenoh_flat::jni_converter::JniConverter::builder() - .class_prefix("Java_io_zenoh_jni_JNISession_") + let mut converter = zenoh_flat::jni_converter::JniConverter::builder() + .class_prefix("Java_io_zenoh_jni_JNISessionNative_") .function_suffix("ViaJNI") .source_module("zenoh_flat::session") .owned_object("crate::owned_object::OwnedObject") @@ -12,6 +12,12 @@ fn main() { .throw_exception("crate::throw_exception") .string_decoder("crate::utils::decode_string") .byte_array_decoder("crate::utils::decode_byte_array") + .kotlin_output("../zenoh-jni/generated-kotlin/io/zenoh/jni/JNISessionNative.kt") + .kotlin_package("io.zenoh.jni") + .kotlin_class("JNISessionNative") + .kotlin_throws("io.zenoh.exceptions.ZError") + .kotlin_init("io.zenoh.ZenohLoad") + .kotlin_on_close("io.zenoh.jni.callbacks.JNIOnCloseCallback") .enum_decoder( "CongestionControl", "crate::utils::decode_congestion_control", @@ -24,32 +30,47 @@ fn main() { .callback_decoder( "Sample", "crate::sample_callback::process_kotlin_sample_callback", + "io.zenoh.jni.callbacks.JNISubscriberCallback", ) .callback_decoder( "Query", "crate::sample_callback::process_kotlin_query_callback", + "io.zenoh.jni.callbacks.JNIQueryableCallback", ) .callback_decoder( "Reply", "crate::sample_callback::process_kotlin_reply_callback", + "io.zenoh.jni.callbacks.JNIGetCallback", + ) + .struct_decoder( + "KeyExpr", + "crate::key_expr::decode_jni_key_expr", + "JNIKeyExpr", + ) + .struct_decoder( + "Encoding", + "crate::utils::decode_jni_encoding", + "JNIEncoding", ) - .struct_decoder("KeyExpr", "crate::key_expr::decode_jni_key_expr") - .struct_decoder("Encoding", "crate::utils::decode_jni_encoding") .struct_decoder( "HistoryConfig", "crate::ext::advanced_subscriber::decode_history_config", + "io.zenoh.jni.ext.HistoryConfig", ) .struct_decoder( "RecoveryConfig", "crate::ext::advanced_subscriber::decode_recovery_config", + "io.zenoh.jni.ext.RecoveryConfig", ) .struct_decoder( "CacheConfig", "crate::ext::advanced_publisher::decode_cache_config", + "io.zenoh.jni.ext.CacheConfig", ) .struct_decoder( "MissDetectionConfig", "crate::ext::advanced_publisher::decode_miss_detection_config", + "io.zenoh.jni.ext.MissDetectionConfig", ) .consume_arg("close_session", "session") .consume_arg("undeclare_key_expr", "key_expr") @@ -58,18 +79,24 @@ fn main() { "jni::sys::jbyteArray", "crate::zenoh_id::zenoh_id_to_byte_array", "jni::objects::JByteArray::default().as_raw()", + "ByteArray", ) .return_wrapper_vec( "ZenohId", "jni::sys::jobject", "crate::zenoh_id::zenoh_ids_to_java_list", "jni::objects::JObject::default().as_raw()", + "List", ) .build(); source .items_all() - .batching(converter.into_closure()) + .batching(converter.as_closure()) .collect::() .write("zenoh_flat_jni.rs"); + + converter + .write_kotlin() + .expect("failed to write generated Kotlin file"); } From 3667af3f9c0d510a6c4073296324fc75be6b1ec4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 24 Apr 2026 22:09:00 +0200 Subject: [PATCH 101/126] auto generate structs --- zenoh-flat/src/ext.rs | 149 ++++++++++++ zenoh-flat/src/jni_converter.rs | 213 +++++++++++++++++- zenoh-flat/src/lib.rs | 2 + zenoh-flat/src/session.rs | 14 +- .../kotlin/io/zenoh/jni/JNISession.kt | 4 - .../kotlin/io/zenoh/jni/ext/CacheConfig.kt | 33 --- .../kotlin/io/zenoh/jni/ext/HistoryConfig.kt | 28 --- .../io/zenoh/jni/ext/MissDetectionConfig.kt | 30 --- .../kotlin/io/zenoh/jni/ext/RecoveryConfig.kt | 28 --- zenoh-jni/build.rs | 21 +- zenoh-jni/src/ext/advanced_publisher.rs | 82 +------ zenoh-jni/src/ext/advanced_subscriber.rs | 64 +----- 12 files changed, 369 insertions(+), 299 deletions(-) create mode 100644 zenoh-flat/src/ext.rs delete mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt delete mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt delete mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt delete mode 100644 zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt diff --git a/zenoh-flat/src/ext.rs b/zenoh-flat/src/ext.rs new file mode 100644 index 00000000..390ec464 --- /dev/null +++ b/zenoh-flat/src/ext.rs @@ -0,0 +1,149 @@ +// Copyright (c) 2026 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, +// + +//! Flat mirrors of the zenoh-ext advanced publisher/subscriber configs. +//! +//! Marked with `#[prebindgen]` so the JNI binding generator in +//! [`crate::jni_converter`] can emit a matching Kotlin `data class` and a +//! Rust JObject decoder for each one. The `TryFrom` impls encapsulate the +//! flat → semantic conversion (enum decoding happens already in the +//! auto-generated decoder, so these impls only deal with size/sign +//! adjustments and the "flag fields decide which builder method to call" +//! semantics). + +use std::time::Duration; + +use zenoh::qos::{CongestionControl, Priority}; + +use crate::errors::{ZError, ZResult}; +use crate::zerror; + +/// Flat cache configuration for an [`zenoh_ext::AdvancedPublisher`]. +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct CacheConfig { + pub max_samples: i64, + pub replies_priority: Priority, + pub replies_congestion_control: CongestionControl, + pub replies_is_express: bool, +} + +/// Flat history configuration for an [`zenoh_ext::AdvancedSubscriber`]. +/// +/// `max_samples <= 0` and `max_age_seconds <= 0.0` mean "unlimited". +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct HistoryConfig { + pub detect_late_publishers: bool, + pub max_samples: i64, + pub max_age_seconds: f64, +} + +/// Flat miss-detection configuration for an [`zenoh_ext::AdvancedPublisher`]. +/// +/// When `enable_heartbeat` is `true`, the heartbeat is configured with +/// `period_ms` — sporadic if `is_sporadic`, regular otherwise. When +/// `enable_heartbeat` is `false`, the other fields are ignored and +/// [`zenoh_ext::MissDetectionConfig::default`] is used as-is. +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct MissDetectionConfig { + pub enable_heartbeat: bool, + pub is_sporadic: bool, + pub period_ms: i64, +} + +/// Flat recovery configuration for an [`zenoh_ext::AdvancedSubscriber`]. +/// +/// When `is_heartbeat` is `true`, `period_ms` is ignored; otherwise +/// `period_ms` is the `periodic_queries` period in milliseconds. +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct RecoveryConfig { + pub is_heartbeat: bool, + pub period_ms: i64, +} + +impl TryFrom for zenoh_ext::CacheConfig { + type Error = ZError; + + fn try_from(c: CacheConfig) -> ZResult { + let max_samples: usize = c + .max_samples + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!("CacheConfig.max_samples: {}", e))?; + let replies = zenoh_ext::RepliesConfig::default() + .priority(c.replies_priority) + .congestion_control(c.replies_congestion_control) + .express(c.replies_is_express); + Ok(zenoh_ext::CacheConfig::default() + .max_samples(max_samples) + .replies_config(replies)) + } +} + +impl TryFrom for zenoh_ext::HistoryConfig { + type Error = ZError; + + fn try_from(c: HistoryConfig) -> ZResult { + let mut cfg = zenoh_ext::HistoryConfig::default(); + if c.detect_late_publishers { + cfg = cfg.detect_late_publishers(); + } + if c.max_samples > 0 { + let n: usize = c.max_samples.try_into().map_err( + |e: std::num::TryFromIntError| zerror!("HistoryConfig.max_samples: {}", e), + )?; + cfg = cfg.max_samples(n); + } + if c.max_age_seconds > 0.0 { + cfg = cfg.max_age(c.max_age_seconds); + } + Ok(cfg) + } +} + +impl TryFrom for zenoh_ext::MissDetectionConfig { + type Error = ZError; + + fn try_from(c: MissDetectionConfig) -> ZResult { + let mut cfg = zenoh_ext::MissDetectionConfig::default(); + if c.enable_heartbeat { + let period_ms: u64 = c.period_ms.try_into().map_err( + |e: std::num::TryFromIntError| zerror!("MissDetectionConfig.period_ms: {}", e), + )?; + let dur = Duration::from_millis(period_ms); + cfg = if c.is_sporadic { + cfg.sporadic_heartbeat(dur) + } else { + cfg.heartbeat(dur) + }; + } + Ok(cfg) + } +} + +impl TryFrom for zenoh_ext::RecoveryConfig { + type Error = ZError; + + fn try_from(c: RecoveryConfig) -> ZResult { + if c.is_heartbeat { + Ok(zenoh_ext::RecoveryConfig::default().heartbeat()) + } else { + let period_ms: u64 = c.period_ms.try_into().map_err( + |e: std::num::TryFromIntError| zerror!("RecoveryConfig.period_ms: {}", e), + )?; + Ok(zenoh_ext::RecoveryConfig::default().periodic_queries(Duration::from_millis(period_ms))) + } + } +} diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 80a2d72d..52d317bd 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -45,6 +45,10 @@ pub struct Builder { class_prefix: String, function_suffix: String, source_module: syn::Path, + /// Module path where `#[prebindgen]` struct types are declared. Used to + /// fully-qualify the struct name in the auto-generated decoder's return + /// type and constructor. Defaults to `source_module` when unset. + struct_source_module: Option, owned_object: syn::Path, zresult: syn::Path, throw_exception: syn::Path, @@ -121,6 +125,7 @@ impl Default for Builder { class_prefix: String::new(), function_suffix: String::new(), source_module: syn::parse_str("crate").unwrap(), + struct_source_module: None, owned_object: syn::parse_str("OwnedObject").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), @@ -158,6 +163,16 @@ impl Builder { self } + /// Fully-qualified path of the module that contains the `#[prebindgen]` + /// struct types (e.g. `"zenoh_flat::ext"`). When unset, defaults to + /// [`Builder::source_module`]. Used to qualify the struct type in + /// auto-generated decoders. + pub fn struct_source_module(mut self, path: impl AsRef) -> Self { + self.struct_source_module = + Some(syn::parse_str(path.as_ref()).expect("invalid struct_source_module path")); + self + } + /// Path of the `OwnedObject` helper used to borrow Arc-pointers. pub fn owned_object(mut self, path: impl AsRef) -> Self { self.owned_object = syn::parse_str(path.as_ref()).expect("invalid owned_object path"); @@ -377,7 +392,9 @@ impl Builder { JniConverter { cfg: self, pending: VecDeque::new(), + buffered: false, kotlin_funs: Vec::new(), + kotlin_data_classes: Vec::new(), kotlin_used_fqns: BTreeSet::new(), } } @@ -411,10 +428,19 @@ impl Default for KotlinConfig { pub struct JniConverter { cfg: Builder, pending: VecDeque<(syn::Item, SourceLocation)>, + /// `true` once the source iterator has been drained and sorted (structs + /// before functions) so that function-arg classification can see every + /// struct decoder the converter is going to auto-register. + buffered: bool, /// Accumulated Kotlin `external fun ...` blocks, one per wrapped function. /// Populated by `convert_fn` when Kotlin output is enabled, consumed by /// [`JniConverter::write_kotlin`]. kotlin_funs: Vec, + /// Accumulated Kotlin `data class ...` blocks, one per `#[prebindgen]` + /// struct seen in the source stream. Emitted by `write_kotlin` BEFORE the + /// `internal object { ... }` block so call sites in the same package can + /// see the types. + kotlin_data_classes: Vec, /// Set of Kotlin FQNs referenced by the emitted externals. Used to derive /// the final `import` block (same-package and bare names are filtered /// out). @@ -426,18 +452,26 @@ impl JniConverter { Builder::default() } - /// Pull one item from `iter`, convert it, and return it. Non-function - /// items are passed through unchanged. Returns `None` once `iter` is - /// exhausted and no buffered items remain. + /// Drain `iter` on the first call, sort so `#[prebindgen]` struct items + /// are processed before functions (so function-arg classification can see + /// every auto-registered struct decoder), then return converted items one + /// at a time from the buffer. Returns `None` once the buffer is drained. pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> where I: Iterator, { - if let Some(buf) = self.pending.pop_front() { - return Some(buf); + if !self.buffered { + self.buffered = true; + let mut all: Vec<(syn::Item, SourceLocation)> = iter.by_ref().collect(); + // Stable sort: structs first (`false` < `true`), original order + // preserved within each group. + all.sort_by_key(|(it, _)| !matches!(it, syn::Item::Struct(_))); + for (item, loc) in all { + let converted = self.convert(item, &loc); + self.pending.push_back((converted, loc)); + } } - let (item, loc) = iter.next()?; - Some((self.convert(item, &loc), loc)) + self.pending.pop_front() } /// Closure suitable for `itertools::batching`. @@ -512,6 +546,10 @@ impl JniConverter { if !imports.is_empty() { out.push('\n'); } + for block in &self.kotlin_data_classes { + out.push_str(block); + out.push_str("\n\n"); + } out.push_str(&format!("internal object {} {{\n", kt.class_name)); if let Some(fqn) = kt.init_load_fqn.as_ref() { let short = fqn.rsplit('.').next().unwrap_or(fqn); @@ -531,6 +569,7 @@ impl JniConverter { fn convert(&mut self, item: syn::Item, loc: &SourceLocation) -> syn::Item { match item { syn::Item::Fn(func) => syn::Item::Fn(self.convert_fn(func, loc)), + syn::Item::Struct(s) => self.convert_struct(s, loc), other => other, } } @@ -976,6 +1015,155 @@ impl JniConverter { syn::parse2(tokens).expect("generated JNI wrapper must parse") } + /// Emit a JNI decoder for a `#[prebindgen]` struct and a matching Kotlin + /// `data class`. The struct item itself is NOT re-emitted into the output + /// stream — only the decoder function is — so the original type stays + /// solely in its home module (e.g. `zenoh_flat::ext`) and is referenced + /// from generated code by its fully-qualified path. + fn convert_struct(&mut self, s: syn::ItemStruct, loc: &SourceLocation) -> syn::Item { + let struct_name = s.ident.to_string(); + let struct_ident = s.ident.clone(); + let decoder_ident = format_ident!("decode_{}", struct_ident); + let zresult = self.cfg.zresult.clone(); + let struct_module = self + .cfg + .struct_source_module + .clone() + .unwrap_or_else(|| self.cfg.source_module.clone()); + + let syn::Fields::Named(named) = &s.fields else { + panic!("tuple / unit structs are not supported at {loc}"); + }; + + let mut field_preludes: Vec = Vec::new(); + let mut field_init: Vec = Vec::new(); + let mut kotlin_field_lines: Vec = Vec::new(); + + for field in &named.named { + let fname_ident = field + .ident + .as_ref() + .unwrap_or_else(|| panic!("unnamed field in struct `{struct_name}` at {loc}")) + .clone(); + let fname = fname_ident.to_string(); + let kotlin_fname = snake_to_camel(&fname); + let err_prefix = format!("{struct_name}.{kotlin_fname}: {{}}"); + + let kind = self.classify_struct_field(&field.ty); + match kind { + StructFieldKind::Bool => { + field_preludes.push(quote! { + let #fname_ident = env.get_field(obj, #kotlin_fname, "Z") + .and_then(|v| v.z()) + .map_err(|err| zerror!(#err_prefix, err))?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines + .push(format!(" val {}: Boolean,", kotlin_fname)); + } + StructFieldKind::I64 => { + field_preludes.push(quote! { + let #fname_ident = env.get_field(obj, #kotlin_fname, "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!(#err_prefix, err))?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!(" val {}: Long,", kotlin_fname)); + } + StructFieldKind::F64 => { + field_preludes.push(quote! { + let #fname_ident = env.get_field(obj, #kotlin_fname, "D") + .and_then(|v| v.d()) + .map_err(|err| zerror!(#err_prefix, err))?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!(" val {}: Double,", kotlin_fname)); + } + StructFieldKind::Enum(decoder) => { + let raw_ident = format_ident!("__{}_raw", fname_ident); + field_preludes.push(quote! { + let #raw_ident = env.get_field(obj, #kotlin_fname, "I") + .and_then(|v| v.i()) + .map_err(|err| zerror!(#err_prefix, err))?; + let #fname_ident = #decoder(#raw_ident)?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!(" val {}: Int,", kotlin_fname)); + } + StructFieldKind::Unsupported => panic!( + "unsupported field type `{}` for `{}.{}` at {loc}", + field.ty.to_token_stream(), + struct_name, + fname + ), + } + } + + let tokens = quote! { + #[allow(non_snake_case, unused_mut, unused_variables)] + pub(crate) fn #decoder_ident( + env: &mut jni::JNIEnv, + obj: &jni::objects::JObject, + ) -> #zresult<#struct_module::#struct_ident> { + #(#field_preludes)* + Ok(#struct_module::#struct_ident { + #(#field_init),* + }) + } + }; + + // Auto-register the decoder for future function-arg classification. + // The decoder lives in the same module as the generated wrappers, so a + // bare `syn::Path` resolves correctly at the wrapper call sites. + let decoder_path: syn::Path = syn::parse_str(&format!("decode_{struct_name}")) + .expect("generated decoder ident must parse as path"); + self.cfg + .struct_decoders + .insert(struct_name.clone(), decoder_path); + if let Some(kt) = self.cfg.kotlin.as_mut() { + // Same package as the generated Kotlin file → bare name, no FQN. + kt.struct_kotlin_types + .insert(struct_name.clone(), struct_name.clone()); + } + + // Accumulate the Kotlin data class for emission by write_kotlin. + if self.cfg.kotlin.is_some() { + let block = format!( + "data class {}(\n{}\n)", + struct_name, + kotlin_field_lines.join("\n") + ); + self.kotlin_data_classes.push(block); + } + + syn::parse2(tokens).expect("generated struct decoder must parse") + } + + /// Classify a `#[prebindgen]` struct field's type for JNI round-tripping. + /// Scope is deliberately narrow (only what the current four configs need) + /// — an unsupported type panics so we notice new cases at codegen time. + fn classify_struct_field(&self, ty: &syn::Type) -> StructFieldKind { + let syn::Type::Path(tp) = ty else { + return StructFieldKind::Unsupported; + }; + let Some(last) = tp.path.segments.last() else { + return StructFieldKind::Unsupported; + }; + let name = last.ident.to_string(); + match name.as_str() { + "bool" => StructFieldKind::Bool, + "i64" => StructFieldKind::I64, + "f64" => StructFieldKind::F64, + _ => { + if let Some(decoder) = self.cfg.enum_decoders.get(&name) { + StructFieldKind::Enum(decoder.clone()) + } else { + StructFieldKind::Unsupported + } + } + } + } + /// Resolve the Kotlin return-type FQN for a `ZResult` inner type `T`. fn lookup_kotlin_return_type(&self, inner: &syn::Type, kt: &KotlinConfig) -> Option { let syn::Type::Path(tp) = inner else { return None }; @@ -1083,6 +1271,17 @@ impl JniConverter { } } +/// Field-type classification for `#[prebindgen]` struct fields — narrower +/// than [`ArgKind`] because structs only need a round-trippable primitive / +/// enum representation (no refs, no callbacks, no `Option<...>`). +enum StructFieldKind { + Bool, + I64, + F64, + Enum(syn::Path), + Unsupported, +} + enum ArgKind { OpaqueRef(syn::Type), KeyExpr, diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index 0b6a6f60..d11f274d 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -5,6 +5,8 @@ pub const FEATURES: &str = prebindgen_proc_macro::features!(); pub mod config; pub mod errors; +#[cfg(feature = "zenoh-ext")] +pub mod ext; pub mod jni_converter; pub mod session; diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index 2b5fdf94..dbc8d023 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -27,10 +27,12 @@ use zenoh::{ Wait, }; +#[cfg(feature = "zenoh-ext")] +use crate::ext::{CacheConfig, HistoryConfig, MissDetectionConfig, RecoveryConfig}; #[cfg(feature = "zenoh-ext")] use zenoh_ext::{ - AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, AdvancedSubscriberBuilderExt, - CacheConfig, HistoryConfig, MissDetectionConfig, RecoveryConfig, + AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, + AdvancedSubscriberBuilderExt, }; /// Open a Zenoh session using a borrowed configuration. @@ -394,10 +396,10 @@ pub fn declare_advanced_subscriber( .callback(callback) .advanced(); if let Some(history) = history { - builder = builder.history(history); + builder = builder.history(history.try_into()?); } if let Some(recovery) = recovery { - builder = builder.recovery(recovery); + builder = builder.recovery(recovery.try_into()?); } if subscriber_detection { builder = builder.subscriber_detection(); @@ -445,10 +447,10 @@ pub fn declare_advanced_publisher( .reliability(reliability) .advanced(); if let Some(cache) = cache { - builder = builder.cache(cache); + builder = builder.cache(cache.try_into()?); } if let Some(miss_detection) = sample_miss_detection { - builder = builder.sample_miss_detection(miss_detection); + builder = builder.sample_miss_detection(miss_detection.try_into()?); } if publisher_detection { builder = builder.publisher_detection(); diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 7baff37d..5a163885 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -36,10 +36,6 @@ import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNIOnCloseCallback import io.zenoh.jni.callbacks.JNIQueryableCallback import io.zenoh.jni.callbacks.JNISubscriberCallback -import io.zenoh.jni.ext.CacheConfig -import io.zenoh.jni.ext.HistoryConfig -import io.zenoh.jni.ext.MissDetectionConfig -import io.zenoh.jni.ext.RecoveryConfig /** Adapter class to handle communication with the Zenoh JNI code for a Session. */ public class JNISession(internal val sessionPtr: Long) { diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt deleted file mode 100644 index a76735a0..00000000 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/CacheConfig.kt +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright (c) 2026 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.ext - -/** - * Cache configuration for an advanced publisher. - * - * Marshaled across JNI as a plain object; the native side reads fields by - * name via `env.get_field(...)`. The nested `RepliesConfig` is represented - * inline (flat fields `repliesPriority`, `repliesCongestionControl`, - * `repliesIsExpress`) to keep the JNI field-lookup simple. - * - * `repliesPriority` and `repliesCongestionControl` are the ordinals of the - * corresponding zenoh enums. - */ -data class CacheConfig( - val maxSamples: Long, - val repliesPriority: Int, - val repliesCongestionControl: Int, - val repliesIsExpress: Boolean, -) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt deleted file mode 100644 index 38f3a008..00000000 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/HistoryConfig.kt +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) 2026 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.ext - -/** - * History configuration for an advanced subscriber. - * - * Marshaled across JNI as a plain object; the native side reads fields by - * name via `env.get_field(...)`. `maxSamples <= 0` and `maxAgeSeconds <= 0.0` - * mean unlimited. - */ -data class HistoryConfig( - val detectLatePublishers: Boolean, - val maxSamples: Long, - val maxAgeSeconds: Double, -) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt deleted file mode 100644 index 2af1479b..00000000 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/MissDetectionConfig.kt +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) 2026 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.ext - -/** - * Miss detection configuration for an advanced publisher. - * - * Marshaled across JNI as a plain object; the native side reads fields by - * name via `env.get_field(...)`. When [enableHeartbeat] is `true`, the - * heartbeat is configured with [periodMs] — sporadic if [isSporadic], regular - * otherwise. When [enableHeartbeat] is `false`, the other fields are ignored - * and `MissDetectionConfig::default()` is used. - */ -data class MissDetectionConfig( - val enableHeartbeat: Boolean, - val isSporadic: Boolean, - val periodMs: Long, -) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt deleted file mode 100644 index f082d929..00000000 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/ext/RecoveryConfig.kt +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) 2026 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.ext - -/** - * Recovery configuration for an advanced subscriber. - * - * Marshaled across JNI as a plain object; the native side reads fields by - * name via `env.get_field(...)`. When [isHeartbeat] is `true`, [periodMs] is - * ignored; otherwise [periodMs] is the `periodic_queries` period in - * milliseconds. - */ -data class RecoveryConfig( - val isHeartbeat: Boolean, - val periodMs: Long, -) diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index e9a4e5b6..6a619b86 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -7,6 +7,7 @@ fn main() { .class_prefix("Java_io_zenoh_jni_JNISessionNative_") .function_suffix("ViaJNI") .source_module("zenoh_flat::session") + .struct_source_module("zenoh_flat::ext") .owned_object("crate::owned_object::OwnedObject") .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") @@ -52,26 +53,6 @@ fn main() { "crate::utils::decode_jni_encoding", "JNIEncoding", ) - .struct_decoder( - "HistoryConfig", - "crate::ext::advanced_subscriber::decode_history_config", - "io.zenoh.jni.ext.HistoryConfig", - ) - .struct_decoder( - "RecoveryConfig", - "crate::ext::advanced_subscriber::decode_recovery_config", - "io.zenoh.jni.ext.RecoveryConfig", - ) - .struct_decoder( - "CacheConfig", - "crate::ext::advanced_publisher::decode_cache_config", - "io.zenoh.jni.ext.CacheConfig", - ) - .struct_decoder( - "MissDetectionConfig", - "crate::ext::advanced_publisher::decode_miss_detection_config", - "io.zenoh.jni.ext.MissDetectionConfig", - ) .consume_arg("close_session", "session") .consume_arg("undeclare_key_expr", "key_expr") .return_wrapper( diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index 572b6a12..c49dc546 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -13,7 +13,6 @@ // use std::sync::Arc; -use std::time::Duration; use jni::objects::JValue; use jni::{ @@ -22,90 +21,13 @@ use jni::{ }; use zenoh::handlers::{Callback, DefaultHandler}; use zenoh::Wait; -use zenoh_ext::{AdvancedPublisher, CacheConfig, MissDetectionConfig, RepliesConfig}; +use zenoh_ext::AdvancedPublisher; use crate::owned_object::OwnedObject; -use crate::utils::{ - decode_congestion_control, decode_priority, get_callback_global_ref, get_java_vm, load_on_close, -}; +use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; use crate::throw_exception; -/// Decode a Kotlin `io.zenoh.jni.ext.CacheConfig` data-class instance into a -/// zenoh-ext [`CacheConfig`] (with nested [`RepliesConfig`] built from the -/// flat `repliesPriority` / `repliesCongestionControl` / `repliesIsExpress` -/// fields). -pub(crate) fn decode_cache_config(env: &mut JNIEnv, obj: &JObject) -> ZResult { - let max_samples = env - .get_field(obj, "maxSamples", "J") - .and_then(|v| v.j()) - .map_err(|err| zerror!("CacheConfig.maxSamples: {}", err))?; - let replies_priority = env - .get_field(obj, "repliesPriority", "I") - .and_then(|v| v.i()) - .map_err(|err| zerror!("CacheConfig.repliesPriority: {}", err))?; - let replies_cc = env - .get_field(obj, "repliesCongestionControl", "I") - .and_then(|v| v.i()) - .map_err(|err| zerror!("CacheConfig.repliesCongestionControl: {}", err))?; - let replies_express = env - .get_field(obj, "repliesIsExpress", "Z") - .and_then(|v| v.z()) - .map_err(|err| zerror!("CacheConfig.repliesIsExpress: {}", err))?; - - let replies = RepliesConfig::default() - .priority(decode_priority(replies_priority)?) - .congestion_control(decode_congestion_control(replies_cc)?) - .express(replies_express); - let cfg = CacheConfig::default() - .max_samples( - max_samples - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, - ) - .replies_config(replies); - Ok(cfg) -} - -/// Decode a Kotlin `io.zenoh.jni.ext.MissDetectionConfig` data-class instance -/// into a zenoh-ext [`MissDetectionConfig`]. Field semantics: -/// - `enableHeartbeat`: whether to call `.heartbeat(...)` or `.sporadic_heartbeat(...)` -/// on the config (otherwise `MissDetectionConfig::default()` is left as-is). -/// - `isSporadic`: picks between `sporadic_heartbeat` and `heartbeat`. -/// - `periodMs`: heartbeat period in milliseconds (used only when -/// `enableHeartbeat` is `true`). -pub(crate) fn decode_miss_detection_config( - env: &mut JNIEnv, - obj: &JObject, -) -> ZResult { - let enable_heartbeat = env - .get_field(obj, "enableHeartbeat", "Z") - .and_then(|v| v.z()) - .map_err(|err| zerror!("MissDetectionConfig.enableHeartbeat: {}", err))?; - let is_sporadic = env - .get_field(obj, "isSporadic", "Z") - .and_then(|v| v.z()) - .map_err(|err| zerror!("MissDetectionConfig.isSporadic: {}", err))?; - let period_ms = env - .get_field(obj, "periodMs", "J") - .and_then(|v| v.j()) - .map_err(|err| zerror!("MissDetectionConfig.periodMs: {}", err))?; - - let mut cfg = MissDetectionConfig::default(); - if enable_heartbeat { - let dur = Duration::from_millis( - period_ms - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, - ); - cfg = if is_sporadic { - cfg.sporadic_heartbeat(dur) - } else { - cfg.heartbeat(dur) - }; - } - Ok(cfg) -} use crate::{ errors::ZResult, utils::{decode_byte_array, decode_jni_encoding}, diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs index deec67e9..513944fb 100644 --- a/zenoh-jni/src/ext/advanced_subscriber.rs +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -13,75 +13,13 @@ // use std::sync::Arc; -use std::time::Duration; use jni::sys::jboolean; use jni::{objects::JClass, JNIEnv}; use zenoh::handlers::{Callback, DefaultHandler}; use zenoh::pubsub::Subscriber; use zenoh_ext::SampleMissListener; -use zenoh_ext::{ - AdvancedSubscriber, HistoryConfig, Miss, RecoveryConfig, SampleMissListenerBuilder, -}; - -/// Decode a Kotlin `io.zenoh.jni.ext.HistoryConfig` data-class instance into -/// a zenoh-ext [`HistoryConfig`]. Matches field names `detectLatePublishers`, -/// `maxSamples`, `maxAgeSeconds`. -pub(crate) fn decode_history_config(env: &mut JNIEnv, obj: &JObject) -> ZResult { - let detect_late_publishers = env - .get_field(obj, "detectLatePublishers", "Z") - .and_then(|v| v.z()) - .map_err(|err| zerror!("HistoryConfig.detectLatePublishers: {}", err))?; - let max_samples = env - .get_field(obj, "maxSamples", "J") - .and_then(|v| v.j()) - .map_err(|err| zerror!("HistoryConfig.maxSamples: {}", err))?; - let max_age_seconds = env - .get_field(obj, "maxAgeSeconds", "D") - .and_then(|v| v.d()) - .map_err(|err| zerror!("HistoryConfig.maxAgeSeconds: {}", err))?; - - let mut cfg = HistoryConfig::default(); - if detect_late_publishers { - cfg = cfg.detect_late_publishers(); - } - if max_samples > 0 { - let n: usize = max_samples - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?; - cfg = cfg.max_samples(n); - } - if max_age_seconds > 0.0 { - cfg = cfg.max_age(max_age_seconds); - } - Ok(cfg) -} - -/// Decode a Kotlin `io.zenoh.jni.ext.RecoveryConfig` data-class instance into -/// a zenoh-ext [`RecoveryConfig`]. Matches field names `isHeartbeat`, -/// `periodMs`. When `isHeartbeat` is `false`, `periodMs` is the -/// `periodic_queries` period in milliseconds. -pub(crate) fn decode_recovery_config(env: &mut JNIEnv, obj: &JObject) -> ZResult { - let is_heartbeat = env - .get_field(obj, "isHeartbeat", "Z") - .and_then(|v| v.z()) - .map_err(|err| zerror!("RecoveryConfig.isHeartbeat: {}", err))?; - let period_ms = env - .get_field(obj, "periodMs", "J") - .and_then(|v| v.j()) - .map_err(|err| zerror!("RecoveryConfig.periodMs: {}", err))?; - - if is_heartbeat { - Ok(RecoveryConfig::default().heartbeat()) - } else { - let dur = Duration::from_millis( - period_ms - .try_into() - .map_err(|e: std::num::TryFromIntError| zerror!(e.to_string()))?, - ); - Ok(RecoveryConfig::default().periodic_queries(dur)) - } -} +use zenoh_ext::{AdvancedSubscriber, Miss, SampleMissListenerBuilder}; use crate::sample_callback::process_kotlin_sample_callback; use jni::objects::JObject; From 65458324e680e844d1b96b7b18ef71469af801c4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 11:43:03 +0200 Subject: [PATCH 102/126] on_drop as parameter --- zenoh-flat/src/jni_converter.rs | 43 ++++++++-------------- zenoh-flat/src/session.rs | 45 +++++++++++++++++++++--- zenoh-jni/build.rs | 6 +++- zenoh-jni/src/ext/advanced_subscriber.rs | 14 ++++---- zenoh-jni/src/liveliness.rs | 8 ++--- zenoh-jni/src/sample_callback.rs | 39 +++++++++++++------- zenoh-jni/src/utils.rs | 23 ++++++++++++ 7 files changed, 121 insertions(+), 57 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 52d317bd..d9a0683b 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -93,10 +93,6 @@ pub(crate) struct KotlinConfig { /// FQN of a singleton referenced inside the generated `init { ... }` block /// to force native-library loading — `None` disables the `init`. init_load_fqn: Option, - /// FQN of the on-close callback type (typically - /// `io.zenoh.jni.callbacks.JNIOnCloseCallback`). Used for the synthetic - /// `OnClose` parameter injected for every callback arg. - on_close_callback_fqn: String, /// Per-source-type Kotlin names (FQN or bare) for struct-decoded args. struct_kotlin_types: HashMap, /// Per-element-type Kotlin names for callback args (e.g. `Sample` → @@ -381,13 +377,6 @@ impl Builder { self } - /// FQN of the Kotlin on-close callback type used for the synthetic - /// `OnClose` parameter injected for each callback argument. - pub fn kotlin_on_close(mut self, fqn: impl Into) -> Self { - self.kotlin.get_or_insert_with(KotlinConfig::default).on_close_callback_fqn = fqn.into(); - self - } - pub fn build(self) -> JniConverter { JniConverter { cfg: self, @@ -408,7 +397,6 @@ impl Default for KotlinConfig { class_name: String::new(), throws_class_fqn: None, init_load_fqn: None, - on_close_callback_fqn: String::new(), struct_kotlin_types: HashMap::new(), callback_kotlin_types: HashMap::new(), return_kotlin_types: HashMap::new(), @@ -778,11 +766,9 @@ impl JniConverter { } } ArgKind::Callback { decoder, element_type_name } => { - let on_close_ident = format_ident!("{}_on_close", name); jni_params.push(quote! { #name: jni::objects::JObject }); - jni_params.push(quote! { #on_close_ident: jni::objects::JObject }); prelude.push(quote! { - let #name = #decoder(&mut env, #name, #on_close_ident)?; + let #name = #decoder(&mut env, #name)?; }); call_args.push(quote! { #name }); if let Some(kt) = kt_cfg { @@ -792,17 +778,11 @@ impl JniConverter { element_type_name )); let cb_short = kotlin_register_fqn(&cb_fqn, &mut local_kotlin_fqns); - let oc_short = kotlin_register_fqn(&kt.on_close_callback_fqn, &mut local_kotlin_fqns); kotlin_params.push(format!( "{}: {}", kotlin_param_name(&name.to_string(), false), cb_short )); - kotlin_params.push(format!( - "{}OnClose: {}", - kotlin_param_name(&name.to_string(), false), - oc_short - )); } } ArgKind::OptionString => { @@ -1209,7 +1189,7 @@ impl JniConverter { } } syn::Type::ImplTrait(it) => { - if let Some(elem) = extract_fn_single_arg_type_name(&it.bounds) { + if let Some(elem) = extract_fn_arg_type_name(&it.bounds) { if let Some(decoder) = self.cfg.callback_decoders.get(&elem) { return ArgKind::Callback { decoder: decoder.clone(), @@ -1308,8 +1288,9 @@ enum ArgKind { decoder: syn::Path, type_name: String, }, - /// `impl Fn(T) + Send + Sync + 'static` → `(JObject callback, JObject on_close)` - /// pair decoded via a callback decoder registered for `T`. + /// `impl Fn(T) + Send + Sync + 'static` → single `JObject` decoded via the + /// callback decoder registered for `T`. Zero-arg `impl Fn() + Send + Sync` + /// callbacks are looked up under the key `"()"`. Callback { decoder: syn::Path, element_type_name: String, @@ -1335,9 +1316,11 @@ fn type_last_segment(ty: &syn::Type) -> Option { tp.path.segments.last().map(|s| s.ident.to_string()) } -/// Look through the trait bounds of an `impl Fn(T) + ...` for a `Fn`-family -/// trait and return the last-segment name of its single argument type `T`. -fn extract_fn_single_arg_type_name( +/// Look through the trait bounds of an `impl Fn(...) + ...` for a `Fn`-family +/// trait and return the lookup key for its argument type: +/// - `impl Fn(T)` → `Some("T")` +/// - `impl Fn()` → `Some("()")` (zero-arg callbacks, e.g. on-close handlers) +fn extract_fn_arg_type_name( bounds: &syn::punctuated::Punctuated, ) -> Option { for bound in bounds { @@ -1347,8 +1330,10 @@ fn extract_fn_single_arg_type_name( continue; } let syn::PathArguments::Parenthesized(p) = &seg.arguments else { continue }; - let first = p.inputs.first()?; - return type_last_segment(first); + return match p.inputs.first() { + Some(first) => type_last_segment(first), + None => Some("()".to_string()), + }; } None } diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index dbc8d023..bac8e2b9 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -35,6 +35,23 @@ use zenoh_ext::{ AdvancedSubscriberBuilderExt, }; +/// Fires `f` exactly once when dropped. Used to bind an `on_close` callback +/// to the lifetime of a data callback closure: when zenoh drops the data +/// closure (subscription/queryable/get teardown), the guard's `Drop` runs +/// `on_close`. +struct CallOnDrop(core::mem::MaybeUninit); +impl CallOnDrop { + fn new(f: F) -> Self { + Self(core::mem::MaybeUninit::new(f)) + } +} +impl Drop for CallOnDrop { + fn drop(&mut self) { + let f = unsafe { self.0.assume_init_read() }; + f(); + } +} + /// Open a Zenoh session using a borrowed configuration. #[prebindgen_proc_macro::prebindgen("jni")] pub fn open_session(config: &Config) -> ZResult { @@ -134,11 +151,16 @@ pub fn declare_subscriber( session: &Session, key_expr: KeyExpr<'static>, callback: impl Fn(Sample) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, ) -> ZResult> { let key_expr_string = key_expr.to_string(); + let guard = CallOnDrop::new(on_close); session .declare_subscriber(key_expr) - .callback(callback) + .callback(move |sample| { + let _ = &guard; + callback(sample); + }) .wait() .map(|subscriber| { trace!("Declared subscriber on '{}'.", key_expr_string); @@ -190,12 +212,17 @@ pub fn declare_queryable( session: &Session, key_expr: KeyExpr<'static>, callback: impl Fn(Query) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, complete: bool, ) -> ZResult> { let key_expr_string = key_expr.to_string(); + let guard = CallOnDrop::new(on_close); session .declare_queryable(key_expr) - .callback(callback) + .callback(move |query| { + let _ = &guard; + callback(query); + }) .complete(complete) .wait() .map(|queryable| { @@ -224,6 +251,7 @@ pub fn get( key_expr: KeyExpr<'static>, selector_params: Option, callback: impl Fn(Reply) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, timeout: Duration, query_target: QueryTarget, consolidation: ConsolidationMode, @@ -237,9 +265,13 @@ pub fn get( ) -> ZResult<()> { let key_expr_string = key_expr.to_string(); let selector = Selector::owned(&key_expr, selector_params.unwrap_or_default()); + let guard = CallOnDrop::new(on_close); let mut get_builder = session .get(selector) - .callback(callback) + .callback(move |reply| { + let _ = &guard; + callback(reply); + }) .target(query_target) .consolidation(consolidation) .congestion_control(congestion_control) @@ -386,14 +418,19 @@ pub fn declare_advanced_subscriber( session: &Session, key_expr: KeyExpr<'static>, callback: impl Fn(Sample) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, history: Option, recovery: Option, subscriber_detection: bool, ) -> ZResult> { let key_expr_string = key_expr.to_string(); + let guard = CallOnDrop::new(on_close); let mut builder = session .declare_subscriber(key_expr) - .callback(callback) + .callback(move |sample| { + let _ = &guard; + callback(sample); + }) .advanced(); if let Some(history) = history { builder = builder.history(history.try_into()?); diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 6a619b86..edd4e6ab 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -18,7 +18,6 @@ fn main() { .kotlin_class("JNISessionNative") .kotlin_throws("io.zenoh.exceptions.ZError") .kotlin_init("io.zenoh.ZenohLoad") - .kotlin_on_close("io.zenoh.jni.callbacks.JNIOnCloseCallback") .enum_decoder( "CongestionControl", "crate::utils::decode_congestion_control", @@ -43,6 +42,11 @@ fn main() { "crate::sample_callback::process_kotlin_reply_callback", "io.zenoh.jni.callbacks.JNIGetCallback", ) + .callback_decoder( + "()", + "crate::sample_callback::process_kotlin_on_close_callback", + "io.zenoh.jni.callbacks.JNIOnCloseCallback", + ) .struct_decoder( "KeyExpr", "crate::key_expr::decode_jni_key_expr", diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs index 513944fb..b2a2813a 100644 --- a/zenoh-jni/src/ext/advanced_subscriber.rs +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -30,7 +30,7 @@ use zenoh::Wait; use crate::owned_object::OwnedObject; -use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close}; +use crate::utils::{get_callback_global_ref, get_java_vm, load_on_close, wrap_with_on_close}; use std::ptr::null; use crate::throw_exception; @@ -138,12 +138,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareDetectPu advanced_subscriber.key_expr() ); + let cb = process_kotlin_sample_callback(&mut env, callback)?; + let cb = wrap_with_on_close(&mut env, on_close, cb)?; let detect_publishers_subscriber = advanced_subscriber .detect_publishers() .history(history != 0) - .callback(process_kotlin_sample_callback( - &mut env, callback, on_close, - )?) + .callback(cb) .wait() .map_err(|err| zerror!("Unable to declare detect publishers subscriber: {}", err))?; @@ -196,12 +196,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareBackgrou advanced_subscriber.key_expr() ); + let cb = process_kotlin_sample_callback(&mut env, callback)?; + let cb = wrap_with_on_close(&mut env, on_close, cb)?; advanced_subscriber .detect_publishers() .history(history != 0) - .callback(process_kotlin_sample_callback( - &mut env, callback, on_close, - )?) + .callback(cb) .background() .wait() .map_err(|err| { diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 4c53e0d1..2a3d177b 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -30,7 +30,7 @@ use crate::{ owned_object::OwnedObject, sample_callback::{on_reply_error, on_reply_success, process_kotlin_sample_callback}, throw_exception, - utils::{get_callback_global_ref, get_java_vm, load_on_close}, + utils::{get_callback_global_ref, get_java_vm, load_on_close, wrap_with_on_close}, }; #[no_mangle] @@ -146,13 +146,13 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessSubscribe let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; tracing::debug!("Declaring liveliness subscriber on '{}'...", key_expr); + let cb = process_kotlin_sample_callback(&mut env, callback)?; + let cb = wrap_with_on_close(&mut env, on_close, cb)?; let subscriber = session .liveliness() .declare_subscriber(key_expr.to_owned()) .history(history != 0) - .callback(process_kotlin_sample_callback( - &mut env, callback, on_close, - )?) + .callback(cb) .wait() .map_err(|err| zerror!("Unable to declare liveliness subscriber: {}", err))?; diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index cd536992..60dc9cf4 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -177,15 +177,11 @@ pub(crate) fn on_reply_error( pub(crate) unsafe fn process_kotlin_sample_callback( env: &mut JNIEnv, callback: JObject, - on_close: JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); Ok(move |sample: Sample| { - on_close.noop(); let _ = || -> ZResult<()> { let mut env = java_vm .attach_current_thread_as_daemon() @@ -250,16 +246,12 @@ pub(crate) unsafe fn process_kotlin_sample_callback( pub(crate) unsafe fn process_kotlin_reply_callback( env: &mut JNIEnv, callback: JObject, - on_close: JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); Ok(move |reply: Reply| { || -> ZResult<()> { - on_close.noop(); tracing::debug!("Receiving reply through JNI: {:?}", reply); let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { zerror!("Unable to attach thread for GET query callback: {}", err) @@ -286,15 +278,11 @@ pub(crate) unsafe fn process_kotlin_reply_callback( pub(crate) unsafe fn process_kotlin_query_callback( env: &mut JNIEnv, callback: JObject, - on_close: JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); Ok(move |query: Query| { - on_close.noop(); let env = match java_vm.attach_current_thread_as_daemon() { Ok(env) => env, Err(err) => { @@ -311,6 +299,33 @@ pub(crate) unsafe fn process_kotlin_query_callback( }) } +/// Decoder for the zero-arg `impl Fn() + Send + Sync + 'static` callback used +/// by zenoh-flat for `on_close` parameters. The returned closure attaches the +/// JVM and invokes `Runnable.run()` on the Kotlin object. zenoh-flat is +/// responsible for binding it to the data closure's lifetime via its own +/// `CallOnDrop` so the on-close fires when the subscription/query is dropped. +pub(crate) unsafe fn process_kotlin_on_close_callback( + env: &mut JNIEnv, + on_close: JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + + Ok(move || { + let mut env = match java_vm.attach_current_thread_as_daemon() { + Ok(env) => env, + Err(err) => { + tracing::error!("Unable to attach thread for 'onClose' callback: {}", err); + return; + } + }; + if let Err(err) = env.call_method(&on_close_global_ref, "run", "()V", &[]) { + _ = env.exception_describe(); + tracing::error!("Error while running 'onClose' callback: {}", err); + } + }) +} + fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> ZResult<()> { let selector_params_jstr = env .new_string(query.parameters().to_string()) diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index e6453ac6..25f85a83 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -175,6 +175,29 @@ impl Drop for CallOnDrop { } } +/// Wrap a decoded data callback so that the supplied Kotlin `on_close` +/// callback fires exactly once when the data closure is dropped (via +/// `CallOnDrop`). Used by hand-written JNI entry points whose Kotlin signatures +/// pair a callback with an `on_close: JNIOnCloseCallback`. Generated entry +/// points (via `JniConverter`) take `on_close` as a separate Rust parameter +/// instead and are wrapped at the zenoh-flat layer. +pub(crate) fn wrap_with_on_close( + env: &mut JNIEnv, + on_close: JObject, + cb: F, +) -> ZResult +where + F: Fn(T) + Send + Sync + 'static, +{ + let java_vm = Arc::new(get_java_vm(env)?); + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let guard = load_on_close(&java_vm, on_close_global_ref); + Ok(move |t| { + guard.noop(); + cb(t); + }) +} + pub(crate) fn load_on_close( java_vm: &Arc, on_close_global_ref: jni::objects::GlobalRef, From b9bbbb842e79c3d6342729cfbffb13def9ec8849 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 12:28:35 +0200 Subject: [PATCH 103/126] comment added --- zenoh-flat/src/session.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs index bac8e2b9..e4498d87 100644 --- a/zenoh-flat/src/session.rs +++ b/zenoh-flat/src/session.rs @@ -158,7 +158,7 @@ pub fn declare_subscriber( session .declare_subscriber(key_expr) .callback(move |sample| { - let _ = &guard; + let _ = &guard; // capture the guard callback(sample); }) .wait() @@ -220,7 +220,7 @@ pub fn declare_queryable( session .declare_queryable(key_expr) .callback(move |query| { - let _ = &guard; + let _ = &guard; // capture the guard callback(query); }) .complete(complete) @@ -269,7 +269,7 @@ pub fn get( let mut get_builder = session .get(selector) .callback(move |reply| { - let _ = &guard; + let _ = &guard; // capture the guard callback(reply); }) .target(query_target) @@ -428,7 +428,7 @@ pub fn declare_advanced_subscriber( let mut builder = session .declare_subscriber(key_expr) .callback(move |sample| { - let _ = &guard; + let _ = &guard; // capture the guard callback(sample); }) .advanced(); From ca65e2237a3e1d777bd505433939260a796eb6aa Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 14:36:59 +0200 Subject: [PATCH 104/126] consume_arg removed --- zenoh-flat/src/jni_converter.rs | 154 +++++++----------- .../kotlin/io/zenoh/jni/JNISession.kt | 3 + zenoh-jni/build.rs | 2 - zenoh-jni/src/session.rs | 19 +++ 4 files changed, 83 insertions(+), 95 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index d9a0683b..deede0cf 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -32,7 +32,7 @@ //! decoder helpers, `OwnedObject`) — those couplings are configurable through //! the [`Builder`] so the converter itself stays data-driven. -use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; +use std::collections::{BTreeSet, HashMap, VecDeque}; use std::path::PathBuf; use proc_macro2::TokenStream; @@ -64,10 +64,6 @@ pub struct Builder { /// parameter's type (e.g. `"HistoryConfig"`). The decoder must have /// signature `fn(&mut JNIEnv, &JObject) -> ZResult`. struct_decoders: HashMap, - /// Per-function set of argument names that must be consumed (taken from - /// the raw pointer via `Arc::from_raw`) instead of borrowed. Used for - /// close/undeclare-style functions that invalidate their handle. - consume_args: HashMap>, /// Return-type wrappers keyed by the last-segment name of `T` in /// `ZResult`. Applies when `T` is a plain (non-`Vec`) type. return_wrappers: HashMap, @@ -130,7 +126,6 @@ impl Default for Builder { enum_decoders: HashMap::new(), callback_decoders: HashMap::new(), struct_decoders: HashMap::new(), - consume_args: HashMap::new(), return_wrappers: HashMap::new(), return_wrappers_vec: HashMap::new(), kotlin: None, @@ -318,27 +313,6 @@ impl Builder { self } - /// Mark a specific argument of a source function as consuming: the - /// generated wrapper will take ownership of the raw pointer via - /// `Arc::from_raw` (dropping the Arc at end of scope), rather than - /// borrowing it through `OwnedObject::from_raw`. Applies to both - /// `OpaqueRef` (`&T`) and `KeyExpr` arguments — for the latter, the - /// string-fallback argument is omitted, leaving just the pointer. - /// - /// Typically used for `close_*` / `undeclare_*` functions that invalidate - /// the handle. - pub fn consume_arg( - mut self, - fn_name: impl Into, - arg_name: impl Into, - ) -> Self { - self.consume_args - .entry(fn_name.into()) - .or_default() - .insert(arg_name.into()); - self - } - /// Enable Kotlin-side prototype generation. `path` is where the `.kt` /// file will be written when [`JniConverter::write_kotlin`] is called. /// Calling this method is what turns on Kotlin output; all other @@ -571,13 +545,6 @@ impl JniConverter { let owned_object = self.cfg.owned_object.clone(); let zresult = self.cfg.zresult.clone(); let throw_exception = self.cfg.throw_exception.clone(); - let empty_consume_set: HashSet = HashSet::new(); - let consume_set: HashSet = self - .cfg - .consume_args - .get(&original_name) - .cloned() - .unwrap_or(empty_consume_set); let mut prelude: Vec = Vec::new(); let mut jni_params: Vec = Vec::new(); @@ -602,15 +569,9 @@ impl JniConverter { ArgKind::OpaqueRef(elem) => { let ptr_ident = format_ident!("{}_ptr", name); jni_params.push(quote! { #ptr_ident: *const #elem }); - if consume_set.contains(&name.to_string()) { - prelude.push(quote! { - let #name = std::sync::Arc::from_raw(#ptr_ident); - }); - } else { - prelude.push(quote! { - let #name = #owned_object::from_raw(#ptr_ident); - }); - } + prelude.push(quote! { + let #name = #owned_object::from_raw(#ptr_ident); + }); call_args.push(quote! { &#name }); if kt_cfg.is_some() { kotlin_params.push(format!( @@ -619,55 +580,55 @@ impl JniConverter { )); } } - ArgKind::KeyExpr => { - let consumed = consume_set.contains(&name.to_string()); - if consumed { - // Consume path: the declared KeyExpr is required (no - // string fallback). Arc::from_raw decrements the - // refcount at end of scope, freeing the handle once - // no other references remain. A cloned inner KeyExpr - // is passed to the callee by value. - let ptr_ident = format_ident!("{}_ptr", name); - let arc_ident = format_ident!("__{}_arc", name); - jni_params.push(quote! { - #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> - }); - prelude.push(quote! { - let #arc_ident = std::sync::Arc::from_raw(#ptr_ident); - let #name = (*#arc_ident).clone(); - }); - call_args.push(quote! { #name }); - } else { - // Non-consume path: single `JObject` holder - // (io.zenoh.jni.JNIKeyExpr) decoded via the - // `KeyExpr` entry in `struct_decoders`. - let decoder = self - .cfg - .struct_decoders + ArgKind::KeyExprBorrow => { + // `&KeyExpr` in the source signature: single `JObject` + // holder (io.zenoh.jni.JNIKeyExpr) decoded via the + // `KeyExpr` entry in `struct_decoders`. + let decoder = self + .cfg + .struct_decoders + .get("KeyExpr") + .expect("struct_decoder(\"KeyExpr\", ...) not configured"); + jni_params.push(quote! { #name: jni::objects::JObject }); + prelude.push(quote! { + let #name = #decoder(&mut env, &#name)?; + }); + call_args.push(quote! { #name }); + if let Some(kt) = kt_cfg { + let fqn = kt + .struct_kotlin_types .get("KeyExpr") - .expect("struct_decoder(\"KeyExpr\", ...) not configured"); - jni_params.push(quote! { #name: jni::objects::JObject }); - prelude.push(quote! { - let #name = #decoder(&mut env, &#name)?; - }); - call_args.push(quote! { #name }); + .cloned() + .expect("struct_decoder(\"KeyExpr\", ...) Kotlin type not configured"); + let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}", + kotlin_param_name(&name.to_string(), false), + short + )); } - if let Some(kt) = kt_cfg { - if consumed { - kotlin_params.push(format!( - "{}: Long", - kotlin_param_name(&name.to_string(), true) - )); - } else { - let fqn = kt.struct_kotlin_types.get("KeyExpr").cloned() - .expect("struct_decoder(\"KeyExpr\", ...) Kotlin type not configured"); - let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); - kotlin_params.push(format!( - "{}: {}", - kotlin_param_name(&name.to_string(), false), - short - )); - } + } + ArgKind::KeyExprConsume => { + // `KeyExpr<'static>` by value in the source signature: + // the caller relinquishes ownership of the declared + // handle. Arc::from_raw decrements the refcount at end + // of scope, freeing the handle once no other references + // remain. A cloned inner KeyExpr is passed by value. + let ptr_ident = format_ident!("{}_ptr", name); + let arc_ident = format_ident!("__{}_arc", name); + jni_params.push(quote! { + #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> + }); + prelude.push(quote! { + let #arc_ident = std::sync::Arc::from_raw(#ptr_ident); + let #name = (*#arc_ident).clone(); + }); + call_args.push(quote! { #name }); + if kt_cfg.is_some() { + kotlin_params.push(format!( + "{}: Long", + kotlin_param_name(&name.to_string(), true) + )); } } ArgKind::String => { @@ -1183,7 +1144,7 @@ impl JniConverter { match ty { syn::Type::Reference(r) if r.mutability.is_none() => { if type_last_segment(&r.elem).map(|s| s == "KeyExpr").unwrap_or(false) { - ArgKind::KeyExpr + ArgKind::KeyExprBorrow } else { ArgKind::OpaqueRef((*r.elem).clone()) } @@ -1211,7 +1172,7 @@ impl JniConverter { return ArgKind::String; } if name == "KeyExpr" { - return ArgKind::KeyExpr; + return ArgKind::KeyExprConsume; } if name == "Duration" { return ArgKind::Duration; @@ -1264,7 +1225,14 @@ enum StructFieldKind { enum ArgKind { OpaqueRef(syn::Type), - KeyExpr, + /// `&KeyExpr` in the source signature — decoded as a `JNIKeyExpr` + /// holder via the registered `KeyExpr` struct decoder. The handle is + /// borrowed; the caller retains ownership. + KeyExprBorrow, + /// `KeyExpr<'static>` (by value) in the source signature — the caller + /// relinquishes ownership of the declared handle. Generated wrapper + /// takes the raw pointer and drops the `Arc` at end of scope. + KeyExprConsume, /// `String` → `JString` decoded via `string_decoder`. String, Enum(syn::Path), diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 5a163885..3cd575f1 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -241,5 +241,8 @@ public class JNISession(internal val sessionPtr: Long) { fun close() { closeSessionViaJNI(sessionPtr) + freePtrViaJNI(sessionPtr) } + + private external fun freePtrViaJNI(ptr: Long) } diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index edd4e6ab..f1063fc8 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -57,8 +57,6 @@ fn main() { "crate::utils::decode_jni_encoding", "JNIEncoding", ) - .consume_arg("close_session", "session") - .consume_arg("undeclare_key_expr", "key_expr") .return_wrapper( "ZenohId", "jni::sys::jbyteArray", diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 26b584f3..0de2dff2 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -13,6 +13,9 @@ // // Types referenced by the generated `zenoh_flat_jni.rs` below must be in scope. +use std::sync::Arc; + +use jni::{objects::JClass, JNIEnv}; use zenoh::{ config::Config, key_expr::KeyExpr, @@ -25,3 +28,19 @@ use zenoh_ext::{AdvancedPublisher, AdvancedSubscriber}; include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); +/// Drop the `Arc` referenced by `session_ptr`. Must be called exactly +/// once per pointer obtained from session-opening JNI calls; pairs with the +/// `Arc::into_raw` performed by the generated wrapper for the open path. +/// +/// Distinct from `closeSessionViaJNI`, which only deactivates the session +/// (network shutdown) without dropping the Rust handle. +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_freePtrViaJNI( + _env: JNIEnv, + _: JClass, + session_ptr: *const Session, +) { + Arc::from_raw(session_ptr); +} + From 431ba1c6959ab4124af98dbd01d8869e71e1a28c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 17:44:48 +0200 Subject: [PATCH 105/126] print destination --- zenoh-jni/build.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index f1063fc8..5779f83a 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -73,12 +73,17 @@ fn main() { ) .build(); - source + let bindings_file =source .items_all() .batching(converter.as_closure()) .collect::() .write("zenoh_flat_jni.rs"); + println!( + "cargo:warning=Generated bindings at: {}", + bindings_file.display() + ); + converter .write_kotlin() .expect("failed to write generated Kotlin file"); From ddd7babce1d2d6a7eee239b66444f890116af32a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 18:30:33 +0200 Subject: [PATCH 106/126] type registry --- zenoh-flat/src/jni_converter.rs | 1354 ++++++++++++++++++------------- zenoh-jni/build.rs | 18 +- 2 files changed, 796 insertions(+), 576 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index deede0cf..22762c3c 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -1,45 +1,255 @@ //! JNI binding generator for functions marked with `#[prebindgen]`. //! -//! This module mirrors the pattern of [`prebindgen::batching::FfiConverter`], but -//! instead of emitting `#[no_mangle] extern "C"` proxy functions, it emits +//! Mirrors the pattern of [`prebindgen::batching::FfiConverter`], but instead +//! of emitting `#[no_mangle] extern "C"` proxy functions, it emits //! `Java__ViaJNI` wrappers that decode JNI arguments, call the -//! original Rust function, and wrap the result into a raw pointer (or throw a +//! original Rust function, and wrap the result into a JNI return (or throw a //! JVM exception on error). //! +//! # Type registry +//! +//! The converter is fully data-driven: every Rust type that can appear in a +//! `#[prebindgen]` function's signature is described by a [`TypeBinding`] +//! registered up-front via [`Builder::type_binding`]. A binding declares up to +//! four forms: +//! +//! * `consume` — used when the type appears by value as a parameter (`T`); +//! * `borrow` — used when the type appears as a shared reference (`&T`); +//! * `returns` — used when the type appears in `ZResult` as a return value; +//! * `returns_vec` — used when the type appears as `ZResult>`. +//! +//! Each form carries a JNI on-the-wire type (e.g. `jni::sys::jlong`, +//! `jni::objects::JObject`, `*const Foo`), the Kotlin-side declaration, and a +//! decoding/encoding strategy. Built-in bindings for `bool`, `String`, +//! `Vec`, and `Duration` are pre-registered with sensible defaults; a +//! handful of builder-level convenience methods (`string_decoder`, +//! `byte_array_decoder`, `enum_decoder`, `struct_decoder`, `return_wrapper`, +//! `return_wrapper_vec`) populate or extend bindings without forcing every +//! call site to spell out a full `TypeBinding`. +//! //! # Pipeline //! //! ```ignore //! use itertools::Itertools; +//! use zenoh_flat::jni_converter::{JniConverter, TypeBinding, JniForm, ArgDecode}; +//! //! let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); -//! let converter = zenoh_flat::jni_converter::JniConverter::builder() +//! let mut converter = JniConverter::builder() //! .class_prefix("Java_io_zenoh_jni_JNISession_") //! .function_suffix("ViaJNI") //! .source_module("zenoh_flat::session") //! .owned_object("crate::owned_object::OwnedObject") //! .zresult("crate::errors::ZResult") //! .throw_exception("crate::throw_exception") -//! .struct_decoder("KeyExpr", "crate::key_expr::decode_jni_key_expr") -//! .enum_decoder("CongestionControl", "crate::utils::decode_congestion_control") +//! .string_decoder("crate::utils::decode_string") +//! .byte_array_decoder("crate::utils::decode_byte_array") +//! .struct_decoder( +//! "KeyExpr", +//! "crate::key_expr::decode_jni_key_expr", +//! "JNIKeyExpr", +//! ) //! .build(); //! source //! .items_all() -//! .batching(converter.into_closure()) +//! .batching(converter.as_closure()) //! .collect::() //! .write("zenoh_flat_jni.rs"); //! ``` -//! -//! This crate is currently coupled to zenoh-jni's type layout (e.g. `KeyExpr`, -//! decoder helpers, `OwnedObject`) — those couplings are configurable through -//! the [`Builder`] so the converter itself stays data-driven. use std::collections::{BTreeSet, HashMap, VecDeque}; use std::path::PathBuf; +use std::sync::Arc; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use prebindgen::SourceLocation; +// ===================================================================== +// TypeBinding: per-type description of JNI representations +// ===================================================================== + +/// Per-type description of how a Rust type is represented across the JNI +/// boundary. A type may declare up to four forms: +/// `consume` (`T` parameter), `borrow` (`&T` parameter), `returns` +/// (`ZResult` return), and `returns_vec` (`ZResult>` return). +#[derive(Clone)] +pub struct TypeBinding { + name: String, + /// Kotlin-side type name (FQN preferred — out-of-package import is + /// auto-derived; bare for same-package). Used as the Kotlin parameter + /// type when the form's wire JNI type is `JObject` and as the Kotlin + /// return type. For primitive-mapped forms (`bool`, `Duration`, + /// `String`, ...) the form's `kotlin_jni_type` is used instead. + kotlin_type: Option, + consume: Option, + borrow: Option, + returns: Option, + returns_vec: Option, +} + +/// Strategy for converting a JNI parameter into a Rust value. +#[derive(Clone)] +pub enum ArgDecode { + /// `let = (&mut env, &)?;` + EnvRefMut(syn::Path), + /// `let = (&env, )?;` — used by the legacy + /// `byte_array_decoder` calling convention. + EnvByVal(syn::Path), + /// `let = ()?;` — pure conversion (e.g. enum decoders). + Pure(syn::Path), + /// `let = ;` — inline transformation built from the input + /// ident. Used for trivial conversions like `bool` (`x != 0`) or + /// `Duration` (`Duration::from_millis(x as u64)`). + Inline(InlineFn), + /// `let = ::from_raw();` — borrows the Arc + /// pointed to by `` via the converter-wide `owned_object` setting. + /// The argument is passed to the wrapped function as `&`. + OwnedRef, + /// Consume an `Arc` raw pointer: reconstructs the Arc, clones the + /// inner value, and drops the Arc at end of scope. + ConsumeArc, +} + +/// Clonable closure that produces a TokenStream from the JNI input ident. +#[derive(Clone)] +pub struct InlineFn(Arc TokenStream + Send + Sync>); + +impl InlineFn { + pub fn new(f: F) -> Self + where + F: Fn(&syn::Ident) -> TokenStream + Send + Sync + 'static, + { + InlineFn(Arc::new(f)) + } + + fn call(&self, ident: &syn::Ident) -> TokenStream { + (self.0)(ident) + } +} + +/// Describes how a JNI parameter for a particular type/form is decoded. +#[derive(Clone)] +pub struct JniForm { + /// On-the-wire JNI type, e.g. `jni::sys::jlong`, `jni::objects::JObject`, + /// `*const Session`. Emitted verbatim in the wrapper signature. + jni_type: syn::Type, + /// Kotlin-side wire type for this form (`"Long"`, `"Boolean"`, `"Int"`, + /// `"String"`, `"ByteArray"`, `"JObject"`). + kotlin_jni_type: String, + /// True for raw-pointer slots — appends `"Ptr"` to the Kotlin parameter + /// name (e.g. `sessionPtr: Long`). + pointer_param: bool, + decode: ArgDecode, +} + +impl JniForm { + pub fn new( + jni_type: impl AsRef, + kotlin_jni_type: impl Into, + decode: ArgDecode, + ) -> Self { + Self { + jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid JniForm jni_type"), + kotlin_jni_type: kotlin_jni_type.into(), + pointer_param: false, + decode, + } + } + + pub fn pointer_param(mut self, p: bool) -> Self { + self.pointer_param = p; + self + } + + /// Whether this form's wire JNI type is a `JObject`-shaped object that + /// supports `is_null()` (used by the `Option` combinator). + fn is_jni_object(&self) -> bool { + matches!(jni_object_kind(&self.jni_type), Some(_)) + } +} + +#[derive(Clone)] +pub enum ReturnEncode { + /// `Ok((&mut env, __result)?)` — wrapping function returns + /// `ZResult`. + Wrapper(syn::Path), + /// `Ok(Arc::into_raw(Arc::new(__result)))` — opaque Arc-handle return. + ArcIntoRaw, +} + +/// Describes how a Rust return value is encoded into a JNI return. +#[derive(Clone)] +pub struct ReturnForm { + jni_type: syn::Type, + kotlin_jni_type: Option, + encode: ReturnEncode, + default_expr: syn::Expr, +} + +impl ReturnForm { + pub fn new( + jni_type: impl AsRef, + encode: ReturnEncode, + default_expr: impl AsRef, + ) -> Self { + Self { + jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid ReturnForm jni_type"), + kotlin_jni_type: None, + encode, + default_expr: syn::parse_str(default_expr.as_ref()) + .expect("invalid ReturnForm default_expr"), + } + } + + pub fn kotlin(mut self, kotlin: impl Into) -> Self { + self.kotlin_jni_type = Some(kotlin.into()); + self + } +} + +impl TypeBinding { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + kotlin_type: None, + consume: None, + borrow: None, + returns: None, + returns_vec: None, + } + } + + pub fn kotlin(mut self, fqn: impl Into) -> Self { + self.kotlin_type = Some(fqn.into()); + self + } + + pub fn consume(mut self, form: JniForm) -> Self { + self.consume = Some(form); + self + } + + pub fn borrow(mut self, form: JniForm) -> Self { + self.borrow = Some(form); + self + } + + pub fn returns(mut self, form: ReturnForm) -> Self { + self.returns = Some(form); + self + } + + pub fn returns_vec(mut self, form: ReturnForm) -> Self { + self.returns_vec = Some(form); + self + } +} + +// ===================================================================== +// Builder +// ===================================================================== + /// Builder for [`JniConverter`]. pub struct Builder { class_prefix: String, @@ -52,33 +262,23 @@ pub struct Builder { owned_object: syn::Path, zresult: syn::Path, throw_exception: syn::Path, - string_decoder: Option, - byte_array_decoder: Option, - enum_decoders: HashMap, + /// Primary type registry keyed by short type name. + types: HashMap, /// Map from callback element type name (e.g. `"Sample"`) to the decoder /// that builds an `impl Fn(T) + Send + Sync + 'static` closure from a - /// `(callback: JObject, on_close: JObject)` pair. + /// single `JObject`. Callbacks have a fundamentally different shape + /// (decoder builds a captured closure rather than a value) so they live + /// outside `types`. callback_decoders: HashMap, - /// Decoders for struct parameters passed across JNI as a plain `JObject` - /// (e.g. a Kotlin `data class`). Keyed by the last-segment name of the - /// parameter's type (e.g. `"HistoryConfig"`). The decoder must have - /// signature `fn(&mut JNIEnv, &JObject) -> ZResult`. - struct_decoders: HashMap, - /// Return-type wrappers keyed by the last-segment name of `T` in - /// `ZResult`. Applies when `T` is a plain (non-`Vec`) type. - return_wrappers: HashMap, - /// Return-type wrappers keyed by the element type name of `Vec` in - /// `ZResult>`. - return_wrappers_vec: HashMap, + /// Per-element-type Kotlin names for callback args (e.g. `"Sample"` → + /// `"io.zenoh.jni.callbacks.JNISubscriberCallback"`). + callback_kotlin_types: HashMap, /// Kotlin output config — if `None`, no Kotlin file is emitted. kotlin: Option, } /// Settings for generating a companion Kotlin file with `external fun` -/// prototypes. Enabled via [`Builder::kotlin_output`]. Per-type Kotlin names -/// are stored alongside each Rust decoder registration (see -/// [`Builder::struct_decoder`], [`Builder::callback_decoder`], and the return -/// wrappers), so one call registers both sides. +/// prototypes. Enabled via [`Builder::kotlin_output`]. pub(crate) struct KotlinConfig { output_path: PathBuf, package: String, @@ -89,31 +289,11 @@ pub(crate) struct KotlinConfig { /// FQN of a singleton referenced inside the generated `init { ... }` block /// to force native-library loading — `None` disables the `init`. init_load_fqn: Option, - /// Per-source-type Kotlin names (FQN or bare) for struct-decoded args. - struct_kotlin_types: HashMap, - /// Per-element-type Kotlin names for callback args (e.g. `Sample` → - /// `io.zenoh.jni.callbacks.JNISubscriberCallback`). - callback_kotlin_types: HashMap, - /// Per-return-type Kotlin names for `ZResult` with a return_wrapper. - return_kotlin_types: HashMap, - /// Per-element-type Kotlin names for `ZResult>` with a - /// return_wrapper_vec. - return_kotlin_types_vec: HashMap, -} - -/// Describes how to render a `ZResult` return value into a JNI-compatible -/// output value. Registered via [`Builder::return_wrapper`] / -/// [`Builder::return_wrapper_vec`]. -#[derive(Clone)] -pub(crate) struct ReturnWrapper { - jni_type: syn::Type, - wrap_fn: syn::Path, - default_expr: syn::Expr, } impl Default for Builder { fn default() -> Self { - Self { + let mut b = Self { class_prefix: String::new(), function_suffix: String::new(), source_module: syn::parse_str("crate").unwrap(), @@ -121,18 +301,58 @@ impl Default for Builder { owned_object: syn::parse_str("OwnedObject").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), - string_decoder: None, - byte_array_decoder: None, - enum_decoders: HashMap::new(), + types: HashMap::new(), callback_decoders: HashMap::new(), - struct_decoders: HashMap::new(), - return_wrappers: HashMap::new(), - return_wrappers_vec: HashMap::new(), + callback_kotlin_types: HashMap::new(), kotlin: None, - } + }; + register_builtins(&mut b.types); + b } } +/// Pre-register built-in language types (`bool`, `Duration`) plus the +/// scaffolding for `String` / `Vec` (whose decoders are filled in by +/// [`Builder::string_decoder`] / [`Builder::byte_array_decoder`]). +fn register_builtins(types: &mut HashMap) { + // bool — jboolean, inline `x != 0`. + types.insert( + "bool".to_string(), + TypeBinding::new("bool").consume( + JniForm::new( + "jni::sys::jboolean", + "Boolean", + ArgDecode::Inline(InlineFn::new(|input| quote! { #input != 0 })), + ), + ), + ); + // Duration — jlong, inline `Duration::from_millis(x as u64)`. + types.insert( + "Duration".to_string(), + TypeBinding::new("Duration").consume( + JniForm::new( + "jni::sys::jlong", + "Long", + ArgDecode::Inline(InlineFn::new( + |input| quote! { std::time::Duration::from_millis(#input as u64) }, + )), + ), + ), + ); + // String — JString, decoder filled by string_decoder(). + types.insert( + "String".to_string(), + TypeBinding::new("String"), + ); + // Vec — keyed under the synthetic name "VecU8" (looked up explicitly + // by classify_arg when it sees `Vec`). Decoder filled by + // byte_array_decoder(). + types.insert( + "VecU8".to_string(), + TypeBinding::new("VecU8"), + ); +} + impl Builder { /// JNI class prefix prepended to each function name, e.g. /// `"Java_io_zenoh_jni_JNISession_"`. @@ -164,7 +384,8 @@ impl Builder { self } - /// Path of the `OwnedObject` helper used to borrow Arc-pointers. + /// Path of the `OwnedObject` helper used to borrow Arc-pointers in the + /// `OwnedRef` decode form. pub fn owned_object(mut self, path: impl AsRef) -> Self { self.owned_object = syn::parse_str(path.as_ref()).expect("invalid owned_object path"); self @@ -183,73 +404,102 @@ impl Builder { self } - /// Path of the function that decodes a `JString` into `String`, e.g. - /// `"crate::utils::decode_string"`. + /// Universal entry point: register or replace a [`TypeBinding`] by name. + /// All sugar methods below delegate to this. + pub fn type_binding(mut self, binding: TypeBinding) -> Self { + self.types.insert(binding.name.clone(), binding); + self + } + + /// Path of the function that decodes a `JString` into `String`. Used by + /// the built-in `String` binding. pub fn string_decoder(mut self, path: impl AsRef) -> Self { - self.string_decoder = - Some(syn::parse_str(path.as_ref()).expect("invalid string_decoder path")); + let p: syn::Path = syn::parse_str(path.as_ref()).expect("invalid string_decoder path"); + let entry = self + .types + .get_mut("String") + .expect("built-in `String` binding missing"); + entry.consume = Some(JniForm::new( + "jni::objects::JString", + "String", + ArgDecode::EnvRefMut(p), + )); self } - /// Path of the function that decodes a `JByteArray` into `Vec`, e.g. - /// `"crate::utils::decode_byte_array"`. Used for both `Vec` and - /// `Option>` parameters. + /// Path of the function that decodes a `JByteArray` into `Vec`. Used + /// by the built-in `Vec` binding (under the internal name `"VecU8"`). pub fn byte_array_decoder(mut self, path: impl AsRef) -> Self { - self.byte_array_decoder = - Some(syn::parse_str(path.as_ref()).expect("invalid byte_array_decoder path")); + let p: syn::Path = + syn::parse_str(path.as_ref()).expect("invalid byte_array_decoder path"); + let entry = self + .types + .get_mut("VecU8") + .expect("built-in `VecU8` binding missing"); + entry.consume = Some(JniForm::new( + "jni::objects::JByteArray", + "ByteArray", + ArgDecode::EnvByVal(p), + )); self } - /// Register a decoder for an enum type. `type_name` is matched against the - /// last segment of the parameter's type path. + /// Register a decoder for an enum type. Equivalent to: + /// ```ignore + /// .type_binding(TypeBinding::new(name) + /// .consume(JniForm::new("jni::sys::jint", "Int", ArgDecode::Pure()))) + /// ``` pub fn enum_decoder( mut self, type_name: impl Into, decoder: impl AsRef, ) -> Self { - let path: syn::Path = + let name = type_name.into(); + let p: syn::Path = syn::parse_str(decoder.as_ref()).expect("invalid enum_decoder path"); - self.enum_decoders.insert(type_name.into(), path); + let binding = self + .types + .entry(name.clone()) + .or_insert_with(|| TypeBinding::new(name)); + binding.consume = Some(JniForm::new( + "jni::sys::jint", + "Int", + ArgDecode::Pure(p), + )); self } - /// Register a decoder for a struct parameter passed across JNI as a - /// plain `JObject` (typically a Kotlin data class). `type_name` matches - /// the last segment of the parameter's type path (e.g. `"HistoryConfig"`). - /// The decoder must have signature - /// `fn(&mut JNIEnv, &JObject) -> ZResult`. Used both for the plain - /// form (`HistoryConfig`) and the `Option` form (nullable JObject). - /// - /// `kotlin_type` is the Kotlin type name — FQN if out-of-package (import - /// is auto-derived) or bare for same-package / built-in. Only read when - /// Kotlin output is enabled via [`Builder::kotlin_output`]. + /// Register a decoder for a struct parameter passed across JNI as a plain + /// `JObject` (typically a Kotlin data class). The decoder must have + /// signature `fn(&mut JNIEnv, &JObject) -> ZResult`. `kotlin_type` is + /// the Kotlin type name (FQN if out-of-package, bare otherwise). pub fn struct_decoder( mut self, type_name: impl Into, decoder: impl AsRef, kotlin_type: impl Into, ) -> Self { - let path: syn::Path = - syn::parse_str(decoder.as_ref()).expect("invalid struct_decoder path"); let name = type_name.into(); let kt = kotlin_type.into(); - self.struct_decoders.insert(name.clone(), path); - if let Some(k) = self.kotlin.as_mut() { - k.struct_kotlin_types.insert(name, kt); - } + let p: syn::Path = + syn::parse_str(decoder.as_ref()).expect("invalid struct_decoder path"); + let binding = self + .types + .entry(name.clone()) + .or_insert_with(|| TypeBinding::new(name)); + binding.kotlin_type = Some(kt); + binding.consume = Some(JniForm::new( + "jni::objects::JObject", + "JObject", + ArgDecode::EnvRefMut(p), + )); self } /// Register a decoder for an `impl Fn(T) + Send + Sync + 'static` callback /// parameter. `element_type_name` is the last path segment of `T` - /// (e.g. `"Sample"`, `"Query"`, `"Reply"`). The decoder must have the - /// signature - /// `fn(&mut JNIEnv, JObject, JObject) -> ZResult`. - /// The generated JNI signature expands the single callback parameter into - /// two JNI args: `: JObject, _on_close: JObject`. - /// - /// `kotlin_type` is the Kotlin callback type name — FQN if out-of-package, - /// bare otherwise. Only read when Kotlin output is enabled. + /// (e.g. `"Sample"`, `"Query"`, `"Reply"`). The decoder must have + /// signature `fn(&mut JNIEnv, JObject) -> ZResult`. pub fn callback_decoder( mut self, element_type_name: impl Into, @@ -261,20 +511,14 @@ impl Builder { let name = element_type_name.into(); let kt = kotlin_type.into(); self.callback_decoders.insert(name.clone(), path); - if let Some(k) = self.kotlin.as_mut() { - k.callback_kotlin_types.insert(name, kt); - } + self.callback_kotlin_types.insert(name, kt); self } /// Register a return-type wrapper for `ZResult` where `T`'s - /// last-segment name equals `type_name`. `jni_type` is the generated - /// `extern "C"` return type. `wrap_fn` must have signature + /// last-segment name equals `type_name`. `wrap_fn` must have signature /// `fn(&mut JNIEnv, T) -> ZResult`. `default_expr` is the value /// returned on error (before the exception is thrown on the JVM side). - /// - /// `kotlin_type` is the Kotlin return type name (FQN or bare). Only read - /// when Kotlin output is enabled. pub fn return_wrapper( mut self, type_name: impl Into, @@ -285,11 +529,15 @@ impl Builder { ) -> Self { let name = type_name.into(); let kt = kotlin_type.into(); - self.return_wrappers - .insert(name.clone(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); - if let Some(k) = self.kotlin.as_mut() { - k.return_kotlin_types.insert(name, kt); - } + let wrap: syn::Path = + syn::parse_str(wrap_fn.as_ref()).expect("invalid return_wrapper wrap_fn path"); + let mut form = ReturnForm::new(jni_type, ReturnEncode::Wrapper(wrap), default_expr); + form.kotlin_jni_type = Some(kt); + let binding = self + .types + .entry(name.clone()) + .or_insert_with(|| TypeBinding::new(name)); + binding.returns = Some(form); self } @@ -305,18 +553,20 @@ impl Builder { ) -> Self { let name = element_type_name.into(); let kt = kotlin_type.into(); - self.return_wrappers_vec - .insert(name.clone(), parse_return_wrapper(jni_type, wrap_fn, default_expr)); - if let Some(k) = self.kotlin.as_mut() { - k.return_kotlin_types_vec.insert(name, kt); - } + let wrap: syn::Path = + syn::parse_str(wrap_fn.as_ref()).expect("invalid return_wrapper_vec wrap_fn path"); + let mut form = ReturnForm::new(jni_type, ReturnEncode::Wrapper(wrap), default_expr); + form.kotlin_jni_type = Some(kt); + let binding = self + .types + .entry(name.clone()) + .or_insert_with(|| TypeBinding::new(name)); + binding.returns_vec = Some(form); self } /// Enable Kotlin-side prototype generation. `path` is where the `.kt` /// file will be written when [`JniConverter::write_kotlin`] is called. - /// Calling this method is what turns on Kotlin output; all other - /// `kotlin_*` methods are optional refinements. pub fn kotlin_output(mut self, path: impl Into) -> Self { self.kotlin.get_or_insert_with(KotlinConfig::default).output_path = path.into(); self @@ -345,7 +595,6 @@ impl Builder { /// FQN of a singleton referenced from the generated `init { ... }` block /// (typically `io.zenoh.ZenohLoad`) to force native-library loading. - /// Unset ⇒ no `init` block. pub fn kotlin_init(mut self, fqn: impl Into) -> Self { self.kotlin.get_or_insert_with(KotlinConfig::default).init_load_fqn = Some(fqn.into()); self @@ -371,41 +620,29 @@ impl Default for KotlinConfig { class_name: String::new(), throws_class_fqn: None, init_load_fqn: None, - struct_kotlin_types: HashMap::new(), - callback_kotlin_types: HashMap::new(), - return_kotlin_types: HashMap::new(), - return_kotlin_types_vec: HashMap::new(), } } } +// ===================================================================== +// JniConverter +// ===================================================================== + /// Converter that transforms `#[prebindgen]`-marked Rust functions into JNI /// `Java_*` wrappers. -/// -/// Intended for use with `itertools::batching`: -/// -/// ```ignore -/// source.items_all().batching(converter.into_closure()) -/// ``` pub struct JniConverter { cfg: Builder, pending: VecDeque<(syn::Item, SourceLocation)>, /// `true` once the source iterator has been drained and sorted (structs /// before functions) so that function-arg classification can see every - /// struct decoder the converter is going to auto-register. + /// auto-registered struct binding. buffered: bool, /// Accumulated Kotlin `external fun ...` blocks, one per wrapped function. - /// Populated by `convert_fn` when Kotlin output is enabled, consumed by - /// [`JniConverter::write_kotlin`]. kotlin_funs: Vec, /// Accumulated Kotlin `data class ...` blocks, one per `#[prebindgen]` - /// struct seen in the source stream. Emitted by `write_kotlin` BEFORE the - /// `internal object { ... }` block so call sites in the same package can - /// see the types. + /// struct seen in the source stream. kotlin_data_classes: Vec, - /// Set of Kotlin FQNs referenced by the emitted externals. Used to derive - /// the final `import` block (same-package and bare names are filtered - /// out). + /// Set of Kotlin FQNs referenced by emitted externals. kotlin_used_fqns: BTreeSet, } @@ -416,8 +653,8 @@ impl JniConverter { /// Drain `iter` on the first call, sort so `#[prebindgen]` struct items /// are processed before functions (so function-arg classification can see - /// every auto-registered struct decoder), then return converted items one - /// at a time from the buffer. Returns `None` once the buffer is drained. + /// every auto-registered struct binding), then return converted items + /// one at a time from the buffer. pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> where I: Iterator, @@ -425,8 +662,6 @@ impl JniConverter { if !self.buffered { self.buffered = true; let mut all: Vec<(syn::Item, SourceLocation)> = iter.by_ref().collect(); - // Stable sort: structs first (`false` < `true`), original order - // preserved within each group. all.sort_by_key(|(it, _)| !matches!(it, syn::Item::Struct(_))); for (item, loc) in all { let converted = self.convert(item, &loc); @@ -436,7 +671,7 @@ impl JniConverter { self.pending.pop_front() } - /// Closure suitable for `itertools::batching`. + /// Closure suitable for `itertools::batching`. Consumes `self`. pub fn into_closure( mut self, ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> @@ -447,9 +682,8 @@ impl JniConverter { } /// Borrowing closure suitable for `itertools::batching`. Unlike - /// [`JniConverter::into_closure`], this does not consume `self`, so the - /// converter survives the pipeline and [`JniConverter::write_kotlin`] can - /// be called after the pipeline completes. + /// [`JniConverter::into_closure`], this does not consume `self`, so + /// [`JniConverter::write_kotlin`] can be called after the pipeline. pub fn as_closure<'a, I>( &'a mut self, ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a @@ -460,9 +694,7 @@ impl JniConverter { } /// Write the accumulated Kotlin `external fun ...` prototypes to the - /// configured output path. No-op when Kotlin output was not enabled via - /// [`Builder::kotlin_output`]. The output file is overwritten if it - /// already exists; parent directories are created as needed. + /// configured output path. No-op when Kotlin output was not enabled. pub fn write_kotlin(&self) -> std::io::Result<()> { let Some(kt) = self.cfg.kotlin.as_ref() else { return Ok(()); @@ -478,8 +710,6 @@ impl JniConverter { } fn render_kotlin(&self, kt: &KotlinConfig) -> String { - // Fold init-block FQN into the used set (deferred to emission time so - // it appears in imports only when the init block is emitted). let mut used = self.kotlin_used_fqns.clone(); if let Some(fqn) = kt.init_load_fqn.as_ref() { if fqn.contains('.') { @@ -539,21 +769,23 @@ impl JniConverter { fn convert_fn(&mut self, func: syn::ItemFn, loc: &SourceLocation) -> syn::ItemFn { let original_name = func.sig.ident.to_string(); let camel = snake_to_camel(&original_name); - let jni_name = format_ident!("{}{}{}", self.cfg.class_prefix, camel, self.cfg.function_suffix); + let jni_name = format_ident!( + "{}{}{}", + self.cfg.class_prefix, + camel, + self.cfg.function_suffix + ); let orig_ident = &func.sig.ident; let source_module = self.cfg.source_module.clone(); - let owned_object = self.cfg.owned_object.clone(); let zresult = self.cfg.zresult.clone(); let throw_exception = self.cfg.throw_exception.clone(); let mut prelude: Vec = Vec::new(); let mut jni_params: Vec = Vec::new(); let mut call_args: Vec = Vec::new(); - // Kotlin param strings accumulated in parallel with `jni_params`. Only - // populated when Kotlin output is enabled. let mut kotlin_params: Vec = Vec::new(); let mut local_kotlin_fqns: BTreeSet = BTreeSet::new(); - let kt_cfg: Option<&KotlinConfig> = self.cfg.kotlin.as_ref(); + let kt_enabled = self.cfg.kotlin.is_some(); for input in &func.sig.inputs { let syn::FnArg::Typed(pat_type) = input else { @@ -565,263 +797,22 @@ impl JniConverter { let name = &pat_ident.ident; let ty = &*pat_type.ty; - match self.classify_arg(ty) { - ArgKind::OpaqueRef(elem) => { - let ptr_ident = format_ident!("{}_ptr", name); - jni_params.push(quote! { #ptr_ident: *const #elem }); - prelude.push(quote! { - let #name = #owned_object::from_raw(#ptr_ident); - }); - call_args.push(quote! { &#name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: Long", - kotlin_param_name(&name.to_string(), /* ptr */ true) - )); - } - } - ArgKind::KeyExprBorrow => { - // `&KeyExpr` in the source signature: single `JObject` - // holder (io.zenoh.jni.JNIKeyExpr) decoded via the - // `KeyExpr` entry in `struct_decoders`. - let decoder = self - .cfg - .struct_decoders - .get("KeyExpr") - .expect("struct_decoder(\"KeyExpr\", ...) not configured"); - jni_params.push(quote! { #name: jni::objects::JObject }); - prelude.push(quote! { - let #name = #decoder(&mut env, &#name)?; - }); - call_args.push(quote! { #name }); - if let Some(kt) = kt_cfg { - let fqn = kt - .struct_kotlin_types - .get("KeyExpr") - .cloned() - .expect("struct_decoder(\"KeyExpr\", ...) Kotlin type not configured"); - let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); - kotlin_params.push(format!( - "{}: {}", - kotlin_param_name(&name.to_string(), false), - short - )); - } - } - ArgKind::KeyExprConsume => { - // `KeyExpr<'static>` by value in the source signature: - // the caller relinquishes ownership of the declared - // handle. Arc::from_raw decrements the refcount at end - // of scope, freeing the handle once no other references - // remain. A cloned inner KeyExpr is passed by value. - let ptr_ident = format_ident!("{}_ptr", name); - let arc_ident = format_ident!("__{}_arc", name); - jni_params.push(quote! { - #ptr_ident: *const zenoh::key_expr::KeyExpr<'static> - }); - prelude.push(quote! { - let #arc_ident = std::sync::Arc::from_raw(#ptr_ident); - let #name = (*#arc_ident).clone(); - }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: Long", - kotlin_param_name(&name.to_string(), true) - )); - } - } - ArgKind::String => { - let decoder = self - .cfg - .string_decoder - .as_ref() - .expect("string_decoder not configured"); - jni_params.push(quote! { #name: jni::objects::JString }); - prelude.push(quote! { - let #name = #decoder(&mut env, &#name)?; - }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: String", - kotlin_param_name(&name.to_string(), false) - )); - } - } - ArgKind::Enum(decoder) => { - jni_params.push(quote! { #name: jni::sys::jint }); - prelude.push(quote! { - let #name = #decoder(#name)?; - }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: Int", - kotlin_param_name(&name.to_string(), false) - )); - } - } - ArgKind::Bool => { - jni_params.push(quote! { #name: jni::sys::jboolean }); - prelude.push(quote! { let #name = #name != 0; }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: Boolean", - kotlin_param_name(&name.to_string(), false) - )); - } - } - ArgKind::Duration => { - jni_params.push(quote! { #name: jni::sys::jlong }); - prelude.push(quote! { - let #name = std::time::Duration::from_millis(#name as u64); - }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: Long", - kotlin_param_name(&name.to_string(), false) - )); - } - } - ArgKind::OptionVecU8 => { - let decoder = self - .cfg - .byte_array_decoder - .as_ref() - .expect("byte_array_decoder not configured"); - jni_params.push(quote! { #name: jni::objects::JByteArray }); - prelude.push(quote! { - let #name = if !#name.is_null() { - Some(#decoder(&env, #name)?) - } else { - None - }; - }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: ByteArray?", - kotlin_param_name(&name.to_string(), false) - )); - } - } - ArgKind::VecU8 => { - let decoder = self - .cfg - .byte_array_decoder - .as_ref() - .expect("byte_array_decoder not configured"); - jni_params.push(quote! { #name: jni::objects::JByteArray }); - prelude.push(quote! { - let #name = #decoder(&env, #name)?; - }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: ByteArray", - kotlin_param_name(&name.to_string(), false) - )); - } - } - ArgKind::Callback { decoder, element_type_name } => { - jni_params.push(quote! { #name: jni::objects::JObject }); - prelude.push(quote! { - let #name = #decoder(&mut env, #name)?; - }); - call_args.push(quote! { #name }); - if let Some(kt) = kt_cfg { - let cb_fqn = kt.callback_kotlin_types.get(&element_type_name).cloned() - .unwrap_or_else(|| panic!( - "callback_decoder({:?}, ...) Kotlin type not configured", - element_type_name - )); - let cb_short = kotlin_register_fqn(&cb_fqn, &mut local_kotlin_fqns); - kotlin_params.push(format!( - "{}: {}", - kotlin_param_name(&name.to_string(), false), - cb_short - )); - } - } - ArgKind::OptionString => { - let decoder = self - .cfg - .string_decoder - .as_ref() - .expect("string_decoder not configured"); - jni_params.push(quote! { #name: jni::objects::JString }); - prelude.push(quote! { - let #name = if !#name.is_null() { - Some(#decoder(&mut env, &#name)?) - } else { - None - }; - }); - call_args.push(quote! { #name }); - if kt_cfg.is_some() { - kotlin_params.push(format!( - "{}: String?", - kotlin_param_name(&name.to_string(), false) - )); - } - } - ArgKind::StructFromJObject { decoder, type_name } => { - jni_params.push(quote! { #name: jni::objects::JObject }); - prelude.push(quote! { - let #name = #decoder(&mut env, &#name)?; - }); - call_args.push(quote! { #name }); - if let Some(kt) = kt_cfg { - let fqn = kt.struct_kotlin_types.get(&type_name).cloned() - .unwrap_or_else(|| panic!( - "struct_decoder({:?}, ...) Kotlin type not configured", - type_name - )); - let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); - kotlin_params.push(format!( - "{}: {}", - kotlin_param_name(&name.to_string(), false), - short - )); - } - } - ArgKind::OptionStructFromJObject { decoder, type_name } => { - jni_params.push(quote! { #name: jni::objects::JObject }); - prelude.push(quote! { - let #name = if !#name.is_null() { - Some(#decoder(&mut env, &#name)?) - } else { - None - }; - }); - call_args.push(quote! { #name }); - if let Some(kt) = kt_cfg { - let fqn = kt.struct_kotlin_types.get(&type_name).cloned() - .unwrap_or_else(|| panic!( - "struct_decoder({:?}, ...) Kotlin type not configured", - type_name - )); - let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); - kotlin_params.push(format!( - "{}: {}?", - kotlin_param_name(&name.to_string(), false), - short - )); - } - } - ArgKind::Unsupported => panic!( - "unsupported parameter type `{}` for `{}` at {loc}", - ty.to_token_stream(), - name - ), - } + let kind = self.classify_arg(ty, name); + self.emit_arg( + kind, + name, + ty, + loc, + &mut prelude, + &mut jni_params, + &mut call_args, + &mut kotlin_params, + &mut local_kotlin_fqns, + kt_enabled, + ); } - // Kotlin return type (None = Unit). Computed in parallel with the Rust - // return type so a single classification drives both outputs. + // Return type. let mut kotlin_ret: Option = None; let (ret_ty_jni, wrap_ok, on_err, closure_ret): ( TokenStream, @@ -840,27 +831,36 @@ impl JniConverter { quote! { () }, quote! { #zresult<()> }, ) - } else if let Some(wrapper) = self.lookup_return_wrapper(&inner) { - if let Some(kt) = kt_cfg { - let fqn = self - .lookup_kotlin_return_type(&inner, kt) - .expect("return_wrapper(...) Kotlin type not configured"); - let short = kotlin_register_fqn(&fqn, &mut local_kotlin_fqns); + } else if let Some(form) = self.lookup_return_form(&inner) { + if kt_enabled { + let kt = form + .kotlin_jni_type + .clone() + .expect("return form Kotlin type not configured"); + let short = kotlin_register_fqn(&kt, &mut local_kotlin_fqns); kotlin_ret = Some(short); } - let ReturnWrapper { - jni_type, - wrap_fn, - default_expr, - } = wrapper; - ( - quote! { #jni_type }, - quote! { #wrap_fn(&mut env, __result) }, - quote! { #default_expr }, - quote! { #zresult<#jni_type> }, - ) + let jni_type = &form.jni_type; + let default_expr = &form.default_expr; + match &form.encode { + ReturnEncode::Wrapper(wrap_fn) => ( + quote! { #jni_type }, + quote! { #wrap_fn(&mut env, __result) }, + quote! { #default_expr }, + quote! { #zresult<#jni_type> }, + ), + ReturnEncode::ArcIntoRaw => ( + quote! { #jni_type }, + quote! { + Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) + }, + quote! { #default_expr }, + quote! { #zresult<#jni_type> }, + ), + } } else { - if kt_cfg.is_some() { + // Fallback: treat as opaque Arc-handle, return `*const T`. + if kt_enabled { kotlin_ret = Some("Long".to_string()); } ( @@ -903,9 +903,8 @@ impl JniConverter { ) -> #ret_ty_jni #body }; - // Assemble and stash the Kotlin `external fun ...` block so - // `write_kotlin()` can emit it later. - if self.cfg.kotlin.is_some() { + // Assemble the Kotlin `external fun ...` block. + if kt_enabled { let kt_fn_name = format!("{}{}", camel, self.cfg.function_suffix); let ret_suffix = match &kotlin_ret { Some(r) => format!(": {}", r), @@ -923,8 +922,6 @@ impl JniConverter { .and_then(|k| k.throws_class_fqn.as_ref()) .is_some() { - // Short name already registered via kotlin_register_fqn for - // the whole file (see write_kotlin); re-derive here. let fqn = self .cfg .kotlin @@ -957,10 +954,8 @@ impl JniConverter { } /// Emit a JNI decoder for a `#[prebindgen]` struct and a matching Kotlin - /// `data class`. The struct item itself is NOT re-emitted into the output - /// stream — only the decoder function is — so the original type stays - /// solely in its home module (e.g. `zenoh_flat::ext`) and is referenced - /// from generated code by its fully-qualified path. + /// `data class`, then auto-register a `TypeBinding` so the struct can + /// appear by value in a wrapped function's signature. fn convert_struct(&mut self, s: syn::ItemStruct, loc: &SourceLocation) -> syn::Item { let struct_name = s.ident.to_string(); let struct_ident = s.ident.clone(); @@ -1053,21 +1048,18 @@ impl JniConverter { } }; - // Auto-register the decoder for future function-arg classification. - // The decoder lives in the same module as the generated wrappers, so a - // bare `syn::Path` resolves correctly at the wrapper call sites. + // Auto-register a TypeBinding for this struct. let decoder_path: syn::Path = syn::parse_str(&format!("decode_{struct_name}")) .expect("generated decoder ident must parse as path"); - self.cfg - .struct_decoders - .insert(struct_name.clone(), decoder_path); - if let Some(kt) = self.cfg.kotlin.as_mut() { - // Same package as the generated Kotlin file → bare name, no FQN. - kt.struct_kotlin_types - .insert(struct_name.clone(), struct_name.clone()); - } + let mut binding = TypeBinding::new(struct_name.clone()); + binding.kotlin_type = Some(struct_name.clone()); // bare — same package + binding.consume = Some(JniForm::new( + "jni::objects::JObject", + "JObject", + ArgDecode::EnvRefMut(decoder_path), + )); + self.cfg.types.insert(struct_name.clone(), binding); - // Accumulate the Kotlin data class for emission by write_kotlin. if self.cfg.kotlin.is_some() { let block = format!( "data class {}(\n{}\n)", @@ -1081,8 +1073,6 @@ impl JniConverter { } /// Classify a `#[prebindgen]` struct field's type for JNI round-tripping. - /// Scope is deliberately narrow (only what the current four configs need) - /// — an unsupported type panics so we notice new cases at codegen time. fn classify_struct_field(&self, ty: &syn::Type) -> StructFieldKind { let syn::Type::Path(tp) = ty else { return StructFieldKind::Unsupported; @@ -1096,34 +1086,21 @@ impl JniConverter { "i64" => StructFieldKind::I64, "f64" => StructFieldKind::F64, _ => { - if let Some(decoder) = self.cfg.enum_decoders.get(&name) { - StructFieldKind::Enum(decoder.clone()) - } else { - StructFieldKind::Unsupported + // Enum decoders are stored as a `Pure` ArgDecode on the + // type's binding's `consume` form. + if let Some(binding) = self.cfg.types.get(&name) { + if let Some(form) = binding.consume.as_ref() { + if let ArgDecode::Pure(p) = &form.decode { + return StructFieldKind::Enum(p.clone()); + } + } } + StructFieldKind::Unsupported } } } - /// Resolve the Kotlin return-type FQN for a `ZResult` inner type `T`. - fn lookup_kotlin_return_type(&self, inner: &syn::Type, kt: &KotlinConfig) -> Option { - let syn::Type::Path(tp) = inner else { return None }; - let seg = tp.path.segments.last()?; - let name = seg.ident.to_string(); - if name == "Vec" { - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return None; - }; - let syn::GenericArgument::Type(elem) = args.args.first()? else { - return None; - }; - let elem_name = type_last_segment(elem)?; - return kt.return_kotlin_types_vec.get(&elem_name).cloned(); - } - kt.return_kotlin_types.get(&name).cloned() - } - - fn lookup_return_wrapper(&self, ty: &syn::Type) -> Option { + fn lookup_return_form(&self, ty: &syn::Type) -> Option<&ReturnForm> { let syn::Type::Path(tp) = ty else { return None }; let seg = tp.path.segments.last()?; let name = seg.ident.to_string(); @@ -1135,18 +1112,43 @@ impl JniConverter { return None; }; let elem_name = type_last_segment(elem)?; - return self.cfg.return_wrappers_vec.get(&elem_name).cloned(); + return self + .cfg + .types + .get(&elem_name) + .and_then(|b| b.returns_vec.as_ref()); } - self.cfg.return_wrappers.get(&name).cloned() + self.cfg.types.get(&name).and_then(|b| b.returns.as_ref()) } - fn classify_arg(&self, ty: &syn::Type) -> ArgKind { + /// Classify a function-arg type into one of the uniform variants below. + fn classify_arg(&self, ty: &syn::Type, _name: &syn::Ident) -> ArgKind { match ty { syn::Type::Reference(r) if r.mutability.is_none() => { - if type_last_segment(&r.elem).map(|s| s == "KeyExpr").unwrap_or(false) { - ArgKind::KeyExprBorrow - } else { - ArgKind::OpaqueRef((*r.elem).clone()) + let elem = &*r.elem; + let last = type_last_segment(elem).unwrap_or_default(); + if let Some(binding) = self.cfg.types.get(&last) { + if let Some(form) = binding.borrow.as_ref() { + return ArgKind::Borrow { + form: form.clone(), + kotlin_override: binding.kotlin_type.clone(), + }; + } + } + // Fallback: treat any unbound `&T` as an `OwnedRef` against a + // raw `*const T` pointer, decoded via the converter-wide + // `owned_object`. This matches the legacy `OpaqueRef` path. + let ptr_ty: syn::Type = syn::parse2(quote! { *const #elem }) + .expect("opaque pointer type must parse"); + let opaque_form = JniForm { + jni_type: ptr_ty, + kotlin_jni_type: "Long".to_string(), + pointer_param: true, + decode: ArgDecode::OwnedRef, + }; + ArgKind::Borrow { + form: opaque_form, + kotlin_override: None, } } syn::Type::ImplTrait(it) => { @@ -1165,56 +1167,304 @@ impl JniConverter { return ArgKind::Unsupported; }; let name = last.ident.to_string(); - if name == "bool" { - return ArgKind::Bool; - } - if name == "String" { - return ArgKind::String; - } - if name == "KeyExpr" { - return ArgKind::KeyExprConsume; - } - if name == "Duration" { - return ArgKind::Duration; - } - if name == "Option" && is_option_of_vec_u8(last) { - return ArgKind::OptionVecU8; - } + if name == "Option" { - if let Some(inner) = option_inner_type_name(last) { - if inner == "String" { - return ArgKind::OptionString; + let inner_seg = last; + if is_option_of_vec_u8(inner_seg) { + if let Some(binding) = self.cfg.types.get("VecU8") { + if let Some(form) = binding.consume.as_ref() { + return ArgKind::OptionConsume { + form: form.clone(), + kotlin_override: None, + }; + } } - if let Some(decoder) = self.cfg.struct_decoders.get(&inner) { - return ArgKind::OptionStructFromJObject { - decoder: decoder.clone(), - type_name: inner, - }; + return ArgKind::Unsupported; + } + if let Some(inner) = option_inner_type_name(inner_seg) { + if let Some(binding) = self.cfg.types.get(&inner) { + if let Some(form) = binding.consume.as_ref() { + return ArgKind::OptionConsume { + form: form.clone(), + kotlin_override: binding.kotlin_type.clone(), + }; + } } } + return ArgKind::Unsupported; } + if name == "Vec" && is_vec_of_u8(last) { - return ArgKind::VecU8; - } - if let Some(decoder) = self.cfg.enum_decoders.get(&name) { - return ArgKind::Enum(decoder.clone()); + if let Some(binding) = self.cfg.types.get("VecU8") { + if let Some(form) = binding.consume.as_ref() { + return ArgKind::Consume { + form: form.clone(), + kotlin_override: None, + }; + } + } + return ArgKind::Unsupported; } - if let Some(decoder) = self.cfg.struct_decoders.get(&name) { - return ArgKind::StructFromJObject { - decoder: decoder.clone(), - type_name: name, - }; + + if let Some(binding) = self.cfg.types.get(&name) { + if let Some(form) = binding.consume.as_ref() { + return ArgKind::Consume { + form: form.clone(), + kotlin_override: binding.kotlin_type.clone(), + }; + } } + ArgKind::Unsupported } _ => ArgKind::Unsupported, } } + + #[allow(clippy::too_many_arguments)] + fn emit_arg( + &self, + kind: ArgKind, + name: &syn::Ident, + ty: &syn::Type, + loc: &SourceLocation, + prelude: &mut Vec, + jni_params: &mut Vec, + call_args: &mut Vec, + kotlin_params: &mut Vec, + local_kotlin_fqns: &mut BTreeSet, + kt_enabled: bool, + ) { + match kind { + ArgKind::Consume { + form, + kotlin_override, + } => { + self.emit_consume_or_borrow( + /* borrow */ false, + form, + kotlin_override, + name, + prelude, + jni_params, + call_args, + kotlin_params, + local_kotlin_fqns, + kt_enabled, + ); + } + ArgKind::Borrow { + form, + kotlin_override, + } => { + self.emit_consume_or_borrow( + /* borrow */ true, + form, + kotlin_override, + name, + prelude, + jni_params, + call_args, + kotlin_params, + local_kotlin_fqns, + kt_enabled, + ); + } + ArgKind::OptionConsume { + form, + kotlin_override, + } => { + if !form.is_jni_object() { + panic!( + "Option<{}> requires a JNI-object form for `{}`", + ty.to_token_stream(), + name + ); + } + let jt = &form.jni_type; + jni_params.push(quote! { #name: #jt }); + let inner = self.decode_expr(&form.decode, name); + prelude.push(quote! { + let #name = if !#name.is_null() { + Some(#inner) + } else { + None + }; + }); + call_args.push(quote! { #name }); + if kt_enabled { + let kt_decl = self.kotlin_arg_type(&form, kotlin_override.as_deref()); + let short = kotlin_register_fqn(&kt_decl, local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}?", + kotlin_param_name(&name.to_string(), form.pointer_param), + short + )); + } + } + ArgKind::Callback { + decoder, + element_type_name, + } => { + jni_params.push(quote! { #name: jni::objects::JObject }); + prelude.push(quote! { + let #name = #decoder(&mut env, #name)?; + }); + call_args.push(quote! { #name }); + if kt_enabled { + let cb_fqn = self + .cfg + .callback_kotlin_types + .get(&element_type_name) + .cloned() + .unwrap_or_else(|| { + panic!( + "callback_decoder({:?}, ...) Kotlin type not configured", + element_type_name + ) + }); + let cb_short = kotlin_register_fqn(&cb_fqn, local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}", + kotlin_param_name(&name.to_string(), false), + cb_short + )); + } + } + ArgKind::Unsupported => panic!( + "unsupported parameter type `{}` for `{}` at {loc}", + ty.to_token_stream(), + name + ), + } + } + + #[allow(clippy::too_many_arguments)] + fn emit_consume_or_borrow( + &self, + borrow: bool, + form: JniForm, + kotlin_override: Option, + name: &syn::Ident, + prelude: &mut Vec, + jni_params: &mut Vec, + call_args: &mut Vec, + kotlin_params: &mut Vec, + local_kotlin_fqns: &mut BTreeSet, + kt_enabled: bool, + ) { + let jt = &form.jni_type; + let pat = if matches!(form.decode, ArgDecode::OwnedRef | ArgDecode::ConsumeArc) { + // Raw-pointer slots get a `_ptr` ident in the JNI signature. + format_ident!("{}_ptr", name) + } else { + name.clone() + }; + jni_params.push(quote! { #pat: #jt }); + + match &form.decode { + ArgDecode::EnvRefMut(path) => { + prelude.push(quote! { let #name = #path(&mut env, &#pat)?; }); + } + ArgDecode::EnvByVal(path) => { + prelude.push(quote! { let #name = #path(&env, #pat)?; }); + } + ArgDecode::Pure(path) => { + prelude.push(quote! { let #name = #path(#pat)?; }); + } + ArgDecode::Inline(f) => { + let expr = f.call(&pat); + prelude.push(quote! { let #name = #expr; }); + } + ArgDecode::OwnedRef => { + let owned = &self.cfg.owned_object; + prelude.push(quote! { let #name = #owned::from_raw(#pat); }); + } + ArgDecode::ConsumeArc => { + let arc_ident = format_ident!("__{}_arc", name); + prelude.push(quote! { + let #arc_ident = std::sync::Arc::from_raw(#pat); + let #name = (*#arc_ident).clone(); + }); + } + } + + // The wrapped function call site: by-value vs by-reference. + if borrow && matches!(form.decode, ArgDecode::OwnedRef) { + // OwnedRef pattern: pass `&name` to match historical OpaqueRef behavior. + call_args.push(quote! { &#name }); + } else { + call_args.push(quote! { #name }); + } + + if kt_enabled { + let kt_decl = self.kotlin_arg_type(&form, kotlin_override.as_deref()); + let short = kotlin_register_fqn(&kt_decl, local_kotlin_fqns); + kotlin_params.push(format!( + "{}: {}", + kotlin_param_name(&name.to_string(), form.pointer_param), + short + )); + } + } + + fn decode_expr(&self, decode: &ArgDecode, input: &syn::Ident) -> TokenStream { + match decode { + ArgDecode::EnvRefMut(path) => quote! { #path(&mut env, &#input)? }, + ArgDecode::EnvByVal(path) => quote! { #path(&env, #input)? }, + ArgDecode::Pure(path) => quote! { #path(#input)? }, + ArgDecode::Inline(f) => f.call(input), + ArgDecode::OwnedRef => { + let owned = &self.cfg.owned_object; + quote! { #owned::from_raw(#input) } + } + ArgDecode::ConsumeArc => { + quote! { (*std::sync::Arc::from_raw(#input)).clone() } + } + } + } + + /// Resolve the Kotlin parameter type for a JniForm. For object-typed + /// JNI wires (`JObject`) we use the binding's `kotlin_type` FQN; for + /// primitive wires (jboolean/jlong/jint/JString/JByteArray/raw ptrs) we + /// use the form's `kotlin_jni_type`. + fn kotlin_arg_type(&self, form: &JniForm, kotlin_override: Option<&str>) -> String { + match jni_object_kind(&form.jni_type) { + Some(JniObjectKind::JObject) => kotlin_override + .map(str::to_string) + .unwrap_or_else(|| form.kotlin_jni_type.clone()), + _ => form.kotlin_jni_type.clone(), + } + } +} + +// ===================================================================== +// ArgKind — the slim, uniform classification +// ===================================================================== + +enum ArgKind { + Consume { + form: JniForm, + kotlin_override: Option, + }, + Borrow { + form: JniForm, + kotlin_override: Option, + }, + OptionConsume { + form: JniForm, + kotlin_override: Option, + }, + Callback { + decoder: syn::Path, + element_type_name: String, + }, + Unsupported, } /// Field-type classification for `#[prebindgen]` struct fields — narrower /// than [`ArgKind`] because structs only need a round-trippable primitive / -/// enum representation (no refs, no callbacks, no `Option<...>`). +/// enum representation. enum StructFieldKind { Bool, I64, @@ -1223,59 +1473,23 @@ enum StructFieldKind { Unsupported, } -enum ArgKind { - OpaqueRef(syn::Type), - /// `&KeyExpr` in the source signature — decoded as a `JNIKeyExpr` - /// holder via the registered `KeyExpr` struct decoder. The handle is - /// borrowed; the caller retains ownership. - KeyExprBorrow, - /// `KeyExpr<'static>` (by value) in the source signature — the caller - /// relinquishes ownership of the declared handle. Generated wrapper - /// takes the raw pointer and drops the `Arc` at end of scope. - KeyExprConsume, - /// `String` → `JString` decoded via `string_decoder`. - String, - Enum(syn::Path), - Bool, - Duration, - /// `Option>` → `JByteArray` decoded via `byte_array_decoder`. - OptionVecU8, - /// `Vec` → `JByteArray` decoded via `byte_array_decoder`. - VecU8, - /// `Option` → nullable `JString`. - OptionString, - /// Struct type registered via `struct_decoder` → single `JObject` arg - /// decoded via the registered decoder. - StructFromJObject { - decoder: syn::Path, - type_name: String, - }, - /// `Option` where `T` is registered via `struct_decoder` → nullable - /// `JObject`, `None` when the JObject is null. - OptionStructFromJObject { - decoder: syn::Path, - type_name: String, - }, - /// `impl Fn(T) + Send + Sync + 'static` → single `JObject` decoded via the - /// callback decoder registered for `T`. Zero-arg `impl Fn() + Send + Sync` - /// callbacks are looked up under the key `"()"`. - Callback { - decoder: syn::Path, - element_type_name: String, - }, - Unsupported, +#[derive(Clone, Copy)] +enum JniObjectKind { + JObject, + JString, + JByteArray, } -fn parse_return_wrapper( - jni_type: impl AsRef, - wrap_fn: impl AsRef, - default_expr: impl AsRef, -) -> ReturnWrapper { - ReturnWrapper { - jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid return wrapper jni_type"), - wrap_fn: syn::parse_str(wrap_fn.as_ref()).expect("invalid return wrapper wrap_fn path"), - default_expr: syn::parse_str(default_expr.as_ref()) - .expect("invalid return wrapper default_expr"), +/// Recognize JNI object-shaped wire types. Used by the `Option` combinator +/// (which needs `is_null()`) and by the Kotlin parameter-type derivation. +fn jni_object_kind(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + let last = tp.path.segments.last()?; + match last.ident.to_string().as_str() { + "JObject" => Some(JniObjectKind::JObject), + "JString" => Some(JniObjectKind::JString), + "JByteArray" => Some(JniObjectKind::JByteArray), + _ => None, } } @@ -1287,7 +1501,7 @@ fn type_last_segment(ty: &syn::Type) -> Option { /// Look through the trait bounds of an `impl Fn(...) + ...` for a `Fn`-family /// trait and return the lookup key for its argument type: /// - `impl Fn(T)` → `Some("T")` -/// - `impl Fn()` → `Some("()")` (zero-arg callbacks, e.g. on-close handlers) +/// - `impl Fn()` → `Some("()")` fn extract_fn_arg_type_name( bounds: &syn::punctuated::Punctuated, ) -> Option { @@ -1306,8 +1520,7 @@ fn extract_fn_arg_type_name( None } -/// Return the last-segment name of the single generic argument of an -/// `Option<...>` path segment, if any (e.g. `Option` → `Some("String")`). +/// Last-segment name of the single generic argument of an `Option<...>`. fn option_inner_type_name(seg: &syn::PathSegment) -> Option { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return None; @@ -1318,7 +1531,6 @@ fn option_inner_type_name(seg: &syn::PathSegment) -> Option { type_last_segment(inner) } -/// Check whether an `Option<...>` path segment wraps exactly `Vec`. fn is_option_of_vec_u8(seg: &syn::PathSegment) -> bool { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return false; @@ -1338,7 +1550,6 @@ fn is_option_of_vec_u8(seg: &syn::PathSegment) -> bool { is_vec_of_u8(inner_seg) } -/// Check whether a `Vec<...>` path segment has element type `u8`. fn is_vec_of_u8(seg: &syn::PathSegment) -> bool { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return false; @@ -1391,7 +1602,7 @@ fn snake_to_camel(s: &str) -> String { } /// Map a Rust snake_case arg name to its Kotlin camelCase form, appending -/// `"Ptr"` for raw-pointer slots (OpaqueRef / consumed KeyExpr). +/// `"Ptr"` for raw-pointer slots. fn kotlin_param_name(rust_name: &str, is_pointer: bool) -> String { let base = snake_to_camel(rust_name); if is_pointer { @@ -1402,8 +1613,7 @@ fn kotlin_param_name(rust_name: &str, is_pointer: bool) -> String { } /// Record `fqn` in `used` if it looks fully-qualified (contains `.`) and -/// return the short name used at the emission site. Bare names are returned -/// as-is and not added to the import set. +/// return the short name used at the emission site. fn kotlin_register_fqn(fqn: &str, used: &mut BTreeSet) -> String { if fqn.contains('.') { used.insert(fqn.to_string()); diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 5779f83a..8f3ac423 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use zenoh_flat::jni_converter::{ArgDecode, JniForm, TypeBinding}; fn main() { let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); @@ -47,10 +48,19 @@ fn main() { "crate::sample_callback::process_kotlin_on_close_callback", "io.zenoh.jni.callbacks.JNIOnCloseCallback", ) - .struct_decoder( - "KeyExpr", - "crate::key_expr::decode_jni_key_expr", - "JNIKeyExpr", + // KeyExpr by-value: the JNI side passes `Arc::into_raw(Arc::new(KeyExpr))` + // as a raw pointer; the wrapper reconstructs the Arc, clones the inner + // KeyExpr, and drops the Arc at end of scope. The full path is required + // so the generated `*const T` parameter type resolves at the include site. + .type_binding( + TypeBinding::new("KeyExpr").consume( + JniForm::new( + "*const zenoh::key_expr::KeyExpr<'static>", + "Long", + ArgDecode::ConsumeArc, + ) + .pointer_param(true), + ), ) .struct_decoder( "Encoding", From d9b1c86bffee06d878d0a1353be20d897eccaeea Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 18:37:05 +0200 Subject: [PATCH 107/126] common type binding method --- zenoh-flat/src/jni_converter.rs | 147 +++++++++----------------------- zenoh-jni/build.rs | 76 +++++++++++------ 2 files changed, 92 insertions(+), 131 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 22762c3c..c6089088 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -43,10 +43,14 @@ //! .throw_exception("crate::throw_exception") //! .string_decoder("crate::utils::decode_string") //! .byte_array_decoder("crate::utils::decode_byte_array") -//! .struct_decoder( -//! "KeyExpr", -//! "crate::key_expr::decode_jni_key_expr", -//! "JNIKeyExpr", +//! .type_binding( +//! TypeBinding::new("KeyExpr").kotlin("JNIKeyExpr").consume( +//! JniForm::new( +//! "jni::objects::JObject", +//! "JObject", +//! ArgDecode::env_ref_mut("crate::key_expr::decode_jni_key_expr"), +//! ), +//! ), //! ) //! .build(); //! source @@ -111,6 +115,36 @@ pub enum ArgDecode { ConsumeArc, } +impl ArgDecode { + /// `ArgDecode::Pure` from a path string (parsed lazily). + pub fn pure(path: impl AsRef) -> Self { + ArgDecode::Pure(syn::parse_str(path.as_ref()).expect("invalid ArgDecode::pure path")) + } + + /// `ArgDecode::EnvRefMut` from a path string. + pub fn env_ref_mut(path: impl AsRef) -> Self { + ArgDecode::EnvRefMut( + syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_ref_mut path"), + ) + } + + /// `ArgDecode::EnvByVal` from a path string. + pub fn env_by_val(path: impl AsRef) -> Self { + ArgDecode::EnvByVal( + syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_by_val path"), + ) + } +} + +impl ReturnEncode { + /// `ReturnEncode::Wrapper` from a path string. + pub fn wrapper(path: impl AsRef) -> Self { + ReturnEncode::Wrapper( + syn::parse_str(path.as_ref()).expect("invalid ReturnEncode::wrapper path"), + ) + } +} + /// Clonable closure that produces a TokenStream from the JNI input ident. #[derive(Clone)] pub struct InlineFn(Arc TokenStream + Send + Sync>); @@ -444,62 +478,13 @@ impl Builder { self } - /// Register a decoder for an enum type. Equivalent to: - /// ```ignore - /// .type_binding(TypeBinding::new(name) - /// .consume(JniForm::new("jni::sys::jint", "Int", ArgDecode::Pure()))) - /// ``` - pub fn enum_decoder( - mut self, - type_name: impl Into, - decoder: impl AsRef, - ) -> Self { - let name = type_name.into(); - let p: syn::Path = - syn::parse_str(decoder.as_ref()).expect("invalid enum_decoder path"); - let binding = self - .types - .entry(name.clone()) - .or_insert_with(|| TypeBinding::new(name)); - binding.consume = Some(JniForm::new( - "jni::sys::jint", - "Int", - ArgDecode::Pure(p), - )); - self - } - - /// Register a decoder for a struct parameter passed across JNI as a plain - /// `JObject` (typically a Kotlin data class). The decoder must have - /// signature `fn(&mut JNIEnv, &JObject) -> ZResult`. `kotlin_type` is - /// the Kotlin type name (FQN if out-of-package, bare otherwise). - pub fn struct_decoder( - mut self, - type_name: impl Into, - decoder: impl AsRef, - kotlin_type: impl Into, - ) -> Self { - let name = type_name.into(); - let kt = kotlin_type.into(); - let p: syn::Path = - syn::parse_str(decoder.as_ref()).expect("invalid struct_decoder path"); - let binding = self - .types - .entry(name.clone()) - .or_insert_with(|| TypeBinding::new(name)); - binding.kotlin_type = Some(kt); - binding.consume = Some(JniForm::new( - "jni::objects::JObject", - "JObject", - ArgDecode::EnvRefMut(p), - )); - self - } - /// Register a decoder for an `impl Fn(T) + Send + Sync + 'static` callback /// parameter. `element_type_name` is the last path segment of `T` /// (e.g. `"Sample"`, `"Query"`, `"Reply"`). The decoder must have /// signature `fn(&mut JNIEnv, JObject) -> ZResult`. + /// Callbacks have a fundamentally different shape (a closure-builder, not + /// a value decoder) so they are registered separately from + /// [`Builder::type_binding`]. pub fn callback_decoder( mut self, element_type_name: impl Into, @@ -515,56 +500,6 @@ impl Builder { self } - /// Register a return-type wrapper for `ZResult` where `T`'s - /// last-segment name equals `type_name`. `wrap_fn` must have signature - /// `fn(&mut JNIEnv, T) -> ZResult`. `default_expr` is the value - /// returned on error (before the exception is thrown on the JVM side). - pub fn return_wrapper( - mut self, - type_name: impl Into, - jni_type: impl AsRef, - wrap_fn: impl AsRef, - default_expr: impl AsRef, - kotlin_type: impl Into, - ) -> Self { - let name = type_name.into(); - let kt = kotlin_type.into(); - let wrap: syn::Path = - syn::parse_str(wrap_fn.as_ref()).expect("invalid return_wrapper wrap_fn path"); - let mut form = ReturnForm::new(jni_type, ReturnEncode::Wrapper(wrap), default_expr); - form.kotlin_jni_type = Some(kt); - let binding = self - .types - .entry(name.clone()) - .or_insert_with(|| TypeBinding::new(name)); - binding.returns = Some(form); - self - } - - /// Like [`Builder::return_wrapper`] but applies when `T` is `Vec` - /// with `E`'s last-segment name equal to `element_type_name`. - pub fn return_wrapper_vec( - mut self, - element_type_name: impl Into, - jni_type: impl AsRef, - wrap_fn: impl AsRef, - default_expr: impl AsRef, - kotlin_type: impl Into, - ) -> Self { - let name = element_type_name.into(); - let kt = kotlin_type.into(); - let wrap: syn::Path = - syn::parse_str(wrap_fn.as_ref()).expect("invalid return_wrapper_vec wrap_fn path"); - let mut form = ReturnForm::new(jni_type, ReturnEncode::Wrapper(wrap), default_expr); - form.kotlin_jni_type = Some(kt); - let binding = self - .types - .entry(name.clone()) - .or_insert_with(|| TypeBinding::new(name)); - binding.returns_vec = Some(form); - self - } - /// Enable Kotlin-side prototype generation. `path` is where the `.kt` /// file will be written when [`JniConverter::write_kotlin`] is called. pub fn kotlin_output(mut self, path: impl Into) -> Self { diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 8f3ac423..cb2e2570 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,5 +1,17 @@ use itertools::Itertools; -use zenoh_flat::jni_converter::{ArgDecode, JniForm, TypeBinding}; +use zenoh_flat::jni_converter::{ArgDecode, JniForm, ReturnEncode, ReturnForm, TypeBinding}; + +fn enum_binding(name: &str, decoder: &str) -> TypeBinding { + TypeBinding::new(name).consume(JniForm::new("jni::sys::jint", "Int", ArgDecode::pure(decoder))) +} + +fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { + TypeBinding::new(name).kotlin(kotlin).consume(JniForm::new( + "jni::objects::JObject", + "JObject", + ArgDecode::env_ref_mut(decoder), + )) +} fn main() { let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); @@ -19,15 +31,6 @@ fn main() { .kotlin_class("JNISessionNative") .kotlin_throws("io.zenoh.exceptions.ZError") .kotlin_init("io.zenoh.ZenohLoad") - .enum_decoder( - "CongestionControl", - "crate::utils::decode_congestion_control", - ) - .enum_decoder("Priority", "crate::utils::decode_priority") - .enum_decoder("Reliability", "crate::utils::decode_reliability") - .enum_decoder("QueryTarget", "crate::utils::decode_query_target") - .enum_decoder("ConsolidationMode", "crate::utils::decode_consolidation") - .enum_decoder("ReplyKeyExpr", "crate::utils::decode_reply_key_expr") .callback_decoder( "Sample", "crate::sample_callback::process_kotlin_sample_callback", @@ -48,6 +51,24 @@ fn main() { "crate::sample_callback::process_kotlin_on_close_callback", "io.zenoh.jni.callbacks.JNIOnCloseCallback", ) + .type_binding(enum_binding( + "CongestionControl", + "crate::utils::decode_congestion_control", + )) + .type_binding(enum_binding("Priority", "crate::utils::decode_priority")) + .type_binding(enum_binding("Reliability", "crate::utils::decode_reliability")) + .type_binding(enum_binding( + "QueryTarget", + "crate::utils::decode_query_target", + )) + .type_binding(enum_binding( + "ConsolidationMode", + "crate::utils::decode_consolidation", + )) + .type_binding(enum_binding( + "ReplyKeyExpr", + "crate::utils::decode_reply_key_expr", + )) // KeyExpr by-value: the JNI side passes `Arc::into_raw(Arc::new(KeyExpr))` // as a raw pointer; the wrapper reconstructs the Arc, clones the inner // KeyExpr, and drops the Arc at end of scope. The full path is required @@ -62,24 +83,29 @@ fn main() { .pointer_param(true), ), ) - .struct_decoder( + .type_binding(jobject_consume( "Encoding", "crate::utils::decode_jni_encoding", "JNIEncoding", - ) - .return_wrapper( - "ZenohId", - "jni::sys::jbyteArray", - "crate::zenoh_id::zenoh_id_to_byte_array", - "jni::objects::JByteArray::default().as_raw()", - "ByteArray", - ) - .return_wrapper_vec( - "ZenohId", - "jni::sys::jobject", - "crate::zenoh_id::zenoh_ids_to_java_list", - "jni::objects::JObject::default().as_raw()", - "List", + )) + .type_binding( + TypeBinding::new("ZenohId") + .returns( + ReturnForm::new( + "jni::sys::jbyteArray", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_id_to_byte_array"), + "jni::objects::JByteArray::default().as_raw()", + ) + .kotlin("ByteArray"), + ) + .returns_vec( + ReturnForm::new( + "jni::sys::jobject", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_ids_to_java_list"), + "jni::objects::JObject::default().as_raw()", + ) + .kotlin("List"), + ), ) .build(); From 0eca3707a38f4e31cc534ea07eb7f2eec10b9f4b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 19:09:43 +0200 Subject: [PATCH 108/126] shared type binding --- zenoh-flat/src/jni_converter.rs | 19 ++++++ zenoh-flat/src/jni_type_binding.rs | 97 ++++++++++++++++++++++++++++++ zenoh-flat/src/lib.rs | 1 + zenoh-jni/build.rs | 46 ++++++++------ 4 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 zenoh-flat/src/jni_type_binding.rs diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index c6089088..66abba23 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -243,6 +243,11 @@ impl ReturnForm { } impl TypeBinding { + /// Short type name this binding is keyed under (e.g. `"KeyExpr"`). + pub fn name(&self) -> &str { + &self.name + } + pub fn new(name: impl Into) -> Self { Self { name: name.into(), @@ -445,6 +450,20 @@ impl Builder { self } + /// Ingest a reusable [`crate::jni_type_binding::JniTypeBinding`] + /// collection. Type bindings, callback decoders, and Kotlin callback + /// names are all merged into the builder; entries in the collection + /// override entries already present in the builder with the same key. + pub fn jni_type_binding( + mut self, + bindings: crate::jni_type_binding::JniTypeBinding, + ) -> Self { + self.types.extend(bindings.types); + self.callback_decoders.extend(bindings.callback_decoders); + self.callback_kotlin_types.extend(bindings.callback_kotlin_types); + self + } + /// Path of the function that decodes a `JString` into `String`. Used by /// the built-in `String` binding. pub fn string_decoder(mut self, path: impl AsRef) -> Self { diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs new file mode 100644 index 00000000..c430e59f --- /dev/null +++ b/zenoh-flat/src/jni_type_binding.rs @@ -0,0 +1,97 @@ +//! Reusable collection of JNI type bindings. +//! +//! [`JniTypeBinding`] aggregates a set of [`TypeBinding`]s plus callback +//! registrations into a single value that can be defined once and ingested +//! into many [`crate::jni_converter::Builder`] instances. Useful when a +//! project emits several `JniConverter` outputs (e.g. one per JNI class) that +//! share a common vocabulary of types. +//! +//! ```ignore +//! use zenoh_flat::jni_converter::{ArgDecode, JniConverter, JniForm, TypeBinding}; +//! use zenoh_flat::jni_type_binding::JniTypeBinding; +//! +//! let common = JniTypeBinding::new() +//! .type_binding( +//! TypeBinding::new("KeyExpr").consume( +//! JniForm::new( +//! "*const zenoh::key_expr::KeyExpr<'static>", +//! "Long", +//! ArgDecode::ConsumeArc, +//! ) +//! .pointer_param(true), +//! ), +//! ) +//! .callback_decoder( +//! "Sample", +//! "crate::sample_callback::process_kotlin_sample_callback", +//! "io.zenoh.jni.callbacks.JNISubscriberCallback", +//! ); +//! +//! let session_converter = JniConverter::builder() +//! .class_prefix("Java_io_zenoh_jni_JNISessionNative_") +//! .jni_type_binding(common.clone()) +//! // ...other builder calls... +//! .build(); +//! +//! let publisher_converter = JniConverter::builder() +//! .class_prefix("Java_io_zenoh_jni_JNIPublisherNative_") +//! .jni_type_binding(common) +//! // ...other builder calls... +//! .build(); +//! ``` + +use std::collections::HashMap; + +use crate::jni_converter::TypeBinding; + +/// Reusable collection of [`TypeBinding`]s and callback registrations. +/// +/// Built fluently and consumed by +/// [`crate::jni_converter::Builder::jni_type_binding`]. `Clone` so the same +/// set can be ingested into multiple builders. +#[derive(Default, Clone)] +pub struct JniTypeBinding { + pub(crate) types: HashMap, + pub(crate) callback_decoders: HashMap, + pub(crate) callback_kotlin_types: HashMap, +} + +impl JniTypeBinding { + pub fn new() -> Self { + Self::default() + } + + /// Add (or replace) a [`TypeBinding`] in this collection. + pub fn type_binding(mut self, binding: TypeBinding) -> Self { + self.types.insert(binding.name().to_string(), binding); + self + } + + /// Register a callback decoder. `element_type_name` is the last path + /// segment of the callback's argument type (e.g. `"Sample"`); use + /// `"()"` for zero-arg callbacks. The decoder must have signature + /// `fn(&mut JNIEnv, JObject) -> ZResult`. + pub fn callback_decoder( + mut self, + element_type_name: impl Into, + decoder: impl AsRef, + kotlin_type: impl Into, + ) -> Self { + let path: syn::Path = syn::parse_str(decoder.as_ref()) + .expect("invalid callback_decoder path"); + let name = element_type_name.into(); + let kt = kotlin_type.into(); + self.callback_decoders.insert(name.clone(), path); + self.callback_kotlin_types.insert(name, kt); + self + } + + /// Merge another [`JniTypeBinding`] into this one. Entries in `other` + /// override entries with the same key in `self`. + pub fn merge(mut self, other: JniTypeBinding) -> Self { + self.types.extend(other.types); + self.callback_decoders.extend(other.callback_decoders); + self.callback_kotlin_types.extend(other.callback_kotlin_types); + self + } +} diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index d11f274d..3cdbcf9e 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -8,5 +8,6 @@ pub mod errors; #[cfg(feature = "zenoh-ext")] pub mod ext; pub mod jni_converter; +pub mod jni_type_binding; pub mod session; diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index cb2e2570..537c3f66 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,5 +1,6 @@ use itertools::Itertools; use zenoh_flat::jni_converter::{ArgDecode, JniForm, ReturnEncode, ReturnForm, TypeBinding}; +use zenoh_flat::jni_type_binding::JniTypeBinding; fn enum_binding(name: &str, decoder: &str) -> TypeBinding { TypeBinding::new(name).consume(JniForm::new("jni::sys::jint", "Int", ArgDecode::pure(decoder))) @@ -13,24 +14,12 @@ fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { )) } -fn main() { - let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); - - let mut converter = zenoh_flat::jni_converter::JniConverter::builder() - .class_prefix("Java_io_zenoh_jni_JNISessionNative_") - .function_suffix("ViaJNI") - .source_module("zenoh_flat::session") - .struct_source_module("zenoh_flat::ext") - .owned_object("crate::owned_object::OwnedObject") - .zresult("crate::errors::ZResult") - .throw_exception("crate::throw_exception") - .string_decoder("crate::utils::decode_string") - .byte_array_decoder("crate::utils::decode_byte_array") - .kotlin_output("../zenoh-jni/generated-kotlin/io/zenoh/jni/JNISessionNative.kt") - .kotlin_package("io.zenoh.jni") - .kotlin_class("JNISessionNative") - .kotlin_throws("io.zenoh.exceptions.ZError") - .kotlin_init("io.zenoh.ZenohLoad") +/// Type vocabulary shared across every `JniConverter` build in this crate. +/// Defined once and ingested via `Builder::jni_type_binding(...)` so each +/// generated JNI surface (session, publisher, subscriber, ...) sees the same +/// types without duplicating registrations. +fn shared_bindings() -> JniTypeBinding { + JniTypeBinding::new() .callback_decoder( "Sample", "crate::sample_callback::process_kotlin_sample_callback", @@ -107,6 +96,27 @@ fn main() { .kotlin("List"), ), ) +} + +fn main() { + let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); + + let mut converter = zenoh_flat::jni_converter::JniConverter::builder() + .class_prefix("Java_io_zenoh_jni_JNISessionNative_") + .function_suffix("ViaJNI") + .source_module("zenoh_flat::session") + .struct_source_module("zenoh_flat::ext") + .owned_object("crate::owned_object::OwnedObject") + .zresult("crate::errors::ZResult") + .throw_exception("crate::throw_exception") + .string_decoder("crate::utils::decode_string") + .byte_array_decoder("crate::utils::decode_byte_array") + .kotlin_output("../zenoh-jni/generated-kotlin/io/zenoh/jni/JNISessionNative.kt") + .kotlin_package("io.zenoh.jni") + .kotlin_class("JNISessionNative") + .kotlin_throws("io.zenoh.exceptions.ZError") + .kotlin_init("io.zenoh.ZenohLoad") + .jni_type_binding(shared_bindings()) .build(); let bindings_file =source From d61291b8aa20e5f41b6fbc0b1df4e99e864eaa0a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 22:44:13 +0200 Subject: [PATCH 109/126] callback in type bindings --- zenoh-flat/src/jni_converter.rs | 110 ++++++++++++++++------------- zenoh-flat/src/jni_type_binding.rs | 34 +++++---- 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 66abba23..ab5ae7bd 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -11,10 +11,12 @@ //! The converter is fully data-driven: every Rust type that can appear in a //! `#[prebindgen]` function's signature is described by a [`TypeBinding`] //! registered up-front via [`Builder::type_binding`]. A binding declares up to -//! four forms: +//! five forms: //! //! * `consume` — used when the type appears by value as a parameter (`T`); //! * `borrow` — used when the type appears as a shared reference (`&T`); +//! * `callback` — used when the type appears as the element of an +//! `impl Fn(T) + Send + Sync + 'static` parameter; //! * `returns` — used when the type appears in `ZResult` as a return value; //! * `returns_vec` — used when the type appears as `ZResult>`. //! @@ -74,8 +76,9 @@ use prebindgen::SourceLocation; // ===================================================================== /// Per-type description of how a Rust type is represented across the JNI -/// boundary. A type may declare up to four forms: -/// `consume` (`T` parameter), `borrow` (`&T` parameter), `returns` +/// boundary. A type may declare up to five forms: +/// `consume` (`T` parameter), `borrow` (`&T` parameter), `callback` +/// (`impl Fn(T) + Send + Sync + 'static` parameter), `returns` /// (`ZResult` return), and `returns_vec` (`ZResult>` return). #[derive(Clone)] pub struct TypeBinding { @@ -88,6 +91,7 @@ pub struct TypeBinding { kotlin_type: Option, consume: Option, borrow: Option, + callback: Option, returns: Option, returns_vec: Option, } @@ -203,6 +207,28 @@ impl JniForm { } } +/// Describes how an `impl Fn(T) + Send + Sync + 'static` callback parameter +/// (keyed under the element type `T`) is decoded. The decoder must have +/// signature +/// `fn(&mut JNIEnv, JObject) -> ZResult`. +#[derive(Clone)] +pub struct CallbackForm { + pub(crate) decoder: syn::Path, + /// Kotlin FQN of the callback interface, e.g. + /// `"io.zenoh.jni.callbacks.JNISubscriberCallback"`. + pub(crate) kotlin_type: String, +} + +impl CallbackForm { + pub fn new(decoder: impl AsRef, kotlin_type: impl Into) -> Self { + Self { + decoder: syn::parse_str(decoder.as_ref()) + .expect("invalid CallbackForm decoder path"), + kotlin_type: kotlin_type.into(), + } + } +} + #[derive(Clone)] pub enum ReturnEncode { /// `Ok((&mut env, __result)?)` — wrapping function returns @@ -254,6 +280,7 @@ impl TypeBinding { kotlin_type: None, consume: None, borrow: None, + callback: None, returns: None, returns_vec: None, } @@ -274,6 +301,11 @@ impl TypeBinding { self } + pub fn callback(mut self, form: CallbackForm) -> Self { + self.callback = Some(form); + self + } + pub fn returns(mut self, form: ReturnForm) -> Self { self.returns = Some(form); self @@ -301,17 +333,10 @@ pub struct Builder { owned_object: syn::Path, zresult: syn::Path, throw_exception: syn::Path, - /// Primary type registry keyed by short type name. + /// Primary type registry keyed by short type name. Callback registrations + /// (for `impl Fn(T) + Send + Sync + 'static` parameters) live here too, + /// as the [`CallbackForm`] slot of the element type's binding. types: HashMap, - /// Map from callback element type name (e.g. `"Sample"`) to the decoder - /// that builds an `impl Fn(T) + Send + Sync + 'static` closure from a - /// single `JObject`. Callbacks have a fundamentally different shape - /// (decoder builds a captured closure rather than a value) so they live - /// outside `types`. - callback_decoders: HashMap, - /// Per-element-type Kotlin names for callback args (e.g. `"Sample"` → - /// `"io.zenoh.jni.callbacks.JNISubscriberCallback"`). - callback_kotlin_types: HashMap, /// Kotlin output config — if `None`, no Kotlin file is emitted. kotlin: Option, } @@ -341,8 +366,6 @@ impl Default for Builder { zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), types: HashMap::new(), - callback_decoders: HashMap::new(), - callback_kotlin_types: HashMap::new(), kotlin: None, }; register_builtins(&mut b.types); @@ -451,16 +474,14 @@ impl Builder { } /// Ingest a reusable [`crate::jni_type_binding::JniTypeBinding`] - /// collection. Type bindings, callback decoders, and Kotlin callback - /// names are all merged into the builder; entries in the collection - /// override entries already present in the builder with the same key. + /// collection. All entries are merged into the builder's type registry; + /// entries in the collection override entries already present in the + /// builder with the same key. pub fn jni_type_binding( mut self, bindings: crate::jni_type_binding::JniTypeBinding, ) -> Self { self.types.extend(bindings.types); - self.callback_decoders.extend(bindings.callback_decoders); - self.callback_kotlin_types.extend(bindings.callback_kotlin_types); self } @@ -499,24 +520,20 @@ impl Builder { /// Register a decoder for an `impl Fn(T) + Send + Sync + 'static` callback /// parameter. `element_type_name` is the last path segment of `T` - /// (e.g. `"Sample"`, `"Query"`, `"Reply"`). The decoder must have - /// signature `fn(&mut JNIEnv, JObject) -> ZResult`. - /// Callbacks have a fundamentally different shape (a closure-builder, not - /// a value decoder) so they are registered separately from - /// [`Builder::type_binding`]. + /// (e.g. `"Sample"`, `"Query"`, `"Reply"`, `"()"`). Sugar over + /// [`Builder::type_binding`] that fills in the binding's [`CallbackForm`]. pub fn callback_decoder( - mut self, + self, element_type_name: impl Into, decoder: impl AsRef, kotlin_type: impl Into, ) -> Self { - let path: syn::Path = - syn::parse_str(decoder.as_ref()).expect("invalid callback_decoder path"); let name = element_type_name.into(); - let kt = kotlin_type.into(); - self.callback_decoders.insert(name.clone(), path); - self.callback_kotlin_types.insert(name, kt); - self + let existing = self.types.get(&name).cloned(); + let binding = existing + .unwrap_or_else(|| TypeBinding::new(name.clone())) + .callback(CallbackForm::new(decoder, kotlin_type)); + self.type_binding(binding) } /// Enable Kotlin-side prototype generation. `path` is where the `.kt` @@ -1107,11 +1124,13 @@ impl JniConverter { } syn::Type::ImplTrait(it) => { if let Some(elem) = extract_fn_arg_type_name(&it.bounds) { - if let Some(decoder) = self.cfg.callback_decoders.get(&elem) { - return ArgKind::Callback { - decoder: decoder.clone(), - element_type_name: elem, - }; + if let Some(binding) = self.cfg.types.get(&elem) { + if let Some(form) = binding.callback.as_ref() { + return ArgKind::Callback { + decoder: form.decoder.clone(), + kotlin_type: form.kotlin_type.clone(), + }; + } } } ArgKind::Unsupported @@ -1258,7 +1277,7 @@ impl JniConverter { } ArgKind::Callback { decoder, - element_type_name, + kotlin_type, } => { jni_params.push(quote! { #name: jni::objects::JObject }); prelude.push(quote! { @@ -1266,18 +1285,7 @@ impl JniConverter { }); call_args.push(quote! { #name }); if kt_enabled { - let cb_fqn = self - .cfg - .callback_kotlin_types - .get(&element_type_name) - .cloned() - .unwrap_or_else(|| { - panic!( - "callback_decoder({:?}, ...) Kotlin type not configured", - element_type_name - ) - }); - let cb_short = kotlin_register_fqn(&cb_fqn, local_kotlin_fqns); + let cb_short = kotlin_register_fqn(&kotlin_type, local_kotlin_fqns); kotlin_params.push(format!( "{}: {}", kotlin_param_name(&name.to_string(), false), @@ -1411,7 +1419,7 @@ enum ArgKind { }, Callback { decoder: syn::Path, - element_type_name: String, + kotlin_type: String, }, Unsupported, } diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index c430e59f..e66e5ed3 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -1,10 +1,11 @@ //! Reusable collection of JNI type bindings. //! -//! [`JniTypeBinding`] aggregates a set of [`TypeBinding`]s plus callback -//! registrations into a single value that can be defined once and ingested -//! into many [`crate::jni_converter::Builder`] instances. Useful when a -//! project emits several `JniConverter` outputs (e.g. one per JNI class) that -//! share a common vocabulary of types. +//! [`JniTypeBinding`] aggregates a set of [`TypeBinding`]s — including +//! callback registrations, which live as [`crate::jni_converter::CallbackForm`] +//! slots on the element type's binding — into a single value that can be +//! defined once and ingested into many [`crate::jni_converter::Builder`] +//! instances. Useful when a project emits several `JniConverter` outputs +//! (e.g. one per JNI class) that share a common vocabulary of types. //! //! ```ignore //! use zenoh_flat::jni_converter::{ArgDecode, JniConverter, JniForm, TypeBinding}; @@ -42,9 +43,9 @@ use std::collections::HashMap; -use crate::jni_converter::TypeBinding; +use crate::jni_converter::{CallbackForm, TypeBinding}; -/// Reusable collection of [`TypeBinding`]s and callback registrations. +/// Reusable collection of [`TypeBinding`]s. /// /// Built fluently and consumed by /// [`crate::jni_converter::Builder::jni_type_binding`]. `Clone` so the same @@ -52,8 +53,6 @@ use crate::jni_converter::TypeBinding; #[derive(Default, Clone)] pub struct JniTypeBinding { pub(crate) types: HashMap, - pub(crate) callback_decoders: HashMap, - pub(crate) callback_kotlin_types: HashMap, } impl JniTypeBinding { @@ -71,27 +70,26 @@ impl JniTypeBinding { /// segment of the callback's argument type (e.g. `"Sample"`); use /// `"()"` for zero-arg callbacks. The decoder must have signature /// `fn(&mut JNIEnv, JObject) -> ZResult`. + /// Sugar over [`JniTypeBinding::type_binding`] that fills in the + /// binding's [`CallbackForm`]. pub fn callback_decoder( - mut self, + self, element_type_name: impl Into, decoder: impl AsRef, kotlin_type: impl Into, ) -> Self { - let path: syn::Path = syn::parse_str(decoder.as_ref()) - .expect("invalid callback_decoder path"); let name = element_type_name.into(); - let kt = kotlin_type.into(); - self.callback_decoders.insert(name.clone(), path); - self.callback_kotlin_types.insert(name, kt); - self + let existing = self.types.get(&name).cloned(); + let binding = existing + .unwrap_or_else(|| TypeBinding::new(name.clone())) + .callback(CallbackForm::new(decoder, kotlin_type)); + self.type_binding(binding) } /// Merge another [`JniTypeBinding`] into this one. Entries in `other` /// override entries with the same key in `self`. pub fn merge(mut self, other: JniTypeBinding) -> Self { self.types.extend(other.types); - self.callback_decoders.extend(other.callback_decoders); - self.callback_kotlin_types.extend(other.callback_kotlin_types); self } } From be10c90750d19f7eca2841fa0d137300b5a22e2b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 22:57:04 +0200 Subject: [PATCH 110/126] no special callback form --- zenoh-flat/src/jni_converter.rs | 109 ++++++----------------- zenoh-flat/src/jni_type_binding.rs | 28 +++--- zenoh-jni/src/ext/advanced_publisher.rs | 4 +- zenoh-jni/src/ext/advanced_subscriber.rs | 8 +- zenoh-jni/src/liveliness.rs | 6 +- zenoh-jni/src/querier.rs | 4 +- zenoh-jni/src/sample_callback.rs | 8 +- zenoh-jni/src/scouting.rs | 4 +- zenoh-jni/src/utils.rs | 4 +- 9 files changed, 66 insertions(+), 109 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index ab5ae7bd..b00690b1 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -11,15 +11,16 @@ //! The converter is fully data-driven: every Rust type that can appear in a //! `#[prebindgen]` function's signature is described by a [`TypeBinding`] //! registered up-front via [`Builder::type_binding`]. A binding declares up to -//! five forms: +//! four forms: //! //! * `consume` — used when the type appears by value as a parameter (`T`); //! * `borrow` — used when the type appears as a shared reference (`&T`); -//! * `callback` — used when the type appears as the element of an -//! `impl Fn(T) + Send + Sync + 'static` parameter; //! * `returns` — used when the type appears in `ZResult` as a return value; //! * `returns_vec` — used when the type appears as `ZResult>`. //! +//! `impl Fn(T) + Send + Sync + 'static` callback parameters reuse the +//! `consume` form on a binding keyed under `"impl Fn()"`. +//! //! Each form carries a JNI on-the-wire type (e.g. `jni::sys::jlong`, //! `jni::objects::JObject`, `*const Foo`), the Kotlin-side declaration, and a //! decoding/encoding strategy. Built-in bindings for `bool`, `String`, @@ -76,10 +77,14 @@ use prebindgen::SourceLocation; // ===================================================================== /// Per-type description of how a Rust type is represented across the JNI -/// boundary. A type may declare up to five forms: -/// `consume` (`T` parameter), `borrow` (`&T` parameter), `callback` -/// (`impl Fn(T) + Send + Sync + 'static` parameter), `returns` +/// boundary. A type may declare up to four forms: +/// `consume` (`T` parameter), `borrow` (`&T` parameter), `returns` /// (`ZResult` return), and `returns_vec` (`ZResult>` return). +/// +/// Callback parameters (`impl Fn(T) + Send + Sync + 'static`) are described +/// by an ordinary `consume` form on a binding keyed under +/// `"impl Fn()"` (e.g. `"impl Fn(Sample)"`, `"impl Fn()"`). The +/// classifier synthesizes that key when it sees an `impl Fn(...)` parameter. #[derive(Clone)] pub struct TypeBinding { name: String, @@ -91,7 +96,6 @@ pub struct TypeBinding { kotlin_type: Option, consume: Option, borrow: Option, - callback: Option, returns: Option, returns_vec: Option, } @@ -207,28 +211,6 @@ impl JniForm { } } -/// Describes how an `impl Fn(T) + Send + Sync + 'static` callback parameter -/// (keyed under the element type `T`) is decoded. The decoder must have -/// signature -/// `fn(&mut JNIEnv, JObject) -> ZResult`. -#[derive(Clone)] -pub struct CallbackForm { - pub(crate) decoder: syn::Path, - /// Kotlin FQN of the callback interface, e.g. - /// `"io.zenoh.jni.callbacks.JNISubscriberCallback"`. - pub(crate) kotlin_type: String, -} - -impl CallbackForm { - pub fn new(decoder: impl AsRef, kotlin_type: impl Into) -> Self { - Self { - decoder: syn::parse_str(decoder.as_ref()) - .expect("invalid CallbackForm decoder path"), - kotlin_type: kotlin_type.into(), - } - } -} - #[derive(Clone)] pub enum ReturnEncode { /// `Ok((&mut env, __result)?)` — wrapping function returns @@ -280,7 +262,6 @@ impl TypeBinding { kotlin_type: None, consume: None, borrow: None, - callback: None, returns: None, returns_vec: None, } @@ -301,11 +282,6 @@ impl TypeBinding { self } - pub fn callback(mut self, form: CallbackForm) -> Self { - self.callback = Some(form); - self - } - pub fn returns(mut self, form: ReturnForm) -> Self { self.returns = Some(form); self @@ -518,24 +494,6 @@ impl Builder { self } - /// Register a decoder for an `impl Fn(T) + Send + Sync + 'static` callback - /// parameter. `element_type_name` is the last path segment of `T` - /// (e.g. `"Sample"`, `"Query"`, `"Reply"`, `"()"`). Sugar over - /// [`Builder::type_binding`] that fills in the binding's [`CallbackForm`]. - pub fn callback_decoder( - self, - element_type_name: impl Into, - decoder: impl AsRef, - kotlin_type: impl Into, - ) -> Self { - let name = element_type_name.into(); - let existing = self.types.get(&name).cloned(); - let binding = existing - .unwrap_or_else(|| TypeBinding::new(name.clone())) - .callback(CallbackForm::new(decoder, kotlin_type)); - self.type_binding(binding) - } - /// Enable Kotlin-side prototype generation. `path` is where the `.kt` /// file will be written when [`JniConverter::write_kotlin`] is called. pub fn kotlin_output(mut self, path: impl Into) -> Self { @@ -1124,11 +1082,12 @@ impl JniConverter { } syn::Type::ImplTrait(it) => { if let Some(elem) = extract_fn_arg_type_name(&it.bounds) { - if let Some(binding) = self.cfg.types.get(&elem) { - if let Some(form) = binding.callback.as_ref() { - return ArgKind::Callback { - decoder: form.decoder.clone(), - kotlin_type: form.kotlin_type.clone(), + let key = callback_binding_key(&elem); + if let Some(binding) = self.cfg.types.get(&key) { + if let Some(form) = binding.consume.as_ref() { + return ArgKind::Consume { + form: form.clone(), + kotlin_override: binding.kotlin_type.clone(), }; } } @@ -1275,24 +1234,6 @@ impl JniConverter { )); } } - ArgKind::Callback { - decoder, - kotlin_type, - } => { - jni_params.push(quote! { #name: jni::objects::JObject }); - prelude.push(quote! { - let #name = #decoder(&mut env, #name)?; - }); - call_args.push(quote! { #name }); - if kt_enabled { - let cb_short = kotlin_register_fqn(&kotlin_type, local_kotlin_fqns); - kotlin_params.push(format!( - "{}: {}", - kotlin_param_name(&name.to_string(), false), - cb_short - )); - } - } ArgKind::Unsupported => panic!( "unsupported parameter type `{}` for `{}` at {loc}", ty.to_token_stream(), @@ -1417,10 +1358,6 @@ enum ArgKind { form: JniForm, kotlin_override: Option, }, - Callback { - decoder: syn::Path, - kotlin_type: String, - }, Unsupported, } @@ -1455,6 +1392,18 @@ fn jni_object_kind(ty: &syn::Type) -> Option { } } +/// Synthesize the type-registry key for an `impl Fn()` parameter. +/// `"Sample"` → `"impl Fn(Sample)"`; `"()"` → `"impl Fn()"`. Used by +/// `classify_arg` and by `JniTypeBinding::callback_decoder` so both ends +/// agree on the lookup name. +pub fn callback_binding_key(element_type_name: &str) -> String { + if element_type_name == "()" { + "impl Fn()".to_string() + } else { + format!("impl Fn({})", element_type_name) + } +} + fn type_last_segment(ty: &syn::Type) -> Option { let syn::Type::Path(tp) = ty else { return None }; tp.path.segments.last().map(|s| s.ident.to_string()) diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index e66e5ed3..a22f270e 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -43,7 +43,9 @@ use std::collections::HashMap; -use crate::jni_converter::{CallbackForm, TypeBinding}; +use crate::jni_converter::{ + callback_binding_key, ArgDecode, JniForm, TypeBinding, +}; /// Reusable collection of [`TypeBinding`]s. /// @@ -69,20 +71,26 @@ impl JniTypeBinding { /// Register a callback decoder. `element_type_name` is the last path /// segment of the callback's argument type (e.g. `"Sample"`); use /// `"()"` for zero-arg callbacks. The decoder must have signature - /// `fn(&mut JNIEnv, JObject) -> ZResult`. - /// Sugar over [`JniTypeBinding::type_binding`] that fills in the - /// binding's [`CallbackForm`]. + /// `fn(&mut JNIEnv, &JObject) -> ZResult`. + /// + /// Sugar over [`JniTypeBinding::type_binding`] that builds an ordinary + /// [`TypeBinding`] keyed under `"impl Fn()"` with a `JObject` + /// `EnvRefMut` consume form — callbacks need no special-case classifier + /// or codegen path. pub fn callback_decoder( self, - element_type_name: impl Into, + element_type_name: impl AsRef, decoder: impl AsRef, kotlin_type: impl Into, ) -> Self { - let name = element_type_name.into(); - let existing = self.types.get(&name).cloned(); - let binding = existing - .unwrap_or_else(|| TypeBinding::new(name.clone())) - .callback(CallbackForm::new(decoder, kotlin_type)); + let key = callback_binding_key(element_type_name.as_ref()); + let binding = TypeBinding::new(key) + .kotlin(kotlin_type) + .consume(JniForm::new( + "jni::objects::JObject", + "JObject", + ArgDecode::env_ref_mut(decoder), + )); self.type_binding(binding) } diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index c49dc546..bf056141 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -59,8 +59,8 @@ impl<'a> SetJniMatchingStatusCallback for MatchingListenerBuilder<'a, DefaultHan on_close: JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); - let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let callback_global_ref = get_callback_global_ref(env, &callback)?; + let on_close_global_ref = get_callback_global_ref(env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let builder = self.callback(move |matching_status| { diff --git a/zenoh-jni/src/ext/advanced_subscriber.rs b/zenoh-jni/src/ext/advanced_subscriber.rs index b2a2813a..6e7f9c18 100644 --- a/zenoh-jni/src/ext/advanced_subscriber.rs +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -56,8 +56,8 @@ impl<'a> SetJniSampleMissListenerCallback for SampleMissListenerBuilder<'a, Defa on_close: JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); - let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let callback_global_ref = get_callback_global_ref(env, &callback)?; + let on_close_global_ref = get_callback_global_ref(env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let builder = self.callback(move |miss| { @@ -138,7 +138,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareDetectPu advanced_subscriber.key_expr() ); - let cb = process_kotlin_sample_callback(&mut env, callback)?; + let cb = process_kotlin_sample_callback(&mut env, &callback)?; let cb = wrap_with_on_close(&mut env, on_close, cb)?; let detect_publishers_subscriber = advanced_subscriber .detect_publishers() @@ -196,7 +196,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedSubscriber_declareBackgrou advanced_subscriber.key_expr() ); - let cb = process_kotlin_sample_callback(&mut env, callback)?; + let cb = process_kotlin_sample_callback(&mut env, &callback)?; let cb = wrap_with_on_close(&mut env, on_close, cb)?; advanced_subscriber .detect_publishers() diff --git a/zenoh-jni/src/liveliness.rs b/zenoh-jni/src/liveliness.rs index 2a3d177b..fbfdaa2e 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -48,8 +48,8 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_livelinessGetViaJNI( let _ = || -> ZResult<()> { let key_expr = unsafe { decode_jni_key_expr(&mut env, &key_expr) }?; let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let callback_global_ref = get_callback_global_ref(&mut env, &callback)?; + let on_close_global_ref = get_callback_global_ref(&mut env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let timeout = Duration::from_millis(timeout_ms as u64); let replies = session @@ -146,7 +146,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareLivelinessSubscribe let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; tracing::debug!("Declaring liveliness subscriber on '{}'...", key_expr); - let cb = process_kotlin_sample_callback(&mut env, callback)?; + let cb = process_kotlin_sample_callback(&mut env, &callback)?; let cb = wrap_with_on_close(&mut env, on_close, cb)?; let subscriber = session .liveliness() diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 82d3205b..5bf3e325 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -70,8 +70,8 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( let _ = || -> ZResult<()> { let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let callback_global_ref = get_callback_global_ref(&mut env, &callback)?; + let on_close_global_ref = get_callback_global_ref(&mut env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let mut get_builder = querier.get().callback(move |reply| { || -> ZResult<()> { diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs index 60dc9cf4..afaae297 100644 --- a/zenoh-jni/src/sample_callback.rs +++ b/zenoh-jni/src/sample_callback.rs @@ -176,7 +176,7 @@ pub(crate) fn on_reply_error( pub(crate) unsafe fn process_kotlin_sample_callback( env: &mut JNIEnv, - callback: JObject, + callback: &JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); let callback_global_ref = get_callback_global_ref(env, callback)?; @@ -245,7 +245,7 @@ pub(crate) unsafe fn process_kotlin_sample_callback( pub(crate) unsafe fn process_kotlin_reply_callback( env: &mut JNIEnv, - callback: JObject, + callback: &JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); let callback_global_ref = get_callback_global_ref(env, callback)?; @@ -277,7 +277,7 @@ pub(crate) unsafe fn process_kotlin_reply_callback( pub(crate) unsafe fn process_kotlin_query_callback( env: &mut JNIEnv, - callback: JObject, + callback: &JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); let callback_global_ref = get_callback_global_ref(env, callback)?; @@ -306,7 +306,7 @@ pub(crate) unsafe fn process_kotlin_query_callback( /// `CallOnDrop` so the on-close fires when the subscription/query is dropped. pub(crate) unsafe fn process_kotlin_on_close_callback( env: &mut JNIEnv, - on_close: JObject, + on_close: &JObject, ) -> ZResult { let java_vm = Arc::new(get_java_vm(env)?); let on_close_global_ref = get_callback_global_ref(env, on_close)?; diff --git a/zenoh-jni/src/scouting.rs b/zenoh-jni/src/scouting.rs index c5ec79d1..cb9e0895 100644 --- a/zenoh-jni/src/scouting.rs +++ b/zenoh-jni/src/scouting.rs @@ -47,9 +47,9 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIScout_00024Companion_scoutViaJNI( config_ptr: /*nullable=*/ *const Config, ) -> *const Scout<()> { || -> ZResult<*const Scout<()>> { - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; + let callback_global_ref = get_callback_global_ref(&mut env, &callback)?; let java_vm = Arc::new(get_java_vm(&mut env)?); - let on_close_global_ref: GlobalRef = get_callback_global_ref(&mut env, on_close)?; + let on_close_global_ref: GlobalRef = get_callback_global_ref(&mut env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let whatAmIMatcher: WhatAmIMatcher = (whatAmI as u8).try_into().unwrap(); // The validity of the operation is guaranteed on the kotlin layer. let config = if config_ptr.is_null() { diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index 25f85a83..356e7474 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -75,7 +75,7 @@ pub(crate) fn get_java_vm(env: &mut JNIEnv) -> ZResult { pub(crate) fn get_callback_global_ref( env: &mut JNIEnv, - callback: JObject, + callback: &JObject, ) -> crate::errors::ZResult { env.new_global_ref(callback) .map_err(|err| zerror!("Unable to get reference to the provided callback: {}", err)) @@ -190,7 +190,7 @@ where F: Fn(T) + Send + Sync + 'static, { let java_vm = Arc::new(get_java_vm(env)?); - let on_close_global_ref = get_callback_global_ref(env, on_close)?; + let on_close_global_ref = get_callback_global_ref(env, &on_close)?; let guard = load_on_close(&java_vm, on_close_global_ref); Ok(move |t| { guard.noop(); From d3c95146f1686230df86b57f7ab23de88fa27d88 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 23:02:27 +0200 Subject: [PATCH 111/126] callback decoder method removed --- zenoh-flat/src/jni_converter.rs | 3 +-- zenoh-flat/src/jni_type_binding.rs | 35 +----------------------------- zenoh-jni/build.rs | 28 ++++++++++++++++-------- 3 files changed, 21 insertions(+), 45 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index b00690b1..58d3015e 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -1394,8 +1394,7 @@ fn jni_object_kind(ty: &syn::Type) -> Option { /// Synthesize the type-registry key for an `impl Fn()` parameter. /// `"Sample"` → `"impl Fn(Sample)"`; `"()"` → `"impl Fn()"`. Used by -/// `classify_arg` and by `JniTypeBinding::callback_decoder` so both ends -/// agree on the lookup name. +/// `classify_arg` and by callback bindings so both ends agree on the lookup name. pub fn callback_binding_key(element_type_name: &str) -> String { if element_type_name == "()" { "impl Fn()".to_string() diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index a22f270e..10e73635 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -21,11 +21,6 @@ //! ) //! .pointer_param(true), //! ), -//! ) -//! .callback_decoder( -//! "Sample", -//! "crate::sample_callback::process_kotlin_sample_callback", -//! "io.zenoh.jni.callbacks.JNISubscriberCallback", //! ); //! //! let session_converter = JniConverter::builder() @@ -43,9 +38,7 @@ use std::collections::HashMap; -use crate::jni_converter::{ - callback_binding_key, ArgDecode, JniForm, TypeBinding, -}; +use crate::jni_converter::TypeBinding; /// Reusable collection of [`TypeBinding`]s. /// @@ -68,32 +61,6 @@ impl JniTypeBinding { self } - /// Register a callback decoder. `element_type_name` is the last path - /// segment of the callback's argument type (e.g. `"Sample"`); use - /// `"()"` for zero-arg callbacks. The decoder must have signature - /// `fn(&mut JNIEnv, &JObject) -> ZResult`. - /// - /// Sugar over [`JniTypeBinding::type_binding`] that builds an ordinary - /// [`TypeBinding`] keyed under `"impl Fn()"` with a `JObject` - /// `EnvRefMut` consume form — callbacks need no special-case classifier - /// or codegen path. - pub fn callback_decoder( - self, - element_type_name: impl AsRef, - decoder: impl AsRef, - kotlin_type: impl Into, - ) -> Self { - let key = callback_binding_key(element_type_name.as_ref()); - let binding = TypeBinding::new(key) - .kotlin(kotlin_type) - .consume(JniForm::new( - "jni::objects::JObject", - "JObject", - ArgDecode::env_ref_mut(decoder), - )); - self.type_binding(binding) - } - /// Merge another [`JniTypeBinding`] into this one. Entries in `other` /// override entries with the same key in `self`. pub fn merge(mut self, other: JniTypeBinding) -> Self { diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 537c3f66..4900e85e 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,5 +1,5 @@ use itertools::Itertools; -use zenoh_flat::jni_converter::{ArgDecode, JniForm, ReturnEncode, ReturnForm, TypeBinding}; +use zenoh_flat::jni_converter::{callback_binding_key, ArgDecode, JniForm, ReturnEncode, ReturnForm, TypeBinding}; use zenoh_flat::jni_type_binding::JniTypeBinding; fn enum_binding(name: &str, decoder: &str) -> TypeBinding { @@ -14,32 +14,42 @@ fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { )) } +fn callback_binding(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { + TypeBinding::new(callback_binding_key(name)) + .kotlin(kotlin) + .consume(JniForm::new( + "jni::objects::JObject", + "JObject", + ArgDecode::env_ref_mut(decoder), + )) +} + /// Type vocabulary shared across every `JniConverter` build in this crate. /// Defined once and ingested via `Builder::jni_type_binding(...)` so each /// generated JNI surface (session, publisher, subscriber, ...) sees the same /// types without duplicating registrations. fn shared_bindings() -> JniTypeBinding { JniTypeBinding::new() - .callback_decoder( + .type_binding(callback_binding( "Sample", "crate::sample_callback::process_kotlin_sample_callback", "io.zenoh.jni.callbacks.JNISubscriberCallback", - ) - .callback_decoder( + )) + .type_binding(callback_binding( "Query", "crate::sample_callback::process_kotlin_query_callback", "io.zenoh.jni.callbacks.JNIQueryableCallback", - ) - .callback_decoder( + )) + .type_binding(callback_binding( "Reply", "crate::sample_callback::process_kotlin_reply_callback", "io.zenoh.jni.callbacks.JNIGetCallback", - ) - .callback_decoder( + )) + .type_binding(callback_binding( "()", "crate::sample_callback::process_kotlin_on_close_callback", "io.zenoh.jni.callbacks.JNIOnCloseCallback", - ) + )) .type_binding(enum_binding( "CongestionControl", "crate::utils::decode_congestion_control", From 3b3db26f38b66cb599414e2c3c8d48381d606637 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 23:13:51 +0200 Subject: [PATCH 112/126] callback fully named --- zenoh-flat/src/jni_converter.rs | 62 ++++++++++----------------------- zenoh-jni/build.rs | 16 +++++---- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 58d3015e..28a3992d 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -256,9 +256,18 @@ impl TypeBinding { &self.name } + /// Construct a binding keyed by `name`. If `name` parses as a Rust type, + /// it is canonicalized through `quote::ToTokens` so whitespace variations + /// in user input match the form the classifier produces from AST nodes + /// (matters for `impl Fn(T) + Send + Sync + 'static`-style names). Falls + /// back to the literal string if parsing fails. pub fn new(name: impl Into) -> Self { + let raw = name.into(); + let canonical = syn::parse_str::(&raw) + .map(|t| t.to_token_stream().to_string()) + .unwrap_or_else(|_| raw); Self { - name: name.into(), + name: canonical, kotlin_type: None, consume: None, borrow: None, @@ -1080,16 +1089,14 @@ impl JniConverter { kotlin_override: None, } } - syn::Type::ImplTrait(it) => { - if let Some(elem) = extract_fn_arg_type_name(&it.bounds) { - let key = callback_binding_key(&elem); - if let Some(binding) = self.cfg.types.get(&key) { - if let Some(form) = binding.consume.as_ref() { - return ArgKind::Consume { - form: form.clone(), - kotlin_override: binding.kotlin_type.clone(), - }; - } + syn::Type::ImplTrait(_) => { + let key = ty.to_token_stream().to_string(); + if let Some(binding) = self.cfg.types.get(&key) { + if let Some(form) = binding.consume.as_ref() { + return ArgKind::Consume { + form: form.clone(), + kotlin_override: binding.kotlin_type.clone(), + }; } } ArgKind::Unsupported @@ -1392,44 +1399,11 @@ fn jni_object_kind(ty: &syn::Type) -> Option { } } -/// Synthesize the type-registry key for an `impl Fn()` parameter. -/// `"Sample"` → `"impl Fn(Sample)"`; `"()"` → `"impl Fn()"`. Used by -/// `classify_arg` and by callback bindings so both ends agree on the lookup name. -pub fn callback_binding_key(element_type_name: &str) -> String { - if element_type_name == "()" { - "impl Fn()".to_string() - } else { - format!("impl Fn({})", element_type_name) - } -} - fn type_last_segment(ty: &syn::Type) -> Option { let syn::Type::Path(tp) = ty else { return None }; tp.path.segments.last().map(|s| s.ident.to_string()) } -/// Look through the trait bounds of an `impl Fn(...) + ...` for a `Fn`-family -/// trait and return the lookup key for its argument type: -/// - `impl Fn(T)` → `Some("T")` -/// - `impl Fn()` → `Some("()")` -fn extract_fn_arg_type_name( - bounds: &syn::punctuated::Punctuated, -) -> Option { - for bound in bounds { - let syn::TypeParamBound::Trait(tb) = bound else { continue }; - let seg = tb.path.segments.last()?; - if !matches!(seg.ident.to_string().as_str(), "Fn" | "FnMut" | "FnOnce") { - continue; - } - let syn::PathArguments::Parenthesized(p) = &seg.arguments else { continue }; - return match p.inputs.first() { - Some(first) => type_last_segment(first), - None => Some("()".to_string()), - }; - } - None -} - /// Last-segment name of the single generic argument of an `Option<...>`. fn option_inner_type_name(seg: &syn::PathSegment) -> Option { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 4900e85e..f33b38d9 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,5 +1,5 @@ use itertools::Itertools; -use zenoh_flat::jni_converter::{callback_binding_key, ArgDecode, JniForm, ReturnEncode, ReturnForm, TypeBinding}; +use zenoh_flat::jni_converter::{ArgDecode, JniForm, ReturnEncode, ReturnForm, TypeBinding}; use zenoh_flat::jni_type_binding::JniTypeBinding; fn enum_binding(name: &str, decoder: &str) -> TypeBinding { @@ -14,8 +14,12 @@ fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { )) } +/// `name` must be the full callback type as it appears in `#[prebindgen]` +/// signatures, e.g. `"impl Fn(Sample) + Send + Sync + 'static"`. Whitespace +/// is normalized by `TypeBinding::new` so the lookup matches the AST form +/// produced by the classifier. fn callback_binding(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { - TypeBinding::new(callback_binding_key(name)) + TypeBinding::new(name) .kotlin(kotlin) .consume(JniForm::new( "jni::objects::JObject", @@ -31,22 +35,22 @@ fn callback_binding(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { fn shared_bindings() -> JniTypeBinding { JniTypeBinding::new() .type_binding(callback_binding( - "Sample", + "impl Fn(Sample) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_sample_callback", "io.zenoh.jni.callbacks.JNISubscriberCallback", )) .type_binding(callback_binding( - "Query", + "impl Fn(Query) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_query_callback", "io.zenoh.jni.callbacks.JNIQueryableCallback", )) .type_binding(callback_binding( - "Reply", + "impl Fn(Reply) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_reply_callback", "io.zenoh.jni.callbacks.JNIGetCallback", )) .type_binding(callback_binding( - "()", + "impl Fn() + Send + Sync + 'static", "crate::sample_callback::process_kotlin_on_close_callback", "io.zenoh.jni.callbacks.JNIOnCloseCallback", )) From 60843c5963e77935a5d49bfbb7ef746d2e8d8c2b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 23:17:26 +0200 Subject: [PATCH 113/126] special callback binding removed --- zenoh-jni/build.rs | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index f33b38d9..a07ea900 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -14,42 +14,28 @@ fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { )) } -/// `name` must be the full callback type as it appears in `#[prebindgen]` -/// signatures, e.g. `"impl Fn(Sample) + Send + Sync + 'static"`. Whitespace -/// is normalized by `TypeBinding::new` so the lookup matches the AST form -/// produced by the classifier. -fn callback_binding(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { - TypeBinding::new(name) - .kotlin(kotlin) - .consume(JniForm::new( - "jni::objects::JObject", - "JObject", - ArgDecode::env_ref_mut(decoder), - )) -} - /// Type vocabulary shared across every `JniConverter` build in this crate. /// Defined once and ingested via `Builder::jni_type_binding(...)` so each /// generated JNI surface (session, publisher, subscriber, ...) sees the same /// types without duplicating registrations. fn shared_bindings() -> JniTypeBinding { JniTypeBinding::new() - .type_binding(callback_binding( + .type_binding(jobject_consume( "impl Fn(Sample) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_sample_callback", "io.zenoh.jni.callbacks.JNISubscriberCallback", )) - .type_binding(callback_binding( + .type_binding(jobject_consume( "impl Fn(Query) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_query_callback", "io.zenoh.jni.callbacks.JNIQueryableCallback", )) - .type_binding(callback_binding( + .type_binding(jobject_consume( "impl Fn(Reply) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_reply_callback", "io.zenoh.jni.callbacks.JNIGetCallback", )) - .type_binding(callback_binding( + .type_binding(jobject_consume( "impl Fn() + Send + Sync + 'static", "crate::sample_callback::process_kotlin_on_close_callback", "io.zenoh.jni.callbacks.JNIOnCloseCallback", From acd677f504f240d1c0ce964940cfc83d51c976d6 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 25 Apr 2026 23:22:48 +0200 Subject: [PATCH 114/126] type binding is on place --- zenoh-flat/src/jni_converter.rs | 79 +----------------------------- zenoh-flat/src/jni_type_binding.rs | 79 +++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 78 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 28a3992d..5c8bf27f 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -72,33 +72,9 @@ use quote::{format_ident, quote, ToTokens}; use prebindgen::SourceLocation; -// ===================================================================== -// TypeBinding: per-type description of JNI representations -// ===================================================================== +pub use crate::jni_type_binding::TypeBinding; -/// Per-type description of how a Rust type is represented across the JNI -/// boundary. A type may declare up to four forms: -/// `consume` (`T` parameter), `borrow` (`&T` parameter), `returns` -/// (`ZResult` return), and `returns_vec` (`ZResult>` return). -/// -/// Callback parameters (`impl Fn(T) + Send + Sync + 'static`) are described -/// by an ordinary `consume` form on a binding keyed under -/// `"impl Fn()"` (e.g. `"impl Fn(Sample)"`, `"impl Fn()"`). The -/// classifier synthesizes that key when it sees an `impl Fn(...)` parameter. -#[derive(Clone)] -pub struct TypeBinding { - name: String, - /// Kotlin-side type name (FQN preferred — out-of-package import is - /// auto-derived; bare for same-package). Used as the Kotlin parameter - /// type when the form's wire JNI type is `JObject` and as the Kotlin - /// return type. For primitive-mapped forms (`bool`, `Duration`, - /// `String`, ...) the form's `kotlin_jni_type` is used instead. - kotlin_type: Option, - consume: Option, - borrow: Option, - returns: Option, - returns_vec: Option, -} +// ===================================================================== /// Strategy for converting a JNI parameter into a Rust value. #[derive(Clone)] @@ -250,57 +226,6 @@ impl ReturnForm { } } -impl TypeBinding { - /// Short type name this binding is keyed under (e.g. `"KeyExpr"`). - pub fn name(&self) -> &str { - &self.name - } - - /// Construct a binding keyed by `name`. If `name` parses as a Rust type, - /// it is canonicalized through `quote::ToTokens` so whitespace variations - /// in user input match the form the classifier produces from AST nodes - /// (matters for `impl Fn(T) + Send + Sync + 'static`-style names). Falls - /// back to the literal string if parsing fails. - pub fn new(name: impl Into) -> Self { - let raw = name.into(); - let canonical = syn::parse_str::(&raw) - .map(|t| t.to_token_stream().to_string()) - .unwrap_or_else(|_| raw); - Self { - name: canonical, - kotlin_type: None, - consume: None, - borrow: None, - returns: None, - returns_vec: None, - } - } - - pub fn kotlin(mut self, fqn: impl Into) -> Self { - self.kotlin_type = Some(fqn.into()); - self - } - - pub fn consume(mut self, form: JniForm) -> Self { - self.consume = Some(form); - self - } - - pub fn borrow(mut self, form: JniForm) -> Self { - self.borrow = Some(form); - self - } - - pub fn returns(mut self, form: ReturnForm) -> Self { - self.returns = Some(form); - self - } - - pub fn returns_vec(mut self, form: ReturnForm) -> Self { - self.returns_vec = Some(form); - self - } -} // ===================================================================== // Builder diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index 10e73635..a08daf3e 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -38,7 +38,84 @@ use std::collections::HashMap; -use crate::jni_converter::TypeBinding; +use quote::ToTokens; +use crate::jni_converter::{JniForm, ReturnForm}; + +/// Per-type description of how a Rust type is represented across the JNI +/// boundary. A type may declare up to four forms: +/// `consume` (`T` parameter), `borrow` (`&T` parameter), `returns` +/// (`ZResult` return), and `returns_vec` (`ZResult>` return). +/// +/// Callback parameters (`impl Fn(T) + Send + Sync + 'static`) are described +/// by an ordinary `consume` form on a binding keyed under +/// `"impl Fn()"` (e.g. `"impl Fn(Sample)"`, `"impl Fn()"`). The +/// classifier synthesizes that key when it sees an `impl Fn(...)` parameter. +#[derive(Clone)] +pub struct TypeBinding { + pub(crate) name: String, + /// Kotlin-side type name (FQN preferred — out-of-package import is + /// auto-derived; bare for same-package). Used as the Kotlin parameter + /// type when the form's wire JNI type is `JObject` and as the Kotlin + /// return type. For primitive-mapped forms (`bool`, `Duration`, + /// `String`, ...) the form's `kotlin_jni_type` is used instead. + pub(crate) kotlin_type: Option, + pub(crate) consume: Option, + pub(crate) borrow: Option, + pub(crate) returns: Option, + pub(crate) returns_vec: Option, +} + +impl TypeBinding { + /// Short type name this binding is keyed under (e.g. `"KeyExpr"`). + pub fn name(&self) -> &str { + &self.name + } + + /// Construct a binding keyed by `name`. If `name` parses as a Rust type, + /// it is canonicalized through `quote::ToTokens` so whitespace variations + /// in user input match the form the classifier produces from AST nodes + /// (matters for `impl Fn(T) + Send + Sync + 'static`-style names). Falls + /// back to the literal string if parsing fails. + pub fn new(name: impl Into) -> Self { + let raw = name.into(); + let canonical = syn::parse_str::(&raw) + .map(|t| t.to_token_stream().to_string()) + .unwrap_or_else(|_| raw); + Self { + name: canonical, + kotlin_type: None, + consume: None, + borrow: None, + returns: None, + returns_vec: None, + } + } + + pub fn kotlin(mut self, fqn: impl Into) -> Self { + self.kotlin_type = Some(fqn.into()); + self + } + + pub fn consume(mut self, form: JniForm) -> Self { + self.consume = Some(form); + self + } + + pub fn borrow(mut self, form: JniForm) -> Self { + self.borrow = Some(form); + self + } + + pub fn returns(mut self, form: ReturnForm) -> Self { + self.returns = Some(form); + self + } + + pub fn returns_vec(mut self, form: ReturnForm) -> Self { + self.returns_vec = Some(form); + self + } +} /// Reusable collection of [`TypeBinding`]s. /// From f0c8cc8007543ea494de6d629d30b6afd5d85562 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 00:09:49 +0200 Subject: [PATCH 115/126] types colleced separated --- zenoh-flat/src/jni_converter.rs | 711 +++++++++++++++-------------- zenoh-flat/src/jni_type_binding.rs | 84 ++-- zenoh-jni/Cargo.lock | 1 + zenoh-jni/Cargo.toml | 1 + zenoh-jni/build.rs | 62 ++- zenoh-jni/src/utils.rs | 2 +- 6 files changed, 483 insertions(+), 378 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 5c8bf27f..e7008881 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -1,17 +1,24 @@ -//! JNI binding generator for functions marked with `#[prebindgen]`. +//! JNI binding generators for items marked with `#[prebindgen]`. //! -//! Mirrors the pattern of [`prebindgen::batching::FfiConverter`], but instead -//! of emitting `#[no_mangle] extern "C"` proxy functions, it emits -//! `Java__ViaJNI` wrappers that decode JNI arguments, call the -//! original Rust function, and wrap the result into a JNI return (or throw a -//! JVM exception on error). +//! Two converters split the work along the natural axis: +//! +//! * [`JniStructConverter`] consumes `#[prebindgen]` `syn::ItemStruct`s, +//! emits a JNI decoder + a Kotlin `data class`, and inserts an +//! auto-generated [`TypeBinding`] into a shared [`JniTypeBinding`]. +//! * [`JniMethodsConverter`] consumes `#[prebindgen]` `syn::ItemFn`s, +//! classifies each argument/return against the (now fully populated) +//! [`JniTypeBinding`], and emits a `Java__ViaJNI` wrapper +//! plus a matching Kotlin `external fun`. +//! +//! The caller threads a single [`JniTypeBinding`] from the struct phase into +//! the methods phase (see `zenoh-jni/build.rs`). Module selection is a plain +//! `.filter(...)` on the prebindgen iterator — there is no per-purpose +//! "struct module" knob. //! //! # Type registry //! -//! The converter is fully data-driven: every Rust type that can appear in a -//! `#[prebindgen]` function's signature is described by a [`TypeBinding`] -//! registered up-front via [`Builder::type_binding`]. A binding declares up to -//! four forms: +//! Every Rust type that can appear in a `#[prebindgen]` function's signature +//! is described by a [`TypeBinding`]. A binding declares up to four forms: //! //! * `consume` — used when the type appears by value as a parameter (`T`); //! * `borrow` — used when the type appears as a shared reference (`&T`); @@ -21,47 +28,9 @@ //! `impl Fn(T) + Send + Sync + 'static` callback parameters reuse the //! `consume` form on a binding keyed under `"impl Fn()"`. //! -//! Each form carries a JNI on-the-wire type (e.g. `jni::sys::jlong`, -//! `jni::objects::JObject`, `*const Foo`), the Kotlin-side declaration, and a -//! decoding/encoding strategy. Built-in bindings for `bool`, `String`, -//! `Vec`, and `Duration` are pre-registered with sensible defaults; a -//! handful of builder-level convenience methods (`string_decoder`, -//! `byte_array_decoder`, `enum_decoder`, `struct_decoder`, `return_wrapper`, -//! `return_wrapper_vec`) populate or extend bindings without forcing every -//! call site to spell out a full `TypeBinding`. -//! -//! # Pipeline -//! -//! ```ignore -//! use itertools::Itertools; -//! use zenoh_flat::jni_converter::{JniConverter, TypeBinding, JniForm, ArgDecode}; -//! -//! let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); -//! let mut converter = JniConverter::builder() -//! .class_prefix("Java_io_zenoh_jni_JNISession_") -//! .function_suffix("ViaJNI") -//! .source_module("zenoh_flat::session") -//! .owned_object("crate::owned_object::OwnedObject") -//! .zresult("crate::errors::ZResult") -//! .throw_exception("crate::throw_exception") -//! .string_decoder("crate::utils::decode_string") -//! .byte_array_decoder("crate::utils::decode_byte_array") -//! .type_binding( -//! TypeBinding::new("KeyExpr").kotlin("JNIKeyExpr").consume( -//! JniForm::new( -//! "jni::objects::JObject", -//! "JObject", -//! ArgDecode::env_ref_mut("crate::key_expr::decode_jni_key_expr"), -//! ), -//! ), -//! ) -//! .build(); -//! source -//! .items_all() -//! .batching(converter.as_closure()) -//! .collect::() -//! .write("zenoh_flat_jni.rs"); -//! ``` +//! Built-in bindings for `bool`, `String`, `Vec`, and `Duration` are +//! pre-registered by [`JniTypeBinding::with_builtins`] and applied +//! automatically inside both builders' `default()` impls. use std::collections::{BTreeSet, HashMap, VecDeque}; use std::path::PathBuf; @@ -72,8 +41,11 @@ use quote::{format_ident, quote, ToTokens}; use prebindgen::SourceLocation; +use crate::jni_type_binding::JniTypeBinding; pub use crate::jni_type_binding::TypeBinding; +// ===================================================================== +// Decode / encode strategies // ===================================================================== /// Strategy for converting a JNI parameter into a Rust value. @@ -183,7 +155,7 @@ impl JniForm { /// Whether this form's wire JNI type is a `JObject`-shaped object that /// supports `is_null()` (used by the `Option` combinator). fn is_jni_object(&self) -> bool { - matches!(jni_object_kind(&self.jni_type), Some(_)) + jni_object_kind(&self.jni_type).is_some() } } @@ -226,33 +198,303 @@ impl ReturnForm { } } +// ===================================================================== +// JniStructConverter +// ===================================================================== + +/// Builder for [`JniStructConverter`]. +pub struct StructBuilder { + /// Module path used to fully-qualify the struct type in the generated + /// `decode_` function (e.g. `"zenoh_flat::ext"`). + source_module: syn::Path, + /// `ZResult` type used in the decoder return signature. + zresult: syn::Path, + /// Type registry that the struct converter mutates as it processes each + /// `#[prebindgen]` struct. Pre-populated with built-ins; the caller + /// typically merges in a shared `JniTypeBinding` (callbacks, enums, ...) + /// before building. + types: JniTypeBinding, +} + +impl Default for StructBuilder { + fn default() -> Self { + Self { + source_module: syn::parse_str("crate").unwrap(), + zresult: syn::parse_str("ZResult").unwrap(), + // No `with_builtins` here: classify_struct_field only handles + // bool/i64/f64 (hardcoded) and enums (looked up from the user's + // shared bindings). Pre-registering String/VecU8/bool/Duration + // would also leak empty entries into the JniTypeBinding handed + // off to the methods phase, which would clobber the methods + // builder's `string_decoder` / `byte_array_decoder` config. + types: JniTypeBinding::new(), + } + } +} + +impl StructBuilder { + /// Module path that contains the `#[prebindgen]` struct types + /// (e.g. `"zenoh_flat::ext"`). Used to qualify the struct type in + /// auto-generated decoders. + pub fn source_module(mut self, path: impl AsRef) -> Self { + self.source_module = syn::parse_str(path.as_ref()).expect("invalid source_module path"); + self + } + + /// Path of the `ZResult` type used in the decoder's return type. + pub fn zresult(mut self, path: impl AsRef) -> Self { + self.zresult = syn::parse_str(path.as_ref()).expect("invalid zresult path"); + self + } + + /// Register or replace a single [`TypeBinding`] in the type registry. + pub fn type_binding(mut self, binding: TypeBinding) -> Self { + self.types.types.insert(binding.name.clone(), binding); + self + } + + /// Merge a reusable [`JniTypeBinding`] into the type registry. Type + /// entries override entries already present with the same key; data-class + /// blocks are appended in order. + pub fn jni_type_binding(mut self, bindings: JniTypeBinding) -> Self { + self.types.types.extend(bindings.types); + self.types + .kotlin_data_classes + .extend(bindings.kotlin_data_classes); + self + } + + pub fn build(self) -> JniStructConverter { + JniStructConverter { + cfg: self, + pending: VecDeque::new(), + buffered: false, + } + } +} + +/// Converter that turns `#[prebindgen]`-marked Rust structs into JNI +/// decoder functions and Kotlin `data class` strings, while populating a +/// shared [`JniTypeBinding`] with one auto-generated entry per struct. +pub struct JniStructConverter { + cfg: StructBuilder, + pending: VecDeque<(syn::Item, SourceLocation)>, + buffered: bool, +} + +impl JniStructConverter { + pub fn builder() -> StructBuilder { + StructBuilder::default() + } + + /// Drain `iter` on the first call, convert each struct item, and queue + /// the result for the next `pop` from this batching closure. + pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> + where + I: Iterator, + { + if !self.buffered { + self.buffered = true; + for (item, loc) in iter.by_ref() { + let converted = self.convert(item, &loc); + self.pending.push_back((converted, loc)); + } + } + self.pending.pop_front() + } + + /// Closure suitable for `itertools::batching`. Borrows `&mut self` so + /// [`JniStructConverter::into_jni_type_binding`] can be called after the + /// pipeline finishes. + pub fn as_closure<'a, I>( + &'a mut self, + ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a + where + I: Iterator, + { + move |iter| self.call(iter) + } + + /// Consume the converter and return the populated [`JniTypeBinding`] — + /// now containing the original entries, the auto-registered struct + /// bindings, and the accumulated Kotlin `data class` strings. + pub fn into_jni_type_binding(self) -> JniTypeBinding { + self.cfg.types + } + + fn convert(&mut self, item: syn::Item, loc: &SourceLocation) -> syn::Item { + match item { + syn::Item::Struct(s) => self.convert_struct(s, loc), + other => panic!( + "JniStructConverter received a non-struct item at {loc}: {}", + other.to_token_stream() + ), + } + } + + /// Emit a JNI decoder for a `#[prebindgen]` struct and a matching Kotlin + /// `data class`, then auto-register a `TypeBinding` so the struct can + /// appear by value in a wrapped function's signature. + fn convert_struct(&mut self, s: syn::ItemStruct, loc: &SourceLocation) -> syn::Item { + let struct_name = s.ident.to_string(); + let struct_ident = s.ident.clone(); + let decoder_ident = format_ident!("decode_{}", struct_ident); + let zresult = self.cfg.zresult.clone(); + let struct_module = self.cfg.source_module.clone(); + + let syn::Fields::Named(named) = &s.fields else { + panic!("tuple / unit structs are not supported at {loc}"); + }; + + let mut field_preludes: Vec = Vec::new(); + let mut field_init: Vec = Vec::new(); + let mut kotlin_field_lines: Vec = Vec::new(); + + for field in &named.named { + let fname_ident = field + .ident + .as_ref() + .unwrap_or_else(|| panic!("unnamed field in struct `{struct_name}` at {loc}")) + .clone(); + let fname = fname_ident.to_string(); + let kotlin_fname = snake_to_camel(&fname); + let err_prefix = format!("{struct_name}.{kotlin_fname}: {{}}"); + + let kind = self.classify_struct_field(&field.ty); + match kind { + StructFieldKind::Bool => { + field_preludes.push(quote! { + let #fname_ident = env.get_field(obj, #kotlin_fname, "Z") + .and_then(|v| v.z()) + .map_err(|err| zerror!(#err_prefix, err))?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!(" val {}: Boolean,", kotlin_fname)); + } + StructFieldKind::I64 => { + field_preludes.push(quote! { + let #fname_ident = env.get_field(obj, #kotlin_fname, "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!(#err_prefix, err))?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!(" val {}: Long,", kotlin_fname)); + } + StructFieldKind::F64 => { + field_preludes.push(quote! { + let #fname_ident = env.get_field(obj, #kotlin_fname, "D") + .and_then(|v| v.d()) + .map_err(|err| zerror!(#err_prefix, err))?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!(" val {}: Double,", kotlin_fname)); + } + StructFieldKind::Enum(decoder) => { + let raw_ident = format_ident!("__{}_raw", fname_ident); + field_preludes.push(quote! { + let #raw_ident = env.get_field(obj, #kotlin_fname, "I") + .and_then(|v| v.i()) + .map_err(|err| zerror!(#err_prefix, err))?; + let #fname_ident = #decoder(#raw_ident)?; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!(" val {}: Int,", kotlin_fname)); + } + StructFieldKind::Unsupported => panic!( + "unsupported field type `{}` for `{}.{}` at {loc}", + field.ty.to_token_stream(), + struct_name, + fname + ), + } + } + + let tokens = quote! { + #[allow(non_snake_case, unused_mut, unused_variables)] + pub(crate) fn #decoder_ident( + env: &mut jni::JNIEnv, + obj: &jni::objects::JObject, + ) -> #zresult<#struct_module::#struct_ident> { + #(#field_preludes)* + Ok(#struct_module::#struct_ident { + #(#field_init),* + }) + } + }; + + let decoder_path: syn::Path = syn::parse_str(&format!("decode_{struct_name}")) + .expect("generated decoder ident must parse as path"); + let mut binding = TypeBinding::new(struct_name.clone()); + binding.kotlin_type = Some(struct_name.clone()); + binding.consume = Some(JniForm::new( + "jni::objects::JObject", + "JObject", + ArgDecode::EnvRefMut(decoder_path), + )); + self.cfg.types.types.insert(struct_name.clone(), binding); + + let block = format!( + "data class {}(\n{}\n)", + struct_name, + kotlin_field_lines.join("\n") + ); + self.cfg.types.kotlin_data_classes.push(block); + + syn::parse2(tokens).expect("generated struct decoder must parse") + } + + /// Classify a `#[prebindgen]` struct field's type for JNI round-tripping. + fn classify_struct_field(&self, ty: &syn::Type) -> StructFieldKind { + let syn::Type::Path(tp) = ty else { + return StructFieldKind::Unsupported; + }; + let Some(last) = tp.path.segments.last() else { + return StructFieldKind::Unsupported; + }; + let name = last.ident.to_string(); + match name.as_str() { + "bool" => StructFieldKind::Bool, + "i64" => StructFieldKind::I64, + "f64" => StructFieldKind::F64, + _ => { + // Enum decoders are stored as a `Pure` ArgDecode on the + // type's binding's `consume` form. + if let Some(binding) = self.cfg.types.types.get(&name) { + if let Some(form) = binding.consume.as_ref() { + if let ArgDecode::Pure(p) = &form.decode { + return StructFieldKind::Enum(p.clone()); + } + } + } + StructFieldKind::Unsupported + } + } + } +} // ===================================================================== -// Builder +// JniMethodsConverter // ===================================================================== -/// Builder for [`JniConverter`]. -pub struct Builder { +/// Builder for [`JniMethodsConverter`]. +pub struct MethodsBuilder { class_prefix: String, function_suffix: String, source_module: syn::Path, - /// Module path where `#[prebindgen]` struct types are declared. Used to - /// fully-qualify the struct name in the auto-generated decoder's return - /// type and constructor. Defaults to `source_module` when unset. - struct_source_module: Option, owned_object: syn::Path, zresult: syn::Path, throw_exception: syn::Path, - /// Primary type registry keyed by short type name. Callback registrations - /// (for `impl Fn(T) + Send + Sync + 'static` parameters) live here too, - /// as the [`CallbackForm`] slot of the element type's binding. - types: HashMap, + /// Type registry. Built-ins are pre-registered; the caller threads in + /// the [`JniTypeBinding`] returned by + /// [`JniStructConverter::into_jni_type_binding`] (which itself contains + /// the user's shared bindings + auto-registered struct entries). + types: JniTypeBinding, /// Kotlin output config — if `None`, no Kotlin file is emitted. kotlin: Option, } /// Settings for generating a companion Kotlin file with `external fun` -/// prototypes. Enabled via [`Builder::kotlin_output`]. +/// prototypes. Enabled via [`MethodsBuilder::kotlin_output`]. pub(crate) struct KotlinConfig { output_path: PathBuf, package: String, @@ -265,67 +507,22 @@ pub(crate) struct KotlinConfig { init_load_fqn: Option, } -impl Default for Builder { +impl Default for MethodsBuilder { fn default() -> Self { - let mut b = Self { + Self { class_prefix: String::new(), function_suffix: String::new(), source_module: syn::parse_str("crate").unwrap(), - struct_source_module: None, owned_object: syn::parse_str("OwnedObject").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), - types: HashMap::new(), + types: JniTypeBinding::new().with_builtins(), kotlin: None, - }; - register_builtins(&mut b.types); - b + } } } -/// Pre-register built-in language types (`bool`, `Duration`) plus the -/// scaffolding for `String` / `Vec` (whose decoders are filled in by -/// [`Builder::string_decoder`] / [`Builder::byte_array_decoder`]). -fn register_builtins(types: &mut HashMap) { - // bool — jboolean, inline `x != 0`. - types.insert( - "bool".to_string(), - TypeBinding::new("bool").consume( - JniForm::new( - "jni::sys::jboolean", - "Boolean", - ArgDecode::Inline(InlineFn::new(|input| quote! { #input != 0 })), - ), - ), - ); - // Duration — jlong, inline `Duration::from_millis(x as u64)`. - types.insert( - "Duration".to_string(), - TypeBinding::new("Duration").consume( - JniForm::new( - "jni::sys::jlong", - "Long", - ArgDecode::Inline(InlineFn::new( - |input| quote! { std::time::Duration::from_millis(#input as u64) }, - )), - ), - ), - ); - // String — JString, decoder filled by string_decoder(). - types.insert( - "String".to_string(), - TypeBinding::new("String"), - ); - // Vec — keyed under the synthetic name "VecU8" (looked up explicitly - // by classify_arg when it sees `Vec`). Decoder filled by - // byte_array_decoder(). - types.insert( - "VecU8".to_string(), - TypeBinding::new("VecU8"), - ); -} - -impl Builder { +impl MethodsBuilder { /// JNI class prefix prepended to each function name, e.g. /// `"Java_io_zenoh_jni_JNISession_"`. pub fn class_prefix(mut self, prefix: impl Into) -> Self { @@ -346,16 +543,6 @@ impl Builder { self } - /// Fully-qualified path of the module that contains the `#[prebindgen]` - /// struct types (e.g. `"zenoh_flat::ext"`). When unset, defaults to - /// [`Builder::source_module`]. Used to qualify the struct type in - /// auto-generated decoders. - pub fn struct_source_module(mut self, path: impl AsRef) -> Self { - self.struct_source_module = - Some(syn::parse_str(path.as_ref()).expect("invalid struct_source_module path")); - self - } - /// Path of the `OwnedObject` helper used to borrow Arc-pointers in the /// `OwnedRef` decode form. pub fn owned_object(mut self, path: impl AsRef) -> Self { @@ -377,21 +564,19 @@ impl Builder { } /// Universal entry point: register or replace a [`TypeBinding`] by name. - /// All sugar methods below delegate to this. pub fn type_binding(mut self, binding: TypeBinding) -> Self { - self.types.insert(binding.name.clone(), binding); + self.types.types.insert(binding.name.clone(), binding); self } - /// Ingest a reusable [`crate::jni_type_binding::JniTypeBinding`] - /// collection. All entries are merged into the builder's type registry; - /// entries in the collection override entries already present in the - /// builder with the same key. - pub fn jni_type_binding( - mut self, - bindings: crate::jni_type_binding::JniTypeBinding, - ) -> Self { - self.types.extend(bindings.types); + /// Merge a reusable [`JniTypeBinding`] into the type registry. Type + /// entries override entries already present with the same key; data-class + /// blocks are appended in order. + pub fn jni_type_binding(mut self, bindings: JniTypeBinding) -> Self { + self.types.types.extend(bindings.types); + self.types + .kotlin_data_classes + .extend(bindings.kotlin_data_classes); self } @@ -400,6 +585,7 @@ impl Builder { pub fn string_decoder(mut self, path: impl AsRef) -> Self { let p: syn::Path = syn::parse_str(path.as_ref()).expect("invalid string_decoder path"); let entry = self + .types .types .get_mut("String") .expect("built-in `String` binding missing"); @@ -417,6 +603,7 @@ impl Builder { let p: syn::Path = syn::parse_str(path.as_ref()).expect("invalid byte_array_decoder path"); let entry = self + .types .types .get_mut("VecU8") .expect("built-in `VecU8` binding missing"); @@ -429,9 +616,12 @@ impl Builder { } /// Enable Kotlin-side prototype generation. `path` is where the `.kt` - /// file will be written when [`JniConverter::write_kotlin`] is called. + /// file will be written when [`JniMethodsConverter::write_kotlin`] is + /// called. pub fn kotlin_output(mut self, path: impl Into) -> Self { - self.kotlin.get_or_insert_with(KotlinConfig::default).output_path = path.into(); + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .output_path = path.into(); self } @@ -445,31 +635,36 @@ impl Builder { /// agree with the JNI class_prefix — i.e. `class_prefix` should end with /// `"__"`. pub fn kotlin_class(mut self, name: impl Into) -> Self { - self.kotlin.get_or_insert_with(KotlinConfig::default).class_name = name.into(); + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .class_name = name.into(); self } /// FQN of the exception type to annotate every `external fun` with via /// `@Throws(::class)`. Unset ⇒ no annotation. pub fn kotlin_throws(mut self, fqn: impl Into) -> Self { - self.kotlin.get_or_insert_with(KotlinConfig::default).throws_class_fqn = Some(fqn.into()); + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .throws_class_fqn = Some(fqn.into()); self } /// FQN of a singleton referenced from the generated `init { ... }` block /// (typically `io.zenoh.ZenohLoad`) to force native-library loading. pub fn kotlin_init(mut self, fqn: impl Into) -> Self { - self.kotlin.get_or_insert_with(KotlinConfig::default).init_load_fqn = Some(fqn.into()); + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .init_load_fqn = Some(fqn.into()); self } - pub fn build(self) -> JniConverter { - JniConverter { + pub fn build(self) -> JniMethodsConverter { + JniMethodsConverter { cfg: self, pending: VecDeque::new(), buffered: false, kotlin_funs: Vec::new(), - kotlin_data_classes: Vec::new(), kotlin_used_fqns: BTreeSet::new(), } } @@ -487,46 +682,32 @@ impl Default for KotlinConfig { } } -// ===================================================================== -// JniConverter -// ===================================================================== - /// Converter that transforms `#[prebindgen]`-marked Rust functions into JNI -/// `Java_*` wrappers. -pub struct JniConverter { - cfg: Builder, +/// `Java_*` wrappers and matching Kotlin `external fun` prototypes. +pub struct JniMethodsConverter { + cfg: MethodsBuilder, pending: VecDeque<(syn::Item, SourceLocation)>, - /// `true` once the source iterator has been drained and sorted (structs - /// before functions) so that function-arg classification can see every - /// auto-registered struct binding. buffered: bool, /// Accumulated Kotlin `external fun ...` blocks, one per wrapped function. kotlin_funs: Vec, - /// Accumulated Kotlin `data class ...` blocks, one per `#[prebindgen]` - /// struct seen in the source stream. - kotlin_data_classes: Vec, /// Set of Kotlin FQNs referenced by emitted externals. kotlin_used_fqns: BTreeSet, } -impl JniConverter { - pub fn builder() -> Builder { - Builder::default() +impl JniMethodsConverter { + pub fn builder() -> MethodsBuilder { + MethodsBuilder::default() } - /// Drain `iter` on the first call, sort so `#[prebindgen]` struct items - /// are processed before functions (so function-arg classification can see - /// every auto-registered struct binding), then return converted items - /// one at a time from the buffer. + /// Drain `iter` on the first call, convert each fn item, and queue the + /// result for the next `pop` from this batching closure. pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> where I: Iterator, { if !self.buffered { self.buffered = true; - let mut all: Vec<(syn::Item, SourceLocation)> = iter.by_ref().collect(); - all.sort_by_key(|(it, _)| !matches!(it, syn::Item::Struct(_))); - for (item, loc) in all { + for (item, loc) in iter.by_ref() { let converted = self.convert(item, &loc); self.pending.push_back((converted, loc)); } @@ -534,19 +715,9 @@ impl JniConverter { self.pending.pop_front() } - /// Closure suitable for `itertools::batching`. Consumes `self`. - pub fn into_closure( - mut self, - ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> - where - I: Iterator, - { - move |iter| self.call(iter) - } - - /// Borrowing closure suitable for `itertools::batching`. Unlike - /// [`JniConverter::into_closure`], this does not consume `self`, so - /// [`JniConverter::write_kotlin`] can be called after the pipeline. + /// Borrowing closure suitable for `itertools::batching`. Does not consume + /// `self` so [`JniMethodsConverter::write_kotlin`] can be called after + /// the pipeline finishes. pub fn as_closure<'a, I>( &'a mut self, ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a @@ -556,8 +727,8 @@ impl JniConverter { move |iter| self.call(iter) } - /// Write the accumulated Kotlin `external fun ...` prototypes to the - /// configured output path. No-op when Kotlin output was not enabled. + /// Write the accumulated Kotlin file to the configured output path. + /// No-op when Kotlin output was not enabled. pub fn write_kotlin(&self) -> std::io::Result<()> { let Some(kt) = self.cfg.kotlin.as_ref() else { return Ok(()); @@ -601,7 +772,7 @@ impl JniConverter { if !imports.is_empty() { out.push('\n'); } - for block in &self.kotlin_data_classes { + for block in &self.cfg.types.kotlin_data_classes { out.push_str(block); out.push_str("\n\n"); } @@ -624,8 +795,10 @@ impl JniConverter { fn convert(&mut self, item: syn::Item, loc: &SourceLocation) -> syn::Item { match item { syn::Item::Fn(func) => syn::Item::Fn(self.convert_fn(func, loc)), - syn::Item::Struct(s) => self.convert_struct(s, loc), - other => other, + other => panic!( + "JniMethodsConverter received a non-fn item at {loc}: {}", + other.to_token_stream() + ), } } @@ -816,153 +989,6 @@ impl JniConverter { syn::parse2(tokens).expect("generated JNI wrapper must parse") } - /// Emit a JNI decoder for a `#[prebindgen]` struct and a matching Kotlin - /// `data class`, then auto-register a `TypeBinding` so the struct can - /// appear by value in a wrapped function's signature. - fn convert_struct(&mut self, s: syn::ItemStruct, loc: &SourceLocation) -> syn::Item { - let struct_name = s.ident.to_string(); - let struct_ident = s.ident.clone(); - let decoder_ident = format_ident!("decode_{}", struct_ident); - let zresult = self.cfg.zresult.clone(); - let struct_module = self - .cfg - .struct_source_module - .clone() - .unwrap_or_else(|| self.cfg.source_module.clone()); - - let syn::Fields::Named(named) = &s.fields else { - panic!("tuple / unit structs are not supported at {loc}"); - }; - - let mut field_preludes: Vec = Vec::new(); - let mut field_init: Vec = Vec::new(); - let mut kotlin_field_lines: Vec = Vec::new(); - - for field in &named.named { - let fname_ident = field - .ident - .as_ref() - .unwrap_or_else(|| panic!("unnamed field in struct `{struct_name}` at {loc}")) - .clone(); - let fname = fname_ident.to_string(); - let kotlin_fname = snake_to_camel(&fname); - let err_prefix = format!("{struct_name}.{kotlin_fname}: {{}}"); - - let kind = self.classify_struct_field(&field.ty); - match kind { - StructFieldKind::Bool => { - field_preludes.push(quote! { - let #fname_ident = env.get_field(obj, #kotlin_fname, "Z") - .and_then(|v| v.z()) - .map_err(|err| zerror!(#err_prefix, err))?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines - .push(format!(" val {}: Boolean,", kotlin_fname)); - } - StructFieldKind::I64 => { - field_preludes.push(quote! { - let #fname_ident = env.get_field(obj, #kotlin_fname, "J") - .and_then(|v| v.j()) - .map_err(|err| zerror!(#err_prefix, err))?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines.push(format!(" val {}: Long,", kotlin_fname)); - } - StructFieldKind::F64 => { - field_preludes.push(quote! { - let #fname_ident = env.get_field(obj, #kotlin_fname, "D") - .and_then(|v| v.d()) - .map_err(|err| zerror!(#err_prefix, err))?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines.push(format!(" val {}: Double,", kotlin_fname)); - } - StructFieldKind::Enum(decoder) => { - let raw_ident = format_ident!("__{}_raw", fname_ident); - field_preludes.push(quote! { - let #raw_ident = env.get_field(obj, #kotlin_fname, "I") - .and_then(|v| v.i()) - .map_err(|err| zerror!(#err_prefix, err))?; - let #fname_ident = #decoder(#raw_ident)?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines.push(format!(" val {}: Int,", kotlin_fname)); - } - StructFieldKind::Unsupported => panic!( - "unsupported field type `{}` for `{}.{}` at {loc}", - field.ty.to_token_stream(), - struct_name, - fname - ), - } - } - - let tokens = quote! { - #[allow(non_snake_case, unused_mut, unused_variables)] - pub(crate) fn #decoder_ident( - env: &mut jni::JNIEnv, - obj: &jni::objects::JObject, - ) -> #zresult<#struct_module::#struct_ident> { - #(#field_preludes)* - Ok(#struct_module::#struct_ident { - #(#field_init),* - }) - } - }; - - // Auto-register a TypeBinding for this struct. - let decoder_path: syn::Path = syn::parse_str(&format!("decode_{struct_name}")) - .expect("generated decoder ident must parse as path"); - let mut binding = TypeBinding::new(struct_name.clone()); - binding.kotlin_type = Some(struct_name.clone()); // bare — same package - binding.consume = Some(JniForm::new( - "jni::objects::JObject", - "JObject", - ArgDecode::EnvRefMut(decoder_path), - )); - self.cfg.types.insert(struct_name.clone(), binding); - - if self.cfg.kotlin.is_some() { - let block = format!( - "data class {}(\n{}\n)", - struct_name, - kotlin_field_lines.join("\n") - ); - self.kotlin_data_classes.push(block); - } - - syn::parse2(tokens).expect("generated struct decoder must parse") - } - - /// Classify a `#[prebindgen]` struct field's type for JNI round-tripping. - fn classify_struct_field(&self, ty: &syn::Type) -> StructFieldKind { - let syn::Type::Path(tp) = ty else { - return StructFieldKind::Unsupported; - }; - let Some(last) = tp.path.segments.last() else { - return StructFieldKind::Unsupported; - }; - let name = last.ident.to_string(); - match name.as_str() { - "bool" => StructFieldKind::Bool, - "i64" => StructFieldKind::I64, - "f64" => StructFieldKind::F64, - _ => { - // Enum decoders are stored as a `Pure` ArgDecode on the - // type's binding's `consume` form. - if let Some(binding) = self.cfg.types.get(&name) { - if let Some(form) = binding.consume.as_ref() { - if let ArgDecode::Pure(p) = &form.decode { - return StructFieldKind::Enum(p.clone()); - } - } - } - StructFieldKind::Unsupported - } - } - } - fn lookup_return_form(&self, ty: &syn::Type) -> Option<&ReturnForm> { let syn::Type::Path(tp) = ty else { return None }; let seg = tp.path.segments.last()?; @@ -978,19 +1004,25 @@ impl JniConverter { return self .cfg .types + .types .get(&elem_name) .and_then(|b| b.returns_vec.as_ref()); } - self.cfg.types.get(&name).and_then(|b| b.returns.as_ref()) + self.cfg + .types + .types + .get(&name) + .and_then(|b| b.returns.as_ref()) } /// Classify a function-arg type into one of the uniform variants below. fn classify_arg(&self, ty: &syn::Type, _name: &syn::Ident) -> ArgKind { + let types: &HashMap = &self.cfg.types.types; match ty { syn::Type::Reference(r) if r.mutability.is_none() => { let elem = &*r.elem; let last = type_last_segment(elem).unwrap_or_default(); - if let Some(binding) = self.cfg.types.get(&last) { + if let Some(binding) = types.get(&last) { if let Some(form) = binding.borrow.as_ref() { return ArgKind::Borrow { form: form.clone(), @@ -1016,7 +1048,7 @@ impl JniConverter { } syn::Type::ImplTrait(_) => { let key = ty.to_token_stream().to_string(); - if let Some(binding) = self.cfg.types.get(&key) { + if let Some(binding) = types.get(&key) { if let Some(form) = binding.consume.as_ref() { return ArgKind::Consume { form: form.clone(), @@ -1035,7 +1067,7 @@ impl JniConverter { if name == "Option" { let inner_seg = last; if is_option_of_vec_u8(inner_seg) { - if let Some(binding) = self.cfg.types.get("VecU8") { + if let Some(binding) = types.get("VecU8") { if let Some(form) = binding.consume.as_ref() { return ArgKind::OptionConsume { form: form.clone(), @@ -1046,7 +1078,7 @@ impl JniConverter { return ArgKind::Unsupported; } if let Some(inner) = option_inner_type_name(inner_seg) { - if let Some(binding) = self.cfg.types.get(&inner) { + if let Some(binding) = types.get(&inner) { if let Some(form) = binding.consume.as_ref() { return ArgKind::OptionConsume { form: form.clone(), @@ -1059,7 +1091,7 @@ impl JniConverter { } if name == "Vec" && is_vec_of_u8(last) { - if let Some(binding) = self.cfg.types.get("VecU8") { + if let Some(binding) = types.get("VecU8") { if let Some(form) = binding.consume.as_ref() { return ArgKind::Consume { form: form.clone(), @@ -1070,7 +1102,7 @@ impl JniConverter { return ArgKind::Unsupported; } - if let Some(binding) = self.cfg.types.get(&name) { + if let Some(binding) = types.get(&name) { if let Some(form) = binding.consume.as_ref() { return ArgKind::Consume { form: form.clone(), @@ -1224,7 +1256,6 @@ impl JniConverter { } } - // The wrapped function call site: by-value vs by-reference. if borrow && matches!(form.decode, ArgDecode::OwnedRef) { // OwnedRef pattern: pass `&name` to match historical OpaqueRef behavior. call_args.push(quote! { &#name }); @@ -1274,7 +1305,7 @@ impl JniConverter { } // ===================================================================== -// ArgKind — the slim, uniform classification +// Internal classification helpers // ===================================================================== enum ArgKind { diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index a08daf3e..9bc55e95 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -1,14 +1,14 @@ //! Reusable collection of JNI type bindings. //! //! [`JniTypeBinding`] aggregates a set of [`TypeBinding`]s — including -//! callback registrations, which live as [`crate::jni_converter::CallbackForm`] -//! slots on the element type's binding — into a single value that can be -//! defined once and ingested into many [`crate::jni_converter::Builder`] -//! instances. Useful when a project emits several `JniConverter` outputs -//! (e.g. one per JNI class) that share a common vocabulary of types. +//! callback registrations, which live as `consume` slots on the element +//! type's binding — into a single value that can be defined once and +//! threaded through both phases of the JNI binding pipeline (a +//! [`crate::jni_converter::JniStructConverter`] mutates it; a +//! [`crate::jni_converter::JniMethodsConverter`] reads it). //! //! ```ignore -//! use zenoh_flat::jni_converter::{ArgDecode, JniConverter, JniForm, TypeBinding}; +//! use zenoh_flat::jni_converter::{ArgDecode, JniForm, TypeBinding}; //! use zenoh_flat::jni_type_binding::JniTypeBinding; //! //! let common = JniTypeBinding::new() @@ -22,24 +22,13 @@ //! .pointer_param(true), //! ), //! ); -//! -//! let session_converter = JniConverter::builder() -//! .class_prefix("Java_io_zenoh_jni_JNISessionNative_") -//! .jni_type_binding(common.clone()) -//! // ...other builder calls... -//! .build(); -//! -//! let publisher_converter = JniConverter::builder() -//! .class_prefix("Java_io_zenoh_jni_JNIPublisherNative_") -//! .jni_type_binding(common) -//! // ...other builder calls... -//! .build(); //! ``` use std::collections::HashMap; -use quote::ToTokens; -use crate::jni_converter::{JniForm, ReturnForm}; +use quote::{quote, ToTokens}; + +use crate::jni_converter::{ArgDecode, InlineFn, JniForm, ReturnForm}; /// Per-type description of how a Rust type is represented across the JNI /// boundary. A type may declare up to four forms: @@ -117,14 +106,19 @@ impl TypeBinding { } } -/// Reusable collection of [`TypeBinding`]s. +/// Reusable collection of [`TypeBinding`]s plus the Kotlin `data class` +/// strings produced by struct processing. /// -/// Built fluently and consumed by -/// [`crate::jni_converter::Builder::jni_type_binding`]. `Clone` so the same -/// set can be ingested into multiple builders. +/// The same value flows through both pipeline phases: the +/// [`crate::jni_converter::JniStructConverter`] inserts an auto-generated +/// `TypeBinding` plus a `data class` block for each `#[prebindgen]` struct +/// it sees; the [`crate::jni_converter::JniMethodsConverter`] then reads the +/// type registry to classify args/returns and reads the data-class strings +/// when emitting the final Kotlin file. #[derive(Default, Clone)] pub struct JniTypeBinding { pub(crate) types: HashMap, + pub(crate) kotlin_data_classes: Vec, } impl JniTypeBinding { @@ -138,10 +132,48 @@ impl JniTypeBinding { self } - /// Merge another [`JniTypeBinding`] into this one. Entries in `other` - /// override entries with the same key in `self`. + /// Merge another [`JniTypeBinding`] into this one. Type entries in + /// `other` override entries with the same key in `self`; data-class + /// blocks are appended in order. pub fn merge(mut self, other: JniTypeBinding) -> Self { self.types.extend(other.types); + self.kotlin_data_classes.extend(other.kotlin_data_classes); + self + } + + /// Pre-register built-in language types (`bool`, `Duration`) plus the + /// scaffolding for `String` / `Vec` (whose decoders are filled in by + /// `JniMethodsConverter::Builder::string_decoder` / + /// `JniMethodsConverter::Builder::byte_array_decoder`). + pub fn with_builtins(mut self) -> Self { + // bool — jboolean, inline `x != 0`. + self.types.insert( + "bool".to_string(), + TypeBinding::new("bool").consume(JniForm::new( + "jni::sys::jboolean", + "Boolean", + ArgDecode::Inline(InlineFn::new(|input| quote! { #input != 0 })), + )), + ); + // Duration — jlong, inline `Duration::from_millis(x as u64)`. + self.types.insert( + "Duration".to_string(), + TypeBinding::new("Duration").consume(JniForm::new( + "jni::sys::jlong", + "Long", + ArgDecode::Inline(InlineFn::new(|input| { + quote! { std::time::Duration::from_millis(#input as u64) } + })), + )), + ); + // String — JString, decoder filled by string_decoder(). + self.types + .insert("String".to_string(), TypeBinding::new("String")); + // Vec — keyed under the synthetic name "VecU8" (looked up + // explicitly by classify_arg when it sees `Vec`). Decoder filled + // by byte_array_decoder(). + self.types + .insert("VecU8".to_string(), TypeBinding::new("VecU8")); self } } diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index ef58dad5..cf99ab53 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -4445,6 +4445,7 @@ dependencies = [ "prebindgen", "rustc_version", "serde_yaml", + "syn 2.0.117", "tracing", "uhlc", "zenoh", diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index acddd84c..17a7021f 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -51,6 +51,7 @@ rustc_version = "0.4.0" prebindgen = "0.4.1" zenoh-flat = { path = "../zenoh-flat", features = ["zenoh-ext"] } itertools = "0.12" +syn = "2" [profile.release] debug = false # If you want debug symbol in release mode, set the env variable: RUSTFLAGS=-g diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index a07ea900..d520cba4 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,5 +1,8 @@ use itertools::Itertools; -use zenoh_flat::jni_converter::{ArgDecode, JniForm, ReturnEncode, ReturnForm, TypeBinding}; +use zenoh_flat::jni_converter::{ + ArgDecode, JniForm, JniMethodsConverter, JniStructConverter, ReturnEncode, ReturnForm, + TypeBinding, +}; use zenoh_flat::jni_type_binding::JniTypeBinding; fn enum_binding(name: &str, decoder: &str) -> TypeBinding { @@ -14,10 +17,10 @@ fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { )) } -/// Type vocabulary shared across every `JniConverter` build in this crate. -/// Defined once and ingested via `Builder::jni_type_binding(...)` so each -/// generated JNI surface (session, publisher, subscriber, ...) sees the same -/// types without duplicating registrations. +/// Type vocabulary shared across every JNI surface generated in this crate. +/// Defined once, threaded into the struct-phase converter (so struct field +/// decoders can resolve enum types), then forwarded — together with the +/// auto-registered struct bindings — into the methods-phase converter. fn shared_bindings() -> JniTypeBinding { JniTypeBinding::new() .type_binding(jobject_consume( @@ -101,11 +104,31 @@ fn shared_bindings() -> JniTypeBinding { fn main() { let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); - let mut converter = zenoh_flat::jni_converter::JniConverter::builder() + // Phase 1: process #[prebindgen] structs from zenoh_flat::ext. + // Each struct adds a TypeBinding (and a Kotlin data class) to the + // shared JniTypeBinding that we forward to the methods converter. + let mut struct_conv = JniStructConverter::builder() + .source_module("zenoh_flat::ext") + .zresult("crate::errors::ZResult") + .jni_type_binding(shared_bindings()) + .build(); + + let struct_items: Vec<_> = source + .items_all() + .filter(|(item, loc)| { + matches!(item, syn::Item::Struct(_)) && loc.file.ends_with("/ext.rs") + }) + .batching(struct_conv.as_closure()) + .collect(); + + let types = struct_conv.into_jni_type_binding(); + + // Phase 2: process #[prebindgen] fns from zenoh_flat::session against + // the now fully-populated type registry. + let mut method_conv = JniMethodsConverter::builder() .class_prefix("Java_io_zenoh_jni_JNISessionNative_") .function_suffix("ViaJNI") .source_module("zenoh_flat::session") - .struct_source_module("zenoh_flat::ext") .owned_object("crate::owned_object::OwnedObject") .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") @@ -116,12 +139,29 @@ fn main() { .kotlin_class("JNISessionNative") .kotlin_throws("io.zenoh.exceptions.ZError") .kotlin_init("io.zenoh.ZenohLoad") - .jni_type_binding(shared_bindings()) + .jni_type_binding(types) .build(); - let bindings_file =source + let method_items: Vec<_> = source .items_all() - .batching(converter.as_closure()) + .filter(|(item, loc)| { + matches!(item, syn::Item::Fn(_)) && loc.file.ends_with("/session.rs") + }) + .batching(method_conv.as_closure()) + .collect(); + + // Pass-through: items that are neither `#[prebindgen]` structs nor fns + // (e.g. the prebindgen feature-mismatch assertion `const _: () = { ... };`). + // The two converters intentionally panic on the wrong item kind, so any + // such items must bypass them and land directly in the destination. + let passthrough = source + .items_all() + .filter(|(item, _)| !matches!(item, syn::Item::Fn(_) | syn::Item::Struct(_))); + + let bindings_file = struct_items + .into_iter() + .chain(method_items) + .chain(passthrough) .collect::() .write("zenoh_flat_jni.rs"); @@ -130,7 +170,7 @@ fn main() { bindings_file.display() ); - converter + method_conv .write_kotlin() .expect("failed to write generated Kotlin file"); } diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index 356e7474..e259268c 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -179,7 +179,7 @@ impl Drop for CallOnDrop { /// callback fires exactly once when the data closure is dropped (via /// `CallOnDrop`). Used by hand-written JNI entry points whose Kotlin signatures /// pair a callback with an `on_close: JNIOnCloseCallback`. Generated entry -/// points (via `JniConverter`) take `on_close` as a separate Rust parameter +/// points (via `JniMethodsConverter`) take `on_close` as a separate Rust parameter /// instead and are wrapped at the zenoh-flat layer. pub(crate) fn wrap_with_on_close( env: &mut JNIEnv, From 6e7ac06e08874fbacf8275e94bf9119328c98637 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 00:25:40 +0200 Subject: [PATCH 116/126] decode_string and decode_byte_array are universal now --- zenoh-flat/src/jni_converter.rs | 58 ++----------------------- zenoh-flat/src/jni_type_binding.rs | 21 ++++----- zenoh-jni/build.rs | 19 +++++++- zenoh-jni/src/ext/advanced_publisher.rs | 6 +-- zenoh-jni/src/publisher.rs | 6 +-- zenoh-jni/src/querier.rs | 4 +- zenoh-jni/src/query.rs | 8 ++-- zenoh-jni/src/utils.rs | 4 +- zenoh-jni/src/zbytes.rs | 4 +- zenoh-jni/src/zbytes_kotlin.rs | 4 +- zenoh-jni/src/zenoh_id.rs | 2 +- 11 files changed, 48 insertions(+), 88 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index e7008881..2926f4e9 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -53,9 +53,6 @@ pub use crate::jni_type_binding::TypeBinding; pub enum ArgDecode { /// `let = (&mut env, &)?;` EnvRefMut(syn::Path), - /// `let = (&env, )?;` — used by the legacy - /// `byte_array_decoder` calling convention. - EnvByVal(syn::Path), /// `let = ()?;` — pure conversion (e.g. enum decoders). Pure(syn::Path), /// `let = ;` — inline transformation built from the input @@ -83,13 +80,6 @@ impl ArgDecode { syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_ref_mut path"), ) } - - /// `ArgDecode::EnvByVal` from a path string. - pub fn env_by_val(path: impl AsRef) -> Self { - ArgDecode::EnvByVal( - syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_by_val path"), - ) - } } impl ReturnEncode { @@ -221,12 +211,9 @@ impl Default for StructBuilder { Self { source_module: syn::parse_str("crate").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), - // No `with_builtins` here: classify_struct_field only handles - // bool/i64/f64 (hardcoded) and enums (looked up from the user's - // shared bindings). Pre-registering String/VecU8/bool/Duration - // would also leak empty entries into the JniTypeBinding handed - // off to the methods phase, which would clobber the methods - // builder's `string_decoder` / `byte_array_decoder` config. + // No `with_builtins` here: `classify_struct_field` handles + // bool/i64/f64 directly and resolves enums from the shared + // bindings the caller merges in. types: JniTypeBinding::new(), } } @@ -580,41 +567,6 @@ impl MethodsBuilder { self } - /// Path of the function that decodes a `JString` into `String`. Used by - /// the built-in `String` binding. - pub fn string_decoder(mut self, path: impl AsRef) -> Self { - let p: syn::Path = syn::parse_str(path.as_ref()).expect("invalid string_decoder path"); - let entry = self - .types - .types - .get_mut("String") - .expect("built-in `String` binding missing"); - entry.consume = Some(JniForm::new( - "jni::objects::JString", - "String", - ArgDecode::EnvRefMut(p), - )); - self - } - - /// Path of the function that decodes a `JByteArray` into `Vec`. Used - /// by the built-in `Vec` binding (under the internal name `"VecU8"`). - pub fn byte_array_decoder(mut self, path: impl AsRef) -> Self { - let p: syn::Path = - syn::parse_str(path.as_ref()).expect("invalid byte_array_decoder path"); - let entry = self - .types - .types - .get_mut("VecU8") - .expect("built-in `VecU8` binding missing"); - entry.consume = Some(JniForm::new( - "jni::objects::JByteArray", - "ByteArray", - ArgDecode::EnvByVal(p), - )); - self - } - /// Enable Kotlin-side prototype generation. `path` is where the `.kt` /// file will be written when [`JniMethodsConverter::write_kotlin`] is /// called. @@ -1233,9 +1185,6 @@ impl JniMethodsConverter { ArgDecode::EnvRefMut(path) => { prelude.push(quote! { let #name = #path(&mut env, &#pat)?; }); } - ArgDecode::EnvByVal(path) => { - prelude.push(quote! { let #name = #path(&env, #pat)?; }); - } ArgDecode::Pure(path) => { prelude.push(quote! { let #name = #path(#pat)?; }); } @@ -1277,7 +1226,6 @@ impl JniMethodsConverter { fn decode_expr(&self, decode: &ArgDecode, input: &syn::Ident) -> TokenStream { match decode { ArgDecode::EnvRefMut(path) => quote! { #path(&mut env, &#input)? }, - ArgDecode::EnvByVal(path) => quote! { #path(&env, #input)? }, ArgDecode::Pure(path) => quote! { #path(#input)? }, ArgDecode::Inline(f) => f.call(input), ArgDecode::OwnedRef => { diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index 9bc55e95..fc696b3f 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -141,10 +141,15 @@ impl JniTypeBinding { self } - /// Pre-register built-in language types (`bool`, `Duration`) plus the - /// scaffolding for `String` / `Vec` (whose decoders are filled in by - /// `JniMethodsConverter::Builder::string_decoder` / - /// `JniMethodsConverter::Builder::byte_array_decoder`). + /// Pre-register built-in language types whose JNI form is fully described + /// without any project-specific decoder path: `bool` (inline `x != 0`) + /// and `Duration` (inline `Duration::from_millis(x as u64)`). + /// + /// Types whose decoder lives outside this crate — `String`, `Vec`, + /// callbacks, enums, opaque handles — are registered by the caller via + /// the universal [`JniTypeBinding::type_binding`] entry point. `Vec` + /// is keyed under the synthetic name `"VecU8"` (looked up explicitly by + /// the methods-phase classifier when it sees `Vec`). pub fn with_builtins(mut self) -> Self { // bool — jboolean, inline `x != 0`. self.types.insert( @@ -166,14 +171,6 @@ impl JniTypeBinding { })), )), ); - // String — JString, decoder filled by string_decoder(). - self.types - .insert("String".to_string(), TypeBinding::new("String")); - // Vec — keyed under the synthetic name "VecU8" (looked up - // explicitly by classify_arg when it sees `Vec`). Decoder filled - // by byte_array_decoder(). - self.types - .insert("VecU8".to_string(), TypeBinding::new("VecU8")); self } } diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index d520cba4..b1deea51 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -23,6 +23,23 @@ fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { /// auto-registered struct bindings — into the methods-phase converter. fn shared_bindings() -> JniTypeBinding { JniTypeBinding::new() + .type_binding( + TypeBinding::new("String").consume(JniForm::new( + "jni::objects::JString", + "String", + ArgDecode::env_ref_mut("crate::utils::decode_string"), + )), + ) + // `Vec` is keyed under the synthetic name "VecU8" — the + // methods-phase classifier looks it up explicitly when it sees + // `Vec`. + .type_binding( + TypeBinding::new("VecU8").consume(JniForm::new( + "jni::objects::JByteArray", + "ByteArray", + ArgDecode::env_ref_mut("crate::utils::decode_byte_array"), + )), + ) .type_binding(jobject_consume( "impl Fn(Sample) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_sample_callback", @@ -132,8 +149,6 @@ fn main() { .owned_object("crate::owned_object::OwnedObject") .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") - .string_decoder("crate::utils::decode_string") - .byte_array_decoder("crate::utils::decode_byte_array") .kotlin_output("../zenoh-jni/generated-kotlin/io/zenoh/jni/JNISessionNative.kt") .kotlin_package("io.zenoh.jni") .kotlin_class("JNISessionNative") diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index bf056141..fba0c274 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -267,12 +267,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_putViaJNI( ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { - let payload = decode_byte_array(&env, payload)?; + let payload = decode_byte_array(&mut env, &payload)?; let mut publication = publisher.put(payload); let encoding = decode_jni_encoding(&mut env, &encoding)?; publication = publication.encoding(encoding); if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; + let attachment = decode_byte_array(&mut env, &attachment)?; publication = publication.attachment::>(attachment) }; publication.wait().map_err(|err| zerror!(err)) @@ -306,7 +306,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_deleteViaJNI( let _ = || -> ZResult<()> { let mut delete = publisher.delete(); if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; + let attachment = decode_byte_array(&mut env, &attachment)?; delete = delete.attachment::>(attachment) }; delete.wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index 3717f1d0..94a9566b 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -56,12 +56,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { - let payload = decode_byte_array(&env, payload)?; + let payload = decode_byte_array(&mut env, &payload)?; let mut publication = publisher.put(payload); let encoding = decode_jni_encoding(&mut env, &encoding)?; publication = publication.encoding(encoding); if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; + let attachment = decode_byte_array(&mut env, &attachment)?; publication = publication.attachment::>(attachment) }; publication.wait().map_err(|err| zerror!(err)) @@ -95,7 +95,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( let _ = || -> ZResult<()> { let mut delete = publisher.delete(); if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; + let attachment = decode_byte_array(&mut env, &attachment)?; delete = delete.attachment::>(attachment) }; delete.wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 5bf3e325..70855bcd 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -103,11 +103,11 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( let encoding = decode_jni_encoding(&mut env, &encoding)?; get_builder = get_builder.encoding(encoding); } - get_builder = get_builder.payload(decode_byte_array(&env, payload)?); + get_builder = get_builder.payload(decode_byte_array(&mut env, &payload)?); } if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; + let attachment = decode_byte_array(&mut env, &attachment)?; get_builder = get_builder.attachment::>(attachment); } diff --git a/zenoh-jni/src/query.rs b/zenoh-jni/src/query.rs index 3db348cf..bdf8bc5b 100644 --- a/zenoh-jni/src/query.rs +++ b/zenoh-jni/src/query.rs @@ -70,7 +70,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; - let payload = decode_byte_array(&env, payload)?; + let payload = decode_byte_array(&mut env, &payload)?; let mut reply_builder = query.reply(key_expr, payload); let encoding = decode_jni_encoding(&mut env, &encoding)?; reply_builder = reply_builder.encoding(encoding); @@ -79,7 +79,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( reply_builder = reply_builder.timestamp(ts) } if !attachment.is_null() { - reply_builder = reply_builder.attachment(decode_byte_array(&env, attachment)?); + reply_builder = reply_builder.attachment(decode_byte_array(&mut env, &attachment)?); } reply_builder = reply_builder.express(qos_express != 0); reply_builder.wait().map_err(|err| zerror!(err)) @@ -117,7 +117,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( let query = Arc::from_raw(query_ptr); let encoding = decode_jni_encoding(&mut env, &encoding)?; query - .reply_err(decode_byte_array(&env, payload)?) + .reply_err(decode_byte_array(&mut env, &payload)?) .encoding(encoding) .wait() .map_err(|err| zerror!(err)) @@ -168,7 +168,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( reply_builder = reply_builder.timestamp(ts) } if !attachment.is_null() { - reply_builder = reply_builder.attachment(decode_byte_array(&env, attachment)?); + reply_builder = reply_builder.attachment(decode_byte_array(&mut env, &attachment)?); } reply_builder = reply_builder.express(qos_express != 0); reply_builder.wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index e259268c..481c2eca 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -82,9 +82,9 @@ pub(crate) fn get_callback_global_ref( } /// Helper function to convert a JByteArray into a Vec. -pub(crate) fn decode_byte_array(env: &JNIEnv<'_>, payload: JByteArray) -> ZResult> { +pub(crate) fn decode_byte_array(env: &mut JNIEnv, payload: &JByteArray) -> ZResult> { let payload_len = env - .get_array_length(&payload) + .get_array_length(payload) .map(|length| length as usize) .map_err(|err| zerror!(err))?; let mut buff = vec![0; payload_len]; diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs index ae6d5405..4de54278 100644 --- a/zenoh-jni/src/zbytes.rs +++ b/zenoh-jni/src/zbytes.rs @@ -248,7 +248,7 @@ fn serialize( } JavaType::ByteArray => { let jbyte_array = JByteArray::from(any); - let bytes = decode_byte_array(env, jbyte_array).map_err(|err| zerror!(err))?; + let bytes = decode_byte_array(env, &jbyte_array).map_err(|err| zerror!(err))?; serializer.serialize(bytes); } JavaType::List(kotlin_type) => { @@ -291,7 +291,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeViaJNI( jtype: JObject, ) -> jobject { || -> ZResult { - let decoded_bytes: Vec = decode_byte_array(&env, bytes)?; + let decoded_bytes: Vec = decode_byte_array(&mut env, &bytes)?; let zbytes = ZBytes::from(decoded_bytes); let mut deserializer = ZDeserializer::new(&zbytes); let jtype = decode_token_type(&mut env, jtype)?; diff --git a/zenoh-jni/src/zbytes_kotlin.rs b/zenoh-jni/src/zbytes_kotlin.rs index 2cd30ca1..11981c9d 100644 --- a/zenoh-jni/src/zbytes_kotlin.rs +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -261,7 +261,7 @@ fn serialize( } KotlinType::ByteArray => { let bytes = - decode_byte_array(env, JByteArray::from(any)).map_err(|err| zerror!(err))?; + decode_byte_array(env, &JByteArray::from(any)).map_err(|err| zerror!(err))?; serializer.serialize(bytes); } KotlinType::UByte => { @@ -366,7 +366,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZBytesKotlin_deserializeViaJNI( ktype: JObject, ) -> jobject { || -> ZResult { - let raw = decode_byte_array(&env, bytes)?; + let raw = decode_byte_array(&mut env, &bytes)?; let zbytes = ZBytes::from(raw); let mut deserializer = ZDeserializer::new(&zbytes); let kotlin_type = decode_ktype(&mut env, ktype)?; diff --git a/zenoh-jni/src/zenoh_id.rs b/zenoh-jni/src/zenoh_id.rs index 765b4421..05b0773f 100644 --- a/zenoh-jni/src/zenoh_id.rs +++ b/zenoh-jni/src/zenoh_id.rs @@ -51,7 +51,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZenohId_toStringViaJNI( zenoh_id: JByteArray, ) -> jstring { || -> ZResult { - let bytes = decode_byte_array(&env, zenoh_id)?; + let bytes = decode_byte_array(&mut env, &zenoh_id)?; let zenohid = ZenohId::try_from(bytes.as_slice()).map_err(|err| zerror!(err))?; env.new_string(zenohid.to_string()) .map_err(|err| zerror!(err)) From 81ebc7458c35e6ea60ff4d50c21cb0906ca9eb5e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 11:22:57 +0200 Subject: [PATCH 117/126] restored ro access to env --- zenoh-flat/src/jni_converter.rs | 18 +++++++++++++++++- zenoh-jni/build.rs | 2 +- zenoh-jni/src/ext/advanced_publisher.rs | 6 +++--- zenoh-jni/src/publisher.rs | 6 +++--- zenoh-jni/src/querier.rs | 4 ++-- zenoh-jni/src/query.rs | 8 ++++---- zenoh-jni/src/utils.rs | 2 +- zenoh-jni/src/zbytes.rs | 2 +- zenoh-jni/src/zbytes_kotlin.rs | 2 +- zenoh-jni/src/zenoh_id.rs | 2 +- 10 files changed, 34 insertions(+), 18 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 2926f4e9..ed5b0f6f 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -51,8 +51,13 @@ pub use crate::jni_type_binding::TypeBinding; /// Strategy for converting a JNI parameter into a Rust value. #[derive(Clone)] pub enum ArgDecode { - /// `let = (&mut env, &)?;` + /// `let = (&mut env, &)?;` — for decoders that need + /// mutable access to the JNI environment (e.g. `JNIEnv::get_string`). EnvRefMut(syn::Path), + /// `let = (&env, &)?;` — for decoders that only + /// need shared access to the JNI environment (e.g. byte-array readers + /// whose `JNIEnv` methods take `&self`). + EnvRef(syn::Path), /// `let = ()?;` — pure conversion (e.g. enum decoders). Pure(syn::Path), /// `let = ;` — inline transformation built from the input @@ -80,6 +85,13 @@ impl ArgDecode { syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_ref_mut path"), ) } + + /// `ArgDecode::EnvRef` from a path string. + pub fn env_ref(path: impl AsRef) -> Self { + ArgDecode::EnvRef( + syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_ref path"), + ) + } } impl ReturnEncode { @@ -1185,6 +1197,9 @@ impl JniMethodsConverter { ArgDecode::EnvRefMut(path) => { prelude.push(quote! { let #name = #path(&mut env, &#pat)?; }); } + ArgDecode::EnvRef(path) => { + prelude.push(quote! { let #name = #path(&env, &#pat)?; }); + } ArgDecode::Pure(path) => { prelude.push(quote! { let #name = #path(#pat)?; }); } @@ -1226,6 +1241,7 @@ impl JniMethodsConverter { fn decode_expr(&self, decode: &ArgDecode, input: &syn::Ident) -> TokenStream { match decode { ArgDecode::EnvRefMut(path) => quote! { #path(&mut env, &#input)? }, + ArgDecode::EnvRef(path) => quote! { #path(&env, &#input)? }, ArgDecode::Pure(path) => quote! { #path(#input)? }, ArgDecode::Inline(f) => f.call(input), ArgDecode::OwnedRef => { diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index b1deea51..dc968feb 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -37,7 +37,7 @@ fn shared_bindings() -> JniTypeBinding { TypeBinding::new("VecU8").consume(JniForm::new( "jni::objects::JByteArray", "ByteArray", - ArgDecode::env_ref_mut("crate::utils::decode_byte_array"), + ArgDecode::env_ref("crate::utils::decode_byte_array"), )), ) .type_binding(jobject_consume( diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs index fba0c274..ca3033bb 100644 --- a/zenoh-jni/src/ext/advanced_publisher.rs +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -267,12 +267,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_putViaJNI( ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { - let payload = decode_byte_array(&mut env, &payload)?; + let payload = decode_byte_array(&env, &payload)?; let mut publication = publisher.put(payload); let encoding = decode_jni_encoding(&mut env, &encoding)?; publication = publication.encoding(encoding); if !attachment.is_null() { - let attachment = decode_byte_array(&mut env, &attachment)?; + let attachment = decode_byte_array(&env, &attachment)?; publication = publication.attachment::>(attachment) }; publication.wait().map_err(|err| zerror!(err)) @@ -306,7 +306,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIAdvancedPublisher_deleteViaJNI( let _ = || -> ZResult<()> { let mut delete = publisher.delete(); if !attachment.is_null() { - let attachment = decode_byte_array(&mut env, &attachment)?; + let attachment = decode_byte_array(&env, &attachment)?; delete = delete.attachment::>(attachment) }; delete.wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index 94a9566b..37d90fc4 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -56,12 +56,12 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( ) { let publisher = OwnedObject::from_raw(publisher_ptr); let _ = || -> ZResult<()> { - let payload = decode_byte_array(&mut env, &payload)?; + let payload = decode_byte_array(&env, &payload)?; let mut publication = publisher.put(payload); let encoding = decode_jni_encoding(&mut env, &encoding)?; publication = publication.encoding(encoding); if !attachment.is_null() { - let attachment = decode_byte_array(&mut env, &attachment)?; + let attachment = decode_byte_array(&env, &attachment)?; publication = publication.attachment::>(attachment) }; publication.wait().map_err(|err| zerror!(err)) @@ -95,7 +95,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( let _ = || -> ZResult<()> { let mut delete = publisher.delete(); if !attachment.is_null() { - let attachment = decode_byte_array(&mut env, &attachment)?; + let attachment = decode_byte_array(&env, &attachment)?; delete = delete.attachment::>(attachment) }; delete.wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 70855bcd..beae7a54 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -103,11 +103,11 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( let encoding = decode_jni_encoding(&mut env, &encoding)?; get_builder = get_builder.encoding(encoding); } - get_builder = get_builder.payload(decode_byte_array(&mut env, &payload)?); + get_builder = get_builder.payload(decode_byte_array(&env, &payload)?); } if !attachment.is_null() { - let attachment = decode_byte_array(&mut env, &attachment)?; + let attachment = decode_byte_array(&env, &attachment)?; get_builder = get_builder.attachment::>(attachment); } diff --git a/zenoh-jni/src/query.rs b/zenoh-jni/src/query.rs index bdf8bc5b..c8f78b21 100644 --- a/zenoh-jni/src/query.rs +++ b/zenoh-jni/src/query.rs @@ -70,7 +70,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; - let payload = decode_byte_array(&mut env, &payload)?; + let payload = decode_byte_array(&env, &payload)?; let mut reply_builder = query.reply(key_expr, payload); let encoding = decode_jni_encoding(&mut env, &encoding)?; reply_builder = reply_builder.encoding(encoding); @@ -79,7 +79,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( reply_builder = reply_builder.timestamp(ts) } if !attachment.is_null() { - reply_builder = reply_builder.attachment(decode_byte_array(&mut env, &attachment)?); + reply_builder = reply_builder.attachment(decode_byte_array(&env, &attachment)?); } reply_builder = reply_builder.express(qos_express != 0); reply_builder.wait().map_err(|err| zerror!(err)) @@ -117,7 +117,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( let query = Arc::from_raw(query_ptr); let encoding = decode_jni_encoding(&mut env, &encoding)?; query - .reply_err(decode_byte_array(&mut env, &payload)?) + .reply_err(decode_byte_array(&env, &payload)?) .encoding(encoding) .wait() .map_err(|err| zerror!(err)) @@ -168,7 +168,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( reply_builder = reply_builder.timestamp(ts) } if !attachment.is_null() { - reply_builder = reply_builder.attachment(decode_byte_array(&mut env, &attachment)?); + reply_builder = reply_builder.attachment(decode_byte_array(&env, &attachment)?); } reply_builder = reply_builder.express(qos_express != 0); reply_builder.wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index 481c2eca..6a0599c1 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -82,7 +82,7 @@ pub(crate) fn get_callback_global_ref( } /// Helper function to convert a JByteArray into a Vec. -pub(crate) fn decode_byte_array(env: &mut JNIEnv, payload: &JByteArray) -> ZResult> { +pub(crate) fn decode_byte_array(env: &JNIEnv, payload: &JByteArray) -> ZResult> { let payload_len = env .get_array_length(payload) .map(|length| length as usize) diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs index 4de54278..c09b60c6 100644 --- a/zenoh-jni/src/zbytes.rs +++ b/zenoh-jni/src/zbytes.rs @@ -291,7 +291,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeViaJNI( jtype: JObject, ) -> jobject { || -> ZResult { - let decoded_bytes: Vec = decode_byte_array(&mut env, &bytes)?; + let decoded_bytes: Vec = decode_byte_array(&env, &bytes)?; let zbytes = ZBytes::from(decoded_bytes); let mut deserializer = ZDeserializer::new(&zbytes); let jtype = decode_token_type(&mut env, jtype)?; diff --git a/zenoh-jni/src/zbytes_kotlin.rs b/zenoh-jni/src/zbytes_kotlin.rs index 11981c9d..6fba5b70 100644 --- a/zenoh-jni/src/zbytes_kotlin.rs +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -366,7 +366,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZBytesKotlin_deserializeViaJNI( ktype: JObject, ) -> jobject { || -> ZResult { - let raw = decode_byte_array(&mut env, &bytes)?; + let raw = decode_byte_array(&env, &bytes)?; let zbytes = ZBytes::from(raw); let mut deserializer = ZDeserializer::new(&zbytes); let kotlin_type = decode_ktype(&mut env, ktype)?; diff --git a/zenoh-jni/src/zenoh_id.rs b/zenoh-jni/src/zenoh_id.rs index 05b0773f..217b2339 100644 --- a/zenoh-jni/src/zenoh_id.rs +++ b/zenoh-jni/src/zenoh_id.rs @@ -51,7 +51,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZenohId_toStringViaJNI( zenoh_id: JByteArray, ) -> jstring { || -> ZResult { - let bytes = decode_byte_array(&mut env, &zenoh_id)?; + let bytes = decode_byte_array(&env, &zenoh_id)?; let zenohid = ZenohId::try_from(bytes.as_slice()).map_err(|err| zerror!(err))?; env.new_string(zenohid.to_string()) .map_err(|err| zerror!(err)) From 25e72ab58efaee46e150a50852b5cfd015fd34c6 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 11:54:56 +0200 Subject: [PATCH 118/126] explicit arc decode ref --- zenoh-flat/src/jni_converter.rs | 63 +++++++++++++++------------------ zenoh-jni/Cargo.lock | 1 + zenoh-jni/Cargo.toml | 1 + zenoh-jni/build.rs | 9 +++-- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index ed5b0f6f..e51b3f20 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -64,13 +64,6 @@ pub enum ArgDecode { /// ident. Used for trivial conversions like `bool` (`x != 0`) or /// `Duration` (`Duration::from_millis(x as u64)`). Inline(InlineFn), - /// `let = ::from_raw();` — borrows the Arc - /// pointed to by `` via the converter-wide `owned_object` setting. - /// The argument is passed to the wrapped function as `&`. - OwnedRef, - /// Consume an `Arc` raw pointer: reconstructs the Arc, clones the - /// inner value, and drops the Arc at end of scope. - ConsumeArc, } impl ArgDecode { @@ -130,8 +123,14 @@ pub struct JniForm { /// `"String"`, `"ByteArray"`, `"JObject"`). kotlin_jni_type: String, /// True for raw-pointer slots — appends `"Ptr"` to the Kotlin parameter - /// name (e.g. `sessionPtr: Long`). + /// name (e.g. `sessionPtr: Long`) and drives `_ptr` rename in the JNI + /// wrapper signature. pointer_param: bool, + /// When true, the decoded value is passed to the wrapped function as + /// `&name` rather than `name`. Used for opaque-handle borrows where the + /// decoder produces an owner (e.g. `OwnedObject`) but the wrapped + /// function expects a shared reference. + call_with_ref: bool, decode: ArgDecode, } @@ -145,6 +144,7 @@ impl JniForm { jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid JniForm jni_type"), kotlin_jni_type: kotlin_jni_type.into(), pointer_param: false, + call_with_ref: false, decode, } } @@ -154,6 +154,11 @@ impl JniForm { self } + pub fn call_with_ref(mut self, v: bool) -> Self { + self.call_with_ref = v; + self + } + /// Whether this form's wire JNI type is a `JObject`-shaped object that /// supports `is_null()` (used by the `Option` combinator). fn is_jni_object(&self) -> bool { @@ -994,16 +999,24 @@ impl JniMethodsConverter { }; } } - // Fallback: treat any unbound `&T` as an `OwnedRef` against a - // raw `*const T` pointer, decoded via the converter-wide - // `owned_object`. This matches the legacy `OpaqueRef` path. + // Fallback: treat any unbound `&T` as an opaque Arc borrow + // against a raw `*const T` pointer. The owned_object path is + // serialized to String (syn::Path is not Send) and re-parsed + // inside the closure. call_with_ref causes the decoded owner + // to be passed as `&name` to the wrapped function. let ptr_ty: syn::Type = syn::parse2(quote! { *const #elem }) .expect("opaque pointer type must parse"); + let owned_str = self.cfg.owned_object.to_token_stream().to_string(); let opaque_form = JniForm { jni_type: ptr_ty, kotlin_jni_type: "Long".to_string(), pointer_param: true, - decode: ArgDecode::OwnedRef, + call_with_ref: true, + decode: ArgDecode::Inline(InlineFn::new(move |input| { + let owned: syn::Path = + syn::parse_str(&owned_str).expect("owned_object must be a valid path"); + quote! { #owned::from_raw(#input) } + })), }; ArgKind::Borrow { form: opaque_form, @@ -1173,7 +1186,7 @@ impl JniMethodsConverter { #[allow(clippy::too_many_arguments)] fn emit_consume_or_borrow( &self, - borrow: bool, + _borrow: bool, form: JniForm, kotlin_override: Option, name: &syn::Ident, @@ -1185,8 +1198,7 @@ impl JniMethodsConverter { kt_enabled: bool, ) { let jt = &form.jni_type; - let pat = if matches!(form.decode, ArgDecode::OwnedRef | ArgDecode::ConsumeArc) { - // Raw-pointer slots get a `_ptr` ident in the JNI signature. + let pat = if form.pointer_param { format_ident!("{}_ptr", name) } else { name.clone() @@ -1207,21 +1219,9 @@ impl JniMethodsConverter { let expr = f.call(&pat); prelude.push(quote! { let #name = #expr; }); } - ArgDecode::OwnedRef => { - let owned = &self.cfg.owned_object; - prelude.push(quote! { let #name = #owned::from_raw(#pat); }); - } - ArgDecode::ConsumeArc => { - let arc_ident = format_ident!("__{}_arc", name); - prelude.push(quote! { - let #arc_ident = std::sync::Arc::from_raw(#pat); - let #name = (*#arc_ident).clone(); - }); - } } - if borrow && matches!(form.decode, ArgDecode::OwnedRef) { - // OwnedRef pattern: pass `&name` to match historical OpaqueRef behavior. + if form.call_with_ref { call_args.push(quote! { &#name }); } else { call_args.push(quote! { #name }); @@ -1244,13 +1244,6 @@ impl JniMethodsConverter { ArgDecode::EnvRef(path) => quote! { #path(&env, &#input)? }, ArgDecode::Pure(path) => quote! { #path(#input)? }, ArgDecode::Inline(f) => f.call(input), - ArgDecode::OwnedRef => { - let owned = &self.cfg.owned_object; - quote! { #owned::from_raw(#input) } - } - ArgDecode::ConsumeArc => { - quote! { (*std::sync::Arc::from_raw(#input)).clone() } - } } } diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index cf99ab53..b3796fb4 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -4443,6 +4443,7 @@ dependencies = [ "json5", "konst", "prebindgen", + "quote", "rustc_version", "serde_yaml", "syn 2.0.117", diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index 17a7021f..b34d4a39 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -52,6 +52,7 @@ prebindgen = "0.4.1" zenoh-flat = { path = "../zenoh-flat", features = ["zenoh-ext"] } itertools = "0.12" syn = "2" +quote = "1" [profile.release] debug = false # If you want debug symbol in release mode, set the env variable: RUSTFLAGS=-g diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index dc968feb..847a5d7c 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,7 +1,8 @@ use itertools::Itertools; +use quote::quote; use zenoh_flat::jni_converter::{ - ArgDecode, JniForm, JniMethodsConverter, JniStructConverter, ReturnEncode, ReturnForm, - TypeBinding, + ArgDecode, InlineFn, JniForm, JniMethodsConverter, JniStructConverter, ReturnEncode, + ReturnForm, TypeBinding, }; use zenoh_flat::jni_type_binding::JniTypeBinding; @@ -87,7 +88,9 @@ fn shared_bindings() -> JniTypeBinding { JniForm::new( "*const zenoh::key_expr::KeyExpr<'static>", "Long", - ArgDecode::ConsumeArc, + ArgDecode::Inline(InlineFn::new(|input| { + quote! { (*std::sync::Arc::from_raw(#input)).clone() } + })), ) .pointer_param(true), ), From bc500daf5cb404bec2b6e9a7016640c409b8b95a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 12:29:01 +0200 Subject: [PATCH 119/126] ArgDecode enum removed --- zenoh-flat/src/jni_converter.rs | 120 +++++++++++------------------ zenoh-flat/src/jni_type_binding.rs | 31 ++++++-- zenoh-jni/build.rs | 22 ++++-- 3 files changed, 85 insertions(+), 88 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index e51b3f20..bb4d1537 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -48,45 +48,6 @@ pub use crate::jni_type_binding::TypeBinding; // Decode / encode strategies // ===================================================================== -/// Strategy for converting a JNI parameter into a Rust value. -#[derive(Clone)] -pub enum ArgDecode { - /// `let = (&mut env, &)?;` — for decoders that need - /// mutable access to the JNI environment (e.g. `JNIEnv::get_string`). - EnvRefMut(syn::Path), - /// `let = (&env, &)?;` — for decoders that only - /// need shared access to the JNI environment (e.g. byte-array readers - /// whose `JNIEnv` methods take `&self`). - EnvRef(syn::Path), - /// `let = ()?;` — pure conversion (e.g. enum decoders). - Pure(syn::Path), - /// `let = ;` — inline transformation built from the input - /// ident. Used for trivial conversions like `bool` (`x != 0`) or - /// `Duration` (`Duration::from_millis(x as u64)`). - Inline(InlineFn), -} - -impl ArgDecode { - /// `ArgDecode::Pure` from a path string (parsed lazily). - pub fn pure(path: impl AsRef) -> Self { - ArgDecode::Pure(syn::parse_str(path.as_ref()).expect("invalid ArgDecode::pure path")) - } - - /// `ArgDecode::EnvRefMut` from a path string. - pub fn env_ref_mut(path: impl AsRef) -> Self { - ArgDecode::EnvRefMut( - syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_ref_mut path"), - ) - } - - /// `ArgDecode::EnvRef` from a path string. - pub fn env_ref(path: impl AsRef) -> Self { - ArgDecode::EnvRef( - syn::parse_str(path.as_ref()).expect("invalid ArgDecode::env_ref path"), - ) - } -} - impl ReturnEncode { /// `ReturnEncode::Wrapper` from a path string. pub fn wrapper(path: impl AsRef) -> Self { @@ -97,6 +58,11 @@ impl ReturnEncode { } /// Clonable closure that produces a TokenStream from the JNI input ident. +/// +/// This is the single decoding mechanism used by [`JniForm`]. Common decoder +/// shapes are exposed via [`InlineFn::pure`], [`InlineFn::env_ref`], and +/// [`InlineFn::env_ref_mut`], which capture a decoder path as a `String` +/// (since `syn::Path` is not `Send`) and re-parse it inside the closure. #[derive(Clone)] pub struct InlineFn(Arc TokenStream + Send + Sync>); @@ -108,6 +74,35 @@ impl InlineFn { InlineFn(Arc::new(f)) } + /// `()?` — pure conversion (e.g. enum decoders). + pub fn pure(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::pure path"); + quote! { #p(#input)? } + }) + } + + /// `(&env, &)?` — decoder needing shared access to the JNI env + /// (e.g. byte-array readers whose `JNIEnv` methods take `&self`). + pub fn env_ref(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::env_ref path"); + quote! { #p(&env, &#input)? } + }) + } + + /// `(&mut env, &)?` — decoder needing mutable access to the + /// JNI env (e.g. `JNIEnv::get_string`). + pub fn env_ref_mut(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::env_ref_mut path"); + quote! { #p(&mut env, &#input)? } + }) + } + fn call(&self, ident: &syn::Ident) -> TokenStream { (self.0)(ident) } @@ -131,14 +126,14 @@ pub struct JniForm { /// decoder produces an owner (e.g. `OwnedObject`) but the wrapped /// function expects a shared reference. call_with_ref: bool, - decode: ArgDecode, + decode: InlineFn, } impl JniForm { pub fn new( jni_type: impl AsRef, kotlin_jni_type: impl Into, - decode: ArgDecode, + decode: InlineFn, ) -> Self { Self { jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid JniForm jni_type"), @@ -426,14 +421,13 @@ impl JniStructConverter { } }; - let decoder_path: syn::Path = syn::parse_str(&format!("decode_{struct_name}")) - .expect("generated decoder ident must parse as path"); + let decoder_path = format!("decode_{struct_name}"); let mut binding = TypeBinding::new(struct_name.clone()); binding.kotlin_type = Some(struct_name.clone()); binding.consume = Some(JniForm::new( "jni::objects::JObject", "JObject", - ArgDecode::EnvRefMut(decoder_path), + InlineFn::env_ref_mut(&decoder_path), )); self.cfg.types.types.insert(struct_name.clone(), binding); @@ -461,13 +455,9 @@ impl JniStructConverter { "i64" => StructFieldKind::I64, "f64" => StructFieldKind::F64, _ => { - // Enum decoders are stored as a `Pure` ArgDecode on the - // type's binding's `consume` form. if let Some(binding) = self.cfg.types.types.get(&name) { - if let Some(form) = binding.consume.as_ref() { - if let ArgDecode::Pure(p) = &form.decode { - return StructFieldKind::Enum(p.clone()); - } + if let Some(p) = binding.enum_decoder.as_ref() { + return StructFieldKind::Enum(p.clone()); } } StructFieldKind::Unsupported @@ -1012,11 +1002,11 @@ impl JniMethodsConverter { kotlin_jni_type: "Long".to_string(), pointer_param: true, call_with_ref: true, - decode: ArgDecode::Inline(InlineFn::new(move |input| { + decode: InlineFn::new(move |input| { let owned: syn::Path = syn::parse_str(&owned_str).expect("owned_object must be a valid path"); quote! { #owned::from_raw(#input) } - })), + }), }; ArgKind::Borrow { form: opaque_form, @@ -1205,21 +1195,8 @@ impl JniMethodsConverter { }; jni_params.push(quote! { #pat: #jt }); - match &form.decode { - ArgDecode::EnvRefMut(path) => { - prelude.push(quote! { let #name = #path(&mut env, &#pat)?; }); - } - ArgDecode::EnvRef(path) => { - prelude.push(quote! { let #name = #path(&env, &#pat)?; }); - } - ArgDecode::Pure(path) => { - prelude.push(quote! { let #name = #path(#pat)?; }); - } - ArgDecode::Inline(f) => { - let expr = f.call(&pat); - prelude.push(quote! { let #name = #expr; }); - } - } + let expr = form.decode.call(&pat); + prelude.push(quote! { let #name = #expr; }); if form.call_with_ref { call_args.push(quote! { &#name }); @@ -1238,13 +1215,8 @@ impl JniMethodsConverter { } } - fn decode_expr(&self, decode: &ArgDecode, input: &syn::Ident) -> TokenStream { - match decode { - ArgDecode::EnvRefMut(path) => quote! { #path(&mut env, &#input)? }, - ArgDecode::EnvRef(path) => quote! { #path(&env, &#input)? }, - ArgDecode::Pure(path) => quote! { #path(#input)? }, - ArgDecode::Inline(f) => f.call(input), - } + fn decode_expr(&self, decode: &InlineFn, input: &syn::Ident) -> TokenStream { + decode.call(input) } /// Resolve the Kotlin parameter type for a JniForm. For object-typed diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index fc696b3f..32d7de40 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -8,8 +8,9 @@ //! [`crate::jni_converter::JniMethodsConverter`] reads it). //! //! ```ignore -//! use zenoh_flat::jni_converter::{ArgDecode, JniForm, TypeBinding}; +//! use zenoh_flat::jni_converter::{InlineFn, JniForm, TypeBinding}; //! use zenoh_flat::jni_type_binding::JniTypeBinding; +//! use quote::quote; //! //! let common = JniTypeBinding::new() //! .type_binding( @@ -17,7 +18,9 @@ //! JniForm::new( //! "*const zenoh::key_expr::KeyExpr<'static>", //! "Long", -//! ArgDecode::ConsumeArc, +//! InlineFn::new(|input| { +//! quote! { (*std::sync::Arc::from_raw(#input)).clone() } +//! }), //! ) //! .pointer_param(true), //! ), @@ -28,7 +31,7 @@ use std::collections::HashMap; use quote::{quote, ToTokens}; -use crate::jni_converter::{ArgDecode, InlineFn, JniForm, ReturnForm}; +use crate::jni_converter::{InlineFn, JniForm, ReturnForm}; /// Per-type description of how a Rust type is represented across the JNI /// boundary. A type may declare up to four forms: @@ -52,6 +55,11 @@ pub struct TypeBinding { pub(crate) borrow: Option, pub(crate) returns: Option, pub(crate) returns_vec: Option, + /// Decoder path for Java-enum-shaped types (`fn(jint) -> ZResult`). + /// Set when the type's JNI representation is an `Int` mapped through a + /// pure decoder. Used by struct-field classification to detect enum + /// fields and emit `env.get_field(..., "I")` + decoder call. + pub(crate) enum_decoder: Option, } impl TypeBinding { @@ -77,6 +85,7 @@ impl TypeBinding { borrow: None, returns: None, returns_vec: None, + enum_decoder: None, } } @@ -85,6 +94,16 @@ impl TypeBinding { self } + /// Mark this binding as a Java-enum-shaped type and record the + /// `fn(jint) -> ZResult` decoder path used for both top-level + /// argument decoding and struct-field decoding. + pub fn enum_decoder(mut self, path: impl AsRef) -> Self { + self.enum_decoder = Some( + syn::parse_str(path.as_ref()).expect("invalid TypeBinding::enum_decoder path"), + ); + self + } + pub fn consume(mut self, form: JniForm) -> Self { self.consume = Some(form); self @@ -157,7 +176,7 @@ impl JniTypeBinding { TypeBinding::new("bool").consume(JniForm::new( "jni::sys::jboolean", "Boolean", - ArgDecode::Inline(InlineFn::new(|input| quote! { #input != 0 })), + InlineFn::new(|input| quote! { #input != 0 }), )), ); // Duration — jlong, inline `Duration::from_millis(x as u64)`. @@ -166,9 +185,9 @@ impl JniTypeBinding { TypeBinding::new("Duration").consume(JniForm::new( "jni::sys::jlong", "Long", - ArgDecode::Inline(InlineFn::new(|input| { + InlineFn::new(|input| { quote! { std::time::Duration::from_millis(#input as u64) } - })), + }), )), ); self diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 847a5d7c..a4bba368 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,20 +1,26 @@ use itertools::Itertools; use quote::quote; use zenoh_flat::jni_converter::{ - ArgDecode, InlineFn, JniForm, JniMethodsConverter, JniStructConverter, ReturnEncode, - ReturnForm, TypeBinding, + InlineFn, JniForm, JniMethodsConverter, JniStructConverter, ReturnEncode, ReturnForm, + TypeBinding, }; use zenoh_flat::jni_type_binding::JniTypeBinding; fn enum_binding(name: &str, decoder: &str) -> TypeBinding { - TypeBinding::new(name).consume(JniForm::new("jni::sys::jint", "Int", ArgDecode::pure(decoder))) + TypeBinding::new(name) + .enum_decoder(decoder) + .consume(JniForm::new( + "jni::sys::jint", + "Int", + InlineFn::pure(decoder), + )) } fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { TypeBinding::new(name).kotlin(kotlin).consume(JniForm::new( "jni::objects::JObject", "JObject", - ArgDecode::env_ref_mut(decoder), + InlineFn::env_ref_mut(decoder), )) } @@ -28,7 +34,7 @@ fn shared_bindings() -> JniTypeBinding { TypeBinding::new("String").consume(JniForm::new( "jni::objects::JString", "String", - ArgDecode::env_ref_mut("crate::utils::decode_string"), + InlineFn::env_ref_mut("crate::utils::decode_string"), )), ) // `Vec` is keyed under the synthetic name "VecU8" — the @@ -38,7 +44,7 @@ fn shared_bindings() -> JniTypeBinding { TypeBinding::new("VecU8").consume(JniForm::new( "jni::objects::JByteArray", "ByteArray", - ArgDecode::env_ref("crate::utils::decode_byte_array"), + InlineFn::env_ref("crate::utils::decode_byte_array"), )), ) .type_binding(jobject_consume( @@ -88,9 +94,9 @@ fn shared_bindings() -> JniTypeBinding { JniForm::new( "*const zenoh::key_expr::KeyExpr<'static>", "Long", - ArgDecode::Inline(InlineFn::new(|input| { + InlineFn::new(|input| { quote! { (*std::sync::Arc::from_raw(#input)).clone() } - })), + }), ) .pointer_param(true), ), From dffcfe06e64401df4abed7055295252bfe1c316e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 12:43:02 +0200 Subject: [PATCH 120/126] returns_vec removed --- zenoh-flat/src/jni_converter.rs | 15 +++++-------- zenoh-flat/src/jni_type_binding.rs | 18 +++++++-------- zenoh-jni/build.rs | 36 +++++++++++++++++------------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index bb4d1537..d857fc7d 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -952,7 +952,7 @@ impl JniMethodsConverter { let syn::Type::Path(tp) = ty else { return None }; let seg = tp.path.segments.last()?; let name = seg.ident.to_string(); - if name == "Vec" { + let key = if name == "Vec" { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return None; }; @@ -960,17 +960,14 @@ impl JniMethodsConverter { return None; }; let elem_name = type_last_segment(elem)?; - return self - .cfg - .types - .types - .get(&elem_name) - .and_then(|b| b.returns_vec.as_ref()); - } + format!("Vec{elem_name}") + } else { + name + }; self.cfg .types .types - .get(&name) + .get(&key) .and_then(|b| b.returns.as_ref()) } diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index 32d7de40..daa32d2d 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -34,9 +34,14 @@ use quote::{quote, ToTokens}; use crate::jni_converter::{InlineFn, JniForm, ReturnForm}; /// Per-type description of how a Rust type is represented across the JNI -/// boundary. A type may declare up to four forms: -/// `consume` (`T` parameter), `borrow` (`&T` parameter), `returns` -/// (`ZResult` return), and `returns_vec` (`ZResult>` return). +/// boundary. A type may declare up to three forms: +/// `consume` (`T` parameter), `borrow` (`&T` parameter), and `returns` +/// (`ZResult` return). +/// +/// `Vec` parameters and `ZResult>` returns are described by a +/// separate binding keyed under the synthetic name `"Vec"` +/// (e.g. `"VecU8"`, `"VecZenohId"`). The classifier synthesizes that key +/// when it sees a `Vec` type. /// /// Callback parameters (`impl Fn(T) + Send + Sync + 'static`) are described /// by an ordinary `consume` form on a binding keyed under @@ -54,7 +59,6 @@ pub struct TypeBinding { pub(crate) consume: Option, pub(crate) borrow: Option, pub(crate) returns: Option, - pub(crate) returns_vec: Option, /// Decoder path for Java-enum-shaped types (`fn(jint) -> ZResult`). /// Set when the type's JNI representation is an `Int` mapped through a /// pure decoder. Used by struct-field classification to detect enum @@ -84,7 +88,6 @@ impl TypeBinding { consume: None, borrow: None, returns: None, - returns_vec: None, enum_decoder: None, } } @@ -118,11 +121,6 @@ impl TypeBinding { self.returns = Some(form); self } - - pub fn returns_vec(mut self, form: ReturnForm) -> Self { - self.returns_vec = Some(form); - self - } } /// Reusable collection of [`TypeBinding`]s plus the Kotlin `data class` diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index a4bba368..a7a83188 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -107,23 +107,27 @@ fn shared_bindings() -> JniTypeBinding { "JNIEncoding", )) .type_binding( - TypeBinding::new("ZenohId") - .returns( - ReturnForm::new( - "jni::sys::jbyteArray", - ReturnEncode::wrapper("crate::zenoh_id::zenoh_id_to_byte_array"), - "jni::objects::JByteArray::default().as_raw()", - ) - .kotlin("ByteArray"), + TypeBinding::new("ZenohId").returns( + ReturnForm::new( + "jni::sys::jbyteArray", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_id_to_byte_array"), + "jni::objects::JByteArray::default().as_raw()", ) - .returns_vec( - ReturnForm::new( - "jni::sys::jobject", - ReturnEncode::wrapper("crate::zenoh_id::zenoh_ids_to_java_list"), - "jni::objects::JObject::default().as_raw()", - ) - .kotlin("List"), - ), + .kotlin("ByteArray"), + ), + ) + // `Vec` is keyed under the synthetic name "VecZenohId" — + // the methods-phase classifier looks it up explicitly when it sees + // `Vec` as a return type. + .type_binding( + TypeBinding::new("VecZenohId").returns( + ReturnForm::new( + "jni::sys::jobject", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_ids_to_java_list"), + "jni::objects::JObject::default().as_raw()", + ) + .kotlin("List"), + ), ) } From b23d30933696784c7822787182fe3db631896df3 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 12:55:48 +0200 Subject: [PATCH 121/126] removed special naming for vecs --- zenoh-flat/src/jni_converter.rs | 106 ++++++++--------------------- zenoh-flat/src/jni_type_binding.rs | 12 ++-- zenoh-jni/build.rs | 10 +-- 3 files changed, 38 insertions(+), 90 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index d857fc7d..3ad0f07c 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -949,21 +949,7 @@ impl JniMethodsConverter { } fn lookup_return_form(&self, ty: &syn::Type) -> Option<&ReturnForm> { - let syn::Type::Path(tp) = ty else { return None }; - let seg = tp.path.segments.last()?; - let name = seg.ident.to_string(); - let key = if name == "Vec" { - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return None; - }; - let syn::GenericArgument::Type(elem) = args.args.first()? else { - return None; - }; - let elem_name = type_last_segment(elem)?; - format!("Vec{elem_name}") - } else { - name - }; + let key = binding_key(ty)?; self.cfg .types .types @@ -1029,44 +1015,27 @@ impl JniMethodsConverter { let name = last.ident.to_string(); if name == "Option" { - let inner_seg = last; - if is_option_of_vec_u8(inner_seg) { - if let Some(binding) = types.get("VecU8") { - if let Some(form) = binding.consume.as_ref() { - return ArgKind::OptionConsume { - form: form.clone(), - kotlin_override: None, - }; - } - } + let Some(inner_ty) = option_inner_type(last) else { return ArgKind::Unsupported; - } - if let Some(inner) = option_inner_type_name(inner_seg) { - if let Some(binding) = types.get(&inner) { - if let Some(form) = binding.consume.as_ref() { - return ArgKind::OptionConsume { - form: form.clone(), - kotlin_override: binding.kotlin_type.clone(), - }; - } - } - } - return ArgKind::Unsupported; - } - - if name == "Vec" && is_vec_of_u8(last) { - if let Some(binding) = types.get("VecU8") { + }; + let Some(key) = binding_key(inner_ty) else { + return ArgKind::Unsupported; + }; + if let Some(binding) = types.get(&key) { if let Some(form) = binding.consume.as_ref() { - return ArgKind::Consume { + return ArgKind::OptionConsume { form: form.clone(), - kotlin_override: None, + kotlin_override: binding.kotlin_type.clone(), }; } } return ArgKind::Unsupported; } - if let Some(binding) = types.get(&name) { + let Some(key) = binding_key(ty) else { + return ArgKind::Unsupported; + }; + if let Some(binding) = types.get(&key) { if let Some(form) = binding.consume.as_ref() { return ArgKind::Consume { form: form.clone(), @@ -1286,47 +1255,32 @@ fn type_last_segment(ty: &syn::Type) -> Option { tp.path.segments.last().map(|s| s.ident.to_string()) } -/// Last-segment name of the single generic argument of an `Option<...>`. -fn option_inner_type_name(seg: &syn::PathSegment) -> Option { +/// Inner type of a `Foo` path segment (used for `Option`). +fn option_inner_type(seg: &syn::PathSegment) -> Option<&syn::Type> { let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return None; }; let syn::GenericArgument::Type(inner) = args.args.first()? else { return None; }; - type_last_segment(inner) + Some(inner) } -fn is_option_of_vec_u8(seg: &syn::PathSegment) -> bool { - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return false; - }; - let Some(syn::GenericArgument::Type(inner)) = args.args.first() else { - return false; - }; - let syn::Type::Path(inner_path) = inner else { - return false; - }; - let Some(inner_seg) = inner_path.path.segments.last() else { - return false; - }; - if inner_seg.ident != "Vec" { - return false; +/// Lookup key for a path type in the [`crate::jni_type_binding::JniTypeBinding`] +/// registry. For `Vec` types we use the canonical +/// `to_token_stream()` form (matching what +/// [`crate::jni_type_binding::TypeBinding::new`] produces from input like +/// `"Vec"`). For other path types we use the last-segment ident, which +/// lets registrations key off short type names without forcing callers to +/// spell out the full path of the user-side type. +fn binding_key(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + let seg = tp.path.segments.last()?; + if seg.ident == "Vec" { + Some(ty.to_token_stream().to_string()) + } else { + Some(seg.ident.to_string()) } - is_vec_of_u8(inner_seg) -} - -fn is_vec_of_u8(seg: &syn::PathSegment) -> bool { - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return false; - }; - let Some(syn::GenericArgument::Type(elem)) = args.args.first() else { - return false; - }; - matches!( - elem, - syn::Type::Path(tp) if tp.path.is_ident("u8") - ) } fn is_unit(ty: &syn::Type) -> bool { diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index daa32d2d..31cb31c1 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -39,9 +39,11 @@ use crate::jni_converter::{InlineFn, JniForm, ReturnForm}; /// (`ZResult` return). /// /// `Vec` parameters and `ZResult>` returns are described by a -/// separate binding keyed under the synthetic name `"Vec"` -/// (e.g. `"VecU8"`, `"VecZenohId"`). The classifier synthesizes that key -/// when it sees a `Vec` type. +/// separate binding registered under the type itself, e.g. +/// `TypeBinding::new("Vec")` or `TypeBinding::new("Vec")`. +/// The classifier looks up `Vec` types by their canonical +/// `to_token_stream()` form, matching whatever +/// [`TypeBinding::new`] canonicalized at registration time. /// /// Callback parameters (`impl Fn(T) + Send + Sync + 'static`) are described /// by an ordinary `consume` form on a binding keyed under @@ -164,9 +166,7 @@ impl JniTypeBinding { /// /// Types whose decoder lives outside this crate — `String`, `Vec`, /// callbacks, enums, opaque handles — are registered by the caller via - /// the universal [`JniTypeBinding::type_binding`] entry point. `Vec` - /// is keyed under the synthetic name `"VecU8"` (looked up explicitly by - /// the methods-phase classifier when it sees `Vec`). + /// the universal [`JniTypeBinding::type_binding`] entry point. pub fn with_builtins(mut self) -> Self { // bool — jboolean, inline `x != 0`. self.types.insert( diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index a7a83188..a5d4053b 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -37,11 +37,8 @@ fn shared_bindings() -> JniTypeBinding { InlineFn::env_ref_mut("crate::utils::decode_string"), )), ) - // `Vec` is keyed under the synthetic name "VecU8" — the - // methods-phase classifier looks it up explicitly when it sees - // `Vec`. .type_binding( - TypeBinding::new("VecU8").consume(JniForm::new( + TypeBinding::new("Vec").consume(JniForm::new( "jni::objects::JByteArray", "ByteArray", InlineFn::env_ref("crate::utils::decode_byte_array"), @@ -116,11 +113,8 @@ fn shared_bindings() -> JniTypeBinding { .kotlin("ByteArray"), ), ) - // `Vec` is keyed under the synthetic name "VecZenohId" — - // the methods-phase classifier looks it up explicitly when it sees - // `Vec` as a return type. .type_binding( - TypeBinding::new("VecZenohId").returns( + TypeBinding::new("Vec").returns( ReturnForm::new( "jni::sys::jobject", ReturnEncode::wrapper("crate::zenoh_id::zenoh_ids_to_java_list"), From 473bc2856d5079d02eef25883c4886e656787daa Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 13:37:12 +0200 Subject: [PATCH 122/126] explicit types --- zenoh-flat/src/jni_converter.rs | 672 +++++------------------------ zenoh-flat/src/jni_type_binding.rs | 416 ++++++++++++------ zenoh-jni/build.rs | 210 +++++---- 3 files changed, 505 insertions(+), 793 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 3ad0f07c..64b6e07d 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -10,195 +10,28 @@ //! [`JniTypeBinding`], and emits a `Java__ViaJNI` wrapper //! plus a matching Kotlin `external fun`. //! -//! The caller threads a single [`JniTypeBinding`] from the struct phase into -//! the methods phase (see `zenoh-jni/build.rs`). Module selection is a plain -//! `.filter(...)` on the prebindgen iterator — there is no per-purpose -//! "struct module" knob. -//! //! # Type registry //! -//! Every Rust type that can appear in a `#[prebindgen]` function's signature -//! is described by a [`TypeBinding`]. A binding declares up to four forms: -//! -//! * `consume` — used when the type appears by value as a parameter (`T`); -//! * `borrow` — used when the type appears as a shared reference (`&T`); -//! * `returns` — used when the type appears in `ZResult` as a return value; -//! * `returns_vec` — used when the type appears as `ZResult>`. -//! -//! `impl Fn(T) + Send + Sync + 'static` callback parameters reuse the -//! `consume` form on a binding keyed under `"impl Fn()"`. +//! Every Rust type-shape that appears in a `#[prebindgen]` function's +//! signature must have an explicit row in the [`JniTypeBinding`] registry, +//! keyed by the canonical `to_token_stream()` form of the type. There are +//! no implicit fallbacks: missing row ⇒ panic with a clear "register ``" +//! message. //! -//! Built-in bindings for `bool`, `String`, `Vec`, and `Duration` are -//! pre-registered by [`JniTypeBinding::with_builtins`] and applied -//! automatically inside both builders' `default()` impls. +//! Built-in rows for `bool` and `Duration` are pre-registered by +//! [`JniTypeBinding::with_builtins`] (applied automatically inside +//! [`MethodsBuilder::default`]). -use std::collections::{BTreeSet, HashMap, VecDeque}; +use std::collections::{BTreeSet, VecDeque}; use std::path::PathBuf; -use std::sync::Arc; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use prebindgen::SourceLocation; -use crate::jni_type_binding::JniTypeBinding; -pub use crate::jni_type_binding::TypeBinding; - -// ===================================================================== -// Decode / encode strategies -// ===================================================================== - -impl ReturnEncode { - /// `ReturnEncode::Wrapper` from a path string. - pub fn wrapper(path: impl AsRef) -> Self { - ReturnEncode::Wrapper( - syn::parse_str(path.as_ref()).expect("invalid ReturnEncode::wrapper path"), - ) - } -} - -/// Clonable closure that produces a TokenStream from the JNI input ident. -/// -/// This is the single decoding mechanism used by [`JniForm`]. Common decoder -/// shapes are exposed via [`InlineFn::pure`], [`InlineFn::env_ref`], and -/// [`InlineFn::env_ref_mut`], which capture a decoder path as a `String` -/// (since `syn::Path` is not `Send`) and re-parse it inside the closure. -#[derive(Clone)] -pub struct InlineFn(Arc TokenStream + Send + Sync>); - -impl InlineFn { - pub fn new(f: F) -> Self - where - F: Fn(&syn::Ident) -> TokenStream + Send + Sync + 'static, - { - InlineFn(Arc::new(f)) - } - - /// `()?` — pure conversion (e.g. enum decoders). - pub fn pure(path: impl AsRef) -> Self { - let s = path.as_ref().to_string(); - InlineFn::new(move |input| { - let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::pure path"); - quote! { #p(#input)? } - }) - } - - /// `(&env, &)?` — decoder needing shared access to the JNI env - /// (e.g. byte-array readers whose `JNIEnv` methods take `&self`). - pub fn env_ref(path: impl AsRef) -> Self { - let s = path.as_ref().to_string(); - InlineFn::new(move |input| { - let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::env_ref path"); - quote! { #p(&env, &#input)? } - }) - } - - /// `(&mut env, &)?` — decoder needing mutable access to the - /// JNI env (e.g. `JNIEnv::get_string`). - pub fn env_ref_mut(path: impl AsRef) -> Self { - let s = path.as_ref().to_string(); - InlineFn::new(move |input| { - let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::env_ref_mut path"); - quote! { #p(&mut env, &#input)? } - }) - } - - fn call(&self, ident: &syn::Ident) -> TokenStream { - (self.0)(ident) - } -} - -/// Describes how a JNI parameter for a particular type/form is decoded. -#[derive(Clone)] -pub struct JniForm { - /// On-the-wire JNI type, e.g. `jni::sys::jlong`, `jni::objects::JObject`, - /// `*const Session`. Emitted verbatim in the wrapper signature. - jni_type: syn::Type, - /// Kotlin-side wire type for this form (`"Long"`, `"Boolean"`, `"Int"`, - /// `"String"`, `"ByteArray"`, `"JObject"`). - kotlin_jni_type: String, - /// True for raw-pointer slots — appends `"Ptr"` to the Kotlin parameter - /// name (e.g. `sessionPtr: Long`) and drives `_ptr` rename in the JNI - /// wrapper signature. - pointer_param: bool, - /// When true, the decoded value is passed to the wrapped function as - /// `&name` rather than `name`. Used for opaque-handle borrows where the - /// decoder produces an owner (e.g. `OwnedObject`) but the wrapped - /// function expects a shared reference. - call_with_ref: bool, - decode: InlineFn, -} - -impl JniForm { - pub fn new( - jni_type: impl AsRef, - kotlin_jni_type: impl Into, - decode: InlineFn, - ) -> Self { - Self { - jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid JniForm jni_type"), - kotlin_jni_type: kotlin_jni_type.into(), - pointer_param: false, - call_with_ref: false, - decode, - } - } - - pub fn pointer_param(mut self, p: bool) -> Self { - self.pointer_param = p; - self - } - - pub fn call_with_ref(mut self, v: bool) -> Self { - self.call_with_ref = v; - self - } - - /// Whether this form's wire JNI type is a `JObject`-shaped object that - /// supports `is_null()` (used by the `Option` combinator). - fn is_jni_object(&self) -> bool { - jni_object_kind(&self.jni_type).is_some() - } -} - -#[derive(Clone)] -pub enum ReturnEncode { - /// `Ok((&mut env, __result)?)` — wrapping function returns - /// `ZResult`. - Wrapper(syn::Path), - /// `Ok(Arc::into_raw(Arc::new(__result)))` — opaque Arc-handle return. - ArcIntoRaw, -} - -/// Describes how a Rust return value is encoded into a JNI return. -#[derive(Clone)] -pub struct ReturnForm { - jni_type: syn::Type, - kotlin_jni_type: Option, - encode: ReturnEncode, - default_expr: syn::Expr, -} - -impl ReturnForm { - pub fn new( - jni_type: impl AsRef, - encode: ReturnEncode, - default_expr: impl AsRef, - ) -> Self { - Self { - jni_type: syn::parse_str(jni_type.as_ref()).expect("invalid ReturnForm jni_type"), - kotlin_jni_type: None, - encode, - default_expr: syn::parse_str(default_expr.as_ref()) - .expect("invalid ReturnForm default_expr"), - } - } - - pub fn kotlin(mut self, kotlin: impl Into) -> Self { - self.kotlin_jni_type = Some(kotlin.into()); - self - } -} +use crate::jni_type_binding::{JniTypeBinding, ReturnEncode}; +pub use crate::jni_type_binding::{InlineFn, TypeBinding}; // ===================================================================== // JniStructConverter @@ -212,9 +45,7 @@ pub struct StructBuilder { /// `ZResult` type used in the decoder return signature. zresult: syn::Path, /// Type registry that the struct converter mutates as it processes each - /// `#[prebindgen]` struct. Pre-populated with built-ins; the caller - /// typically merges in a shared `JniTypeBinding` (callbacks, enums, ...) - /// before building. + /// `#[prebindgen]` struct. types: JniTypeBinding, } @@ -223,9 +54,6 @@ impl Default for StructBuilder { Self { source_module: syn::parse_str("crate").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), - // No `with_builtins` here: `classify_struct_field` handles - // bool/i64/f64 directly and resolves enums from the shared - // bindings the caller merges in. types: JniTypeBinding::new(), } } @@ -233,8 +61,7 @@ impl Default for StructBuilder { impl StructBuilder { /// Module path that contains the `#[prebindgen]` struct types - /// (e.g. `"zenoh_flat::ext"`). Used to qualify the struct type in - /// auto-generated decoders. + /// (e.g. `"zenoh_flat::ext"`). pub fn source_module(mut self, path: impl AsRef) -> Self { self.source_module = syn::parse_str(path.as_ref()).expect("invalid source_module path"); self @@ -248,13 +75,11 @@ impl StructBuilder { /// Register or replace a single [`TypeBinding`] in the type registry. pub fn type_binding(mut self, binding: TypeBinding) -> Self { - self.types.types.insert(binding.name.clone(), binding); + self.types.types.insert(binding.name().to_string(), binding); self } - /// Merge a reusable [`JniTypeBinding`] into the type registry. Type - /// entries override entries already present with the same key; data-class - /// blocks are appended in order. + /// Merge a reusable [`JniTypeBinding`] into the type registry. pub fn jni_type_binding(mut self, bindings: JniTypeBinding) -> Self { self.types.types.extend(bindings.types); self.types @@ -302,9 +127,7 @@ impl JniStructConverter { self.pending.pop_front() } - /// Closure suitable for `itertools::batching`. Borrows `&mut self` so - /// [`JniStructConverter::into_jni_type_binding`] can be called after the - /// pipeline finishes. + /// Closure suitable for `itertools::batching`. pub fn as_closure<'a, I>( &'a mut self, ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a @@ -314,9 +137,7 @@ impl JniStructConverter { move |iter| self.call(iter) } - /// Consume the converter and return the populated [`JniTypeBinding`] — - /// now containing the original entries, the auto-registered struct - /// bindings, and the accumulated Kotlin `data class` strings. + /// Consume the converter and return the populated [`JniTypeBinding`]. pub fn into_jni_type_binding(self) -> JniTypeBinding { self.cfg.types } @@ -422,14 +243,13 @@ impl JniStructConverter { }; let decoder_path = format!("decode_{struct_name}"); - let mut binding = TypeBinding::new(struct_name.clone()); - binding.kotlin_type = Some(struct_name.clone()); - binding.consume = Some(JniForm::new( + let row = TypeBinding::param( + &struct_name, + &struct_name, "jni::objects::JObject", - "JObject", InlineFn::env_ref_mut(&decoder_path), - )); - self.cfg.types.types.insert(struct_name.clone(), binding); + ); + self.cfg.types.types.insert(row.name().to_string(), row); let block = format!( "data class {}(\n{}\n)", @@ -456,7 +276,7 @@ impl JniStructConverter { "f64" => StructFieldKind::F64, _ => { if let Some(binding) = self.cfg.types.types.get(&name) { - if let Some(p) = binding.enum_decoder.as_ref() { + if let Some(p) = binding.enum_field_decoder_path() { return StructFieldKind::Enum(p.clone()); } } @@ -475,15 +295,9 @@ pub struct MethodsBuilder { class_prefix: String, function_suffix: String, source_module: syn::Path, - owned_object: syn::Path, zresult: syn::Path, throw_exception: syn::Path, - /// Type registry. Built-ins are pre-registered; the caller threads in - /// the [`JniTypeBinding`] returned by - /// [`JniStructConverter::into_jni_type_binding`] (which itself contains - /// the user's shared bindings + auto-registered struct entries). types: JniTypeBinding, - /// Kotlin output config — if `None`, no Kotlin file is emitted. kotlin: Option, } @@ -493,11 +307,7 @@ pub(crate) struct KotlinConfig { output_path: PathBuf, package: String, class_name: String, - /// FQN of the `@Throws(::class)` exception — `None` disables the - /// annotation. throws_class_fqn: Option, - /// FQN of a singleton referenced inside the generated `init { ... }` block - /// to force native-library loading — `None` disables the `init`. init_load_fqn: Option, } @@ -507,7 +317,6 @@ impl Default for MethodsBuilder { class_prefix: String::new(), function_suffix: String::new(), source_module: syn::parse_str("crate").unwrap(), - owned_object: syn::parse_str("OwnedObject").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), throw_exception: syn::parse_str("throw_exception").unwrap(), types: JniTypeBinding::new().with_builtins(), @@ -517,55 +326,45 @@ impl Default for MethodsBuilder { } impl MethodsBuilder { - /// JNI class prefix prepended to each function name, e.g. - /// `"Java_io_zenoh_jni_JNISession_"`. + /// JNI class prefix prepended to each function name. pub fn class_prefix(mut self, prefix: impl Into) -> Self { self.class_prefix = prefix.into(); self } - /// Suffix appended to the camel-case function name, e.g. `"ViaJNI"`. + /// Suffix appended to the camel-case function name. pub fn function_suffix(mut self, suffix: impl Into) -> Self { self.function_suffix = suffix.into(); self } /// Fully-qualified path of the module that contains the original Rust - /// functions being wrapped, e.g. `"zenoh_flat::session"`. + /// functions being wrapped. pub fn source_module(mut self, path: impl AsRef) -> Self { self.source_module = syn::parse_str(path.as_ref()).expect("invalid source_module path"); self } - /// Path of the `OwnedObject` helper used to borrow Arc-pointers in the - /// `OwnedRef` decode form. - pub fn owned_object(mut self, path: impl AsRef) -> Self { - self.owned_object = syn::parse_str(path.as_ref()).expect("invalid owned_object path"); - self - } - /// Path of the `ZResult` type used in the closure's return type. pub fn zresult(mut self, path: impl AsRef) -> Self { self.zresult = syn::parse_str(path.as_ref()).expect("invalid zresult path"); self } - /// Path of the `throw_exception!` macro (will be called as `!`). + /// Path of the `throw_exception!` macro. pub fn throw_exception(mut self, path: impl AsRef) -> Self { self.throw_exception = syn::parse_str(path.as_ref()).expect("invalid throw_exception path"); self } - /// Universal entry point: register or replace a [`TypeBinding`] by name. + /// Register or replace a [`TypeBinding`] by name. pub fn type_binding(mut self, binding: TypeBinding) -> Self { - self.types.types.insert(binding.name.clone(), binding); + self.types.types.insert(binding.name().to_string(), binding); self } - /// Merge a reusable [`JniTypeBinding`] into the type registry. Type - /// entries override entries already present with the same key; data-class - /// blocks are appended in order. + /// Merge a reusable [`JniTypeBinding`] into the type registry. pub fn jni_type_binding(mut self, bindings: JniTypeBinding) -> Self { self.types.types.extend(bindings.types); self.types @@ -574,9 +373,7 @@ impl MethodsBuilder { self } - /// Enable Kotlin-side prototype generation. `path` is where the `.kt` - /// file will be written when [`JniMethodsConverter::write_kotlin`] is - /// called. + /// Enable Kotlin-side prototype generation. pub fn kotlin_output(mut self, path: impl Into) -> Self { self.kotlin .get_or_insert_with(KotlinConfig::default) @@ -584,15 +381,13 @@ impl MethodsBuilder { self } - /// Kotlin `package` of the generated file (e.g. `"io.zenoh.jni"`). + /// Kotlin `package` of the generated file. pub fn kotlin_package(mut self, pkg: impl Into) -> Self { self.kotlin.get_or_insert_with(KotlinConfig::default).package = pkg.into(); self } - /// Name of the generated Kotlin `object` (e.g. `"JNISessionNative"`). Must - /// agree with the JNI class_prefix — i.e. `class_prefix` should end with - /// `"__"`. + /// Name of the generated Kotlin `object`. pub fn kotlin_class(mut self, name: impl Into) -> Self { self.kotlin .get_or_insert_with(KotlinConfig::default) @@ -601,7 +396,7 @@ impl MethodsBuilder { } /// FQN of the exception type to annotate every `external fun` with via - /// `@Throws(::class)`. Unset ⇒ no annotation. + /// `@Throws(::class)`. pub fn kotlin_throws(mut self, fqn: impl Into) -> Self { self.kotlin .get_or_insert_with(KotlinConfig::default) @@ -609,8 +404,7 @@ impl MethodsBuilder { self } - /// FQN of a singleton referenced from the generated `init { ... }` block - /// (typically `io.zenoh.ZenohLoad`) to force native-library loading. + /// FQN of a singleton referenced from the generated `init { ... }` block. pub fn kotlin_init(mut self, fqn: impl Into) -> Self { self.kotlin .get_or_insert_with(KotlinConfig::default) @@ -647,9 +441,7 @@ pub struct JniMethodsConverter { cfg: MethodsBuilder, pending: VecDeque<(syn::Item, SourceLocation)>, buffered: bool, - /// Accumulated Kotlin `external fun ...` blocks, one per wrapped function. kotlin_funs: Vec, - /// Set of Kotlin FQNs referenced by emitted externals. kotlin_used_fqns: BTreeSet, } @@ -658,8 +450,6 @@ impl JniMethodsConverter { MethodsBuilder::default() } - /// Drain `iter` on the first call, convert each fn item, and queue the - /// result for the next `pop` from this batching closure. pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> where I: Iterator, @@ -674,9 +464,6 @@ impl JniMethodsConverter { self.pending.pop_front() } - /// Borrowing closure suitable for `itertools::batching`. Does not consume - /// `self` so [`JniMethodsConverter::write_kotlin`] can be called after - /// the pipeline finishes. pub fn as_closure<'a, I>( &'a mut self, ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a @@ -792,9 +579,7 @@ impl JniMethodsConverter { let name = &pat_ident.ident; let ty = &*pat_type.ty; - let kind = self.classify_arg(ty, name); self.emit_arg( - kind, name, ty, loc, @@ -826,43 +611,46 @@ impl JniMethodsConverter { quote! { () }, quote! { #zresult<()> }, ) - } else if let Some(form) = self.lookup_return_form(&inner) { - if kt_enabled { - let kt = form - .kotlin_jni_type - .clone() - .expect("return form Kotlin type not configured"); - let short = kotlin_register_fqn(&kt, &mut local_kotlin_fqns); - kotlin_ret = Some(short); - } - let jni_type = &form.jni_type; - let default_expr = &form.default_expr; - match &form.encode { - ReturnEncode::Wrapper(wrap_fn) => ( - quote! { #jni_type }, - quote! { #wrap_fn(&mut env, __result) }, - quote! { #default_expr }, - quote! { #zresult<#jni_type> }, - ), - ReturnEncode::ArcIntoRaw => ( - quote! { #jni_type }, - quote! { - Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) - }, - quote! { #default_expr }, - quote! { #zresult<#jni_type> }, - ), - } } else { - // Fallback: treat as opaque Arc-handle, return `*const T`. + let key = ty.to_token_stream().to_string(); + let binding = self.cfg.types.types.get(&key).unwrap_or_else(|| { + panic!( + "unsupported return type `{}` for `{}` at {loc}: \ + register a TypeBinding keyed `{}`", + ty.to_token_stream(), + original_name, + key + ) + }); + let encode = binding.encode().unwrap_or_else(|| { + panic!( + "TypeBinding `{}` has no encode (return direction) at {loc}", + key + ) + }); + let default_expr = binding + .default_expr() + .expect("encode-bearing row must have default_expr"); + let jni_type = binding.jni_type(); + if kt_enabled { - kotlin_ret = Some("Long".to_string()); + let kt = binding.kotlin_type(); + let short = kotlin_register_fqn(kt, &mut local_kotlin_fqns); + kotlin_ret = Some(short); } + + let wrap_ok_ts = match encode { + ReturnEncode::Wrapper(p) => quote! { #p(&mut env, __result) }, + ReturnEncode::ArcIntoRaw => quote! { + Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) + }, + }; + ( - quote! { *const #inner }, - quote! { Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) }, - quote! { std::ptr::null() }, - quote! { #zresult<*const #inner> }, + quote! { #jni_type }, + wrap_ok_ts, + quote! { #default_expr }, + quote! { #zresult<#jni_type> }, ) } } @@ -948,112 +736,9 @@ impl JniMethodsConverter { syn::parse2(tokens).expect("generated JNI wrapper must parse") } - fn lookup_return_form(&self, ty: &syn::Type) -> Option<&ReturnForm> { - let key = binding_key(ty)?; - self.cfg - .types - .types - .get(&key) - .and_then(|b| b.returns.as_ref()) - } - - /// Classify a function-arg type into one of the uniform variants below. - fn classify_arg(&self, ty: &syn::Type, _name: &syn::Ident) -> ArgKind { - let types: &HashMap = &self.cfg.types.types; - match ty { - syn::Type::Reference(r) if r.mutability.is_none() => { - let elem = &*r.elem; - let last = type_last_segment(elem).unwrap_or_default(); - if let Some(binding) = types.get(&last) { - if let Some(form) = binding.borrow.as_ref() { - return ArgKind::Borrow { - form: form.clone(), - kotlin_override: binding.kotlin_type.clone(), - }; - } - } - // Fallback: treat any unbound `&T` as an opaque Arc borrow - // against a raw `*const T` pointer. The owned_object path is - // serialized to String (syn::Path is not Send) and re-parsed - // inside the closure. call_with_ref causes the decoded owner - // to be passed as `&name` to the wrapped function. - let ptr_ty: syn::Type = syn::parse2(quote! { *const #elem }) - .expect("opaque pointer type must parse"); - let owned_str = self.cfg.owned_object.to_token_stream().to_string(); - let opaque_form = JniForm { - jni_type: ptr_ty, - kotlin_jni_type: "Long".to_string(), - pointer_param: true, - call_with_ref: true, - decode: InlineFn::new(move |input| { - let owned: syn::Path = - syn::parse_str(&owned_str).expect("owned_object must be a valid path"); - quote! { #owned::from_raw(#input) } - }), - }; - ArgKind::Borrow { - form: opaque_form, - kotlin_override: None, - } - } - syn::Type::ImplTrait(_) => { - let key = ty.to_token_stream().to_string(); - if let Some(binding) = types.get(&key) { - if let Some(form) = binding.consume.as_ref() { - return ArgKind::Consume { - form: form.clone(), - kotlin_override: binding.kotlin_type.clone(), - }; - } - } - ArgKind::Unsupported - } - syn::Type::Path(tp) => { - let Some(last) = tp.path.segments.last() else { - return ArgKind::Unsupported; - }; - let name = last.ident.to_string(); - - if name == "Option" { - let Some(inner_ty) = option_inner_type(last) else { - return ArgKind::Unsupported; - }; - let Some(key) = binding_key(inner_ty) else { - return ArgKind::Unsupported; - }; - if let Some(binding) = types.get(&key) { - if let Some(form) = binding.consume.as_ref() { - return ArgKind::OptionConsume { - form: form.clone(), - kotlin_override: binding.kotlin_type.clone(), - }; - } - } - return ArgKind::Unsupported; - } - - let Some(key) = binding_key(ty) else { - return ArgKind::Unsupported; - }; - if let Some(binding) = types.get(&key) { - if let Some(form) = binding.consume.as_ref() { - return ArgKind::Consume { - form: form.clone(), - kotlin_override: binding.kotlin_type.clone(), - }; - } - } - - ArgKind::Unsupported - } - _ => ArgKind::Unsupported, - } - } - #[allow(clippy::too_many_arguments)] fn emit_arg( &self, - kind: ArgKind, name: &syn::Ident, ty: &syn::Type, loc: &SourceLocation, @@ -1064,164 +749,58 @@ impl JniMethodsConverter { local_kotlin_fqns: &mut BTreeSet, kt_enabled: bool, ) { - match kind { - ArgKind::Consume { - form, - kotlin_override, - } => { - self.emit_consume_or_borrow( - /* borrow */ false, - form, - kotlin_override, - name, - prelude, - jni_params, - call_args, - kotlin_params, - local_kotlin_fqns, - kt_enabled, - ); - } - ArgKind::Borrow { - form, - kotlin_override, - } => { - self.emit_consume_or_borrow( - /* borrow */ true, - form, - kotlin_override, - name, - prelude, - jni_params, - call_args, - kotlin_params, - local_kotlin_fqns, - kt_enabled, - ); - } - ArgKind::OptionConsume { - form, - kotlin_override, - } => { - if !form.is_jni_object() { - panic!( - "Option<{}> requires a JNI-object form for `{}`", - ty.to_token_stream(), - name - ); - } - let jt = &form.jni_type; - jni_params.push(quote! { #name: #jt }); - let inner = self.decode_expr(&form.decode, name); - prelude.push(quote! { - let #name = if !#name.is_null() { - Some(#inner) - } else { - None - }; - }); - call_args.push(quote! { #name }); - if kt_enabled { - let kt_decl = self.kotlin_arg_type(&form, kotlin_override.as_deref()); - let short = kotlin_register_fqn(&kt_decl, local_kotlin_fqns); - kotlin_params.push(format!( - "{}: {}?", - kotlin_param_name(&name.to_string(), form.pointer_param), - short - )); - } - } - ArgKind::Unsupported => panic!( - "unsupported parameter type `{}` for `{}` at {loc}", + let key = ty.to_token_stream().to_string(); + let binding = self.cfg.types.types.get(&key).unwrap_or_else(|| { + panic!( + "unsupported parameter type `{}` for `{}` at {loc}: \ + register a TypeBinding keyed `{}`", ty.to_token_stream(), - name - ), - } - } - - #[allow(clippy::too_many_arguments)] - fn emit_consume_or_borrow( - &self, - _borrow: bool, - form: JniForm, - kotlin_override: Option, - name: &syn::Ident, - prelude: &mut Vec, - jni_params: &mut Vec, - call_args: &mut Vec, - kotlin_params: &mut Vec, - local_kotlin_fqns: &mut BTreeSet, - kt_enabled: bool, - ) { - let jt = &form.jni_type; - let pat = if form.pointer_param { + name, + key + ) + }); + let decode = binding.decode().unwrap_or_else(|| { + panic!( + "TypeBinding `{}` has no decode (param direction) at {loc}", + key + ) + }); + + let pat = if binding.is_pointer() { format_ident!("{}_ptr", name) } else { name.clone() }; + let jt = binding.jni_type(); jni_params.push(quote! { #pat: #jt }); - let expr = form.decode.call(&pat); + let expr = decode.call(&pat); prelude.push(quote! { let #name = #expr; }); - if form.call_with_ref { + if binding.is_borrow() { call_args.push(quote! { &#name }); } else { call_args.push(quote! { #name }); } if kt_enabled { - let kt_decl = self.kotlin_arg_type(&form, kotlin_override.as_deref()); - let short = kotlin_register_fqn(&kt_decl, local_kotlin_fqns); + let short = kotlin_register_fqn(binding.kotlin_type(), local_kotlin_fqns); + let suffix = if binding.is_option() { "?" } else { "" }; kotlin_params.push(format!( - "{}: {}", - kotlin_param_name(&name.to_string(), form.pointer_param), - short + "{}: {}{}", + kotlin_param_name(&name.to_string(), binding.is_pointer()), + short, + suffix )); } } - - fn decode_expr(&self, decode: &InlineFn, input: &syn::Ident) -> TokenStream { - decode.call(input) - } - - /// Resolve the Kotlin parameter type for a JniForm. For object-typed - /// JNI wires (`JObject`) we use the binding's `kotlin_type` FQN; for - /// primitive wires (jboolean/jlong/jint/JString/JByteArray/raw ptrs) we - /// use the form's `kotlin_jni_type`. - fn kotlin_arg_type(&self, form: &JniForm, kotlin_override: Option<&str>) -> String { - match jni_object_kind(&form.jni_type) { - Some(JniObjectKind::JObject) => kotlin_override - .map(str::to_string) - .unwrap_or_else(|| form.kotlin_jni_type.clone()), - _ => form.kotlin_jni_type.clone(), - } - } } // ===================================================================== -// Internal classification helpers +// Internal helpers // ===================================================================== -enum ArgKind { - Consume { - form: JniForm, - kotlin_override: Option, - }, - Borrow { - form: JniForm, - kotlin_override: Option, - }, - OptionConsume { - form: JniForm, - kotlin_override: Option, - }, - Unsupported, -} - -/// Field-type classification for `#[prebindgen]` struct fields — narrower -/// than [`ArgKind`] because structs only need a round-trippable primitive / -/// enum representation. +/// Field-type classification for `#[prebindgen]` struct fields. enum StructFieldKind { Bool, I64, @@ -1230,59 +809,6 @@ enum StructFieldKind { Unsupported, } -#[derive(Clone, Copy)] -enum JniObjectKind { - JObject, - JString, - JByteArray, -} - -/// Recognize JNI object-shaped wire types. Used by the `Option` combinator -/// (which needs `is_null()`) and by the Kotlin parameter-type derivation. -fn jni_object_kind(ty: &syn::Type) -> Option { - let syn::Type::Path(tp) = ty else { return None }; - let last = tp.path.segments.last()?; - match last.ident.to_string().as_str() { - "JObject" => Some(JniObjectKind::JObject), - "JString" => Some(JniObjectKind::JString), - "JByteArray" => Some(JniObjectKind::JByteArray), - _ => None, - } -} - -fn type_last_segment(ty: &syn::Type) -> Option { - let syn::Type::Path(tp) = ty else { return None }; - tp.path.segments.last().map(|s| s.ident.to_string()) -} - -/// Inner type of a `Foo` path segment (used for `Option`). -fn option_inner_type(seg: &syn::PathSegment) -> Option<&syn::Type> { - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return None; - }; - let syn::GenericArgument::Type(inner) = args.args.first()? else { - return None; - }; - Some(inner) -} - -/// Lookup key for a path type in the [`crate::jni_type_binding::JniTypeBinding`] -/// registry. For `Vec` types we use the canonical -/// `to_token_stream()` form (matching what -/// [`crate::jni_type_binding::TypeBinding::new`] produces from input like -/// `"Vec"`). For other path types we use the last-segment ident, which -/// lets registrations key off short type names without forcing callers to -/// spell out the full path of the user-side type. -fn binding_key(ty: &syn::Type) -> Option { - let syn::Type::Path(tp) = ty else { return None }; - let seg = tp.path.segments.last()?; - if seg.ident == "Vec" { - Some(ty.to_token_stream().to_string()) - } else { - Some(seg.ident.to_string()) - } -} - fn is_unit(ty: &syn::Type) -> bool { matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) } diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index 31cb31c1..dfe213c4 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -1,139 +1,277 @@ -//! Reusable collection of JNI type bindings. +//! Flat JNI type registry. //! -//! [`JniTypeBinding`] aggregates a set of [`TypeBinding`]s — including -//! callback registrations, which live as `consume` slots on the element -//! type's binding — into a single value that can be defined once and -//! threaded through both phases of the JNI binding pipeline (a -//! [`crate::jni_converter::JniStructConverter`] mutates it; a -//! [`crate::jni_converter::JniMethodsConverter`] reads it). +//! Each [`TypeBinding`] is a single row keyed by the canonical +//! `to_token_stream()` form of a Rust type-shape, e.g. `"String"`, +//! `"& Session"`, `"Vec < u8 >"`, `"Option < KeyExpr < 'static > >"`, +//! `"ZResult < ZenohId >"`, `"impl Fn (Sample) + Send + Sync + 'static"`. //! -//! ```ignore -//! use zenoh_flat::jni_converter::{InlineFn, JniForm, TypeBinding}; -//! use zenoh_flat::jni_type_binding::JniTypeBinding; -//! use quote::quote; +//! A row carries: //! -//! let common = JniTypeBinding::new() -//! .type_binding( -//! TypeBinding::new("KeyExpr").consume( -//! JniForm::new( -//! "*const zenoh::key_expr::KeyExpr<'static>", -//! "Long", -//! InlineFn::new(|input| { -//! quote! { (*std::sync::Arc::from_raw(#input)).clone() } -//! }), -//! ) -//! .pointer_param(true), -//! ), -//! ); -//! ``` +//! * `kotlin_type` — the Kotlin parameter or return type +//! * `jni_type` — the on-the-wire JNI type emitted in the wrapper signature +//! * `decode` — JNI value → Rust value (param-direction rows) +//! * `encode` + `default_expr` — Rust value → JNI value (return-direction rows) +//! * `enum_field_decoder` — only for enum-shaped rows, used by struct-field +//! classification +//! +//! Wrapper types (`&T`, `Vec`, `Option`, `ZResult`) are **not** +//! decomposed by the classifier — each must have its own explicit row. The +//! [`TypeBinding::opaque_borrow`], [`TypeBinding::opaque_arc_return`] and +//! [`TypeBinding::option_of`] convenience constructors keep registration +//! concise. use std::collections::HashMap; +use std::sync::Arc; +use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use crate::jni_converter::{InlineFn, JniForm, ReturnForm}; - -/// Per-type description of how a Rust type is represented across the JNI -/// boundary. A type may declare up to three forms: -/// `consume` (`T` parameter), `borrow` (`&T` parameter), and `returns` -/// (`ZResult` return). -/// -/// `Vec` parameters and `ZResult>` returns are described by a -/// separate binding registered under the type itself, e.g. -/// `TypeBinding::new("Vec")` or `TypeBinding::new("Vec")`. -/// The classifier looks up `Vec` types by their canonical -/// `to_token_stream()` form, matching whatever -/// [`TypeBinding::new`] canonicalized at registration time. -/// -/// Callback parameters (`impl Fn(T) + Send + Sync + 'static`) are described -/// by an ordinary `consume` form on a binding keyed under -/// `"impl Fn()"` (e.g. `"impl Fn(Sample)"`, `"impl Fn()"`). The -/// classifier synthesizes that key when it sees an `impl Fn(...)` parameter. +/// Clonable closure that produces a `TokenStream` from the JNI input ident. +#[derive(Clone)] +pub struct InlineFn(Arc TokenStream + Send + Sync>); + +impl InlineFn { + pub fn new(f: F) -> Self + where + F: Fn(&syn::Ident) -> TokenStream + Send + Sync + 'static, + { + InlineFn(Arc::new(f)) + } + + /// `()?` — pure conversion (e.g. enum decoders). + pub fn pure(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::pure path"); + quote! { #p(#input)? } + }) + } + + /// `(&env, &)?` — decoder needing shared access to the JNI env. + pub fn env_ref(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::env_ref path"); + quote! { #p(&env, &#input)? } + }) + } + + /// `(&mut env, &)?` — decoder needing mutable access to the JNI env. + pub fn env_ref_mut(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = + syn::parse_str(&s).expect("invalid InlineFn::env_ref_mut path"); + quote! { #p(&mut env, &#input)? } + }) + } + + pub(crate) fn call(&self, ident: &syn::Ident) -> TokenStream { + (self.0)(ident) + } +} + +/// How a Rust return value is encoded into a JNI return. +#[derive(Clone)] +pub enum ReturnEncode { + /// `(&mut env, __result)` — wrapping function returns + /// `ZResult`. + Wrapper(syn::Path), + /// `Ok(Arc::into_raw(Arc::new(__result)))` — opaque Arc-handle return. + ArcIntoRaw, +} + +impl ReturnEncode { + pub fn wrapper(path: impl AsRef) -> Self { + ReturnEncode::Wrapper( + syn::parse_str(path.as_ref()).expect("invalid ReturnEncode::wrapper path"), + ) + } +} + +/// Per-row binding from a Rust type-shape to its JNI/Kotlin representation. #[derive(Clone)] pub struct TypeBinding { - pub(crate) name: String, - /// Kotlin-side type name (FQN preferred — out-of-package import is - /// auto-derived; bare for same-package). Used as the Kotlin parameter - /// type when the form's wire JNI type is `JObject` and as the Kotlin - /// return type. For primitive-mapped forms (`bool`, `Duration`, - /// `String`, ...) the form's `kotlin_jni_type` is used instead. - pub(crate) kotlin_type: Option, - pub(crate) consume: Option, - pub(crate) borrow: Option, - pub(crate) returns: Option, + /// Canonical Rust type-shape (token-stream form). The lookup key. + pub(crate) rust_type: String, + /// Kotlin parameter / return type (FQN preferred for object types, + /// bare for primitives). For Option-shaped rows this is the inner + /// type's Kotlin name; the `?` suffix is added by the emitter from + /// the row's rust_type prefix. + pub(crate) kotlin_type: String, + /// On-the-wire JNI type emitted in the wrapper signature. + pub(crate) jni_type: syn::Type, + /// JNI value → Rust value. None for return-only rows. + pub(crate) decode: Option, + /// Rust value → JNI value. None for param-only rows. + pub(crate) encode: Option, + /// Default JNI value emitted on the throw-return path. Required when + /// `encode` is set. + pub(crate) default_expr: Option, /// Decoder path for Java-enum-shaped types (`fn(jint) -> ZResult`). - /// Set when the type's JNI representation is an `Int` mapped through a - /// pure decoder. Used by struct-field classification to detect enum - /// fields and emit `env.get_field(..., "I")` + decoder call. - pub(crate) enum_decoder: Option, + /// Used by struct-field classification to detect enum fields and emit + /// `env.get_field(..., "I")` + decoder call. + pub(crate) enum_field_decoder: Option, } impl TypeBinding { - /// Short type name this binding is keyed under (e.g. `"KeyExpr"`). - pub fn name(&self) -> &str { - &self.name - } - - /// Construct a binding keyed by `name`. If `name` parses as a Rust type, - /// it is canonicalized through `quote::ToTokens` so whitespace variations - /// in user input match the form the classifier produces from AST nodes - /// (matters for `impl Fn(T) + Send + Sync + 'static`-style names). Falls - /// back to the literal string if parsing fails. - pub fn new(name: impl Into) -> Self { - let raw = name.into(); - let canonical = syn::parse_str::(&raw) - .map(|t| t.to_token_stream().to_string()) - .unwrap_or_else(|_| raw); + /// Param-direction row. `rust_type` is canonicalized via `syn::Type` parse. + pub fn param( + rust_type: impl AsRef, + kotlin_type: impl Into, + jni_type: impl AsRef, + decode: InlineFn, + ) -> Self { Self { - name: canonical, - kotlin_type: None, - consume: None, - borrow: None, - returns: None, - enum_decoder: None, + rust_type: canon_type(rust_type.as_ref()), + kotlin_type: kotlin_type.into(), + jni_type: parse_type(jni_type.as_ref()), + decode: Some(decode), + encode: None, + default_expr: None, + enum_field_decoder: None, } } - pub fn kotlin(mut self, fqn: impl Into) -> Self { - self.kotlin_type = Some(fqn.into()); - self + /// Return-direction row. + pub fn returns( + rust_type: impl AsRef, + kotlin_type: impl Into, + jni_type: impl AsRef, + encode: ReturnEncode, + default_expr: impl AsRef, + ) -> Self { + Self { + rust_type: canon_type(rust_type.as_ref()), + kotlin_type: kotlin_type.into(), + jni_type: parse_type(jni_type.as_ref()), + decode: None, + encode: Some(encode), + default_expr: Some( + syn::parse_str(default_expr.as_ref()) + .expect("invalid TypeBinding::returns default_expr"), + ), + enum_field_decoder: None, + } } - /// Mark this binding as a Java-enum-shaped type and record the - /// `fn(jint) -> ZResult` decoder path used for both top-level - /// argument decoding and struct-field decoding. - pub fn enum_decoder(mut self, path: impl AsRef) -> Self { - self.enum_decoder = Some( - syn::parse_str(path.as_ref()).expect("invalid TypeBinding::enum_decoder path"), + /// Mark this binding as a Java-enum-shaped type. Used by struct-field + /// classification to emit `env.get_field(..., "I")` + `(raw)?`. + pub fn enum_field_decoder(mut self, path: impl AsRef) -> Self { + self.enum_field_decoder = Some( + syn::parse_str(path.as_ref()) + .expect("invalid TypeBinding::enum_field_decoder path"), ); self } - pub fn consume(mut self, form: JniForm) -> Self { - self.consume = Some(form); - self + /// Convenience: opaque borrow `&T` — JNI side passes raw `*const T`, + /// decoded via `::from_raw`. Because the row's key starts + /// with `&`, the wrapped fn receives `&name` automatically. + pub fn opaque_borrow(t: impl AsRef, owned_object: impl AsRef) -> Self { + let t = t.as_ref().to_string(); + let owned_str = owned_object.as_ref().to_string(); + // Validate the owner path parses now so errors surface at registration. + let _: syn::Path = + syn::parse_str(&owned_str).expect("opaque_borrow: invalid owned_object path"); + Self::param( + format!("&{}", t), + "Long", + format!("*const {}", t), + InlineFn::new(move |input| { + let owned: syn::Path = + syn::parse_str(&owned_str).expect("owned_object must parse"); + quote! { #owned::from_raw(#input) } + }), + ) } - pub fn borrow(mut self, form: JniForm) -> Self { - self.borrow = Some(form); - self + /// Convenience: opaque Arc return for `ZResult` — encode via + /// `Arc::into_raw(Arc::new(__result))`, default to `std::ptr::null()`. + pub fn opaque_arc_return(t: impl AsRef) -> Self { + let t = t.as_ref(); + Self::returns( + format!("ZResult<{}>", t), + "Long", + format!("*const {}", t), + ReturnEncode::ArcIntoRaw, + "std::ptr::null()", + ) } - pub fn returns(mut self, form: ReturnForm) -> Self { - self.returns = Some(form); - self + /// Convenience: `Option` row that lifts `inner`'s decode with a + /// JNI-side null check. Inner's wire type must be JNI-object-shaped. + pub fn option_of(inner: &TypeBinding) -> Self { + let inner_decode = inner + .decode + .as_ref() + .expect("option_of: inner must be a param row") + .clone(); + assert!( + jni_object_shaped(&inner.jni_type), + "option_of requires a JNI-object inner form, got `{}`", + inner.jni_type.to_token_stream() + ); + Self { + rust_type: canon_type(&format!("Option<{}>", inner.rust_type)), + kotlin_type: inner.kotlin_type.clone(), + jni_type: inner.jni_type.clone(), + decode: Some(InlineFn::new(move |input| { + let inner_expr = inner_decode.call(input); + quote! { + if !#input.is_null() { + Some(#inner_expr) + } else { + None + } + } + })), + encode: None, + default_expr: None, + enum_field_decoder: None, + } + } + + /// Canonical type-shape this binding is keyed under. + pub fn name(&self) -> &str { + &self.rust_type + } + + pub(crate) fn jni_type(&self) -> &syn::Type { + &self.jni_type + } + pub(crate) fn kotlin_type(&self) -> &str { + &self.kotlin_type + } + pub(crate) fn decode(&self) -> Option<&InlineFn> { + self.decode.as_ref() + } + pub(crate) fn encode(&self) -> Option<&ReturnEncode> { + self.encode.as_ref() + } + pub(crate) fn default_expr(&self) -> Option<&syn::Expr> { + self.default_expr.as_ref() + } + pub(crate) fn enum_field_decoder_path(&self) -> Option<&syn::Path> { + self.enum_field_decoder.as_ref() + } + /// `&T` row — wrapped fn receives `&name`. + pub(crate) fn is_borrow(&self) -> bool { + self.rust_type.starts_with('&') + } + /// `*const _` / `*mut _` wire type — Kotlin name gets `Ptr` suffix and + /// the Rust ident gets `_ptr` suffix. + pub(crate) fn is_pointer(&self) -> bool { + matches!(self.jni_type, syn::Type::Ptr(_)) + } + /// `Option<_>` row — Kotlin emission appends `?`. + pub(crate) fn is_option(&self) -> bool { + self.rust_type.starts_with("Option <") } } /// Reusable collection of [`TypeBinding`]s plus the Kotlin `data class` /// strings produced by struct processing. -/// -/// The same value flows through both pipeline phases: the -/// [`crate::jni_converter::JniStructConverter`] inserts an auto-generated -/// `TypeBinding` plus a `data class` block for each `#[prebindgen]` struct -/// it sees; the [`crate::jni_converter::JniMethodsConverter`] then reads the -/// type registry to classify args/returns and reads the data-class strings -/// when emitting the final Kotlin file. #[derive(Default, Clone)] pub struct JniTypeBinding { pub(crate) types: HashMap, @@ -147,10 +285,17 @@ impl JniTypeBinding { /// Add (or replace) a [`TypeBinding`] in this collection. pub fn type_binding(mut self, binding: TypeBinding) -> Self { - self.types.insert(binding.name().to_string(), binding); + self.types.insert(binding.rust_type.clone(), binding); self } + /// Look up a registered [`TypeBinding`] by its canonical type-shape key + /// (e.g. `"HistoryConfig"`, `"Vec < u8 >"`). The key is canonicalized + /// via `syn::Type` parse so callers can pass either spacing form. + pub fn type_by_key(&self, key: &str) -> Option<&TypeBinding> { + self.types.get(&canon_type(key)) + } + /// Merge another [`JniTypeBinding`] into this one. Type entries in /// `other` override entries with the same key in `self`; data-class /// blocks are appended in order. @@ -163,31 +308,50 @@ impl JniTypeBinding { /// Pre-register built-in language types whose JNI form is fully described /// without any project-specific decoder path: `bool` (inline `x != 0`) /// and `Duration` (inline `Duration::from_millis(x as u64)`). - /// - /// Types whose decoder lives outside this crate — `String`, `Vec`, - /// callbacks, enums, opaque handles — are registered by the caller via - /// the universal [`JniTypeBinding::type_binding`] entry point. pub fn with_builtins(mut self) -> Self { - // bool — jboolean, inline `x != 0`. - self.types.insert( - "bool".to_string(), - TypeBinding::new("bool").consume(JniForm::new( - "jni::sys::jboolean", - "Boolean", - InlineFn::new(|input| quote! { #input != 0 }), - )), + let bool_row = TypeBinding::param( + "bool", + "Boolean", + "jni::sys::jboolean", + InlineFn::new(|input| quote! { #input != 0 }), ); - // Duration — jlong, inline `Duration::from_millis(x as u64)`. - self.types.insert( - "Duration".to_string(), - TypeBinding::new("Duration").consume(JniForm::new( - "jni::sys::jlong", - "Long", - InlineFn::new(|input| { - quote! { std::time::Duration::from_millis(#input as u64) } - }), - )), + self.types.insert(bool_row.rust_type.clone(), bool_row); + + let duration_row = TypeBinding::param( + "Duration", + "Long", + "jni::sys::jlong", + InlineFn::new(|input| { + quote! { std::time::Duration::from_millis(#input as u64) } + }), ); + self.types + .insert(duration_row.rust_type.clone(), duration_row); self } } + +/// Canonical type-shape string. Parses through `syn::Type` so whitespace +/// variations in user input (`"Vec"` vs `"Vec < u8 >"`) match the form +/// the classifier produces from AST nodes via `to_token_stream()`. +pub(crate) fn canon_type(s: &str) -> String { + syn::parse_str::(s) + .map(|t| t.to_token_stream().to_string()) + .unwrap_or_else(|e| panic!("TypeBinding: cannot parse `{}` as a type: {}", s, e)) +} + +fn parse_type(s: &str) -> syn::Type { + syn::parse_str(s).unwrap_or_else(|e| panic!("invalid JNI wire type `{}`: {}", s, e)) +} + +/// True if `ty` is a JNI object-shaped wire type that supports `is_null()`. +pub(crate) fn jni_object_shaped(ty: &syn::Type) -> bool { + let syn::Type::Path(tp) = ty else { return false }; + let Some(last) = tp.path.segments.last() else { + return false; + }; + matches!( + last.ident.to_string().as_str(), + "JObject" | "JString" | "JByteArray" + ) +} diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index a5d4053b..076978bf 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,128 +1,153 @@ use itertools::Itertools; use quote::quote; use zenoh_flat::jni_converter::{ - InlineFn, JniForm, JniMethodsConverter, JniStructConverter, ReturnEncode, ReturnForm, - TypeBinding, + InlineFn, JniMethodsConverter, JniStructConverter, TypeBinding, }; -use zenoh_flat::jni_type_binding::JniTypeBinding; - -fn enum_binding(name: &str, decoder: &str) -> TypeBinding { - TypeBinding::new(name) - .enum_decoder(decoder) - .consume(JniForm::new( - "jni::sys::jint", - "Int", - InlineFn::pure(decoder), - )) +use zenoh_flat::jni_type_binding::{JniTypeBinding, ReturnEncode}; + +const OWNED_OBJECT: &str = "crate::owned_object::OwnedObject"; + +fn enum_param(name: &str, decoder: &str) -> TypeBinding { + TypeBinding::param(name, "Int", "jni::sys::jint", InlineFn::pure(decoder)) + .enum_field_decoder(decoder) } -fn jobject_consume(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { - TypeBinding::new(name).kotlin(kotlin).consume(JniForm::new( +fn jobject_param(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { + TypeBinding::param( + name, + kotlin, "jni::objects::JObject", - "JObject", InlineFn::env_ref_mut(decoder), - )) + ) +} + +fn wrapped_return(rust_type: &str, kotlin: &str, jni_type: &str, wrapper: &str, default: &str) -> TypeBinding { + TypeBinding::returns( + rust_type, + kotlin, + jni_type, + ReturnEncode::wrapper(wrapper), + default, + ) } /// Type vocabulary shared across every JNI surface generated in this crate. -/// Defined once, threaded into the struct-phase converter (so struct field -/// decoders can resolve enum types), then forwarded — together with the -/// auto-registered struct bindings — into the methods-phase converter. +/// Defined once, threaded into the struct-phase converter, then forwarded — +/// together with the auto-registered struct bindings — into the methods phase. fn shared_bindings() -> JniTypeBinding { JniTypeBinding::new() - .type_binding( - TypeBinding::new("String").consume(JniForm::new( - "jni::objects::JString", - "String", - InlineFn::env_ref_mut("crate::utils::decode_string"), - )), - ) - .type_binding( - TypeBinding::new("Vec").consume(JniForm::new( - "jni::objects::JByteArray", - "ByteArray", - InlineFn::env_ref("crate::utils::decode_byte_array"), - )), - ) - .type_binding(jobject_consume( + // Strings & byte arrays. + .type_binding(TypeBinding::param( + "String", + "String", + "jni::objects::JString", + InlineFn::env_ref_mut("crate::utils::decode_string"), + )) + .type_binding(TypeBinding::param( + "Vec", + "ByteArray", + "jni::objects::JByteArray", + InlineFn::env_ref("crate::utils::decode_byte_array"), + )) + // Callbacks. + .type_binding(jobject_param( "impl Fn(Sample) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_sample_callback", "io.zenoh.jni.callbacks.JNISubscriberCallback", )) - .type_binding(jobject_consume( + .type_binding(jobject_param( "impl Fn(Query) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_query_callback", "io.zenoh.jni.callbacks.JNIQueryableCallback", )) - .type_binding(jobject_consume( + .type_binding(jobject_param( "impl Fn(Reply) + Send + Sync + 'static", "crate::sample_callback::process_kotlin_reply_callback", "io.zenoh.jni.callbacks.JNIGetCallback", )) - .type_binding(jobject_consume( + .type_binding(jobject_param( "impl Fn() + Send + Sync + 'static", "crate::sample_callback::process_kotlin_on_close_callback", "io.zenoh.jni.callbacks.JNIOnCloseCallback", )) - .type_binding(enum_binding( - "CongestionControl", - "crate::utils::decode_congestion_control", - )) - .type_binding(enum_binding("Priority", "crate::utils::decode_priority")) - .type_binding(enum_binding("Reliability", "crate::utils::decode_reliability")) - .type_binding(enum_binding( - "QueryTarget", - "crate::utils::decode_query_target", - )) - .type_binding(enum_binding( - "ConsolidationMode", - "crate::utils::decode_consolidation", - )) - .type_binding(enum_binding( - "ReplyKeyExpr", - "crate::utils::decode_reply_key_expr", - )) - // KeyExpr by-value: the JNI side passes `Arc::into_raw(Arc::new(KeyExpr))` + // Java-enum-shaped types. + .type_binding(enum_param("CongestionControl", "crate::utils::decode_congestion_control")) + .type_binding(enum_param("Priority", "crate::utils::decode_priority")) + .type_binding(enum_param("Reliability", "crate::utils::decode_reliability")) + .type_binding(enum_param("QueryTarget", "crate::utils::decode_query_target")) + .type_binding(enum_param("ConsolidationMode", "crate::utils::decode_consolidation")) + .type_binding(enum_param("ReplyKeyExpr", "crate::utils::decode_reply_key_expr")) + // KeyExpr by-value: JNI side passes `Arc::into_raw(Arc::new(KeyExpr))` // as a raw pointer; the wrapper reconstructs the Arc, clones the inner // KeyExpr, and drops the Arc at end of scope. The full path is required // so the generated `*const T` parameter type resolves at the include site. - .type_binding( - TypeBinding::new("KeyExpr").consume( - JniForm::new( - "*const zenoh::key_expr::KeyExpr<'static>", - "Long", - InlineFn::new(|input| { - quote! { (*std::sync::Arc::from_raw(#input)).clone() } - }), - ) - .pointer_param(true), - ), - ) - .type_binding(jobject_consume( + .type_binding(TypeBinding::param( + "KeyExpr<'static>", + "Long", + "*const zenoh::key_expr::KeyExpr<'static>", + InlineFn::new(|input| quote! { (*std::sync::Arc::from_raw(#input)).clone() }), + )) + // Encoding via JObject + custom decoder. + .type_binding(jobject_param( "Encoding", "crate::utils::decode_jni_encoding", - "JNIEncoding", + "io.zenoh.jni.JNIEncoding", )) - .type_binding( - TypeBinding::new("ZenohId").returns( - ReturnForm::new( - "jni::sys::jbyteArray", - ReturnEncode::wrapper("crate::zenoh_id::zenoh_id_to_byte_array"), - "jni::objects::JByteArray::default().as_raw()", - ) - .kotlin("ByteArray"), - ), - ) - .type_binding( - TypeBinding::new("Vec").returns( - ReturnForm::new( - "jni::sys::jobject", - ReturnEncode::wrapper("crate::zenoh_id::zenoh_ids_to_java_list"), - "jni::objects::JObject::default().as_raw()", - ) - .kotlin("List"), - ), - ) + // Borrows: opaque Arc handles received as `*const T` and re-borrowed + // via OwnedObject::from_raw. The `&` prefix on the row's key tells + // the converter to pass `&name` to the wrapped fn. + .type_binding(TypeBinding::opaque_borrow("Session", OWNED_OBJECT)) + .type_binding(TypeBinding::opaque_borrow("Config", OWNED_OBJECT)) + // Returns: ZenohId / Vec via custom encoders. + .type_binding(wrapped_return( + "ZResult", + "ByteArray", + "jni::sys::jbyteArray", + "crate::zenoh_id::zenoh_id_to_byte_array", + "jni::objects::JByteArray::default().as_raw()", + )) + .type_binding(wrapped_return( + "ZResult>", + "List", + "jni::sys::jobject", + "crate::zenoh_id::zenoh_ids_to_java_list", + "jni::objects::JObject::default().as_raw()", + )) + // Returns: opaque Arc handles. Each emits `*const T` and + // `Arc::into_raw(Arc::new(__result))` with a null default. + .type_binding(TypeBinding::opaque_arc_return("Session")) + .type_binding(TypeBinding::opaque_arc_return("Publisher<'static>")) + .type_binding(TypeBinding::opaque_arc_return("KeyExpr<'static>")) + .type_binding(TypeBinding::opaque_arc_return("Subscriber<()>")) + .type_binding(TypeBinding::opaque_arc_return("Querier<'static>")) + .type_binding(TypeBinding::opaque_arc_return("Queryable<()>")) + .type_binding(TypeBinding::opaque_arc_return("AdvancedSubscriber<()>")) + .type_binding(TypeBinding::opaque_arc_return("AdvancedPublisher<'static>")) +} + +/// Build `Option` rows for every X that can appear under `Option<...>` in +/// the wrapped fn signatures. Must be called after struct-converter has +/// registered the auto-generated struct rows. +fn add_option_rows(types: JniTypeBinding) -> JniTypeBinding { + let inner_keys = [ + "String", + "Vec", + "Encoding", + "HistoryConfig", + "RecoveryConfig", + "CacheConfig", + "MissDetectionConfig", + ]; + let mut out = types; + for key in inner_keys { + let inner = out + .type_by_key(key) + .unwrap_or_else(|| panic!("add_option_rows: missing inner row `{}`", key)) + .clone(); + let opt = TypeBinding::option_of(&inner); + out = out.type_binding(opt); + } + out } fn main() { @@ -145,7 +170,7 @@ fn main() { .batching(struct_conv.as_closure()) .collect(); - let types = struct_conv.into_jni_type_binding(); + let types = add_option_rows(struct_conv.into_jni_type_binding()); // Phase 2: process #[prebindgen] fns from zenoh_flat::session against // the now fully-populated type registry. @@ -153,7 +178,6 @@ fn main() { .class_prefix("Java_io_zenoh_jni_JNISessionNative_") .function_suffix("ViaJNI") .source_module("zenoh_flat::session") - .owned_object("crate::owned_object::OwnedObject") .zresult("crate::errors::ZResult") .throw_exception("crate::throw_exception") .kotlin_output("../zenoh-jni/generated-kotlin/io/zenoh/jni/JNISessionNative.kt") @@ -174,8 +198,6 @@ fn main() { // Pass-through: items that are neither `#[prebindgen]` structs nor fns // (e.g. the prebindgen feature-mismatch assertion `const _: () = { ... };`). - // The two converters intentionally panic on the wrong item kind, so any - // such items must bypass them and land directly in the destination. let passthrough = source .items_all() .filter(|(item, _)| !matches!(item, syn::Item::Fn(_) | syn::Item::Struct(_))); From 8681a260ab052a4225046428c20e802141cf4e18 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 13:41:38 +0200 Subject: [PATCH 123/126] helper functions removed --- zenoh-jni/build.rs | 99 +++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index 076978bf..f9a55013 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -7,30 +7,6 @@ use zenoh_flat::jni_type_binding::{JniTypeBinding, ReturnEncode}; const OWNED_OBJECT: &str = "crate::owned_object::OwnedObject"; -fn enum_param(name: &str, decoder: &str) -> TypeBinding { - TypeBinding::param(name, "Int", "jni::sys::jint", InlineFn::pure(decoder)) - .enum_field_decoder(decoder) -} - -fn jobject_param(name: &str, decoder: &str, kotlin: &str) -> TypeBinding { - TypeBinding::param( - name, - kotlin, - "jni::objects::JObject", - InlineFn::env_ref_mut(decoder), - ) -} - -fn wrapped_return(rust_type: &str, kotlin: &str, jni_type: &str, wrapper: &str, default: &str) -> TypeBinding { - TypeBinding::returns( - rust_type, - kotlin, - jni_type, - ReturnEncode::wrapper(wrapper), - default, - ) -} - /// Type vocabulary shared across every JNI surface generated in this crate. /// Defined once, threaded into the struct-phase converter, then forwarded — /// together with the auto-registered struct bindings — into the methods phase. @@ -50,33 +26,67 @@ fn shared_bindings() -> JniTypeBinding { InlineFn::env_ref("crate::utils::decode_byte_array"), )) // Callbacks. - .type_binding(jobject_param( + .type_binding(TypeBinding::param( "impl Fn(Sample) + Send + Sync + 'static", - "crate::sample_callback::process_kotlin_sample_callback", "io.zenoh.jni.callbacks.JNISubscriberCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_sample_callback"), )) - .type_binding(jobject_param( + .type_binding(TypeBinding::param( "impl Fn(Query) + Send + Sync + 'static", - "crate::sample_callback::process_kotlin_query_callback", "io.zenoh.jni.callbacks.JNIQueryableCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_query_callback"), )) - .type_binding(jobject_param( + .type_binding(TypeBinding::param( "impl Fn(Reply) + Send + Sync + 'static", - "crate::sample_callback::process_kotlin_reply_callback", "io.zenoh.jni.callbacks.JNIGetCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_reply_callback"), )) - .type_binding(jobject_param( + .type_binding(TypeBinding::param( "impl Fn() + Send + Sync + 'static", - "crate::sample_callback::process_kotlin_on_close_callback", "io.zenoh.jni.callbacks.JNIOnCloseCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_on_close_callback"), )) // Java-enum-shaped types. - .type_binding(enum_param("CongestionControl", "crate::utils::decode_congestion_control")) - .type_binding(enum_param("Priority", "crate::utils::decode_priority")) - .type_binding(enum_param("Reliability", "crate::utils::decode_reliability")) - .type_binding(enum_param("QueryTarget", "crate::utils::decode_query_target")) - .type_binding(enum_param("ConsolidationMode", "crate::utils::decode_consolidation")) - .type_binding(enum_param("ReplyKeyExpr", "crate::utils::decode_reply_key_expr")) + .type_binding(TypeBinding::param( + "CongestionControl", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_congestion_control"), + ).enum_field_decoder("crate::utils::decode_congestion_control")) + .type_binding(TypeBinding::param( + "Priority", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_priority"), + ).enum_field_decoder("crate::utils::decode_priority")) + .type_binding(TypeBinding::param( + "Reliability", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_reliability"), + ).enum_field_decoder("crate::utils::decode_reliability")) + .type_binding(TypeBinding::param( + "QueryTarget", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_query_target"), + ).enum_field_decoder("crate::utils::decode_query_target")) + .type_binding(TypeBinding::param( + "ConsolidationMode", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_consolidation"), + ).enum_field_decoder("crate::utils::decode_consolidation")) + .type_binding(TypeBinding::param( + "ReplyKeyExpr", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_reply_key_expr"), + ).enum_field_decoder("crate::utils::decode_reply_key_expr")) // KeyExpr by-value: JNI side passes `Arc::into_raw(Arc::new(KeyExpr))` // as a raw pointer; the wrapper reconstructs the Arc, clones the inner // KeyExpr, and drops the Arc at end of scope. The full path is required @@ -88,10 +98,11 @@ fn shared_bindings() -> JniTypeBinding { InlineFn::new(|input| quote! { (*std::sync::Arc::from_raw(#input)).clone() }), )) // Encoding via JObject + custom decoder. - .type_binding(jobject_param( + .type_binding(TypeBinding::param( "Encoding", - "crate::utils::decode_jni_encoding", "io.zenoh.jni.JNIEncoding", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::utils::decode_jni_encoding"), )) // Borrows: opaque Arc handles received as `*const T` and re-borrowed // via OwnedObject::from_raw. The `&` prefix on the row's key tells @@ -99,18 +110,18 @@ fn shared_bindings() -> JniTypeBinding { .type_binding(TypeBinding::opaque_borrow("Session", OWNED_OBJECT)) .type_binding(TypeBinding::opaque_borrow("Config", OWNED_OBJECT)) // Returns: ZenohId / Vec via custom encoders. - .type_binding(wrapped_return( + .type_binding(TypeBinding::returns( "ZResult", "ByteArray", "jni::sys::jbyteArray", - "crate::zenoh_id::zenoh_id_to_byte_array", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_id_to_byte_array"), "jni::objects::JByteArray::default().as_raw()", )) - .type_binding(wrapped_return( + .type_binding(TypeBinding::returns( "ZResult>", "List", "jni::sys::jobject", - "crate::zenoh_id::zenoh_ids_to_java_list", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_ids_to_java_list"), "jni::objects::JObject::default().as_raw()", )) // Returns: opaque Arc handles. Each emits `*const T` and From a9b5eb509ceedd840dd15b7797aec27293fb88c9 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 16:20:14 +0200 Subject: [PATCH 124/126] decode_field removed --- zenoh-flat/src/jni_converter.rs | 133 +++++++++++++---------------- zenoh-flat/src/jni_type_binding.rs | 42 ++++----- zenoh-jni/build.rs | 12 +-- 3 files changed, 85 insertions(+), 102 deletions(-) diff --git a/zenoh-flat/src/jni_converter.rs b/zenoh-flat/src/jni_converter.rs index 64b6e07d..e59e9cd4 100644 --- a/zenoh-flat/src/jni_converter.rs +++ b/zenoh-flat/src/jni_converter.rs @@ -54,7 +54,7 @@ impl Default for StructBuilder { Self { source_module: syn::parse_str("crate").unwrap(), zresult: syn::parse_str("ZResult").unwrap(), - types: JniTypeBinding::new(), + types: JniTypeBinding::new().with_builtins(), } } } @@ -180,53 +180,42 @@ impl JniStructConverter { let kotlin_fname = snake_to_camel(&fname); let err_prefix = format!("{struct_name}.{kotlin_fname}: {{}}"); - let kind = self.classify_struct_field(&field.ty); - match kind { - StructFieldKind::Bool => { - field_preludes.push(quote! { - let #fname_ident = env.get_field(obj, #kotlin_fname, "Z") - .and_then(|v| v.z()) - .map_err(|err| zerror!(#err_prefix, err))?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines.push(format!(" val {}: Boolean,", kotlin_fname)); - } - StructFieldKind::I64 => { - field_preludes.push(quote! { - let #fname_ident = env.get_field(obj, #kotlin_fname, "J") - .and_then(|v| v.j()) - .map_err(|err| zerror!(#err_prefix, err))?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines.push(format!(" val {}: Long,", kotlin_fname)); - } - StructFieldKind::F64 => { - field_preludes.push(quote! { - let #fname_ident = env.get_field(obj, #kotlin_fname, "D") - .and_then(|v| v.d()) - .map_err(|err| zerror!(#err_prefix, err))?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines.push(format!(" val {}: Double,", kotlin_fname)); - } - StructFieldKind::Enum(decoder) => { - let raw_ident = format_ident!("__{}_raw", fname_ident); - field_preludes.push(quote! { - let #raw_ident = env.get_field(obj, #kotlin_fname, "I") - .and_then(|v| v.i()) - .map_err(|err| zerror!(#err_prefix, err))?; - let #fname_ident = #decoder(#raw_ident)?; - }); - field_init.push(quote! { #fname_ident }); - kotlin_field_lines.push(format!(" val {}: Int,", kotlin_fname)); - } - StructFieldKind::Unsupported => panic!( + let binding = self.lookup_struct_field_binding(&field.ty).unwrap_or_else(|| { + panic!( "unsupported field type `{}` for `{}.{}` at {loc}", field.ty.to_token_stream(), struct_name, fname - ), - } + ) + }); + let (jni_sig, jvalue_method) = + jni_primitive_signature(binding.jni_type()).unwrap_or_else(|| { + panic!( + "field `{}.{}` at {loc}: type `{}` has non-primitive JNI wire form `{}`", + struct_name, + fname, + field.ty.to_token_stream(), + binding.jni_type().to_token_stream() + ) + }); + let raw_ident = format_ident!("__{}_raw", fname_ident); + let jni_type = binding.jni_type(); + let decode_expr = binding + .decode() + .expect("struct-field binding must have a decode") + .call(&raw_ident); + field_preludes.push(quote! { + let #raw_ident: #jni_type = env.get_field(obj, #kotlin_fname, #jni_sig) + .and_then(|v| v.#jvalue_method()) + .map_err(|err| zerror!(#err_prefix, err))? as _; + let #fname_ident = #decode_expr; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!( + " val {}: {},", + kotlin_fname, + binding.kotlin_type() + )); } let tokens = quote! { @@ -261,28 +250,15 @@ impl JniStructConverter { syn::parse2(tokens).expect("generated struct decoder must parse") } - /// Classify a `#[prebindgen]` struct field's type for JNI round-tripping. - fn classify_struct_field(&self, ty: &syn::Type) -> StructFieldKind { - let syn::Type::Path(tp) = ty else { - return StructFieldKind::Unsupported; - }; - let Some(last) = tp.path.segments.last() else { - return StructFieldKind::Unsupported; - }; + /// Look up a `#[prebindgen]` struct field's type in the registry. Fields + /// must use the type's bare path-tail name (e.g. `bool`, `i64`, + /// `CongestionControl`) and must resolve to a registered binding whose + /// JNI wire form is one of the primitive `j*` types. + fn lookup_struct_field_binding(&self, ty: &syn::Type) -> Option<&TypeBinding> { + let syn::Type::Path(tp) = ty else { return None }; + let last = tp.path.segments.last()?; let name = last.ident.to_string(); - match name.as_str() { - "bool" => StructFieldKind::Bool, - "i64" => StructFieldKind::I64, - "f64" => StructFieldKind::F64, - _ => { - if let Some(binding) = self.cfg.types.types.get(&name) { - if let Some(p) = binding.enum_field_decoder_path() { - return StructFieldKind::Enum(p.clone()); - } - } - StructFieldKind::Unsupported - } - } + self.cfg.types.types.get(&name) } } @@ -800,13 +776,26 @@ impl JniMethodsConverter { // Internal helpers // ===================================================================== -/// Field-type classification for `#[prebindgen]` struct fields. -enum StructFieldKind { - Bool, - I64, - F64, - Enum(syn::Path), - Unsupported, +/// Map a primitive JNI wire type (`jni::sys::j*`) to the JVM field +/// signature character and the matching `JValue` accessor method. +/// Returns `None` for non-primitive (object-shaped) wire types. +fn jni_primitive_signature(jni_type: &syn::Type) -> Option<(&'static str, syn::Ident)> { + let syn::Type::Path(tp) = jni_type else { + return None; + }; + let last = tp.path.segments.last()?; + let (sig, accessor) = match last.ident.to_string().as_str() { + "jboolean" => ("Z", "z"), + "jbyte" => ("B", "b"), + "jchar" => ("C", "c"), + "jshort" => ("S", "s"), + "jint" => ("I", "i"), + "jlong" => ("J", "j"), + "jfloat" => ("F", "f"), + "jdouble" => ("D", "d"), + _ => return None, + }; + Some((sig, format_ident!("{}", accessor))) } fn is_unit(ty: &syn::Type) -> bool { diff --git a/zenoh-flat/src/jni_type_binding.rs b/zenoh-flat/src/jni_type_binding.rs index dfe213c4..031b5a21 100644 --- a/zenoh-flat/src/jni_type_binding.rs +++ b/zenoh-flat/src/jni_type_binding.rs @@ -11,8 +11,6 @@ //! * `jni_type` — the on-the-wire JNI type emitted in the wrapper signature //! * `decode` — JNI value → Rust value (param-direction rows) //! * `encode` + `default_expr` — Rust value → JNI value (return-direction rows) -//! * `enum_field_decoder` — only for enum-shaped rows, used by struct-field -//! classification //! //! Wrapper types (`&T`, `Vec`, `Option`, `ZResult`) are **not** //! decomposed by the classifier — each must have its own explicit row. The @@ -108,10 +106,6 @@ pub struct TypeBinding { /// Default JNI value emitted on the throw-return path. Required when /// `encode` is set. pub(crate) default_expr: Option, - /// Decoder path for Java-enum-shaped types (`fn(jint) -> ZResult`). - /// Used by struct-field classification to detect enum fields and emit - /// `env.get_field(..., "I")` + decoder call. - pub(crate) enum_field_decoder: Option, } impl TypeBinding { @@ -129,7 +123,6 @@ impl TypeBinding { decode: Some(decode), encode: None, default_expr: None, - enum_field_decoder: None, } } @@ -151,20 +144,9 @@ impl TypeBinding { syn::parse_str(default_expr.as_ref()) .expect("invalid TypeBinding::returns default_expr"), ), - enum_field_decoder: None, } } - /// Mark this binding as a Java-enum-shaped type. Used by struct-field - /// classification to emit `env.get_field(..., "I")` + `(raw)?`. - pub fn enum_field_decoder(mut self, path: impl AsRef) -> Self { - self.enum_field_decoder = Some( - syn::parse_str(path.as_ref()) - .expect("invalid TypeBinding::enum_field_decoder path"), - ); - self - } - /// Convenience: opaque borrow `&T` — JNI side passes raw `*const T`, /// decoded via `::from_raw`. Because the row's key starts /// with `&`, the wrapped fn receives `&name` automatically. @@ -228,7 +210,6 @@ impl TypeBinding { })), encode: None, default_expr: None, - enum_field_decoder: None, } } @@ -252,9 +233,6 @@ impl TypeBinding { pub(crate) fn default_expr(&self) -> Option<&syn::Expr> { self.default_expr.as_ref() } - pub(crate) fn enum_field_decoder_path(&self) -> Option<&syn::Path> { - self.enum_field_decoder.as_ref() - } /// `&T` row — wrapped fn receives `&name`. pub(crate) fn is_borrow(&self) -> bool { self.rust_type.starts_with('&') @@ -306,8 +284,8 @@ impl JniTypeBinding { } /// Pre-register built-in language types whose JNI form is fully described - /// without any project-specific decoder path: `bool` (inline `x != 0`) - /// and `Duration` (inline `Duration::from_millis(x as u64)`). + /// without any project-specific decoder path: `bool`, `i64`, `f64`, and + /// `Duration`. pub fn with_builtins(mut self) -> Self { let bool_row = TypeBinding::param( "bool", @@ -317,6 +295,22 @@ impl JniTypeBinding { ); self.types.insert(bool_row.rust_type.clone(), bool_row); + let i64_row = TypeBinding::param( + "i64", + "Long", + "jni::sys::jlong", + InlineFn::new(|input| quote! { #input }), + ); + self.types.insert(i64_row.rust_type.clone(), i64_row); + + let f64_row = TypeBinding::param( + "f64", + "Double", + "jni::sys::jdouble", + InlineFn::new(|input| quote! { #input }), + ); + self.types.insert(f64_row.rust_type.clone(), f64_row); + let duration_row = TypeBinding::param( "Duration", "Long", diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index f9a55013..c6455c95 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -56,37 +56,37 @@ fn shared_bindings() -> JniTypeBinding { "Int", "jni::sys::jint", InlineFn::pure("crate::utils::decode_congestion_control"), - ).enum_field_decoder("crate::utils::decode_congestion_control")) + )) .type_binding(TypeBinding::param( "Priority", "Int", "jni::sys::jint", InlineFn::pure("crate::utils::decode_priority"), - ).enum_field_decoder("crate::utils::decode_priority")) + )) .type_binding(TypeBinding::param( "Reliability", "Int", "jni::sys::jint", InlineFn::pure("crate::utils::decode_reliability"), - ).enum_field_decoder("crate::utils::decode_reliability")) + )) .type_binding(TypeBinding::param( "QueryTarget", "Int", "jni::sys::jint", InlineFn::pure("crate::utils::decode_query_target"), - ).enum_field_decoder("crate::utils::decode_query_target")) + )) .type_binding(TypeBinding::param( "ConsolidationMode", "Int", "jni::sys::jint", InlineFn::pure("crate::utils::decode_consolidation"), - ).enum_field_decoder("crate::utils::decode_consolidation")) + )) .type_binding(TypeBinding::param( "ReplyKeyExpr", "Int", "jni::sys::jint", InlineFn::pure("crate::utils::decode_reply_key_expr"), - ).enum_field_decoder("crate::utils::decode_reply_key_expr")) + )) // KeyExpr by-value: JNI side passes `Arc::into_raw(Arc::new(KeyExpr))` // as a raw pointer; the wrapper reconstructs the Arc, clones the inner // KeyExpr, and drops the Arc at end of scope. The full path is required From 7c7bc04754b08a7510cf3ca6a6fae634a6e31eff Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 26 Apr 2026 23:44:34 +0200 Subject: [PATCH 125/126] jni converter moved to separate crate --- prebindgen-ext/Cargo.lock | 352 ++++++++++++++++++ prebindgen-ext/Cargo.toml | 28 ++ .../src/jni_converter.rs | 0 .../src/jni_type_binding.rs | 0 prebindgen-ext/src/lib.rs | 9 + zenoh-flat/Cargo.lock | 16 +- zenoh-flat/Cargo.toml | 5 +- zenoh-flat/src/lib.rs | 4 +- zenoh-jni/Cargo.lock | 16 +- 9 files changed, 416 insertions(+), 14 deletions(-) create mode 100644 prebindgen-ext/Cargo.lock create mode 100644 prebindgen-ext/Cargo.toml rename {zenoh-flat => prebindgen-ext}/src/jni_converter.rs (100%) rename {zenoh-flat => prebindgen-ext}/src/jni_type_binding.rs (100%) create mode 100644 prebindgen-ext/src/lib.rs diff --git a/prebindgen-ext/Cargo.lock b/prebindgen-ext/Cargo.lock new file mode 100644 index 00000000..0821f335 --- /dev/null +++ b/prebindgen-ext/Cargo.lock @@ -0,0 +1,352 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools 0.14.0", + "konst", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn", + "toml", +] + +[[package]] +name = "prebindgen-ext" +version = "1.9.0" +dependencies = [ + "itertools 0.12.1", + "prebindgen", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/prebindgen-ext/Cargo.toml b/prebindgen-ext/Cargo.toml new file mode 100644 index 00000000..8af82cba --- /dev/null +++ b/prebindgen-ext/Cargo.toml @@ -0,0 +1,28 @@ +# +# 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] +name = "prebindgen-ext" +version = "1.9.0" +edition = "2021" +license = "EPL-2.0 OR Apache-2.0" +description = "Prebindgen JNI extensions for Zenoh." +repository = "https://github.com/eclipse-zenoh/zenoh" + +[dependencies] +syn = { version = "2", features = ["full", "extra-traits"] } +quote = "1" +proc-macro2 = "1" +prebindgen = "0.4.1" +itertools = "0.12" diff --git a/zenoh-flat/src/jni_converter.rs b/prebindgen-ext/src/jni_converter.rs similarity index 100% rename from zenoh-flat/src/jni_converter.rs rename to prebindgen-ext/src/jni_converter.rs diff --git a/zenoh-flat/src/jni_type_binding.rs b/prebindgen-ext/src/jni_type_binding.rs similarity index 100% rename from zenoh-flat/src/jni_type_binding.rs rename to prebindgen-ext/src/jni_type_binding.rs diff --git a/prebindgen-ext/src/lib.rs b/prebindgen-ext/src/lib.rs new file mode 100644 index 00000000..c28323a8 --- /dev/null +++ b/prebindgen-ext/src/lib.rs @@ -0,0 +1,9 @@ +//! Prebindgen JNI extensions for Zenoh. +//! +//! This crate provides JNI binding generators for items marked with `#[prebindgen]`. + +pub mod jni_converter; +pub mod jni_type_binding; + +pub use jni_converter::{JniStructConverter, JniMethodsConverter}; +pub use jni_type_binding::{JniTypeBinding, TypeBinding, InlineFn, ReturnEncode}; diff --git a/zenoh-flat/Cargo.lock b/zenoh-flat/Cargo.lock index 400db158..c8945b8d 100644 --- a/zenoh-flat/Cargo.lock +++ b/zenoh-flat/Cargo.lock @@ -1222,6 +1222,17 @@ dependencies = [ "toml", ] +[[package]] +name = "prebindgen-ext" +version = "1.9.0" +dependencies = [ + "itertools 0.12.1", + "prebindgen", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "prebindgen-proc-macro" version = "0.4.1" @@ -2573,14 +2584,11 @@ dependencies = [ name = "zenoh-flat" version = "1.9.0" dependencies = [ - "itertools 0.12.1", "json5", "prebindgen", + "prebindgen-ext", "prebindgen-proc-macro", - "proc-macro2", - "quote", "serde_yaml", - "syn 2.0.117", "tracing", "zenoh", "zenoh-ext", diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml index 1b427055..e247a48d 100644 --- a/zenoh-flat/Cargo.toml +++ b/zenoh-flat/Cargo.toml @@ -19,10 +19,7 @@ serde_yaml = "0.9.19" tracing = "0.1" prebindgen-proc-macro = "0.4.1" prebindgen = "0.4.1" -syn = { version = "2", features = ["full", "extra-traits"] } -quote = "1" -proc-macro2 = "1" -itertools = "0.12" +prebindgen-ext = { path = "../prebindgen-ext" } [build-dependencies] prebindgen = "0.4.1" diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs index 3cdbcf9e..1c0d5ca4 100644 --- a/zenoh-flat/src/lib.rs +++ b/zenoh-flat/src/lib.rs @@ -7,7 +7,7 @@ pub mod config; pub mod errors; #[cfg(feature = "zenoh-ext")] pub mod ext; -pub mod jni_converter; -pub mod jni_type_binding; pub mod session; +pub use prebindgen_ext::{jni_converter, jni_type_binding}; + diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index b3796fb4..ccbe3ee5 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -1861,6 +1861,17 @@ dependencies = [ "toml", ] +[[package]] +name = "prebindgen-ext" +version = "1.9.0" +dependencies = [ + "itertools 0.12.1", + "prebindgen", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "prebindgen-proc-macro" version = "0.4.1" @@ -4058,14 +4069,11 @@ dependencies = [ name = "zenoh-flat" version = "1.9.0" dependencies = [ - "itertools 0.12.1", "json5", "prebindgen", + "prebindgen-ext", "prebindgen-proc-macro", - "proc-macro2", - "quote", "serde_yaml", - "syn 2.0.117", "tracing", "zenoh", "zenoh-ext", From 69f76611aed75c6c10bd1b725f1e10974af78028 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 27 Apr 2026 00:29:34 +0200 Subject: [PATCH 126/126] keyexpr type fix --- .../commonMain/kotlin/io/zenoh/jni/JNISession.kt | 2 +- zenoh-jni/build.rs | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 3cd575f1..bac21ad5 100644 --- a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -96,7 +96,7 @@ public class JNISession(internal val sessionPtr: Long) { fun declareKeyExpr(keyExpr: String): JNIKeyExpr = JNIKeyExpr(declareKeyExprViaJNI(sessionPtr, keyExpr), keyExpr) @Throws(ZError::class) - fun undeclareKeyExpr(jniKeyExpr: JNIKeyExpr) = undeclareKeyExprViaJNI(sessionPtr, jniKeyExpr.ptr) + fun undeclareKeyExpr(jniKeyExpr: JNIKeyExpr) = undeclareKeyExprViaJNI(sessionPtr, jniKeyExpr) @Throws(ZError::class) fun get( diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs index c6455c95..9214a6ca 100644 --- a/zenoh-jni/build.rs +++ b/zenoh-jni/build.rs @@ -1,5 +1,4 @@ use itertools::Itertools; -use quote::quote; use zenoh_flat::jni_converter::{ InlineFn, JniMethodsConverter, JniStructConverter, TypeBinding, }; @@ -87,15 +86,13 @@ fn shared_bindings() -> JniTypeBinding { "jni::sys::jint", InlineFn::pure("crate::utils::decode_reply_key_expr"), )) - // KeyExpr by-value: JNI side passes `Arc::into_raw(Arc::new(KeyExpr))` - // as a raw pointer; the wrapper reconstructs the Arc, clones the inner - // KeyExpr, and drops the Arc at end of scope. The full path is required - // so the generated `*const T` parameter type resolves at the include site. + // KeyExpr by-value: JNI side passes the JNIKeyExpr holder object + // (`ptr: Long`, `str: String`), decoded through decode_jni_key_expr. .type_binding(TypeBinding::param( "KeyExpr<'static>", - "Long", - "*const zenoh::key_expr::KeyExpr<'static>", - InlineFn::new(|input| quote! { (*std::sync::Arc::from_raw(#input)).clone() }), + "io.zenoh.jni.JNIKeyExpr", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::key_expr::decode_jni_key_expr"), )) // Encoding via JObject + custom decoder. .type_binding(TypeBinding::param(