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. 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 00000000..a4b76b95 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ 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 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..dfb36284 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") @@ -30,13 +28,11 @@ 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") - apply(plugin = "org.mozilla.rust-android-gradle.rust-android") - configureCargo() configureAndroid() } @@ -66,34 +62,35 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + api(project(":zenoh-jni-runtime")) implementation("commons-net:commons-net:3.9.0") implementation("com.google.guava:guava:33.3.1-jre") } } + // 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. + // 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(jvmAndAndroidMain) } val androidUnitTest by getting { dependencies { implementation(kotlin("test-junit")) } } } - 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 { dependsOn(jvmAndAndroidMain) } - 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) { @@ -161,55 +158,8 @@ tasks.withType { } } -tasks.whenObjectAdded { - if ((this.name == "mergeDebugJniLibFolders" || this.name == "mergeReleaseJniLibFolders")) { - this.dependsOn("cargoBuild") - } -} - 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() { @@ -249,20 +199,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", - ) - } -} diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Config.kt index 91bc0e01..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 JNIConfig.loadDefaultConfig() + 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 JNIConfig.loadConfigFile(file) + 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 JNIConfig.loadConfigFile(path) + 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 JNIConfig.loadJsonConfig(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 JNIConfig.loadJson5Config(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 JNIConfig.loadYamlConfig(config) + return Config(JNIConfig.loadFromYaml(config)) } /** diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Logger.kt index eba6681c..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,11 +24,6 @@ internal class Logger { internal const val LOG_ENV: String = "RUST_LOG" - @Throws(ZError::class) - fun start(filter: String) { - startLogsViaJNI(filter) - } - /** * Redirects the rust logs either to logcat for Android systems or to the standard output (for non-android * systems). @@ -35,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-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index d3b125f9..e60e6b4f 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,9 @@ 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 ke = KeyExpr(keyExpr, declareKeyExpr(keyExpr)) + strongDeclarations.add(ke) + ke } ?: throw sessionClosedException } @@ -400,8 +417,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 { + undeclareKeyExpr(this) + keyExpr.jniKeyExpr = null + } ?: throw ZError("Attempting to undeclare a non declared key expression.") } ?: throw (sessionClosedException) } @@ -576,7 +596,20 @@ 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 publisher = Publisher( + keyExpr, + options.congestionControl, + options.priority, + options.encoding, + declarePublisher( + keyExpr.jniKeyExpr, + keyExpr.keyExpr, + options.congestionControl.value, + options.priority.value, + options.express, + options.reliability.ordinal + ) + ) weakDeclarations.add(WeakReference(publisher)) publisher } ?: throw (sessionClosedException) @@ -587,7 +620,22 @@ 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 subscriber = HandlerSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, handler::onClose), handler.receiver()) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -598,7 +646,22 @@ 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 subscriber = CallbackSubscriber(keyExpr, declareSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, fun() {})) strongDeclarations.add(subscriber) subscriber } ?: throw (sessionClosedException) @@ -609,7 +672,24 @@ 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 queryable = HandlerQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr, keyExpr.keyExpr, queryCallback, handler::onClose, options.complete), handler.receiver()) strongDeclarations.add(queryable) queryable } ?: throw (sessionClosedException) @@ -620,18 +700,50 @@ 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 queryable = CallbackQueryable(keyExpr, declareQueryable(keyExpr.jniKeyExpr, keyExpr.keyExpr, queryCallback, fun() {}, options.complete)) 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 querier = Querier( + keyExpr, + QoS(congestionControl = options.congestionControl, priority = options.priority, express = options.express), + declareQuerier( + keyExpr.jniKeyExpr, + 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 } ?: throw sessionClosedException @@ -643,11 +755,48 @@ 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() + get( + sel.keyExpr.jniKeyExpr, + sel.keyExpr.keyExpr, + sel.parameters?.toString(), + 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 +805,102 @@ 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() + get( + sel.keyExpr.jniKeyExpr, + sel.keyExpr.keyExpr, + sel.parameters?.toString(), + 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() + put( + keyExpr.jniKeyExpr, + keyExpr.keyExpr, + 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 { + delete( + keyExpr.jniKeyExpr, + keyExpr.keyExpr, + 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(getZid()) } ?: throw sessionClosedException } @Throws(ZError::class) internal fun getPeersId(): List { - return jniSession?.peersZid() ?: throw sessionClosedException + return jniSession?.run { getPeersZid().map { ZenohId(it) } } ?: throw sessionClosedException } @Throws(ZError::class) internal fun getRoutersId(): List { - return jniSession?.routersZid() ?: throw sessionClosedException + return jniSession?.run { getRoutersZid().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) 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..e0c60c6c 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,11 @@ 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 } + return HandlerScout(JNIScout.scout(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig), handler.receiver()) } /** @@ -74,10 +78,11 @@ 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 } + return HandlerScout(JNIScout.scout(binaryWhatAmI, scoutCallback, handler::onClose, scoutOptions.config?.jniConfig), handler.receiver()) } /** @@ -94,9 +99,11 @@ 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 } + return CallbackScout(JNIScout.scout(binaryWhatAmI, scoutCallback, fun() {}, scoutOptions.config?.jniConfig)) } /** @@ -136,9 +143,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/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/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 096a6c59..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ /dev/null @@ -1,548 +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) - } - - @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..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 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, 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.includes(this, other) + 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 JNIKeyExpr.relationTo(this, other) + 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 JNIKeyExpr.joinViaJNI(this, 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 JNIKeyExpr.concatViaJNI(this, 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 96711557..62aff6c5 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,27 @@ 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.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 +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 JNILiveliness.declareToken(jniSession, keyExpr) + return LivelinessToken(jniSession.declareLivelinessToken(keyExpr.jniKeyExpr, keyExpr.keyExpr)) } /** @@ -66,14 +76,15 @@ 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) + jniSession.livelinessGet( + keyExpr.jniKeyExpr, + keyExpr.keyExpr, + getCallback, + timeout.toMillis(), + handler::onClose ) + return handler.receiver() } /** @@ -89,7 +100,13 @@ 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, {}) + jniSession.livelinessGet( + keyExpr.jniKeyExpr, + keyExpr.keyExpr, + buildGetCallback(callback), + timeout.toMillis(), + fun() {} + ) } /** @@ -106,17 +123,38 @@ 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 + jniSession.livelinessGet( + keyExpr.jniKeyExpr, + 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 +169,8 @@ 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(), - options.history, - handler::onClose - ) + val subCallback = buildSubscriberCallback(handler::handle) + return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, options.history, handler::onClose), handler.receiver()) } /** @@ -156,13 +188,8 @@ 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, - options.history, - fun() {} - ) + val subCallback = buildSubscriberCallback(callback) + return CallbackSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, options.history, fun() {})) } /** @@ -181,15 +208,25 @@ 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(), - options.history, - handler::onClose - ) + val subCallback = buildSubscriberCallback(handler::handle) + return HandlerSubscriber(keyExpr, jniSession.declareLivelinessSubscriber(keyExpr.jniKeyExpr, keyExpr.keyExpr, subCallback, options.history, handler::onClose), 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..faca37a8 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,74 @@ 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.get( + keyExpr.jniKeyExpr, + 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.get( + keyExpr.jniKeyExpr, + 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..c2128efa 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, + 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, + 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-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt b/zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZDeserializer.kt similarity index 98% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt rename to zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZDeserializer.kt index 1d039cbe..fadbac89 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZDeserializer.kt +++ b/zenoh-java/src/jvmAndAndroidMain/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().bytes, this.type) as T } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt b/zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZSerializer.kt similarity index 98% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt rename to zenoh-java/src/jvmAndAndroidMain/kotlin/io/zenoh/ext/ZSerializer.kt index 8ee4a1b7..5f47b7eb 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/ext/ZSerializer.kt +++ b/zenoh-java/src/jvmAndAndroidMain/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 ZBytes(JNIZBytes.serialize(t as Any, this.type)) } } diff --git a/zenoh-jni-runtime/build.gradle.kts b/zenoh-jni-runtime/build.gradle.kts new file mode 100644 index 00000000..5e6d4def --- /dev/null +++ b/zenoh-jni-runtime/build.gradle.kts @@ -0,0 +1,252 @@ +// +// 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 {} + // 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. + // 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(jvmAndAndroidMain) } + val androidUnitTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + } + val jvmMain by getting { + dependsOn(jvmAndAndroidMain) + 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")) + dependencies { + implementation(kotlin("test-junit")) + } + } + } + + 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-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 @@ + + 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-java/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/ZenohLoad.kt similarity index 65% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/ZenohLoad.kt index 53ecb5dc..a2d21403 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt +++ b/zenoh-jni-runtime/src/commonMain/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 @@ -12,16 +12,9 @@ // ZettaScale Zenoh Team, // -package io.zenoh.jni +package io.zenoh -import io.zenoh.ZenohLoad - -internal object JNIZenohId { - - init { - ZenohLoad - } - - external fun toStringViaJNI(bytes: ByteArray): String - -} +/** + * Static singleton object to load the Zenoh native library once and only once. + */ +public expect object ZenohLoad diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt similarity index 92% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt rename to zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt index c63a19ce..1984bd23 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/exceptions/ZError.kt +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/exceptions/ZError.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 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..62d822f6 --- /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(private val ptr: Long) { + + @Throws(ZError::class) + fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { + putViaJNI(ptr, payload, encodingId, encodingSchema, attachment) + } + + @Throws(ZError::class) + fun delete(attachment: ByteArray?) { + deleteViaJNI(ptr, attachment) + } + + @Throws(ZError::class) + fun declareMatchingListener(callback: JNIMatchingListenerCallback, onClose: JNIOnCloseCallback): JNIMatchingListener = + JNIMatchingListener(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( + ptr: Long, payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray? + ) + + @Throws(ZError::class) + private external fun deleteViaJNI(ptr: Long, attachment: ByteArray?) + + @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..084b00f9 --- /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(private val ptr: Long) { + + @Throws(ZError::class) + fun declareDetectPublishersSubscriber( + history: Boolean, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(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, + ): JNISampleMissListener = JNISampleMissListener(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 56% 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..9cdd403c 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. */ +public class JNIConfig(internal val ptr: Long) { companion object { @@ -28,37 +26,17 @@ 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 loadDefault(): JNIConfig = JNIConfig(loadDefaultConfigViaJNI()) @Throws(ZError::class) - fun loadConfigFile(file: File): Config = loadConfigFile(file.toPath()) + fun loadFromFile(path: String): JNIConfig = JNIConfig(loadConfigFileViaJNI(path)) @Throws(ZError::class) - fun loadJsonConfig(rawConfig: String): Config { - val cfgPtr = loadJsonConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) - } + fun loadFromJson(rawConfig: String): JNIConfig = JNIConfig(loadJsonConfigViaJNI(rawConfig)) @Throws(ZError::class) - fun loadJson5Config(rawConfig: String): Config { - val cfgPtr = loadJsonConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) - } - - @Throws(ZError::class) - fun loadYamlConfig(rawConfig: String): Config { - val cfgPtr = loadYamlConfigViaJNI(rawConfig) - return Config(JNIConfig(cfgPtr)) - } + fun loadFromYaml(rawConfig: String): JNIConfig = JNIConfig(loadYamlConfigViaJNI(rawConfig)) @Throws(ZError::class) private external fun loadDefaultConfigViaJNI(): Long @@ -78,7 +56,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) @@ -90,12 +67,13 @@ internal class JNIConfig(internal 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-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt similarity index 52% 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..c13395c2 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. */ +public class JNIKeyExpr(internal val ptr: Long) { companion object { init { @@ -27,57 +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 = tryFromViaJNI(keyExpr) @Throws(ZError::class) - fun autocanonize(keyExpr: String): KeyExpr { - return KeyExpr(autocanonizeViaJNI(keyExpr)) - } + fun autocanonize(keyExpr: String): String = 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 - ) + private external fun tryFromViaJNI(keyExpr: String): String @Throws(ZError::class) - fun includes(keyExprA: KeyExpr, keyExprB: KeyExpr): Boolean = includesViaJNI( - keyExprA.jniKeyExpr?.ptr ?: 0, - keyExprA.keyExpr, - keyExprB.jniKeyExpr?.ptr ?: 0, - keyExprB.keyExpr - ) + private external fun autocanonizeViaJNI(keyExpr: String): String @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 intersects(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Boolean = + intersectsViaJNI(a?.ptr ?: 0, aStr, b?.ptr ?: 0, bStr) @Throws(ZError::class) - fun joinViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(joinViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) - } + 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) - fun concatViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(concatViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) - } + fun relationTo(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Int = + relationToViaJNI(a?.ptr ?: 0, aStr, b?.ptr ?: 0, bStr) @Throws(ZError::class) - private external fun tryFromViaJNI(keyExpr: String): String + fun join(a: JNIKeyExpr?, aStr: String, other: String): String = + joinViaJNI(a?.ptr ?: 0, aStr, other) @Throws(ZError::class) - private external fun autocanonizeViaJNI(keyExpr: 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 @@ -99,6 +78,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/JNILivelinessToken.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.kt new file mode 100644 index 00000000..1f88d3ad --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNILivelinessToken.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 LivelinessToken. */ +public class JNILivelinessToken(private val ptr: Long) { + + fun undeclare() { + undeclareViaJNI(ptr) + } + + private external fun undeclareViaJNI(ptr: Long) +} 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-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..7c58edee --- /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(private 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..7f800597 --- /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(private val ptr: Long) { + + @Throws(ZError::class) + fun put(payload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?) { + putViaJNI(ptr, payload, encodingId, encodingSchema, attachment) + } + + @Throws(ZError::class) + fun delete(attachment: ByteArray?) { + deleteViaJNI(ptr, attachment) + } + + fun close() { + freePtrViaJNI(ptr) + } + + @Throws(ZError::class) + private external fun putViaJNI( + ptr: Long, valuePayload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray? + ) + + @Throws(ZError::class) + private external fun deleteViaJNI(ptr: Long, attachment: ByteArray?) + + 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..7757c086 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -0,0 +1,58 @@ +// +// 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(private val ptr: Long) { + + @Throws(ZError::class) + fun get( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + parameters: String?, + callback: JNIGetCallback, + onClose: JNIOnCloseCallback, + attachmentBytes: ByteArray?, + payload: ByteArray?, + encodingId: Int, + encodingSchema: String?, + ) { + getViaJNI(ptr, jniKeyExpr?.ptr ?: 0, keyExprString, parameters, callback, onClose, attachmentBytes, payload, encodingId, encodingSchema) + } + + @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) + + 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..78bd5dc3 --- /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( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + payload: ByteArray, + encodingId: Int, + encodingSchema: String?, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) { + replySuccessViaJNI(ptr, jniKeyExpr?.ptr ?: 0, 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( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) { + replyDeleteViaJNI(ptr, jniKeyExpr?.ptr ?: 0, 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..ba64f7e9 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(private 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..dea27edf --- /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(private 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..1b3d1bd0 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIScout.kt @@ -0,0 +1,56 @@ +// +// 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) + 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, + configPtr: Long, + ): Long + + private 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..688b3eb0 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -0,0 +1,375 @@ +// +// 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(internal val sessionPtr: Long) { + + companion object { + init { + ZenohLoad + } + + @Throws(ZError::class) + fun open(config: JNIConfig): JNISession { + 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?, + keyExprString: String, + congestionControl: Int, + priority: Int, + express: Boolean, + reliability: Int + ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, congestionControl, priority, express, reliability)) + + @Throws(ZError::class) + private external fun declarePublisherViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + congestionControl: Int, + priority: Int, + express: Boolean, + reliability: Int + ): Long + + @Throws(ZError::class) + fun declareSubscriber( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, onClose)) + + @Throws(ZError::class) + private external fun declareSubscriberViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): Long + + @Throws(ZError::class) + fun declareQueryable( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + callback: JNIQueryableCallback, + onClose: JNIOnCloseCallback, + complete: Boolean + ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, callback, onClose, complete)) + + @Throws(ZError::class) + private external fun declareQueryableViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + callback: JNIQueryableCallback, + onClose: JNIOnCloseCallback, + complete: Boolean + ): Long + + @Throws(ZError::class) + fun declareQuerier( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + target: Int, + consolidation: Int, + congestionControl: Int, + priority: Int, + express: Boolean, + timeoutMs: Long, + acceptReplies: Int + ): JNIQuerier = JNIQuerier(declareQuerierViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) + + @Throws(ZError::class) + private external fun declareQuerierViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + 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)) + + @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?, + 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(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( + sessionPtr: Long, + 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, + ) + + @Throws(ZError::class) + fun put( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + valuePayload: ByteArray, + valueEncoding: Int, + valueEncodingSchema: String?, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) = putViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, valuePayload, valueEncoding, valueEncodingSchema, congestionControl, priority, express, attachmentBytes, reliability) + + @Throws(ZError::class) + private external fun putViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + valuePayload: ByteArray, + valueEncoding: Int, + valueEncodingSchema: String?, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) + + @Throws(ZError::class) + fun delete( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) = deleteViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, keyExprString, congestionControl, priority, express, attachmentBytes, reliability) + + @Throws(ZError::class) + private external fun deleteViaJNI( + sessionPtr: Long, + keyExprPtr: Long, + keyExprString: String, + 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?, + 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)) + + @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, + ): Long + + @Throws(ZError::class) + fun declareAdvancedPublisher( + jniKeyExpr: JNIKeyExpr?, + 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, 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( + sessionPtr: Long, + 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, + ): Long + + @Throws(ZError::class) + 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( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + callback: JNISubscriberCallback, + history: Boolean, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(declareLivelinessSubscriberViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, 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( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + callback: JNIGetCallback, + timeoutMs: Long, + onClose: JNIOnCloseCallback, + ) = livelinessGetViaJNI(sessionPtr, jniKeyExpr?.ptr ?: 0, 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-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..b9877d70 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(private val ptr: Long) { fun close() { freePtrViaJNI(ptr) } - /** Frees the underlying native Subscriber. */ private external fun freePtrViaJNI(ptr: Long) - } 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 new file mode 100644 index 00000000..0eb1f394 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIZenohId.kt @@ -0,0 +1,29 @@ +// +// 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 + +/** Adapter object for interacting with Zenoh IDs through JNI. */ +public object JNIZenohId { + + init { + ZenohLoad + } + + fun toString(bytes: ByteArray): String = toStringViaJNI(bytes) + + private 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/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytes.kt similarity index 64% rename from zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt rename to zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytes.kt index 799b9cac..f9690eec 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt +++ b/zenoh-jni-runtime/src/jvmAndAndroidMain/kotlin/io/zenoh/jni/JNIZBytes.kt @@ -15,19 +15,21 @@ package io.zenoh.jni import io.zenoh.ZenohLoad -import io.zenoh.bytes.ZBytes import java.lang.reflect.Type -@PublishedApi -internal object JNIZBytes { +object JNIZBytes { init { ZenohLoad } + fun serialize(any: Any, type: Type): ByteArray = serializeViaJNI(any, type) + + fun deserialize(bytes: ByteArray, type: Type): Any = deserializeViaJNI(bytes, type) + @JvmStatic - external fun serializeViaJNI(any: Any, type: Type): ZBytes + private external fun serializeViaJNI(any: Any, type: Type): ByteArray @JvmStatic - external fun deserializeViaJNI(zBytes: ZBytes, type: Type): Any + private external fun deserializeViaJNI(bytes: ByteArray, type: Type): Any } 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-java/src/jvmMain/kotlin/io/zenoh/Target.kt b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/Target.kt similarity index 95% 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..6460f7ce 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 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 99% 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..aa542844 100644 --- a/zenoh-java/src/jvmMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-jni-runtime/src/jvmMain/kotlin/io/zenoh/ZenohLoad.kt @@ -24,7 +24,7 @@ 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. */ -internal actual object ZenohLoad { +public actual object ZenohLoad { private const val ZENOH_LIB_NAME = "zenoh_jni" init { 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/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/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/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs new file mode 100644 index 00000000..51500880 --- /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, + publisher_ptr: *const AdvancedPublisher<'static>, + payload: JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, + 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)?; + 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, + publisher_ptr: *const AdvancedPublisher<'static>, + attachment: /*nullable*/ JByteArray, +) { + 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/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/lib.rs b/zenoh-jni/src/lib.rs index ff3981a4..f8603617 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -14,19 +14,25 @@ mod config; mod errors; +#[cfg(feature = "zenoh-ext")] +mod ext; 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; 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/liveliness.rs b/zenoh-jni/src/liveliness.rs index 8b05c925..8acdc551 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -15,31 +15,30 @@ 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, }; #[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, @@ -49,7 +48,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,20 +97,19 @@ pub extern "C" fn Java_io_zenoh_jni_JNILiveliness_getViaJNI( .map_err(|err| { throw_exception!(env, err); }); - std::mem::forget(session); } #[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, 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,16 +122,14 @@ 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] #[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) }; @@ -141,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_JNISession_declareLivelinessSubscriberViaJNI( mut env: JNIEnv, _class: JClass, session_ptr: *const Session, @@ -151,88 +147,20 @@ 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 = 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) }?; + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; tracing::debug!("Declaring liveliness subscriber on '{}'...", key_expr); - let result = session + let subscriber = 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))?; + .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); - std::mem::forget(session); Ok(Arc::into_raw(Arc::new(subscriber))) }() .unwrap_or_else(|err| { 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, 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/publisher.rs b/zenoh-jni/src/publisher.rs index ead60c3f..e2e76339 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, @@ -50,13 +51,13 @@ 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 = 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. @@ -91,10 +91,10 @@ 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 = 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/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/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 5f0848b2..15a23a50 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}, @@ -29,6 +29,17 @@ use zenoh::{ 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, }; @@ -44,11 +55,11 @@ use crate::{ /// # 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. +/// - `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_00024Companion_openSessionViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( mut env: JNIEnv, _class: JClass, config_ptr: *const Config, @@ -69,12 +80,10 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_00024Companion_openSession /// 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. @@ -217,16 +226,16 @@ 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, 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)?; @@ -246,9 +255,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. @@ -282,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, @@ -294,7 +301,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)?; @@ -322,7 +329,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. @@ -354,16 +360,16 @@ 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, 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)?; @@ -388,7 +394,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. @@ -421,91 +426,24 @@ 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<()> { - 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)?; - 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); - std::mem::forget(session); Ok(Arc::into_raw(Arc::new(subscriber))) }() .unwrap_or_else(|err| { @@ -536,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, @@ -547,7 +485,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)?; @@ -571,7 +509,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| { @@ -613,15 +550,15 @@ 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, ) -> *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)?; @@ -657,9 +594,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<()> { @@ -776,8 +711,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()) @@ -794,9 +729,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. @@ -827,7 +760,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() { @@ -839,7 +772,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 } @@ -882,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, @@ -900,7 +832,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)?); @@ -971,7 +903,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( @@ -1130,8 +1061,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)) @@ -1139,9 +1070,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. @@ -1153,8 +1082,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)) @@ -1162,9 +1091,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. @@ -1175,8 +1102,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()) @@ -1185,9 +1112,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 { @@ -1199,3 +1124,243 @@ fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result< } Ok(array_list.as_raw()) } + +/// 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)?; + 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 { + 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() + }) +} + +/// 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 +} 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)?; diff --git a/zenoh-jni/src/zbytes_kotlin.rs b/zenoh-jni/src/zbytes_kotlin.rs new file mode 100644 index 00000000..997bd51b --- /dev/null +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -0,0 +1,576 @@ +// +// 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); + } + KotlinType::UByte => { + let v = env + .get_field(&any, "data", "B") + .map_err(|err| zerror!(err))? + .b() + .map_err(|err| zerror!(err))?; + serializer.serialize(v as u8); + } + KotlinType::UShort => { + let v = env + .get_field(&any, "data", "S") + .map_err(|err| zerror!(err))? + .s() + .map_err(|err| zerror!(err))?; + serializer.serialize(v as u16); + } + KotlinType::UInt => { + let v = env + .get_field(&any, "data", "I") + .map_err(|err| zerror!(err))? + .i() + .map_err(|err| zerror!(err))?; + serializer.serialize(v as u32); + } + KotlinType::ULong => { + let v = env + .get_field(&any, "data", "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()) + } + } +}