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/.gitignore b/.gitignore index 53d109f6..9784c16c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ /build/ /.gradle/ */build/ + +# Generated Kotlin prototypes emitted by zenoh-jni's build.rs +/zenoh-jni/generated-kotlin/ diff --git a/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/prebindgen-ext/Cargo.lock b/prebindgen-ext/Cargo.lock new file mode 100644 index 00000000..0821f335 --- /dev/null +++ b/prebindgen-ext/Cargo.lock @@ -0,0 +1,352 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools 0.14.0", + "konst", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn", + "toml", +] + +[[package]] +name = "prebindgen-ext" +version = "1.9.0" +dependencies = [ + "itertools 0.12.1", + "prebindgen", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/prebindgen-ext/Cargo.toml b/prebindgen-ext/Cargo.toml new file mode 100644 index 00000000..8af82cba --- /dev/null +++ b/prebindgen-ext/Cargo.toml @@ -0,0 +1,28 @@ +# +# Copyright (c) 2023 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# + +[package] +name = "prebindgen-ext" +version = "1.9.0" +edition = "2021" +license = "EPL-2.0 OR Apache-2.0" +description = "Prebindgen JNI extensions for Zenoh." +repository = "https://github.com/eclipse-zenoh/zenoh" + +[dependencies] +syn = { version = "2", features = ["full", "extra-traits"] } +quote = "1" +proc-macro2 = "1" +prebindgen = "0.4.1" +itertools = "0.12" diff --git a/prebindgen-ext/src/jni_converter.rs b/prebindgen-ext/src/jni_converter.rs new file mode 100644 index 00000000..e59e9cd4 --- /dev/null +++ b/prebindgen-ext/src/jni_converter.rs @@ -0,0 +1,859 @@ +//! JNI binding generators for items marked with `#[prebindgen]`. +//! +//! Two converters split the work along the natural axis: +//! +//! * [`JniStructConverter`] consumes `#[prebindgen]` `syn::ItemStruct`s, +//! emits a JNI decoder + a Kotlin `data class`, and inserts an +//! auto-generated [`TypeBinding`] into a shared [`JniTypeBinding`]. +//! * [`JniMethodsConverter`] consumes `#[prebindgen]` `syn::ItemFn`s, +//! classifies each argument/return against the (now fully populated) +//! [`JniTypeBinding`], and emits a `Java__ViaJNI` wrapper +//! plus a matching Kotlin `external fun`. +//! +//! # Type registry +//! +//! Every Rust type-shape that appears in a `#[prebindgen]` function's +//! signature must have an explicit row in the [`JniTypeBinding`] registry, +//! keyed by the canonical `to_token_stream()` form of the type. There are +//! no implicit fallbacks: missing row ⇒ panic with a clear "register ``" +//! message. +//! +//! Built-in rows for `bool` and `Duration` are pre-registered by +//! [`JniTypeBinding::with_builtins`] (applied automatically inside +//! [`MethodsBuilder::default`]). + +use std::collections::{BTreeSet, VecDeque}; +use std::path::PathBuf; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; + +use prebindgen::SourceLocation; + +use crate::jni_type_binding::{JniTypeBinding, ReturnEncode}; +pub use crate::jni_type_binding::{InlineFn, TypeBinding}; + +// ===================================================================== +// JniStructConverter +// ===================================================================== + +/// Builder for [`JniStructConverter`]. +pub struct StructBuilder { + /// Module path used to fully-qualify the struct type in the generated + /// `decode_` function (e.g. `"zenoh_flat::ext"`). + source_module: syn::Path, + /// `ZResult` type used in the decoder return signature. + zresult: syn::Path, + /// Type registry that the struct converter mutates as it processes each + /// `#[prebindgen]` struct. + types: JniTypeBinding, +} + +impl Default for StructBuilder { + fn default() -> Self { + Self { + source_module: syn::parse_str("crate").unwrap(), + zresult: syn::parse_str("ZResult").unwrap(), + types: JniTypeBinding::new().with_builtins(), + } + } +} + +impl StructBuilder { + /// Module path that contains the `#[prebindgen]` struct types + /// (e.g. `"zenoh_flat::ext"`). + pub fn source_module(mut self, path: impl AsRef) -> Self { + self.source_module = syn::parse_str(path.as_ref()).expect("invalid source_module path"); + self + } + + /// Path of the `ZResult` type used in the decoder's return type. + pub fn zresult(mut self, path: impl AsRef) -> Self { + self.zresult = syn::parse_str(path.as_ref()).expect("invalid zresult path"); + self + } + + /// Register or replace a single [`TypeBinding`] in the type registry. + pub fn type_binding(mut self, binding: TypeBinding) -> Self { + self.types.types.insert(binding.name().to_string(), binding); + self + } + + /// Merge a reusable [`JniTypeBinding`] into the type registry. + pub fn jni_type_binding(mut self, bindings: JniTypeBinding) -> Self { + self.types.types.extend(bindings.types); + self.types + .kotlin_data_classes + .extend(bindings.kotlin_data_classes); + self + } + + pub fn build(self) -> JniStructConverter { + JniStructConverter { + cfg: self, + pending: VecDeque::new(), + buffered: false, + } + } +} + +/// Converter that turns `#[prebindgen]`-marked Rust structs into JNI +/// decoder functions and Kotlin `data class` strings, while populating a +/// shared [`JniTypeBinding`] with one auto-generated entry per struct. +pub struct JniStructConverter { + cfg: StructBuilder, + pending: VecDeque<(syn::Item, SourceLocation)>, + buffered: bool, +} + +impl JniStructConverter { + pub fn builder() -> StructBuilder { + StructBuilder::default() + } + + /// Drain `iter` on the first call, convert each struct item, and queue + /// the result for the next `pop` from this batching closure. + pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> + where + I: Iterator, + { + if !self.buffered { + self.buffered = true; + for (item, loc) in iter.by_ref() { + let converted = self.convert(item, &loc); + self.pending.push_back((converted, loc)); + } + } + self.pending.pop_front() + } + + /// Closure suitable for `itertools::batching`. + pub fn as_closure<'a, I>( + &'a mut self, + ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a + where + I: Iterator, + { + move |iter| self.call(iter) + } + + /// Consume the converter and return the populated [`JniTypeBinding`]. + pub fn into_jni_type_binding(self) -> JniTypeBinding { + self.cfg.types + } + + fn convert(&mut self, item: syn::Item, loc: &SourceLocation) -> syn::Item { + match item { + syn::Item::Struct(s) => self.convert_struct(s, loc), + other => panic!( + "JniStructConverter received a non-struct item at {loc}: {}", + other.to_token_stream() + ), + } + } + + /// Emit a JNI decoder for a `#[prebindgen]` struct and a matching Kotlin + /// `data class`, then auto-register a `TypeBinding` so the struct can + /// appear by value in a wrapped function's signature. + fn convert_struct(&mut self, s: syn::ItemStruct, loc: &SourceLocation) -> syn::Item { + let struct_name = s.ident.to_string(); + let struct_ident = s.ident.clone(); + let decoder_ident = format_ident!("decode_{}", struct_ident); + let zresult = self.cfg.zresult.clone(); + let struct_module = self.cfg.source_module.clone(); + + let syn::Fields::Named(named) = &s.fields else { + panic!("tuple / unit structs are not supported at {loc}"); + }; + + let mut field_preludes: Vec = Vec::new(); + let mut field_init: Vec = Vec::new(); + let mut kotlin_field_lines: Vec = Vec::new(); + + for field in &named.named { + let fname_ident = field + .ident + .as_ref() + .unwrap_or_else(|| panic!("unnamed field in struct `{struct_name}` at {loc}")) + .clone(); + let fname = fname_ident.to_string(); + let kotlin_fname = snake_to_camel(&fname); + let err_prefix = format!("{struct_name}.{kotlin_fname}: {{}}"); + + let binding = self.lookup_struct_field_binding(&field.ty).unwrap_or_else(|| { + panic!( + "unsupported field type `{}` for `{}.{}` at {loc}", + field.ty.to_token_stream(), + struct_name, + fname + ) + }); + let (jni_sig, jvalue_method) = + jni_primitive_signature(binding.jni_type()).unwrap_or_else(|| { + panic!( + "field `{}.{}` at {loc}: type `{}` has non-primitive JNI wire form `{}`", + struct_name, + fname, + field.ty.to_token_stream(), + binding.jni_type().to_token_stream() + ) + }); + let raw_ident = format_ident!("__{}_raw", fname_ident); + let jni_type = binding.jni_type(); + let decode_expr = binding + .decode() + .expect("struct-field binding must have a decode") + .call(&raw_ident); + field_preludes.push(quote! { + let #raw_ident: #jni_type = env.get_field(obj, #kotlin_fname, #jni_sig) + .and_then(|v| v.#jvalue_method()) + .map_err(|err| zerror!(#err_prefix, err))? as _; + let #fname_ident = #decode_expr; + }); + field_init.push(quote! { #fname_ident }); + kotlin_field_lines.push(format!( + " val {}: {},", + kotlin_fname, + binding.kotlin_type() + )); + } + + let tokens = quote! { + #[allow(non_snake_case, unused_mut, unused_variables)] + pub(crate) fn #decoder_ident( + env: &mut jni::JNIEnv, + obj: &jni::objects::JObject, + ) -> #zresult<#struct_module::#struct_ident> { + #(#field_preludes)* + Ok(#struct_module::#struct_ident { + #(#field_init),* + }) + } + }; + + let decoder_path = format!("decode_{struct_name}"); + let row = TypeBinding::param( + &struct_name, + &struct_name, + "jni::objects::JObject", + InlineFn::env_ref_mut(&decoder_path), + ); + self.cfg.types.types.insert(row.name().to_string(), row); + + let block = format!( + "data class {}(\n{}\n)", + struct_name, + kotlin_field_lines.join("\n") + ); + self.cfg.types.kotlin_data_classes.push(block); + + syn::parse2(tokens).expect("generated struct decoder must parse") + } + + /// Look up a `#[prebindgen]` struct field's type in the registry. Fields + /// must use the type's bare path-tail name (e.g. `bool`, `i64`, + /// `CongestionControl`) and must resolve to a registered binding whose + /// JNI wire form is one of the primitive `j*` types. + fn lookup_struct_field_binding(&self, ty: &syn::Type) -> Option<&TypeBinding> { + let syn::Type::Path(tp) = ty else { return None }; + let last = tp.path.segments.last()?; + let name = last.ident.to_string(); + self.cfg.types.types.get(&name) + } +} + +// ===================================================================== +// JniMethodsConverter +// ===================================================================== + +/// Builder for [`JniMethodsConverter`]. +pub struct MethodsBuilder { + class_prefix: String, + function_suffix: String, + source_module: syn::Path, + zresult: syn::Path, + throw_exception: syn::Path, + types: JniTypeBinding, + kotlin: Option, +} + +/// Settings for generating a companion Kotlin file with `external fun` +/// prototypes. Enabled via [`MethodsBuilder::kotlin_output`]. +pub(crate) struct KotlinConfig { + output_path: PathBuf, + package: String, + class_name: String, + throws_class_fqn: Option, + init_load_fqn: Option, +} + +impl Default for MethodsBuilder { + fn default() -> Self { + Self { + class_prefix: String::new(), + function_suffix: String::new(), + source_module: syn::parse_str("crate").unwrap(), + zresult: syn::parse_str("ZResult").unwrap(), + throw_exception: syn::parse_str("throw_exception").unwrap(), + types: JniTypeBinding::new().with_builtins(), + kotlin: None, + } + } +} + +impl MethodsBuilder { + /// JNI class prefix prepended to each function name. + pub fn class_prefix(mut self, prefix: impl Into) -> Self { + self.class_prefix = prefix.into(); + self + } + + /// Suffix appended to the camel-case function name. + pub fn function_suffix(mut self, suffix: impl Into) -> Self { + self.function_suffix = suffix.into(); + self + } + + /// Fully-qualified path of the module that contains the original Rust + /// functions being wrapped. + pub fn source_module(mut self, path: impl AsRef) -> Self { + self.source_module = syn::parse_str(path.as_ref()).expect("invalid source_module path"); + self + } + + /// Path of the `ZResult` type used in the closure's return type. + pub fn zresult(mut self, path: impl AsRef) -> Self { + self.zresult = syn::parse_str(path.as_ref()).expect("invalid zresult path"); + self + } + + /// Path of the `throw_exception!` macro. + pub fn throw_exception(mut self, path: impl AsRef) -> Self { + self.throw_exception = + syn::parse_str(path.as_ref()).expect("invalid throw_exception path"); + self + } + + /// Register or replace a [`TypeBinding`] by name. + pub fn type_binding(mut self, binding: TypeBinding) -> Self { + self.types.types.insert(binding.name().to_string(), binding); + self + } + + /// Merge a reusable [`JniTypeBinding`] into the type registry. + pub fn jni_type_binding(mut self, bindings: JniTypeBinding) -> Self { + self.types.types.extend(bindings.types); + self.types + .kotlin_data_classes + .extend(bindings.kotlin_data_classes); + self + } + + /// Enable Kotlin-side prototype generation. + pub fn kotlin_output(mut self, path: impl Into) -> Self { + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .output_path = path.into(); + self + } + + /// Kotlin `package` of the generated file. + pub fn kotlin_package(mut self, pkg: impl Into) -> Self { + self.kotlin.get_or_insert_with(KotlinConfig::default).package = pkg.into(); + self + } + + /// Name of the generated Kotlin `object`. + pub fn kotlin_class(mut self, name: impl Into) -> Self { + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .class_name = name.into(); + self + } + + /// FQN of the exception type to annotate every `external fun` with via + /// `@Throws(::class)`. + pub fn kotlin_throws(mut self, fqn: impl Into) -> Self { + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .throws_class_fqn = Some(fqn.into()); + self + } + + /// FQN of a singleton referenced from the generated `init { ... }` block. + pub fn kotlin_init(mut self, fqn: impl Into) -> Self { + self.kotlin + .get_or_insert_with(KotlinConfig::default) + .init_load_fqn = Some(fqn.into()); + self + } + + pub fn build(self) -> JniMethodsConverter { + JniMethodsConverter { + cfg: self, + pending: VecDeque::new(), + buffered: false, + kotlin_funs: Vec::new(), + kotlin_used_fqns: BTreeSet::new(), + } + } +} + +impl Default for KotlinConfig { + fn default() -> Self { + Self { + output_path: PathBuf::new(), + package: String::new(), + class_name: String::new(), + throws_class_fqn: None, + init_load_fqn: None, + } + } +} + +/// Converter that transforms `#[prebindgen]`-marked Rust functions into JNI +/// `Java_*` wrappers and matching Kotlin `external fun` prototypes. +pub struct JniMethodsConverter { + cfg: MethodsBuilder, + pending: VecDeque<(syn::Item, SourceLocation)>, + buffered: bool, + kotlin_funs: Vec, + kotlin_used_fqns: BTreeSet, +} + +impl JniMethodsConverter { + pub fn builder() -> MethodsBuilder { + MethodsBuilder::default() + } + + pub fn call(&mut self, iter: &mut I) -> Option<(syn::Item, SourceLocation)> + where + I: Iterator, + { + if !self.buffered { + self.buffered = true; + for (item, loc) in iter.by_ref() { + let converted = self.convert(item, &loc); + self.pending.push_back((converted, loc)); + } + } + self.pending.pop_front() + } + + pub fn as_closure<'a, I>( + &'a mut self, + ) -> impl FnMut(&mut I) -> Option<(syn::Item, SourceLocation)> + 'a + where + I: Iterator, + { + move |iter| self.call(iter) + } + + /// Write the accumulated Kotlin file to the configured output path. + /// No-op when Kotlin output was not enabled. + pub fn write_kotlin(&self) -> std::io::Result<()> { + let Some(kt) = self.cfg.kotlin.as_ref() else { + return Ok(()); + }; + if kt.output_path.as_os_str().is_empty() { + return Ok(()); + } + let contents = self.render_kotlin(kt); + if let Some(parent) = kt.output_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&kt.output_path, contents) + } + + fn render_kotlin(&self, kt: &KotlinConfig) -> String { + let mut used = self.kotlin_used_fqns.clone(); + if let Some(fqn) = kt.init_load_fqn.as_ref() { + if fqn.contains('.') { + used.insert(fqn.clone()); + } + } + + let mut imports: Vec = used + .into_iter() + .filter(|fqn| { + let pkg = fqn.rsplit_once('.').map(|(p, _)| p).unwrap_or(""); + !pkg.is_empty() && pkg != kt.package + }) + .collect(); + imports.sort(); + imports.dedup(); + + let mut out = String::new(); + out.push_str("// Auto-generated by JniConverter — do not edit by hand.\n"); + if !kt.package.is_empty() { + out.push_str(&format!("package {}\n\n", kt.package)); + } + for imp in &imports { + out.push_str(&format!("import {}\n", imp)); + } + if !imports.is_empty() { + out.push('\n'); + } + for block in &self.cfg.types.kotlin_data_classes { + out.push_str(block); + out.push_str("\n\n"); + } + out.push_str(&format!("internal object {} {{\n", kt.class_name)); + if let Some(fqn) = kt.init_load_fqn.as_ref() { + let short = fqn.rsplit('.').next().unwrap_or(fqn); + out.push_str(&format!(" init {{ {} }}\n\n", short)); + } + for (i, block) in self.kotlin_funs.iter().enumerate() { + if i > 0 { + out.push('\n'); + } + out.push_str(block); + out.push('\n'); + } + out.push_str("}\n"); + out + } + + fn convert(&mut self, item: syn::Item, loc: &SourceLocation) -> syn::Item { + match item { + syn::Item::Fn(func) => syn::Item::Fn(self.convert_fn(func, loc)), + other => panic!( + "JniMethodsConverter received a non-fn item at {loc}: {}", + other.to_token_stream() + ), + } + } + + fn convert_fn(&mut self, func: syn::ItemFn, loc: &SourceLocation) -> syn::ItemFn { + let original_name = func.sig.ident.to_string(); + let camel = snake_to_camel(&original_name); + let jni_name = format_ident!( + "{}{}{}", + self.cfg.class_prefix, + camel, + self.cfg.function_suffix + ); + let orig_ident = &func.sig.ident; + let source_module = self.cfg.source_module.clone(); + let zresult = self.cfg.zresult.clone(); + let throw_exception = self.cfg.throw_exception.clone(); + + let mut prelude: Vec = Vec::new(); + let mut jni_params: Vec = Vec::new(); + let mut call_args: Vec = Vec::new(); + let mut kotlin_params: Vec = Vec::new(); + let mut local_kotlin_fqns: BTreeSet = BTreeSet::new(); + let kt_enabled = self.cfg.kotlin.is_some(); + + for input in &func.sig.inputs { + let syn::FnArg::Typed(pat_type) = input else { + panic!("receiver args not supported at {loc}"); + }; + let syn::Pat::Ident(pat_ident) = &*pat_type.pat else { + panic!("non-ident param pattern at {loc}"); + }; + let name = &pat_ident.ident; + let ty = &*pat_type.ty; + + self.emit_arg( + name, + ty, + loc, + &mut prelude, + &mut jni_params, + &mut call_args, + &mut kotlin_params, + &mut local_kotlin_fqns, + kt_enabled, + ); + } + + // Return type. + let mut kotlin_ret: Option = None; + let (ret_ty_jni, wrap_ok, on_err, closure_ret): ( + TokenStream, + TokenStream, + TokenStream, + TokenStream, + ) = match &func.sig.output { + syn::ReturnType::Type(_, ty) => { + let inner = extract_zresult_inner(ty).unwrap_or_else(|| { + panic!("return must be ZResult for `{original_name}` at {loc}") + }); + if is_unit(&inner) { + ( + quote! { () }, + quote! { Ok(()) }, + quote! { () }, + quote! { #zresult<()> }, + ) + } else { + let key = ty.to_token_stream().to_string(); + let binding = self.cfg.types.types.get(&key).unwrap_or_else(|| { + panic!( + "unsupported return type `{}` for `{}` at {loc}: \ + register a TypeBinding keyed `{}`", + ty.to_token_stream(), + original_name, + key + ) + }); + let encode = binding.encode().unwrap_or_else(|| { + panic!( + "TypeBinding `{}` has no encode (return direction) at {loc}", + key + ) + }); + let default_expr = binding + .default_expr() + .expect("encode-bearing row must have default_expr"); + let jni_type = binding.jni_type(); + + if kt_enabled { + let kt = binding.kotlin_type(); + let short = kotlin_register_fqn(kt, &mut local_kotlin_fqns); + kotlin_ret = Some(short); + } + + let wrap_ok_ts = match encode { + ReturnEncode::Wrapper(p) => quote! { #p(&mut env, __result) }, + ReturnEncode::ArcIntoRaw => quote! { + Ok(std::sync::Arc::into_raw(std::sync::Arc::new(__result))) + }, + }; + + ( + quote! { #jni_type }, + wrap_ok_ts, + quote! { #default_expr }, + quote! { #zresult<#jni_type> }, + ) + } + } + syn::ReturnType::Default => ( + quote! { () }, + quote! { Ok(()) }, + quote! { () }, + quote! { #zresult<()> }, + ), + }; + + let body = quote! { + { + (|| -> #closure_ret { + #(#prelude)* + let __result = #source_module::#orig_ident( #(#call_args),* )?; + #wrap_ok + })() + .unwrap_or_else(|err| { + #throw_exception!(env, err); + #on_err + }) + } + }; + + let tokens = quote! { + #[no_mangle] + #[allow(non_snake_case, unused_mut, unused_variables)] + pub unsafe extern "C" fn #jni_name( + mut env: jni::JNIEnv, + _class: jni::objects::JClass, + #(#jni_params),* + ) -> #ret_ty_jni #body + }; + + // Assemble the Kotlin `external fun ...` block. + if kt_enabled { + let kt_fn_name = format!("{}{}", camel, self.cfg.function_suffix); + let ret_suffix = match &kotlin_ret { + Some(r) => format!(": {}", r), + None => String::new(), + }; + let params_joined = if kotlin_params.is_empty() { + String::new() + } else { + format!("\n {},\n ", kotlin_params.join(",\n ")) + }; + let throws_line = if self + .cfg + .kotlin + .as_ref() + .and_then(|k| k.throws_class_fqn.as_ref()) + .is_some() + { + let fqn = self + .cfg + .kotlin + .as_ref() + .unwrap() + .throws_class_fqn + .as_ref() + .unwrap(); + let short = fqn.rsplit('.').next().unwrap_or(fqn); + format!("@Throws({}::class)\n ", short) + } else { + String::new() + }; + let block = format!( + " @JvmStatic\n {}external fun {}({}){}", + throws_line, kt_fn_name, params_joined, ret_suffix + ); + self.kotlin_funs.push(block); + self.kotlin_used_fqns.extend(local_kotlin_fqns); + if let Some(kt) = self.cfg.kotlin.as_ref() { + if let Some(fqn) = kt.throws_class_fqn.as_ref() { + if fqn.contains('.') { + self.kotlin_used_fqns.insert(fqn.clone()); + } + } + } + } + + syn::parse2(tokens).expect("generated JNI wrapper must parse") + } + + #[allow(clippy::too_many_arguments)] + fn emit_arg( + &self, + name: &syn::Ident, + ty: &syn::Type, + loc: &SourceLocation, + prelude: &mut Vec, + jni_params: &mut Vec, + call_args: &mut Vec, + kotlin_params: &mut Vec, + local_kotlin_fqns: &mut BTreeSet, + kt_enabled: bool, + ) { + let key = ty.to_token_stream().to_string(); + let binding = self.cfg.types.types.get(&key).unwrap_or_else(|| { + panic!( + "unsupported parameter type `{}` for `{}` at {loc}: \ + register a TypeBinding keyed `{}`", + ty.to_token_stream(), + name, + key + ) + }); + let decode = binding.decode().unwrap_or_else(|| { + panic!( + "TypeBinding `{}` has no decode (param direction) at {loc}", + key + ) + }); + + let pat = if binding.is_pointer() { + format_ident!("{}_ptr", name) + } else { + name.clone() + }; + let jt = binding.jni_type(); + jni_params.push(quote! { #pat: #jt }); + + let expr = decode.call(&pat); + prelude.push(quote! { let #name = #expr; }); + + if binding.is_borrow() { + call_args.push(quote! { &#name }); + } else { + call_args.push(quote! { #name }); + } + + if kt_enabled { + let short = kotlin_register_fqn(binding.kotlin_type(), local_kotlin_fqns); + let suffix = if binding.is_option() { "?" } else { "" }; + kotlin_params.push(format!( + "{}: {}{}", + kotlin_param_name(&name.to_string(), binding.is_pointer()), + short, + suffix + )); + } + } +} + +// ===================================================================== +// Internal helpers +// ===================================================================== + +/// Map a primitive JNI wire type (`jni::sys::j*`) to the JVM field +/// signature character and the matching `JValue` accessor method. +/// Returns `None` for non-primitive (object-shaped) wire types. +fn jni_primitive_signature(jni_type: &syn::Type) -> Option<(&'static str, syn::Ident)> { + let syn::Type::Path(tp) = jni_type else { + return None; + }; + let last = tp.path.segments.last()?; + let (sig, accessor) = match last.ident.to_string().as_str() { + "jboolean" => ("Z", "z"), + "jbyte" => ("B", "b"), + "jchar" => ("C", "c"), + "jshort" => ("S", "s"), + "jint" => ("I", "i"), + "jlong" => ("J", "j"), + "jfloat" => ("F", "f"), + "jdouble" => ("D", "d"), + _ => return None, + }; + Some((sig, format_ident!("{}", accessor))) +} + +fn is_unit(ty: &syn::Type) -> bool { + matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) +} + +fn extract_zresult_inner(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + let seg = tp.path.segments.last()?; + if seg.ident != "ZResult" { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return None; + }; + let arg = args.args.first()?; + let syn::GenericArgument::Type(inner) = arg else { + return None; + }; + Some(inner.clone()) +} + +fn snake_to_camel(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut upper_next = false; + for (i, c) in s.chars().enumerate() { + if c == '_' { + upper_next = true; + } else if upper_next { + out.extend(c.to_uppercase()); + upper_next = false; + } else if i == 0 { + out.extend(c.to_lowercase()); + } else { + out.push(c); + } + } + out +} + +/// Map a Rust snake_case arg name to its Kotlin camelCase form, appending +/// `"Ptr"` for raw-pointer slots. +fn kotlin_param_name(rust_name: &str, is_pointer: bool) -> String { + let base = snake_to_camel(rust_name); + if is_pointer { + format!("{}Ptr", base) + } else { + base + } +} + +/// Record `fqn` in `used` if it looks fully-qualified (contains `.`) and +/// return the short name used at the emission site. +fn kotlin_register_fqn(fqn: &str, used: &mut BTreeSet) -> String { + if fqn.contains('.') { + used.insert(fqn.to_string()); + fqn.rsplit('.').next().unwrap_or(fqn).to_string() + } else { + fqn.to_string() + } +} diff --git a/prebindgen-ext/src/jni_type_binding.rs b/prebindgen-ext/src/jni_type_binding.rs new file mode 100644 index 00000000..031b5a21 --- /dev/null +++ b/prebindgen-ext/src/jni_type_binding.rs @@ -0,0 +1,351 @@ +//! Flat JNI type registry. +//! +//! Each [`TypeBinding`] is a single row keyed by the canonical +//! `to_token_stream()` form of a Rust type-shape, e.g. `"String"`, +//! `"& Session"`, `"Vec < u8 >"`, `"Option < KeyExpr < 'static > >"`, +//! `"ZResult < ZenohId >"`, `"impl Fn (Sample) + Send + Sync + 'static"`. +//! +//! A row carries: +//! +//! * `kotlin_type` — the Kotlin parameter or return type +//! * `jni_type` — the on-the-wire JNI type emitted in the wrapper signature +//! * `decode` — JNI value → Rust value (param-direction rows) +//! * `encode` + `default_expr` — Rust value → JNI value (return-direction rows) +//! +//! Wrapper types (`&T`, `Vec`, `Option`, `ZResult`) are **not** +//! decomposed by the classifier — each must have its own explicit row. The +//! [`TypeBinding::opaque_borrow`], [`TypeBinding::opaque_arc_return`] and +//! [`TypeBinding::option_of`] convenience constructors keep registration +//! concise. + +use std::collections::HashMap; +use std::sync::Arc; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +/// Clonable closure that produces a `TokenStream` from the JNI input ident. +#[derive(Clone)] +pub struct InlineFn(Arc TokenStream + Send + Sync>); + +impl InlineFn { + pub fn new(f: F) -> Self + where + F: Fn(&syn::Ident) -> TokenStream + Send + Sync + 'static, + { + InlineFn(Arc::new(f)) + } + + /// `()?` — pure conversion (e.g. enum decoders). + pub fn pure(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::pure path"); + quote! { #p(#input)? } + }) + } + + /// `(&env, &)?` — decoder needing shared access to the JNI env. + pub fn env_ref(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = syn::parse_str(&s).expect("invalid InlineFn::env_ref path"); + quote! { #p(&env, &#input)? } + }) + } + + /// `(&mut env, &)?` — decoder needing mutable access to the JNI env. + pub fn env_ref_mut(path: impl AsRef) -> Self { + let s = path.as_ref().to_string(); + InlineFn::new(move |input| { + let p: syn::Path = + syn::parse_str(&s).expect("invalid InlineFn::env_ref_mut path"); + quote! { #p(&mut env, &#input)? } + }) + } + + pub(crate) fn call(&self, ident: &syn::Ident) -> TokenStream { + (self.0)(ident) + } +} + +/// How a Rust return value is encoded into a JNI return. +#[derive(Clone)] +pub enum ReturnEncode { + /// `(&mut env, __result)` — wrapping function returns + /// `ZResult`. + Wrapper(syn::Path), + /// `Ok(Arc::into_raw(Arc::new(__result)))` — opaque Arc-handle return. + ArcIntoRaw, +} + +impl ReturnEncode { + pub fn wrapper(path: impl AsRef) -> Self { + ReturnEncode::Wrapper( + syn::parse_str(path.as_ref()).expect("invalid ReturnEncode::wrapper path"), + ) + } +} + +/// Per-row binding from a Rust type-shape to its JNI/Kotlin representation. +#[derive(Clone)] +pub struct TypeBinding { + /// Canonical Rust type-shape (token-stream form). The lookup key. + pub(crate) rust_type: String, + /// Kotlin parameter / return type (FQN preferred for object types, + /// bare for primitives). For Option-shaped rows this is the inner + /// type's Kotlin name; the `?` suffix is added by the emitter from + /// the row's rust_type prefix. + pub(crate) kotlin_type: String, + /// On-the-wire JNI type emitted in the wrapper signature. + pub(crate) jni_type: syn::Type, + /// JNI value → Rust value. None for return-only rows. + pub(crate) decode: Option, + /// Rust value → JNI value. None for param-only rows. + pub(crate) encode: Option, + /// Default JNI value emitted on the throw-return path. Required when + /// `encode` is set. + pub(crate) default_expr: Option, +} + +impl TypeBinding { + /// Param-direction row. `rust_type` is canonicalized via `syn::Type` parse. + pub fn param( + rust_type: impl AsRef, + kotlin_type: impl Into, + jni_type: impl AsRef, + decode: InlineFn, + ) -> Self { + Self { + rust_type: canon_type(rust_type.as_ref()), + kotlin_type: kotlin_type.into(), + jni_type: parse_type(jni_type.as_ref()), + decode: Some(decode), + encode: None, + default_expr: None, + } + } + + /// Return-direction row. + pub fn returns( + rust_type: impl AsRef, + kotlin_type: impl Into, + jni_type: impl AsRef, + encode: ReturnEncode, + default_expr: impl AsRef, + ) -> Self { + Self { + rust_type: canon_type(rust_type.as_ref()), + kotlin_type: kotlin_type.into(), + jni_type: parse_type(jni_type.as_ref()), + decode: None, + encode: Some(encode), + default_expr: Some( + syn::parse_str(default_expr.as_ref()) + .expect("invalid TypeBinding::returns default_expr"), + ), + } + } + + /// Convenience: opaque borrow `&T` — JNI side passes raw `*const T`, + /// decoded via `::from_raw`. Because the row's key starts + /// with `&`, the wrapped fn receives `&name` automatically. + pub fn opaque_borrow(t: impl AsRef, owned_object: impl AsRef) -> Self { + let t = t.as_ref().to_string(); + let owned_str = owned_object.as_ref().to_string(); + // Validate the owner path parses now so errors surface at registration. + let _: syn::Path = + syn::parse_str(&owned_str).expect("opaque_borrow: invalid owned_object path"); + Self::param( + format!("&{}", t), + "Long", + format!("*const {}", t), + InlineFn::new(move |input| { + let owned: syn::Path = + syn::parse_str(&owned_str).expect("owned_object must parse"); + quote! { #owned::from_raw(#input) } + }), + ) + } + + /// Convenience: opaque Arc return for `ZResult` — encode via + /// `Arc::into_raw(Arc::new(__result))`, default to `std::ptr::null()`. + pub fn opaque_arc_return(t: impl AsRef) -> Self { + let t = t.as_ref(); + Self::returns( + format!("ZResult<{}>", t), + "Long", + format!("*const {}", t), + ReturnEncode::ArcIntoRaw, + "std::ptr::null()", + ) + } + + /// Convenience: `Option` row that lifts `inner`'s decode with a + /// JNI-side null check. Inner's wire type must be JNI-object-shaped. + pub fn option_of(inner: &TypeBinding) -> Self { + let inner_decode = inner + .decode + .as_ref() + .expect("option_of: inner must be a param row") + .clone(); + assert!( + jni_object_shaped(&inner.jni_type), + "option_of requires a JNI-object inner form, got `{}`", + inner.jni_type.to_token_stream() + ); + Self { + rust_type: canon_type(&format!("Option<{}>", inner.rust_type)), + kotlin_type: inner.kotlin_type.clone(), + jni_type: inner.jni_type.clone(), + decode: Some(InlineFn::new(move |input| { + let inner_expr = inner_decode.call(input); + quote! { + if !#input.is_null() { + Some(#inner_expr) + } else { + None + } + } + })), + encode: None, + default_expr: None, + } + } + + /// Canonical type-shape this binding is keyed under. + pub fn name(&self) -> &str { + &self.rust_type + } + + pub(crate) fn jni_type(&self) -> &syn::Type { + &self.jni_type + } + pub(crate) fn kotlin_type(&self) -> &str { + &self.kotlin_type + } + pub(crate) fn decode(&self) -> Option<&InlineFn> { + self.decode.as_ref() + } + pub(crate) fn encode(&self) -> Option<&ReturnEncode> { + self.encode.as_ref() + } + pub(crate) fn default_expr(&self) -> Option<&syn::Expr> { + self.default_expr.as_ref() + } + /// `&T` row — wrapped fn receives `&name`. + pub(crate) fn is_borrow(&self) -> bool { + self.rust_type.starts_with('&') + } + /// `*const _` / `*mut _` wire type — Kotlin name gets `Ptr` suffix and + /// the Rust ident gets `_ptr` suffix. + pub(crate) fn is_pointer(&self) -> bool { + matches!(self.jni_type, syn::Type::Ptr(_)) + } + /// `Option<_>` row — Kotlin emission appends `?`. + pub(crate) fn is_option(&self) -> bool { + self.rust_type.starts_with("Option <") + } +} + +/// Reusable collection of [`TypeBinding`]s plus the Kotlin `data class` +/// strings produced by struct processing. +#[derive(Default, Clone)] +pub struct JniTypeBinding { + pub(crate) types: HashMap, + pub(crate) kotlin_data_classes: Vec, +} + +impl JniTypeBinding { + pub fn new() -> Self { + Self::default() + } + + /// Add (or replace) a [`TypeBinding`] in this collection. + pub fn type_binding(mut self, binding: TypeBinding) -> Self { + self.types.insert(binding.rust_type.clone(), binding); + self + } + + /// Look up a registered [`TypeBinding`] by its canonical type-shape key + /// (e.g. `"HistoryConfig"`, `"Vec < u8 >"`). The key is canonicalized + /// via `syn::Type` parse so callers can pass either spacing form. + pub fn type_by_key(&self, key: &str) -> Option<&TypeBinding> { + self.types.get(&canon_type(key)) + } + + /// Merge another [`JniTypeBinding`] into this one. Type entries in + /// `other` override entries with the same key in `self`; data-class + /// blocks are appended in order. + pub fn merge(mut self, other: JniTypeBinding) -> Self { + self.types.extend(other.types); + self.kotlin_data_classes.extend(other.kotlin_data_classes); + self + } + + /// Pre-register built-in language types whose JNI form is fully described + /// without any project-specific decoder path: `bool`, `i64`, `f64`, and + /// `Duration`. + pub fn with_builtins(mut self) -> Self { + let bool_row = TypeBinding::param( + "bool", + "Boolean", + "jni::sys::jboolean", + InlineFn::new(|input| quote! { #input != 0 }), + ); + self.types.insert(bool_row.rust_type.clone(), bool_row); + + let i64_row = TypeBinding::param( + "i64", + "Long", + "jni::sys::jlong", + InlineFn::new(|input| quote! { #input }), + ); + self.types.insert(i64_row.rust_type.clone(), i64_row); + + let f64_row = TypeBinding::param( + "f64", + "Double", + "jni::sys::jdouble", + InlineFn::new(|input| quote! { #input }), + ); + self.types.insert(f64_row.rust_type.clone(), f64_row); + + let duration_row = TypeBinding::param( + "Duration", + "Long", + "jni::sys::jlong", + InlineFn::new(|input| { + quote! { std::time::Duration::from_millis(#input as u64) } + }), + ); + self.types + .insert(duration_row.rust_type.clone(), duration_row); + self + } +} + +/// Canonical type-shape string. Parses through `syn::Type` so whitespace +/// variations in user input (`"Vec"` vs `"Vec < u8 >"`) match the form +/// the classifier produces from AST nodes via `to_token_stream()`. +pub(crate) fn canon_type(s: &str) -> String { + syn::parse_str::(s) + .map(|t| t.to_token_stream().to_string()) + .unwrap_or_else(|e| panic!("TypeBinding: cannot parse `{}` as a type: {}", s, e)) +} + +fn parse_type(s: &str) -> syn::Type { + syn::parse_str(s).unwrap_or_else(|e| panic!("invalid JNI wire type `{}`: {}", s, e)) +} + +/// True if `ty` is a JNI object-shaped wire type that supports `is_null()`. +pub(crate) fn jni_object_shaped(ty: &syn::Type) -> bool { + let syn::Type::Path(tp) = ty else { return false }; + let Some(last) = tp.path.segments.last() else { + return false; + }; + matches!( + last.ident.to_string().as_str(), + "JObject" | "JString" | "JByteArray" + ) +} diff --git a/prebindgen-ext/src/lib.rs b/prebindgen-ext/src/lib.rs new file mode 100644 index 00000000..c28323a8 --- /dev/null +++ b/prebindgen-ext/src/lib.rs @@ -0,0 +1,9 @@ +//! Prebindgen JNI extensions for Zenoh. +//! +//! This crate provides JNI binding generators for items marked with `#[prebindgen]`. + +pub mod jni_converter; +pub mod jni_type_binding; + +pub use jni_converter::{JniStructConverter, JniMethodsConverter}; +pub use jni_type_binding::{JniTypeBinding, TypeBinding, InlineFn, ReturnEncode}; diff --git a/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-flat/Cargo.lock b/zenoh-flat/Cargo.lock new file mode 100644 index 00000000..c8945b8d --- /dev/null +++ b/zenoh-flat/Cargo.lock @@ -0,0 +1,2827 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst 0.2.20", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keyed-set" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d255a6b6ecd77bb93ce91de984d7039bff7503f500eb4851a1269732f22baf" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nonempty-collections" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e216d0e8cf9d54fa66e5780f6e1d5dc96d1c1b3c25aeba3b6758548bcbbd8b9d" +dependencies = [ + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "serde", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools 0.14.0", + "konst 0.3.17", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen-ext" +version = "1.9.0" +dependencies = [ + "itertools 0.12.1", + "prebindgen", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prebindgen-proc-macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a68d8870ef9ba6f8abbe9d4bca5775e1d854d5fa6dac8c27dd1c79ecebe398" +dependencies = [ + "prebindgen", + "proc-macro2", + "quote", + "rand 0.9.4", + "serde", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ringbuffer-spsc" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3e7aa0a681b232e7cd7f856a53b10603df88ca74b79a8d8088845185492e35" +dependencies = [ + "array-init", + "crossbeam", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "either", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "stabby" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976399a0c48ea769ef7f5dc303bb88240ab8d84008647a6b2303eced3dab3945" +dependencies = [ + "rustversion", + "stabby-abi", +] + +[[package]] +name = "stabby-abi" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b54832a9a1f92a0e55e74a5c0332744426edc515bb3fbad82f10b874a87f0d" +dependencies = [ + "rustc_version", + "rustversion", + "sha2-const-stable", + "stabby-macros", +] + +[[package]] +name = "stabby-macros" +version = "72.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a768b1e51e4dbfa4fa52ae5c01241c0a41e2938fdffbb84add0c8238092f9091" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "rand 0.8.6", + "syn 1.0.109", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "token-cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb48920ae769b58126c8c93269805011c793201f95fde28b479b81a9a531bbde" +dependencies = [ + "paste", + "portable-atomic", + "rustversion", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uhlc" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62a645e3e4e6c85b7abe49b086aa3204119431f42b6123b0070419fb6e9d24e" +dependencies = [ + "humantime", + "lazy_static", + "log", + "rand 0.8.6", + "serde", + "spin 0.10.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unzip-n" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5bb2756c16fb66f80cfbf5fb0e0c09a7001e739f453c9ec241b9c8b1556fda" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "validated_struct" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869a93e8a7286e339e1128630051d82babbcd75d585975af07b9f3327220e60e" +dependencies = [ + "json5", + "serde", + "serde_json", + "validated_struct_macros", +] + +[[package]] +name = "validated_struct_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c44ce98e7227a04eeb4cf9c784109a5c9710e54849ceb4f09f8597247897f1e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "unzip-n", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zenoh" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "ahash", + "arc-swap", + "async-trait", + "bytes", + "const_format", + "flate2", + "flume", + "futures", + "git-version", + "itertools 0.14.0", + "json5", + "lazy_static", + "nonempty-collections", + "once_cell", + "petgraph", + "phf", + "rand 0.8.6", + "rustc_version", + "serde", + "serde_json", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "uhlc", + "vec_map", + "zenoh-buffers", + "zenoh-codec", + "zenoh-collections", + "zenoh-config", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-link", + "zenoh-link-commons", + "zenoh-macros", + "zenoh-plugin-trait", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-transport", + "zenoh-util", +] + +[[package]] +name = "zenoh-buffers" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "zenoh-collections", +] + +[[package]] +name = "zenoh-codec" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "tracing", + "uhlc", + "zenoh-buffers", + "zenoh-protocol", +] + +[[package]] +name = "zenoh-collections" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "ahash", +] + +[[package]] +name = "zenoh-config" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "json5", + "nonempty-collections", + "num_cpus", + "secrecy", + "serde", + "serde_json", + "serde_with", + "serde_yaml", + "toml", + "tracing", + "uhlc", + "validated_struct", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-protocol", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-core" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "lazy_static", + "tokio", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-crypto" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "aes", + "hmac", + "rand 0.8.6", + "rand_chacha 0.3.1", + "sha3", + "zenoh-result", +] + +[[package]] +name = "zenoh-ext" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "async-trait", + "bincode", + "flume", + "futures", + "leb128", + "serde", + "tokio", + "tracing", + "uhlc", + "zenoh", + "zenoh-macros", + "zenoh-util", +] + +[[package]] +name = "zenoh-flat" +version = "1.9.0" +dependencies = [ + "json5", + "prebindgen", + "prebindgen-ext", + "prebindgen-proc-macro", + "serde_yaml", + "tracing", + "zenoh", + "zenoh-ext", +] + +[[package]] +name = "zenoh-keyexpr" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "getrandom 0.2.17", + "hashbrown 0.16.1", + "keyed-set", + "rand 0.8.6", + "schemars 1.2.1", + "serde", + "token-cell", + "zenoh-result", +] + +[[package]] +name = "zenoh-link" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "zenoh-config", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-commons" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "async-trait", + "flume", + "futures", + "serde", + "socket2 0.5.10", + "time", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-core", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-util", +] + +[[package]] +name = "zenoh-macros" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "zenoh-keyexpr", +] + +[[package]] +name = "zenoh-plugin-trait" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "git-version", + "libloading", + "serde", + "stabby", + "tracing", + "zenoh-config", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-protocol" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "const_format", + "rand 0.8.6", + "serde", + "uhlc", + "zenoh-buffers", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-result" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "anyhow", +] + +[[package]] +name = "zenoh-runtime" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "lazy_static", + "ron", + "serde", + "tokio", + "tracing", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-sync" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "arc-swap", + "event-listener", + "futures", + "tokio", + "zenoh-buffers", + "zenoh-collections", + "zenoh-core", +] + +[[package]] +name = "zenoh-task" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "futures", + "tokio", + "tokio-util", + "tracing", + "zenoh-core", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-transport" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "async-trait", + "crossbeam-utils", + "flume", + "futures", + "lazy_static", + "lz4_flex", + "rand 0.8.6", + "ringbuffer-spsc", + "serde", + "sha3", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-config", + "zenoh-core", + "zenoh-crypto", + "zenoh-link", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-util", +] + +[[package]] +name = "zenoh-util" +version = "1.9.0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#7792ebbb2fb0c8311e51419994171cf91fb619d7" +dependencies = [ + "async-trait", + "const_format", + "flume", + "home", + "humantime", + "lazy_static", + "libc", + "libloading", + "pnet_datalink", + "schemars 1.2.1", + "serde", + "serde_json", + "shellexpand", + "tokio", + "tracing", + "tracing-subscriber", + "winapi", + "zenoh-core", + "zenoh-result", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/zenoh-flat/Cargo.toml b/zenoh-flat/Cargo.toml new file mode 100644 index 00000000..e247a48d --- /dev/null +++ b/zenoh-flat/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "zenoh-flat" +version = "1.9.0" +edition = "2021" +license = "EPL-2.0 OR Apache-2.0" +description = "Zenoh flat data interchange support." +repository = "https://github.com/eclipse-zenoh/zenoh" +build = "build.rs" + +[features] +default = [] +zenoh-ext = ["dep:zenoh-ext"] + +[dependencies] +zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } +zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false, optional = true } +json5 = "0.4.1" +serde_yaml = "0.9.19" +tracing = "0.1" +prebindgen-proc-macro = "0.4.1" +prebindgen = "0.4.1" +prebindgen-ext = { path = "../prebindgen-ext" } + +[build-dependencies] +prebindgen = "0.4.1" diff --git a/zenoh-flat/build.rs b/zenoh-flat/build.rs new file mode 100644 index 00000000..cc8fe95d --- /dev/null +++ b/zenoh-flat/build.rs @@ -0,0 +1,3 @@ +fn main() { + prebindgen::init_prebindgen_out_dir(); +} diff --git a/zenoh-flat/src/config.rs b/zenoh-flat/src/config.rs new file mode 100644 index 00000000..35328830 --- /dev/null +++ b/zenoh-flat/src/config.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use crate::zerror; +use zenoh::config::Config; + +/// Load a Zenoh configuration from a JSON string. +pub fn load_json_config(json: &str) -> crate::errors::ZResult { + let mut deserializer = json5::Deserializer::from_str(json).map_err(|err| zerror!(err))?; + Config::from_deserializer(&mut deserializer).map_err(|err| match err { + Ok(c) => zerror!("Invalid configuration: {}", c), + Err(e) => zerror!("JSON error: {}", e), + }) +} + +/// Load a Zenoh configuration from a YAML string. +pub fn load_yaml_config(yaml: &str) -> crate::errors::ZResult { + let deserializer = serde_yaml::Deserializer::from_str(yaml); + Config::from_deserializer(deserializer).map_err(|err| match err { + Ok(c) => zerror!("Invalid configuration: {}", c), + Err(e) => zerror!("YAML error: {}", e), + }) +} diff --git a/zenoh-flat/src/errors.rs b/zenoh-flat/src/errors.rs new file mode 100644 index 00000000..7ac177ab --- /dev/null +++ b/zenoh-flat/src/errors.rs @@ -0,0 +1,35 @@ +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use std::fmt; + +#[macro_export] +macro_rules! zerror { + ($arg:expr) => { + $crate::errors::ZError($arg.to_string()) + }; + ($fmt:expr, $($arg:tt)*) => { + $crate::errors::ZError(format!($fmt, $($arg)*)) + }; +} + +pub(crate) type ZResult = core::result::Result; + +#[derive(Debug)] +pub struct ZError(pub String); + +impl fmt::Display for ZError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/zenoh-flat/src/ext.rs b/zenoh-flat/src/ext.rs new file mode 100644 index 00000000..390ec464 --- /dev/null +++ b/zenoh-flat/src/ext.rs @@ -0,0 +1,149 @@ +// Copyright (c) 2026 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +//! Flat mirrors of the zenoh-ext advanced publisher/subscriber configs. +//! +//! Marked with `#[prebindgen]` so the JNI binding generator in +//! [`crate::jni_converter`] can emit a matching Kotlin `data class` and a +//! Rust JObject decoder for each one. The `TryFrom` impls encapsulate the +//! flat → semantic conversion (enum decoding happens already in the +//! auto-generated decoder, so these impls only deal with size/sign +//! adjustments and the "flag fields decide which builder method to call" +//! semantics). + +use std::time::Duration; + +use zenoh::qos::{CongestionControl, Priority}; + +use crate::errors::{ZError, ZResult}; +use crate::zerror; + +/// Flat cache configuration for an [`zenoh_ext::AdvancedPublisher`]. +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct CacheConfig { + pub max_samples: i64, + pub replies_priority: Priority, + pub replies_congestion_control: CongestionControl, + pub replies_is_express: bool, +} + +/// Flat history configuration for an [`zenoh_ext::AdvancedSubscriber`]. +/// +/// `max_samples <= 0` and `max_age_seconds <= 0.0` mean "unlimited". +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct HistoryConfig { + pub detect_late_publishers: bool, + pub max_samples: i64, + pub max_age_seconds: f64, +} + +/// Flat miss-detection configuration for an [`zenoh_ext::AdvancedPublisher`]. +/// +/// When `enable_heartbeat` is `true`, the heartbeat is configured with +/// `period_ms` — sporadic if `is_sporadic`, regular otherwise. When +/// `enable_heartbeat` is `false`, the other fields are ignored and +/// [`zenoh_ext::MissDetectionConfig::default`] is used as-is. +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct MissDetectionConfig { + pub enable_heartbeat: bool, + pub is_sporadic: bool, + pub period_ms: i64, +} + +/// Flat recovery configuration for an [`zenoh_ext::AdvancedSubscriber`]. +/// +/// When `is_heartbeat` is `true`, `period_ms` is ignored; otherwise +/// `period_ms` is the `periodic_queries` period in milliseconds. +#[prebindgen_proc_macro::prebindgen("jni")] +#[derive(Debug, Clone)] +pub struct RecoveryConfig { + pub is_heartbeat: bool, + pub period_ms: i64, +} + +impl TryFrom for zenoh_ext::CacheConfig { + type Error = ZError; + + fn try_from(c: CacheConfig) -> ZResult { + let max_samples: usize = c + .max_samples + .try_into() + .map_err(|e: std::num::TryFromIntError| zerror!("CacheConfig.max_samples: {}", e))?; + let replies = zenoh_ext::RepliesConfig::default() + .priority(c.replies_priority) + .congestion_control(c.replies_congestion_control) + .express(c.replies_is_express); + Ok(zenoh_ext::CacheConfig::default() + .max_samples(max_samples) + .replies_config(replies)) + } +} + +impl TryFrom for zenoh_ext::HistoryConfig { + type Error = ZError; + + fn try_from(c: HistoryConfig) -> ZResult { + let mut cfg = zenoh_ext::HistoryConfig::default(); + if c.detect_late_publishers { + cfg = cfg.detect_late_publishers(); + } + if c.max_samples > 0 { + let n: usize = c.max_samples.try_into().map_err( + |e: std::num::TryFromIntError| zerror!("HistoryConfig.max_samples: {}", e), + )?; + cfg = cfg.max_samples(n); + } + if c.max_age_seconds > 0.0 { + cfg = cfg.max_age(c.max_age_seconds); + } + Ok(cfg) + } +} + +impl TryFrom for zenoh_ext::MissDetectionConfig { + type Error = ZError; + + fn try_from(c: MissDetectionConfig) -> ZResult { + let mut cfg = zenoh_ext::MissDetectionConfig::default(); + if c.enable_heartbeat { + let period_ms: u64 = c.period_ms.try_into().map_err( + |e: std::num::TryFromIntError| zerror!("MissDetectionConfig.period_ms: {}", e), + )?; + let dur = Duration::from_millis(period_ms); + cfg = if c.is_sporadic { + cfg.sporadic_heartbeat(dur) + } else { + cfg.heartbeat(dur) + }; + } + Ok(cfg) + } +} + +impl TryFrom for zenoh_ext::RecoveryConfig { + type Error = ZError; + + fn try_from(c: RecoveryConfig) -> ZResult { + if c.is_heartbeat { + Ok(zenoh_ext::RecoveryConfig::default().heartbeat()) + } else { + let period_ms: u64 = c.period_ms.try_into().map_err( + |e: std::num::TryFromIntError| zerror!("RecoveryConfig.period_ms: {}", e), + )?; + Ok(zenoh_ext::RecoveryConfig::default().periodic_queries(Duration::from_millis(period_ms))) + } + } +} diff --git a/zenoh-flat/src/lib.rs b/zenoh-flat/src/lib.rs new file mode 100644 index 00000000..1c0d5ca4 --- /dev/null +++ b/zenoh-flat/src/lib.rs @@ -0,0 +1,13 @@ +//! `zenoh-flat` is a placeholder Rust crate for Zenoh flat data support. + +pub const PREBINDGEN_OUT_DIR: &str = prebindgen_proc_macro::prebindgen_out_dir!(); +pub const FEATURES: &str = prebindgen_proc_macro::features!(); + +pub mod config; +pub mod errors; +#[cfg(feature = "zenoh-ext")] +pub mod ext; +pub mod session; + +pub use prebindgen_ext::{jni_converter, jni_type_binding}; + diff --git a/zenoh-flat/src/session.rs b/zenoh-flat/src/session.rs new file mode 100644 index 00000000..e4498d87 --- /dev/null +++ b/zenoh-flat/src/session.rs @@ -0,0 +1,508 @@ +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use crate::{errors::ZResult, zerror}; +use tracing::{error, trace}; +use std::time::Duration; + +use zenoh::{ + bytes::Encoding, + config::Config, + key_expr::KeyExpr, + pubsub::{Publisher, Subscriber}, + query::{ConsolidationMode, Query, QueryTarget, Queryable, Querier, Reply, ReplyKeyExpr, Selector}, + sample::Sample, + qos::{CongestionControl, Priority, Reliability}, + session::{Session, ZenohId}, + Wait, +}; + +#[cfg(feature = "zenoh-ext")] +use crate::ext::{CacheConfig, HistoryConfig, MissDetectionConfig, RecoveryConfig}; +#[cfg(feature = "zenoh-ext")] +use zenoh_ext::{ + AdvancedPublisher, AdvancedPublisherBuilderExt, AdvancedSubscriber, + AdvancedSubscriberBuilderExt, +}; + +/// Fires `f` exactly once when dropped. Used to bind an `on_close` callback +/// to the lifetime of a data callback closure: when zenoh drops the data +/// closure (subscription/queryable/get teardown), the guard's `Drop` runs +/// `on_close`. +struct CallOnDrop(core::mem::MaybeUninit); +impl CallOnDrop { + fn new(f: F) -> Self { + Self(core::mem::MaybeUninit::new(f)) + } +} +impl Drop for CallOnDrop { + fn drop(&mut self) { + let f = unsafe { self.0.assume_init_read() }; + f(); + } +} + +/// Open a Zenoh session using a borrowed configuration. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn open_session(config: &Config) -> ZResult { + zenoh::open(config.clone()) + .wait() + .map(|session| { + trace!("Opened Zenoh session."); + session + }) + .map_err(|err| { + error!("Unable to open session: {}", err); + zerror!(err) + }) +} + +/// Declare a publisher through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_publisher( + session: &Session, + key_expr: KeyExpr<'static>, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + reliability: Reliability, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + session + .declare_publisher(key_expr) + .congestion_control(congestion_control) + .priority(priority) + .express(express) + .reliability(reliability) + .wait() + .map(|publisher| { + trace!("Declared publisher on '{}'.", key_expr_string); + publisher + }) + .map_err(|err| { + error!("Unable to declare publisher on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + +/// Declare a key expression through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_key_expr(session: &Session, key_expr: String) -> ZResult> { + let key_expr_clone = key_expr.clone(); + session + .declare_keyexpr(key_expr) + .wait() + .map(|ke| { + trace!("Declared key expression '{}'.", key_expr_clone); + ke + }) + .map_err(|err| { + error!( + "Unable to declare key expression '{}': {}", + key_expr_clone, err + ); + zerror!( + "Unable to declare key expression '{}': {}", + key_expr_clone, + err + ) + }) +} + +/// Undeclare a previously-declared key expression on a Zenoh session. +/// +/// Takes the `KeyExpr` by value so the caller can relinquish ownership. After +/// this call the original JNI-side raw pointer is invalid. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn undeclare_key_expr(session: &Session, key_expr: KeyExpr<'static>) -> ZResult<()> { + let key_expr_string = key_expr.to_string(); + session + .undeclare(key_expr) + .wait() + .map(|_| { + trace!("Undeclared key expression '{}'.", key_expr_string); + }) + .map_err(|err| { + error!( + "Unable to undeclare key expression '{}': {}", + key_expr_string, err + ); + zerror!( + "Unable to undeclare key expression '{}': {}", + key_expr_string, + err + ) + }) +} + +/// Declare a subscriber through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_subscriber( + session: &Session, + key_expr: KeyExpr<'static>, + callback: impl Fn(Sample) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + let guard = CallOnDrop::new(on_close); + session + .declare_subscriber(key_expr) + .callback(move |sample| { + let _ = &guard; // capture the guard + callback(sample); + }) + .wait() + .map(|subscriber| { + trace!("Declared subscriber on '{}'.", key_expr_string); + subscriber + }) + .map_err(|err| { + error!("Unable to declare subscriber on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + +/// Declare a querier through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_querier( + session: &Session, + key_expr: KeyExpr<'static>, + query_target: QueryTarget, + consolidation: ConsolidationMode, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + timeout: Duration, + reply_key_expr: ReplyKeyExpr, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + session + .declare_querier(key_expr) + .congestion_control(congestion_control) + .consolidation(consolidation) + .express(express) + .target(query_target) + .priority(priority) + .timeout(timeout) + .accept_replies(reply_key_expr) + .wait() + .map(|querier| { + trace!("Declared querier on '{}'.", key_expr_string); + querier + }) + .map_err(|err| { + error!("Unable to declare querier on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + +/// Declare a queryable through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_queryable( + session: &Session, + key_expr: KeyExpr<'static>, + callback: impl Fn(Query) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, + complete: bool, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + let guard = CallOnDrop::new(on_close); + session + .declare_queryable(key_expr) + .callback(move |query| { + let _ = &guard; // capture the guard + callback(query); + }) + .complete(complete) + .wait() + .map(|queryable| { + trace!("Declared queryable on '{}'.", key_expr_string); + queryable + }) + .map_err(|err| { + error!("Unable to declare queryable on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + +/// Perform a get (query) through an existing Zenoh session. +/// +/// Each [`Reply`] received from the network is delivered to the `callback`. +/// `selector_params` is appended to the `key_expr` to form the query selector +/// (pass `None` for no parameters). `payload` and `encoding` are coupled: +/// if a payload is given, encoding is attached; if no payload is given, +/// encoding is ignored. +/// +/// Parameter order matches the JNI calling convention so the wrapper can be +/// generated by `zenoh_flat::jni_converter` without reordering. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn get( + session: &Session, + key_expr: KeyExpr<'static>, + selector_params: Option, + callback: impl Fn(Reply) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, + timeout: Duration, + query_target: QueryTarget, + consolidation: ConsolidationMode, + attachment: Option>, + payload: Option>, + encoding: Option, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + reply_key_expr: ReplyKeyExpr, +) -> ZResult<()> { + let key_expr_string = key_expr.to_string(); + let selector = Selector::owned(&key_expr, selector_params.unwrap_or_default()); + let guard = CallOnDrop::new(on_close); + let mut get_builder = session + .get(selector) + .callback(move |reply| { + let _ = &guard; // capture the guard + callback(reply); + }) + .target(query_target) + .consolidation(consolidation) + .congestion_control(congestion_control) + .priority(priority) + .express(express) + .timeout(timeout) + .accept_replies(reply_key_expr); + + if let Some(payload) = payload { + if let Some(encoding) = encoding { + get_builder = get_builder.encoding(encoding); + } + get_builder = get_builder.payload(payload); + } + + if let Some(attachment) = attachment { + get_builder = get_builder.attachment::>(attachment); + } + + get_builder + .wait() + .map(|_| { + trace!("Performing get on '{}'.", key_expr_string); + }) + .map_err(|err| { + error!("Unable to perform get on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + +/// Perform a put operation through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn put( + session: &Session, + key_expr: KeyExpr<'static>, + payload: Vec, + encoding: Encoding, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + attachment: Option>, + reliability: Reliability, +) -> ZResult<()> { + let key_expr_string = key_expr.to_string(); + let mut put_builder = session + .put(&key_expr, payload) + .congestion_control(congestion_control) + .encoding(encoding) + .express(express) + .priority(priority) + .reliability(reliability); + + if let Some(attachment) = attachment { + put_builder = put_builder.attachment(attachment); + } + + put_builder + .wait() + .map(|_| { + trace!("Put on '{}'.", key_expr_string); + }) + .map_err(|err| { + error!("Unable to put on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + +/// Perform a delete operation through an existing Zenoh session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn delete( + session: &Session, + key_expr: KeyExpr<'static>, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + attachment: Option>, + reliability: Reliability, +) -> ZResult<()> { + let key_expr_string = key_expr.to_string(); + let mut delete_builder = session + .delete(&key_expr) + .congestion_control(congestion_control) + .express(express) + .priority(priority) + .reliability(reliability); + + if let Some(attachment) = attachment { + delete_builder = delete_builder.attachment(attachment); + } + + delete_builder + .wait() + .map(|_| { + trace!("Delete on '{}'.", key_expr_string); + }) + .map_err(|err| { + error!("Unable to delete on '{}': {}", key_expr_string, err); + zerror!(err) + }) +} + +/// Return the Zenoh ID of the session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn get_zid(session: &Session) -> ZResult { + Ok(session.info().zid().wait()) +} + +/// Return the Zenoh IDs of the peers connected to this session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn get_peers_zid(session: &Session) -> ZResult> { + Ok(session.info().peers_zid().wait().collect()) +} + +/// Return the Zenoh IDs of the routers connected to this session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn get_routers_zid(session: &Session) -> ZResult> { + Ok(session.info().routers_zid().wait().collect()) +} + +/// Close a Zenoh session using a reference to the session. +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn close_session(session: &Session) -> ZResult<()> { + session + .close() + .wait() + .map(|_| { + trace!("Closed Zenoh session."); + }) + .map_err(|err| { + error!("Unable to close session: {}", err); + zerror!(err) + }) +} + +/// Declare an advanced subscriber through an existing Zenoh session. +/// +/// Builds on top of the regular subscriber chain and applies the supplied +/// advanced configuration. The JNI wrapper is expected to assemble the +/// `HistoryConfig` / `RecoveryConfig` from primitive arguments and pass them +/// here. +#[cfg(feature = "zenoh-ext")] +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_advanced_subscriber( + session: &Session, + key_expr: KeyExpr<'static>, + callback: impl Fn(Sample) + Send + Sync + 'static, + on_close: impl Fn() + Send + Sync + 'static, + history: Option, + recovery: Option, + subscriber_detection: bool, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + let guard = CallOnDrop::new(on_close); + let mut builder = session + .declare_subscriber(key_expr) + .callback(move |sample| { + let _ = &guard; // capture the guard + callback(sample); + }) + .advanced(); + if let Some(history) = history { + builder = builder.history(history.try_into()?); + } + if let Some(recovery) = recovery { + builder = builder.recovery(recovery.try_into()?); + } + if subscriber_detection { + builder = builder.subscriber_detection(); + } + builder + .wait() + .map(|subscriber| { + trace!("Declared advanced subscriber on '{}'.", key_expr_string); + subscriber + }) + .map_err(|err| { + error!( + "Unable to declare advanced subscriber on '{}': {}", + key_expr_string, err + ); + zerror!(err) + }) +} + +/// Declare an advanced publisher through an existing Zenoh session. +/// +/// Builds on top of the regular publisher chain and applies the supplied +/// advanced configuration. The JNI wrapper is expected to assemble the +/// `CacheConfig` / `MissDetectionConfig` from primitive arguments and pass +/// them here. +#[cfg(feature = "zenoh-ext")] +#[prebindgen_proc_macro::prebindgen("jni")] +pub fn declare_advanced_publisher( + session: &Session, + key_expr: KeyExpr<'static>, + congestion_control: CongestionControl, + priority: Priority, + express: bool, + reliability: Reliability, + cache: Option, + sample_miss_detection: Option, + publisher_detection: bool, +) -> ZResult> { + let key_expr_string = key_expr.to_string(); + let mut builder = session + .declare_publisher(key_expr) + .congestion_control(congestion_control) + .priority(priority) + .express(express) + .reliability(reliability) + .advanced(); + if let Some(cache) = cache { + builder = builder.cache(cache.try_into()?); + } + if let Some(miss_detection) = sample_miss_detection { + builder = builder.sample_miss_detection(miss_detection.try_into()?); + } + if publisher_detection { + builder = builder.publisher_detection(); + } + builder + .wait() + .map(|publisher| { + trace!("Declared advanced publisher on '{}'.", key_expr_string); + publisher + }) + .map_err(|err| { + error!( + "Unable to declare advanced publisher on '{}': {}", + key_expr_string, err + ); + zerror!(err) + }) +} diff --git a/zenoh-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..e19b102c 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,47 @@ 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?.toJni(), + options.qos.congestionControl.value, + options.qos.priority.value, + options.qos.express, + options.acceptReplies.ordinal + ) + handler.receiver() + } ?: throw sessionClosedException } @Throws(ZError::class) @@ -656,42 +804,100 @@ 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?.toJni(), + 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()).toJni() + put( + keyExpr.jniKeyExpr, + keyExpr.keyExpr, + payload.into().bytes, + encoding, + 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/bytes/Encoding.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt index a4f77815..35995ff9 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt @@ -14,6 +14,8 @@ package io.zenoh.bytes +import io.zenoh.jni.JNIEncoding + /** * Default encoding values used by Zenoh. * @@ -499,4 +501,7 @@ class Encoding private constructor( override fun hashCode(): Int { return id.hashCode() } + + /** Project this public [Encoding] into a JNI-boundary holder. */ + internal fun toJni(): JNIEncoding = JNIEncoding(id, schema) } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/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/JNIKeyExpr.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt deleted file mode 100644 index 29e419e3..00000000 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ /dev/null @@ -1,104 +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.ZenohLoad -import io.zenoh.exceptions.ZError -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.keyexpr.SetIntersectionLevel - -internal class JNIKeyExpr(internal val ptr: Long) { - - companion object { - init { - ZenohLoad - } - - @Throws(ZError::class) - fun tryFrom(keyExpr: String): KeyExpr { - return KeyExpr(tryFromViaJNI(keyExpr)) - } - - @Throws(ZError::class) - fun autocanonize(keyExpr: String): KeyExpr { - return KeyExpr(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 - ) - - @Throws(ZError::class) - fun includes(keyExprA: KeyExpr, keyExprB: KeyExpr): Boolean = includesViaJNI( - keyExprA.jniKeyExpr?.ptr ?: 0, - keyExprA.keyExpr, - keyExprB.jniKeyExpr?.ptr ?: 0, - keyExprB.keyExpr - ) - - @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) - } - - @Throws(ZError::class) - fun joinViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(joinViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) - } - - @Throws(ZError::class) - fun concatViaJNI(keyExpr: KeyExpr, other: String): KeyExpr { - return KeyExpr(concatViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, other)) - } - - @Throws(ZError::class) - private external fun tryFromViaJNI(keyExpr: String): String - - @Throws(ZError::class) - private external fun autocanonizeViaJNI(keyExpr: String): String - - @Throws(ZError::class) - private external fun intersectsViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean - - @Throws(ZError::class) - private external fun includesViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean - - @Throws(ZError::class) - private external fun relationToViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int - - @Throws(ZError::class) - private external fun joinViaJNI(ptrA: Long, keyExprA: String, other: String): String - - @Throws(ZError::class) - private external fun concatViaJNI(ptrA: Long, keyExprA: String, other: String): String - } - - fun close() { - freePtrViaJNI(ptr) - } - - /** Frees the underlying native KeyExpr. */ - private external fun freePtrViaJNI(ptr: Long) -} 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..fee4ca14 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt @@ -79,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.toJni(), null) ?: throw publisherNotValid } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @Throws(ZError::class) fun put(payload: IntoZBytes, options: PutOptions) { - jniPublisher?.put(payload, options.encoding ?: this.encoding, options.attachment) ?: throw publisherNotValid + val enc = options.encoding ?: this.encoding + jniPublisher?.put(payload.into().bytes, enc.toJni(), options.attachment?.into()?.bytes) ?: throw publisherNotValid } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @@ -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..b7be5c16 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,72 @@ 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?.toJni() + ) } 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?.toJni() + ) + return handler.receiver() } } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt index 3a8184da..8902732a 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,21 @@ 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 ?: Encoding.defaultEncoding()).toJni(), + 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 +103,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 +127,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 ?: Encoding.defaultEncoding()).toJni()) 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..716137b8 --- /dev/null +++ b/zenoh-jni-runtime/build.gradle.kts @@ -0,0 +1,254 @@ +// +// 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 { + kotlin.srcDir("$rootDir/zenoh-jni/generated-kotlin") + } + // jvmAndAndroidMain is an intermediate source set between commonMain and both jvmMain/androidMain. + // It holds code that uses java.lang.reflect.Type — available on JVM and Android (ART), + // but absent on Kotlin/Native and Kotlin/JS targets. Placing such code here (rather than + // 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..619a8ab5 --- /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, encoding: JNIEncoding, attachment: ByteArray?) { + putViaJNI(ptr, payload, encoding, 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, encoding: JNIEncoding, 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-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIEncoding.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIEncoding.kt new file mode 100644 index 00000000..f4b7a6f3 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIEncoding.kt @@ -0,0 +1,26 @@ +// +// Copyright (c) 2026 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh.jni + +/** + * JNI-boundary holder for a Zenoh encoding. + * + * Marshaled across JNI as a single object; the native side reads [id] and + * [schema] via `env.get_field(...)`. Higher layers (e.g. `zenoh-java`'s + * `io.zenoh.bytes.Encoding`) construct this wrapper at the JNI call + * boundary. Keeping it here preserves the layering rule that + * `zenoh-jni-runtime` must not depend on `zenoh-java`. + */ +data class JNIEncoding(val id: Int, val schema: String?) diff --git a/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt new file mode 100644 index 00000000..4afb60da --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt @@ -0,0 +1,105 @@ +// +// 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 native Zenoh key expressions. + * + * Carries both the native-handle pointer and the source string so it can be + * passed across JNI as a single `JObject`. `ptr == 0L` means "not declared on + * a session — use [str] at the native side"; `ptr != 0L` means "declared — + * use the pointer and ignore [str]". + */ +public class JNIKeyExpr(internal val ptr: Long, internal val str: String) { + + companion object { + init { + ZenohLoad + } + + /** + * Build a JNI holder for an undeclared key expression (string-only). + * Used at JNI call boundaries where the caller may not have a + * declared [JNIKeyExpr] and needs to pass the raw string instead. + */ + fun undeclared(keyExpr: String): JNIKeyExpr = JNIKeyExpr(0L, keyExpr) + + /** + * Build a JNI holder from an optional declared expression and a + * source string. If [declared] is non-null its pointer is preserved + * (and the string is passed along for diagnostics / fallback); + * otherwise the result represents the undeclared [keyExpr]. + */ + fun of(declared: JNIKeyExpr?, keyExpr: String): JNIKeyExpr = + declared ?: JNIKeyExpr(0L, keyExpr) + + @Throws(ZError::class) + fun tryFrom(keyExpr: String): String = tryFromViaJNI(keyExpr) + + @Throws(ZError::class) + fun autocanonize(keyExpr: String): String = autocanonizeViaJNI(keyExpr) + + @Throws(ZError::class) + private external fun tryFromViaJNI(keyExpr: String): String + + @Throws(ZError::class) + private external fun autocanonizeViaJNI(keyExpr: String): String + + @Throws(ZError::class) + fun intersects(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Boolean = + intersectsViaJNI(a?.ptr ?: 0, aStr, b?.ptr ?: 0, bStr) + + @Throws(ZError::class) + 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 relationTo(a: JNIKeyExpr?, aStr: String, b: JNIKeyExpr?, bStr: String): Int = + relationToViaJNI(a?.ptr ?: 0, aStr, b?.ptr ?: 0, bStr) + + @Throws(ZError::class) + fun join(a: JNIKeyExpr?, aStr: String, other: String): String = + joinViaJNI(a?.ptr ?: 0, aStr, other) + + @Throws(ZError::class) + fun concat(a: JNIKeyExpr?, aStr: String, other: String): String = + concatViaJNI(a?.ptr ?: 0, aStr, other) + + @Throws(ZError::class) + private external fun intersectsViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean + + @Throws(ZError::class) + private external fun includesViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Boolean + + @Throws(ZError::class) + private external fun relationToViaJNI(ptrA: Long, keyExprA: String, ptrB: Long, keyExprB: String): Int + + @Throws(ZError::class) + private external fun joinViaJNI(ptrA: Long, keyExprA: String, other: String): String + + @Throws(ZError::class) + private external fun concatViaJNI(ptrA: Long, keyExprA: String, other: String): String + } + + fun close() { + freePtrViaJNI(ptr) + } + + 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..f220d369 --- /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, encoding: JNIEncoding, attachment: ByteArray?) { + putViaJNI(ptr, payload, encoding, 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, encoding: JNIEncoding, 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..408b4307 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuerier.kt @@ -0,0 +1,55 @@ +// +// 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?, + encoding: JNIEncoding?, + ) { + getViaJNI(ptr, JNIKeyExpr.of(jniKeyExpr, keyExprString), parameters, callback, onClose, attachmentBytes, payload, encoding) + } + + @Throws(ZError::class) + private external fun getViaJNI( + querierPtr: Long, + keyExpr: JNIKeyExpr, + parameters: String?, + callback: JNIGetCallback, + onClose: JNIOnCloseCallback, + attachmentBytes: ByteArray?, + payload: ByteArray?, + encoding: JNIEncoding?, + ) + + 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..372da155 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -0,0 +1,91 @@ +// +// 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, + encoding: JNIEncoding, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) { + replySuccessViaJNI(ptr, JNIKeyExpr.of(jniKeyExpr, keyExprString), payload, encoding, timestampEnabled, timestampNtp64, attachment, qosExpress) + } + + @Throws(ZError::class) + fun replyError(errorPayload: ByteArray, encoding: JNIEncoding) { + replyErrorViaJNI(ptr, errorPayload, encoding) + } + + @Throws(ZError::class) + fun replyDelete( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) { + replyDeleteViaJNI(ptr, JNIKeyExpr.of(jniKeyExpr, keyExprString), timestampEnabled, timestampNtp64, attachment, qosExpress) + } + + fun close() { + freePtrViaJNI(ptr) + } + + @Throws(ZError::class) + private external fun replySuccessViaJNI( + queryPtr: Long, + keyExpr: JNIKeyExpr, + valuePayload: ByteArray, + valueEncoding: JNIEncoding, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + ) + + @Throws(ZError::class) + private external fun replyErrorViaJNI( + queryPtr: Long, + errorValuePayload: ByteArray, + errorValueEncoding: JNIEncoding, + ) + + @Throws(ZError::class) + private external fun replyDeleteViaJNI( + queryPtr: Long, + keyExpr: JNIKeyExpr, + 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..bac21ad5 --- /dev/null +++ b/zenoh-jni-runtime/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -0,0 +1,248 @@ +// +// 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.JNISessionNative.closeSessionViaJNI +import io.zenoh.jni.JNISessionNative.declareAdvancedPublisherViaJNI +import io.zenoh.jni.JNISessionNative.declareAdvancedSubscriberViaJNI +import io.zenoh.jni.JNISessionNative.declareKeyExprViaJNI +import io.zenoh.jni.JNISessionNative.declarePublisherViaJNI +import io.zenoh.jni.JNISessionNative.declareQuerierViaJNI +import io.zenoh.jni.JNISessionNative.declareQueryableViaJNI +import io.zenoh.jni.JNISessionNative.declareSubscriberViaJNI +import io.zenoh.jni.JNISessionNative.deleteViaJNI +import io.zenoh.jni.JNISessionNative.getPeersZidViaJNI +import io.zenoh.jni.JNISessionNative.getRoutersZidViaJNI +import io.zenoh.jni.JNISessionNative.getViaJNI +import io.zenoh.jni.JNISessionNative.getZidViaJNI +import io.zenoh.jni.JNISessionNative.openSessionViaJNI +import io.zenoh.jni.JNISessionNative.putViaJNI +import io.zenoh.jni.JNISessionNative.undeclareKeyExprViaJNI +import io.zenoh.jni.callbacks.JNIGetCallback +import io.zenoh.jni.callbacks.JNIOnCloseCallback +import io.zenoh.jni.callbacks.JNIQueryableCallback +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) + } + } + + @Throws(ZError::class) + fun declarePublisher( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + congestionControl: Int, + priority: Int, + express: Boolean, + reliability: Int + ): JNIPublisher = JNIPublisher(declarePublisherViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), congestionControl, priority, express, reliability)) + + @Throws(ZError::class) + fun declareSubscriber( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(declareSubscriberViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, onClose)) + + @Throws(ZError::class) + fun declareQueryable( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + callback: JNIQueryableCallback, + onClose: JNIOnCloseCallback, + complete: Boolean + ): JNIQueryable = JNIQueryable(declareQueryableViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, onClose, complete)) + + @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.of(jniKeyExpr, keyExprString), target, consolidation, congestionControl, priority, express, timeoutMs, acceptReplies)) + + @Throws(ZError::class) + fun declareKeyExpr(keyExpr: String): JNIKeyExpr = JNIKeyExpr(declareKeyExprViaJNI(sessionPtr, keyExpr), keyExpr) + + @Throws(ZError::class) + fun undeclareKeyExpr(jniKeyExpr: JNIKeyExpr) = undeclareKeyExprViaJNI(sessionPtr, jniKeyExpr) + + @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?, + encoding: JNIEncoding?, + congestionControl: Int, + priority: Int, + express: Boolean, + acceptReplies: Int, + ) = getViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), selectorParams, callback, onClose, timeoutMs, target, consolidation, attachmentBytes, payload, encoding, congestionControl, priority, express, acceptReplies) + + @Throws(ZError::class) + fun put( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + valuePayload: ByteArray, + valueEncoding: JNIEncoding, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) = putViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), valuePayload, valueEncoding, congestionControl, priority, express, attachmentBytes, reliability) + + @Throws(ZError::class) + fun delete( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + congestionControl: Int, + priority: Int, + express: Boolean, + attachmentBytes: ByteArray?, + reliability: Int + ) = deleteViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), congestionControl, priority, express, attachmentBytes, reliability) + + @Throws(ZError::class) + fun getZid(): ByteArray = getZidViaJNI(sessionPtr) + + @Throws(ZError::class) + fun getPeersZid(): List = getPeersZidViaJNI(sessionPtr) + + @Throws(ZError::class) + fun getRoutersZid(): List = getRoutersZidViaJNI(sessionPtr) + + @Throws(ZError::class) + fun declareAdvancedSubscriber( + jniKeyExpr: JNIKeyExpr?, + keyExprStr: String, + callback: JNISubscriberCallback, + onClose: JNIOnCloseCallback, + history: HistoryConfig?, + recovery: RecoveryConfig?, + subscriberDetection: Boolean, + ): JNIAdvancedSubscriber = JNIAdvancedSubscriber( + declareAdvancedSubscriberViaJNI( + sessionPtr, + JNIKeyExpr.of(jniKeyExpr, keyExprStr), + callback, + onClose, + history, + recovery, + subscriberDetection, + ) + ) + + @Throws(ZError::class) + fun declareAdvancedPublisher( + jniKeyExpr: JNIKeyExpr?, + keyExprStr: String, + congestionControl: Int, + priority: Int, + isExpress: Boolean, + reliability: Int, + cache: CacheConfig?, + sampleMissDetection: MissDetectionConfig?, + publisherDetection: Boolean, + ): JNIAdvancedPublisher = JNIAdvancedPublisher( + declareAdvancedPublisherViaJNI( + sessionPtr, + JNIKeyExpr.of(jniKeyExpr, keyExprStr), + congestionControl, + priority, + isExpress, + reliability, + cache, + sampleMissDetection, + publisherDetection, + ) + ) + + @Throws(ZError::class) + fun declareLivelinessToken(jniKeyExpr: JNIKeyExpr?, keyExprString: String): JNILivelinessToken = + JNILivelinessToken(declareLivelinessTokenViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString))) + + @Throws(ZError::class) + private external fun declareLivelinessTokenViaJNI(sessionPtr: Long, keyExpr: JNIKeyExpr): Long + + @Throws(ZError::class) + fun declareLivelinessSubscriber( + jniKeyExpr: JNIKeyExpr?, + keyExprString: String, + callback: JNISubscriberCallback, + history: Boolean, + onClose: JNIOnCloseCallback, + ): JNISubscriber = JNISubscriber(declareLivelinessSubscriberViaJNI(sessionPtr, JNIKeyExpr.of(jniKeyExpr, keyExprString), callback, history, onClose)) + + @Throws(ZError::class) + private external fun declareLivelinessSubscriberViaJNI( + sessionPtr: Long, + keyExpr: JNIKeyExpr, + 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.of(jniKeyExpr, keyExprString), callback, timeoutMs, onClose) + + @Throws(ZError::class) + private external fun livelinessGetViaJNI( + sessionPtr: Long, + keyExpr: JNIKeyExpr, + callback: JNIGetCallback, + timeoutMs: Long, + onClose: JNIOnCloseCallback, + ) + + fun close() { + closeSessionViaJNI(sessionPtr) + freePtrViaJNI(sessionPtr) + } + + private external fun freePtrViaJNI(ptr: Long) +} 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.lock b/zenoh-jni/Cargo.lock index bd456c8a..ccbe3ee5 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -350,6 +350,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1094,6 +1103,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if_rust_version" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" + [[package]] name = "indexmap" version = "1.9.3" @@ -1135,6 +1150,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1233,6 +1257,33 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1790,6 +1841,52 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prebindgen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b005ec2d8a59f83ecc1b63ef25ab4d42fa33d9642e24d5d8dab5e750d5f6f46" +dependencies = [ + "if_rust_version", + "itertools 0.14.0", + "konst", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "roxygen", + "serde", + "serde_json", + "syn 2.0.117", + "toml", +] + +[[package]] +name = "prebindgen-ext" +version = "1.9.0" +dependencies = [ + "itertools 0.12.1", + "prebindgen", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prebindgen-proc-macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a68d8870ef9ba6f8abbe9d4bca5775e1d854d5fa6dac8c27dd1c79ecebe398" +dependencies = [ + "prebindgen", + "proc-macro2", + "quote", + "rand 0.9.2", + "serde", + "serde_json", + "syn 2.0.117", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2085,6 +2182,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "roxygen" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa650dd372f29f0a6be64b2896707f9536962ba28915e3b39bcafd5a6221873b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3053,6 +3161,21 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "ucd-trie" version = "0.1.7" @@ -3811,7 +3934,7 @@ dependencies = [ "flume 0.11.1", "futures", "git-version", - "itertools", + "itertools 0.14.0", "json5", "lazy_static", "nonempty-collections", @@ -3942,6 +4065,20 @@ dependencies = [ "zenoh-util", ] +[[package]] +name = "zenoh-flat" +version = "1.9.0" +dependencies = [ + "json5", + "prebindgen", + "prebindgen-ext", + "prebindgen-proc-macro", + "serde_yaml", + "tracing", + "zenoh", + "zenoh-ext", +] + [[package]] name = "zenoh-keyexpr" version = "1.9.0" @@ -4309,14 +4446,20 @@ dependencies = [ "async-std", "clap", "flume 0.10.14", + "itertools 0.12.1", "jni", "json5", + "konst", + "prebindgen", + "quote", "rustc_version", "serde_yaml", + "syn 2.0.117", "tracing", "uhlc", "zenoh", "zenoh-ext", + "zenoh-flat", ] [[package]] diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index a88e31ec..b34d4a39 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -25,6 +25,7 @@ name = "zenoh_jni" [features] default = ["zenoh/default", "zenoh-ext"] +zenoh-ext = ["dep:zenoh-ext", "zenoh-flat/zenoh-ext"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -37,14 +38,21 @@ 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"] } +zenoh-flat = { path = "../zenoh-flat" } +konst = "0.3.0" [lib] name = "zenoh_jni" crate_type = ["staticlib", "dylib"] [build-dependencies] rustc_version = "0.4.0" +prebindgen = "0.4.1" +zenoh-flat = { path = "../zenoh-flat", features = ["zenoh-ext"] } +itertools = "0.12" +syn = "2" +quote = "1" [profile.release] debug = false # If you want debug symbol in release mode, set the env variable: RUSTFLAGS=-g diff --git a/zenoh-jni/build.rs b/zenoh-jni/build.rs new file mode 100644 index 00000000..9214a6ca --- /dev/null +++ b/zenoh-jni/build.rs @@ -0,0 +1,228 @@ +use itertools::Itertools; +use zenoh_flat::jni_converter::{ + InlineFn, JniMethodsConverter, JniStructConverter, TypeBinding, +}; +use zenoh_flat::jni_type_binding::{JniTypeBinding, ReturnEncode}; + +const OWNED_OBJECT: &str = "crate::owned_object::OwnedObject"; + +/// Type vocabulary shared across every JNI surface generated in this crate. +/// Defined once, threaded into the struct-phase converter, then forwarded — +/// together with the auto-registered struct bindings — into the methods phase. +fn shared_bindings() -> JniTypeBinding { + JniTypeBinding::new() + // Strings & byte arrays. + .type_binding(TypeBinding::param( + "String", + "String", + "jni::objects::JString", + InlineFn::env_ref_mut("crate::utils::decode_string"), + )) + .type_binding(TypeBinding::param( + "Vec", + "ByteArray", + "jni::objects::JByteArray", + InlineFn::env_ref("crate::utils::decode_byte_array"), + )) + // Callbacks. + .type_binding(TypeBinding::param( + "impl Fn(Sample) + Send + Sync + 'static", + "io.zenoh.jni.callbacks.JNISubscriberCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_sample_callback"), + )) + .type_binding(TypeBinding::param( + "impl Fn(Query) + Send + Sync + 'static", + "io.zenoh.jni.callbacks.JNIQueryableCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_query_callback"), + )) + .type_binding(TypeBinding::param( + "impl Fn(Reply) + Send + Sync + 'static", + "io.zenoh.jni.callbacks.JNIGetCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_reply_callback"), + )) + .type_binding(TypeBinding::param( + "impl Fn() + Send + Sync + 'static", + "io.zenoh.jni.callbacks.JNIOnCloseCallback", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::sample_callback::process_kotlin_on_close_callback"), + )) + // Java-enum-shaped types. + .type_binding(TypeBinding::param( + "CongestionControl", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_congestion_control"), + )) + .type_binding(TypeBinding::param( + "Priority", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_priority"), + )) + .type_binding(TypeBinding::param( + "Reliability", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_reliability"), + )) + .type_binding(TypeBinding::param( + "QueryTarget", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_query_target"), + )) + .type_binding(TypeBinding::param( + "ConsolidationMode", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_consolidation"), + )) + .type_binding(TypeBinding::param( + "ReplyKeyExpr", + "Int", + "jni::sys::jint", + InlineFn::pure("crate::utils::decode_reply_key_expr"), + )) + // KeyExpr by-value: JNI side passes the JNIKeyExpr holder object + // (`ptr: Long`, `str: String`), decoded through decode_jni_key_expr. + .type_binding(TypeBinding::param( + "KeyExpr<'static>", + "io.zenoh.jni.JNIKeyExpr", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::key_expr::decode_jni_key_expr"), + )) + // Encoding via JObject + custom decoder. + .type_binding(TypeBinding::param( + "Encoding", + "io.zenoh.jni.JNIEncoding", + "jni::objects::JObject", + InlineFn::env_ref_mut("crate::utils::decode_jni_encoding"), + )) + // Borrows: opaque Arc handles received as `*const T` and re-borrowed + // via OwnedObject::from_raw. The `&` prefix on the row's key tells + // the converter to pass `&name` to the wrapped fn. + .type_binding(TypeBinding::opaque_borrow("Session", OWNED_OBJECT)) + .type_binding(TypeBinding::opaque_borrow("Config", OWNED_OBJECT)) + // Returns: ZenohId / Vec via custom encoders. + .type_binding(TypeBinding::returns( + "ZResult", + "ByteArray", + "jni::sys::jbyteArray", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_id_to_byte_array"), + "jni::objects::JByteArray::default().as_raw()", + )) + .type_binding(TypeBinding::returns( + "ZResult>", + "List", + "jni::sys::jobject", + ReturnEncode::wrapper("crate::zenoh_id::zenoh_ids_to_java_list"), + "jni::objects::JObject::default().as_raw()", + )) + // Returns: opaque Arc handles. Each emits `*const T` and + // `Arc::into_raw(Arc::new(__result))` with a null default. + .type_binding(TypeBinding::opaque_arc_return("Session")) + .type_binding(TypeBinding::opaque_arc_return("Publisher<'static>")) + .type_binding(TypeBinding::opaque_arc_return("KeyExpr<'static>")) + .type_binding(TypeBinding::opaque_arc_return("Subscriber<()>")) + .type_binding(TypeBinding::opaque_arc_return("Querier<'static>")) + .type_binding(TypeBinding::opaque_arc_return("Queryable<()>")) + .type_binding(TypeBinding::opaque_arc_return("AdvancedSubscriber<()>")) + .type_binding(TypeBinding::opaque_arc_return("AdvancedPublisher<'static>")) +} + +/// Build `Option` rows for every X that can appear under `Option<...>` in +/// the wrapped fn signatures. Must be called after struct-converter has +/// registered the auto-generated struct rows. +fn add_option_rows(types: JniTypeBinding) -> JniTypeBinding { + let inner_keys = [ + "String", + "Vec", + "Encoding", + "HistoryConfig", + "RecoveryConfig", + "CacheConfig", + "MissDetectionConfig", + ]; + let mut out = types; + for key in inner_keys { + let inner = out + .type_by_key(key) + .unwrap_or_else(|| panic!("add_option_rows: missing inner row `{}`", key)) + .clone(); + let opt = TypeBinding::option_of(&inner); + out = out.type_binding(opt); + } + out +} + +fn main() { + let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); + + // Phase 1: process #[prebindgen] structs from zenoh_flat::ext. + // Each struct adds a TypeBinding (and a Kotlin data class) to the + // shared JniTypeBinding that we forward to the methods converter. + let mut struct_conv = JniStructConverter::builder() + .source_module("zenoh_flat::ext") + .zresult("crate::errors::ZResult") + .jni_type_binding(shared_bindings()) + .build(); + + let struct_items: Vec<_> = source + .items_all() + .filter(|(item, loc)| { + matches!(item, syn::Item::Struct(_)) && loc.file.ends_with("/ext.rs") + }) + .batching(struct_conv.as_closure()) + .collect(); + + let types = add_option_rows(struct_conv.into_jni_type_binding()); + + // Phase 2: process #[prebindgen] fns from zenoh_flat::session against + // the now fully-populated type registry. + let mut method_conv = JniMethodsConverter::builder() + .class_prefix("Java_io_zenoh_jni_JNISessionNative_") + .function_suffix("ViaJNI") + .source_module("zenoh_flat::session") + .zresult("crate::errors::ZResult") + .throw_exception("crate::throw_exception") + .kotlin_output("../zenoh-jni/generated-kotlin/io/zenoh/jni/JNISessionNative.kt") + .kotlin_package("io.zenoh.jni") + .kotlin_class("JNISessionNative") + .kotlin_throws("io.zenoh.exceptions.ZError") + .kotlin_init("io.zenoh.ZenohLoad") + .jni_type_binding(types) + .build(); + + let method_items: Vec<_> = source + .items_all() + .filter(|(item, loc)| { + matches!(item, syn::Item::Fn(_)) && loc.file.ends_with("/session.rs") + }) + .batching(method_conv.as_closure()) + .collect(); + + // Pass-through: items that are neither `#[prebindgen]` structs nor fns + // (e.g. the prebindgen feature-mismatch assertion `const _: () = { ... };`). + let passthrough = source + .items_all() + .filter(|(item, _)| !matches!(item, syn::Item::Fn(_) | syn::Item::Struct(_))); + + let bindings_file = struct_items + .into_iter() + .chain(method_items) + .chain(passthrough) + .collect::() + .write("zenoh_flat_jni.rs"); + + println!( + "cargo:warning=Generated bindings at: {}", + bindings_file.display() + ); + + method_conv + .write_kotlin() + .expect("failed to write generated Kotlin file"); +} diff --git a/zenoh-jni/src/config.rs b/zenoh-jni/src/config.rs index 0ada1340..54c966ad 100644 --- a/zenoh-jni/src/config.rs +++ b/zenoh-jni/src/config.rs @@ -21,7 +21,8 @@ use jni::{ }; use zenoh::Config; -use crate::{errors::ZResult, zerror}; +use crate::errors::ZResult; +use crate::owned_object::OwnedObject; use crate::{throw_exception, utils::decode_string}; /// Loads the default configuration, returning a raw pointer to it. @@ -78,12 +79,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadJsonConfigViaJN ) -> *const Config { || -> ZResult<*const Config> { let json_config = decode_string(&mut env, &json_config)?; - let mut deserializer = - json5::Deserializer::from_str(&json_config).map_err(|err| zerror!(err))?; - let config = Config::from_deserializer(&mut deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("JSON error: {}", e), - })?; + let config = zenoh_flat::config::load_json_config(&json_config)?; Ok(Arc::into_raw(Arc::new(config))) }() .unwrap_or_else(|err| { @@ -107,11 +103,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIConfig_00024Companion_loadYamlConfigViaJN ) -> *const Config { || -> ZResult<*const Config> { let yaml_config = decode_string(&mut env, &yaml_config)?; - let deserializer = serde_yaml::Deserializer::from_str(&yaml_config); - let config = Config::from_deserializer(deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("YAML error: {}", e), - })?; + let config = zenoh_flat::config::load_yaml_config(&yaml_config)?; Ok(Arc::into_raw(Arc::new(config))) }() .unwrap_or_else(|err| { @@ -130,8 +122,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 +132,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/errors.rs b/zenoh-jni/src/errors.rs index 23687d4c..09dfdd32 100644 --- a/zenoh-jni/src/errors.rs +++ b/zenoh-jni/src/errors.rs @@ -12,46 +12,30 @@ // ZettaScale Zenoh Team, // -use std::fmt; - use jni::JNIEnv; #[macro_export] macro_rules! throw_exception { ($env:expr, $err:expr) => {{ - let _ = $err.throw_on_jvm(&mut $env).map_err(|err| { - tracing::error!("Unable to throw exception: {}", err); - }); + let _ = + ::throw_on_jvm(&$err, &mut $env) + .map_err(|err| { + tracing::error!("Unable to throw exception: {}", err); + }); }}; } -#[macro_export] -macro_rules! zerror { - ($arg:expr) => { - $crate::errors::ZError($arg.to_string()) - }; - ($fmt:expr, $($arg:tt)*) => { - $crate::errors::ZError(format!($fmt, $($arg)*)) - }; -} - +pub(crate) type ZError = zenoh_flat::errors::ZError; pub(crate) type ZResult = core::result::Result; -#[derive(Debug)] -pub(crate) struct ZError(pub String); - -impl fmt::Display for ZError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } +pub(crate) trait ThrowOnJvm { + fn throw_on_jvm(&self, env: &mut JNIEnv) -> ZResult<()>; } -impl ZError { - const KOTLIN_EXCEPTION_NAME: &'static str = "io/zenoh/exceptions/ZError"; - - pub fn throw_on_jvm(&self, env: &mut JNIEnv) -> ZResult<()> { +impl ThrowOnJvm for ZError { + fn throw_on_jvm(&self, env: &mut JNIEnv) -> ZResult<()> { let exception_class = env - .find_class(Self::KOTLIN_EXCEPTION_NAME) + .find_class("io/zenoh/exceptions/ZError") .map_err(|err| zerror!("Failed to retrieve exception class: {}", err))?; env.throw_new(exception_class, self.to_string()) .map_err(|err| zerror!("Failed to throw exception: {}", err)) diff --git a/zenoh-jni/src/ext/advanced_publisher.rs b/zenoh-jni/src/ext/advanced_publisher.rs new file mode 100644 index 00000000..ca3033bb --- /dev/null +++ b/zenoh-jni/src/ext/advanced_publisher.rs @@ -0,0 +1,337 @@ +// +// 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}, + 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_jni_encoding}, +}; +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: JObject, + attachment: /*nullable*/ JByteArray, +) { + let publisher = OwnedObject::from_raw(publisher_ptr); + let _ = || -> ZResult<()> { + let payload = decode_byte_array(&env, &payload)?; + let mut publication = publisher.put(payload); + let encoding = decode_jni_encoding(&mut env, &encoding)?; + 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..6e7f9c18 --- /dev/null +++ b/zenoh-jni/src/ext/advanced_subscriber.rs @@ -0,0 +1,362 @@ +// +// 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::process_kotlin_sample_callback; +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, wrap_with_on_close}; +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 cb = process_kotlin_sample_callback(&mut env, &callback)?; + let cb = wrap_with_on_close(&mut env, on_close, cb)?; + let detect_publishers_subscriber = advanced_subscriber + .detect_publishers() + .history(history != 0) + .callback(cb) + .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() + ); + + let cb = process_kotlin_sample_callback(&mut env, &callback)?; + let cb = wrap_with_on_close(&mut env, on_close, cb)?; + advanced_subscriber + .detect_publishers() + .history(history != 0) + .callback(cb) + .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..e709c6f7 --- /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, +// + +pub(crate) mod advanced_publisher; +pub(crate) 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..2b52c9ac 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,8 +20,9 @@ use jni::{objects::JString, JNIEnv}; use zenoh::key_expr::KeyExpr; use crate::errors::ZResult; +use crate::owned_object::OwnedObject; +use crate::throw_exception; use crate::utils::decode_string; -use crate::{throw_exception, zerror}; /// Validates the provided `key_expr` to be a valid key expression, returning it back /// in case of success or throwing an exception in case of failure. @@ -325,9 +325,28 @@ 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()) } } + +/// Decode a Kotlin `io.zenoh.jni.JNIKeyExpr` holder into a +/// [`KeyExpr<'static>`]. The holder carries `ptr: Long` and `str: String`; +/// `ptr != 0` means the KeyExpr was declared on a session and the pointer is +/// an `Arc::into_raw(Arc::new(KeyExpr))`; `ptr == 0` means to build the +/// expression from the string. +pub(crate) unsafe fn decode_jni_key_expr( + env: &mut JNIEnv, + obj: &jni::objects::JObject, +) -> ZResult> { + let ptr = env + .get_field(obj, "ptr", "J") + .and_then(|v| v.j()) + .map_err(|err| zerror!("JNIKeyExpr.ptr: {}", err))?; + let str_obj = env + .get_field(obj, "str", "Ljava/lang/String;") + .and_then(|v| v.l()) + .map_err(|err| zerror!("JNIKeyExpr.str: {}", err))?; + let str_js: JString = str_obj.into(); + process_kotlin_key_expr(env, &str_js, ptr as *const KeyExpr<'static>) +} diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index ff3981a4..77725468 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -12,21 +12,30 @@ // ZettaScale Zenoh Team, // +#[macro_use] +extern crate zenoh_flat; + mod config; mod errors; +#[cfg(feature = "zenoh-ext")] +pub(crate) 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..fbfdaa2e 100644 --- a/zenoh-jni/src/liveliness.rs +++ b/zenoh-jni/src/liveliness.rs @@ -15,46 +15,41 @@ use std::{ptr::null, sync::Arc, time::Duration}; use jni::{ - objects::{JByteArray, JClass, JObject, JString, JValue}, - sys::{jboolean, jint, jlong}, + objects::{JClass, JObject}, + sys::{jboolean, jlong}, JNIEnv, }; use zenoh::{ - internal::runtime::ZRuntime, key_expr::KeyExpr, liveliness::LivelinessToken, - pubsub::Subscriber, sample::Sample, Session, Wait, + internal::runtime::ZRuntime, liveliness::LivelinessToken, pubsub::Subscriber, Session, Wait, }; use crate::{ errors::ZResult, - key_expr::process_kotlin_key_expr, - session::{on_reply_error, on_reply_success}, + key_expr::decode_jni_key_expr, + owned_object::OwnedObject, + sample_callback::{on_reply_error, on_reply_success, process_kotlin_sample_callback}, throw_exception, - utils::{ - bytes_to_java_array, get_callback_global_ref, get_java_vm, load_on_close, - slice_to_java_string, - }, - zerror, + utils::{get_callback_global_ref, get_java_vm, load_on_close, wrap_with_on_close}, }; #[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, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, callback: JObject, timeout_ms: jlong, on_close: JObject, ) { - let session = unsafe { 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 key_expr = unsafe { decode_jni_key_expr(&mut env, &key_expr) }?; let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let callback_global_ref = get_callback_global_ref(&mut env, &callback)?; + let on_close_global_ref = get_callback_global_ref(&mut env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let timeout = Duration::from_millis(timeout_ms as u64); let replies = session @@ -98,21 +93,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, + key_expr: JObject, ) -> *const LivelinessToken { - let session = unsafe { Arc::from_raw(session_ptr) }; - let ptr = || -> ZResult<*const LivelinessToken> { - let key_expr = unsafe { process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) }?; + let session = unsafe { OwnedObject::from_raw(session_ptr) }; + || -> ZResult<*const LivelinessToken> { + let key_expr = unsafe { decode_jni_key_expr(&mut env, &key_expr) }?; tracing::trace!("Declaring liveliness token on '{key_expr}'."); let token = session .liveliness() @@ -124,16 +117,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,98 +132,31 @@ 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, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, callback: JObject, history: jboolean, on_close: JObject, ) -> *const Subscriber<()> { - let session = 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 = decode_jni_key_expr(&mut env, &key_expr)?; tracing::debug!("Declaring liveliness subscriber on '{}'...", key_expr); - let result = session + let cb = process_kotlin_sample_callback(&mut env, &callback)?; + let cb = wrap_with_on_close(&mut env, on_close, cb)?; + let subscriber = session .liveliness() .declare_subscriber(key_expr.to_owned()) .history(history != 0) - .callback(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))?; + .callback(cb) + .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..c91ae0d7 100644 --- a/zenoh-jni/src/logger.rs +++ b/zenoh-jni/src/logger.rs @@ -17,7 +17,7 @@ use jni::{ JNIEnv, }; -use crate::{errors::ZResult, throw_exception, zerror}; +use crate::{errors::ZResult, throw_exception}; /// Redirects the Rust logs either to logcat for Android systems or to the standard output (for non-Android systems). /// @@ -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..37d90fc4 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -15,17 +15,16 @@ use std::sync::Arc; use jni::{ - objects::{JByteArray, JClass, JString}, - sys::jint, + objects::{JByteArray, JClass, JObject}, JNIEnv, }; use zenoh::{pubsub::Publisher, Wait}; +use crate::owned_object::OwnedObject; use crate::throw_exception; use crate::{ errors::ZResult, - utils::{decode_byte_array, decode_encoding}, - zerror, + utils::{decode_byte_array, decode_jni_encoding}, }; /// Performs a PUT operation on a Zenoh publisher via JNI. @@ -50,26 +49,24 @@ 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, + encoding: JObject, 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 payload = decode_byte_array(&env, &payload)?; let mut publication = publisher.put(payload); - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let encoding = decode_jni_encoding(&mut env, &encoding)?; publication = publication.encoding(encoding); if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; + 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)); - std::mem::forget(publisher); } /// Performs a DELETE operation on a Zenoh publisher via JNI. @@ -91,20 +88,19 @@ 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() { - let attachment = decode_byte_array(&env, attachment)?; + 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)); - std::mem::forget(publisher) } /// Frees the publisher. diff --git a/zenoh-jni/src/querier.rs b/zenoh-jni/src/querier.rs index 8c239498..beae7a54 100644 --- a/zenoh-jni/src/querier.rs +++ b/zenoh-jni/src/querier.rs @@ -16,21 +16,20 @@ use std::sync::Arc; use jni::{ objects::{JByteArray, JClass, JObject, JString}, - sys::jint, JNIEnv, }; -use zenoh::{key_expr::KeyExpr, query::Querier, Wait}; +use zenoh::{query::Querier, Wait}; use crate::{ errors::ZResult, - key_expr::process_kotlin_key_expr, - session::{on_reply_error, on_reply_success}, + key_expr::decode_jni_key_expr, + owned_object::OwnedObject, + sample_callback::{on_reply_error, on_reply_success}, throw_exception, utils::{ - decode_byte_array, decode_encoding, decode_string, get_callback_global_ref, get_java_vm, + decode_byte_array, decode_jni_encoding, decode_string, get_callback_global_ref, get_java_vm, load_on_close, }, - zerror, }; /// Perform a Zenoh GET through a querier. @@ -59,22 +58,20 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( mut env: JNIEnv, _class: JClass, querier_ptr: *const Querier, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, selector_params: /*nullable*/ JString, callback: JObject, on_close: JObject, attachment: /*nullable*/ JByteArray, payload: /*nullable*/ JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, ) { - let querier = 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 key_expr = decode_jni_key_expr(&mut env, &key_expr)?; let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let callback_global_ref = get_callback_global_ref(&mut env, &callback)?; + let on_close_global_ref = get_callback_global_ref(&mut env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let mut get_builder = querier.get().callback(move |reply| { || -> ZResult<()> { @@ -102,13 +99,15 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIQuerier_getViaJNI( }; if !payload.is_null() { - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; - get_builder = get_builder.encoding(encoding); - get_builder = get_builder.payload(decode_byte_array(&env, payload)?); + if !encoding.is_null() { + let encoding = decode_jni_encoding(&mut env, &encoding)?; + get_builder = get_builder.encoding(encoding); + } + get_builder = get_builder.payload(decode_byte_array(&env, &payload)?); } if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; + let attachment = decode_byte_array(&env, &attachment)?; get_builder = get_builder.attachment::>(attachment); } @@ -118,7 +117,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/query.rs b/zenoh-jni/src/query.rs index 9c843892..c8f78b21 100644 --- a/zenoh-jni/src/query.rs +++ b/zenoh-jni/src/query.rs @@ -14,17 +14,15 @@ use std::sync::Arc; -use crate::utils::{decode_byte_array, decode_encoding}; -use crate::zerror; -use crate::{errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception}; +use crate::utils::{decode_byte_array, decode_jni_encoding}; +use crate::{errors::ZResult, key_expr::decode_jni_key_expr, throw_exception}; use jni::{ - objects::{JByteArray, JClass, JString}, - sys::{jboolean, jint, jlong}, + objects::{JByteArray, JClass, JObject}, + sys::{jboolean, jlong}, JNIEnv, }; use uhlc::ID; use zenoh::{ - key_expr::KeyExpr, query::Query, time::{Timestamp, NTP64}, Wait, @@ -61,11 +59,9 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( mut env: JNIEnv, _class: JClass, query_ptr: *const Query, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, payload: JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, timestamp_enabled: jboolean, timestamp_ntp_64: jlong, attachment: /*nullable*/ JByteArray, @@ -73,17 +69,17 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( ) { let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let payload = decode_byte_array(&env, payload)?; + let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; + let payload = decode_byte_array(&env, &payload)?; let mut reply_builder = query.reply(key_expr, payload); - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let encoding = decode_jni_encoding(&mut env, &encoding)?; reply_builder = reply_builder.encoding(encoding); if timestamp_enabled != 0 { let ts = Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand()); reply_builder = reply_builder.timestamp(ts) } if !attachment.is_null() { - reply_builder = reply_builder.attachment(decode_byte_array(&env, attachment)?); + reply_builder = reply_builder.attachment(decode_byte_array(&env, &attachment)?); } reply_builder = reply_builder.express(qos_express != 0); reply_builder.wait().map_err(|err| zerror!(err)) @@ -115,14 +111,13 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( _class: JClass, query_ptr: *const Query, payload: JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, + encoding: JObject, ) { let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let encoding = decode_jni_encoding(&mut env, &encoding)?; query - .reply_err(decode_byte_array(&env, payload)?) + .reply_err(decode_byte_array(&env, &payload)?) .encoding(encoding) .wait() .map_err(|err| zerror!(err)) @@ -158,8 +153,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( mut env: JNIEnv, _class: JClass, query_ptr: *const Query, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, + key_expr: JObject, timestamp_enabled: jboolean, timestamp_ntp_64: jlong, attachment: /*nullable*/ JByteArray, @@ -167,14 +161,14 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( ) { let _ = || -> ZResult<()> { let query = Arc::from_raw(query_ptr); - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let key_expr = decode_jni_key_expr(&mut env, &key_expr)?; let mut reply_builder = query.reply_del(key_expr); if timestamp_enabled != 0 { let ts = Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand()); reply_builder = reply_builder.timestamp(ts) } if !attachment.is_null() { - reply_builder = reply_builder.attachment(decode_byte_array(&env, attachment)?); + reply_builder = reply_builder.attachment(decode_byte_array(&env, &attachment)?); } reply_builder = reply_builder.express(qos_express != 0); reply_builder.wait().map_err(|err| zerror!(err)) diff --git a/zenoh-jni/src/sample_callback.rs b/zenoh-jni/src/sample_callback.rs new file mode 100644 index 00000000..afaae297 --- /dev/null +++ b/zenoh-jni/src/sample_callback.rs @@ -0,0 +1,411 @@ +// +// 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::{GlobalRef, JByteArray, JObject, JString, JValue}, + sys::{jint, jlong}, + JNIEnv, +}; +use zenoh::{ + query::{Query, Reply, ReplyError, ReplyKeyExpr}, + sample::Sample, + session::EntityGlobalId, +}; + +use crate::{errors::ZResult, utils::*}; + +pub(crate) fn on_reply_success( + env: &mut JNIEnv, + replier_id: Option, + sample: &Sample, + callback_global_ref: &GlobalRef, +) -> ZResult<()> { + let zenoh_id = replier_id + .map_or_else( + || Ok(JByteArray::default()), + |replier_id| { + env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) + .map_err(|err| zerror!(err)) + }, + ) + .map(|value| env.auto_local(value))?; + let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); + + let byte_array = + bytes_to_java_array(env, sample.payload()).map(|value| env.auto_local(value))?; + let encoding: jint = sample.encoding().id() as jint; + let encoding_schema = sample + .encoding() + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(env, schema), + ) + .map(|value| env.auto_local(value))?; + let kind = sample.kind() as jint; + + let (timestamp, is_valid) = sample + .timestamp() + .map(|timestamp| (timestamp.get_time().as_u64(), true)) + .unwrap_or((0, false)); + + let attachment_bytes = sample + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(env, attachment), + ) + .map(|value| env.auto_local(value)) + .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; + + let key_expr_str = env + .new_string(sample.key_expr().to_string()) + .map(|value| env.auto_local(value)) + .map_err(|err| { + zerror!( + "Could not create a JString through JNI for the Sample key expression. {}", + err + ) + })?; + + let express = sample.express(); + let priority = sample.priority() as jint; + let cc = sample.congestion_control() as jint; + + let result = match env.call_method( + callback_global_ref, + "run", + "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&zenoh_id), + JValue::from(eid), + JValue::from(true), + JValue::from(&key_expr_str), + JValue::from(&byte_array), + JValue::from(encoding), + JValue::from(&encoding_schema), + JValue::from(kind), + JValue::from(timestamp as i64), + JValue::from(is_valid), + JValue::from(&attachment_bytes), + JValue::from(express), + JValue::from(priority), + JValue::from(cc), + ], + ) { + Ok(_) => Ok(()), + Err(err) => { + _ = env.exception_describe(); + Err(zerror!("On GET callback error: {}", err)) + } + }; + result +} + +pub(crate) fn on_reply_error( + env: &mut JNIEnv, + replier_id: Option, + reply_error: &ReplyError, + callback_global_ref: &GlobalRef, +) -> ZResult<()> { + let zenoh_id = replier_id + .map_or_else( + || Ok(JByteArray::default()), + |replier_id| { + env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) + .map_err(|err| zerror!(err)) + }, + ) + .map(|value| env.auto_local(value))?; + let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); + + let payload = + bytes_to_java_array(env, reply_error.payload()).map(|value| env.auto_local(value))?; + let encoding_id: jint = reply_error.encoding().id() as jint; + let encoding_schema = reply_error + .encoding() + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(env, schema), + ) + .map(|value| env.auto_local(value))?; + let result = match env.call_method( + callback_global_ref, + "run", + "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&zenoh_id), + JValue::from(eid), + JValue::from(false), + JValue::from(&JString::default()), + JValue::from(&payload), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + // The remaining parameters aren't used in case of replying error, so we set them to default. + JValue::from(0 as jint), + JValue::from(0_i64), + JValue::from(false), + JValue::from(&JByteArray::default()), + JValue::from(false), + JValue::from(0 as jint), + JValue::from(0 as jint), + ], + ) { + Ok(_) => Ok(()), + Err(err) => { + _ = env.exception_describe(); + Err(zerror!("On GET callback error: {}", err)) + } + }; + result +} + +pub(crate) unsafe fn process_kotlin_sample_callback( + env: &mut JNIEnv, + callback: &JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + + Ok(move |sample: Sample| { + let _ = || -> ZResult<()> { + let mut env = java_vm + .attach_current_thread_as_daemon() + .map_err(|err| zerror!("Unable to attach thread for sample callback: {}", err))?; + let byte_array = + bytes_to_java_array(&env, sample.payload()).map(|array| env.auto_local(array))?; + + let encoding_id: jint = sample.encoding().id() as jint; + let encoding_schema = match sample.encoding().schema() { + Some(schema) => slice_to_java_string(&env, schema)?, + None => JString::default(), + }; + let kind = sample.kind() as jint; + let (timestamp, is_valid) = sample + .timestamp() + .map(|timestamp| (timestamp.get_time().as_u64(), true)) + .unwrap_or((0, false)); + + let attachment_bytes = sample + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(&env, attachment), + ) + .map(|array| env.auto_local(array)) + .map_err(|err| zerror!("Error processing attachment: {}", err))?; + + let key_expr_str = env.auto_local( + env.new_string(sample.key_expr().to_string()) + .map_err(|err| zerror!("Error processing sample key expr: {}", err))?, + ); + + let express = sample.express(); + let priority = sample.priority() as jint; + let cc = sample.congestion_control() as jint; + + env.call_method( + &callback_global_ref, + "run", + "(Ljava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&key_expr_str), + JValue::from(&byte_array), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + JValue::from(kind), + JValue::from(timestamp as i64), + JValue::from(is_valid), + JValue::from(&attachment_bytes), + JValue::from(express), + JValue::from(priority), + JValue::from(cc), + ], + ) + .map_err(|err| zerror!(err))?; + Ok(()) + }() + .map_err(|err| tracing::error!("On sample callback error: {err}")); + }) +} + +pub(crate) unsafe fn process_kotlin_reply_callback( + env: &mut JNIEnv, + callback: &JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + + Ok(move |reply: Reply| { + || -> ZResult<()> { + tracing::debug!("Receiving reply through JNI: {:?}", reply); + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + zerror!("Unable to attach thread for GET query callback: {}", err) + })?; + match reply.result() { + Ok(sample) => crate::sample_callback::on_reply_success( + &mut env, + reply.replier_id(), + sample, + &callback_global_ref, + ), + Err(error) => crate::sample_callback::on_reply_error( + &mut env, + reply.replier_id(), + error, + &callback_global_ref, + ), + } + }() + .unwrap_or_else(|err| tracing::error!("Error on get callback: {err}")); + }) +} + +pub(crate) unsafe fn process_kotlin_query_callback( + env: &mut JNIEnv, + callback: &JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let callback_global_ref = get_callback_global_ref(env, callback)?; + + Ok(move |query: Query| { + let env = match java_vm.attach_current_thread_as_daemon() { + Ok(env) => env, + Err(err) => { + tracing::error!("Unable to attach thread for queryable callback: {}", err); + return; + } + }; + + tracing::debug!("Receiving query through JNI: {}", query.to_string()); + match on_query(env, query, &callback_global_ref) { + Ok(_) => tracing::debug!("Queryable callback called successfully."), + Err(err) => tracing::error!("Error calling queryable callback: {}", err), + } + }) +} + +/// Decoder for the zero-arg `impl Fn() + Send + Sync + 'static` callback used +/// by zenoh-flat for `on_close` parameters. The returned closure attaches the +/// JVM and invokes `Runnable.run()` on the Kotlin object. zenoh-flat is +/// responsible for binding it to the data closure's lifetime via its own +/// `CallOnDrop` so the on-close fires when the subscription/query is dropped. +pub(crate) unsafe fn process_kotlin_on_close_callback( + env: &mut JNIEnv, + on_close: &JObject, +) -> ZResult { + let java_vm = Arc::new(get_java_vm(env)?); + let on_close_global_ref = get_callback_global_ref(env, on_close)?; + + Ok(move || { + let mut env = match java_vm.attach_current_thread_as_daemon() { + Ok(env) => env, + Err(err) => { + tracing::error!("Unable to attach thread for 'onClose' callback: {}", err); + return; + } + }; + if let Err(err) = env.call_method(&on_close_global_ref, "run", "()V", &[]) { + _ = env.exception_describe(); + tracing::error!("Error while running 'onClose' callback: {}", err); + } + }) +} + +fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> ZResult<()> { + let selector_params_jstr = env + .new_string(query.parameters().to_string()) + .map(|value| env.auto_local(value)) + .map_err(|err| { + zerror!( + "Could not create a JString through JNI for the Query key expression. {}", + err + ) + })?; + + let (payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() { + let encoding = query.encoding().unwrap(); //If there is payload, there is encoding. + let encoding_id = encoding.id() as jint; + let encoding_schema = encoding + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(&env, schema), + ) + .map(|value| env.auto_local(value))?; + let byte_array = bytes_to_java_array(&env, payload).map(|value| env.auto_local(value))?; + (byte_array, encoding_id, encoding_schema) + } else { + ( + env.auto_local(JByteArray::default()), + 0, + env.auto_local(JString::default()), + ) + }; + + let attachment_bytes = query + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(&env, attachment), + ) + .map(|value| env.auto_local(value)) + .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; + + let key_expr_str = env + .new_string(query.key_expr().to_string()) + .map(|key_expr| env.auto_local(key_expr)) + .map_err(|err| { + zerror!( + "Could not create a JString through JNI for the Query key expression: {}.", + err + ) + })?; + + let accepts_replies: jint = match query.accepts_replies() { + ReplyKeyExpr::MatchingQuery => 0, + ReplyKeyExpr::Any => 1, + }; + + let query_ptr = Arc::into_raw(Arc::new(query)); + + let result = env + .call_method( + callback_global_ref, + "run", + "(Ljava/lang/String;Ljava/lang/String;[BILjava/lang/String;[BJI)V", + &[ + JValue::from(&key_expr_str), + JValue::from(&selector_params_jstr), + JValue::from(&payload), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + JValue::from(&attachment_bytes), + JValue::from(query_ptr as jlong), + JValue::from(accepts_replies), + ], + ) + .map(|_| ()) + .map_err(|err| { + unsafe { + Arc::from_raw(query_ptr); + }; + _ = env.exception_describe(); + zerror!(err) + }); + result +} diff --git a/zenoh-jni/src/scouting.rs b/zenoh-jni/src/scouting.rs index b0a665c1..cb9e0895 100644 --- a/zenoh-jni/src/scouting.rs +++ b/zenoh-jni/src/scouting.rs @@ -22,8 +22,9 @@ 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}; +use crate::{errors::ZResult, throw_exception}; /// Start a scout. /// @@ -46,18 +47,16 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIScout_00024Companion_scoutViaJNI( config_ptr: /*nullable=*/ *const Config, ) -> *const Scout<()> { || -> ZResult<*const Scout<()>> { - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; + let callback_global_ref = get_callback_global_ref(&mut env, &callback)?; let java_vm = Arc::new(get_java_vm(&mut env)?); - let on_close_global_ref: GlobalRef = get_callback_global_ref(&mut env, on_close)?; + let on_close_global_ref: GlobalRef = get_callback_global_ref(&mut env, &on_close)?; let on_close = load_on_close(&java_vm, on_close_global_ref); let whatAmIMatcher: WhatAmIMatcher = (whatAmI as u8).try_into().unwrap(); // The validity of the operation is guaranteed on the kotlin layer. let config = if config_ptr.is_null() { 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..0de2dff2 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -12,1190 +12,35 @@ // ZettaScale Zenoh Team, // -use std::{mem, ops::Deref, ptr::null, sync::Arc, time::Duration}; +// Types referenced by the generated `zenoh_flat_jni.rs` below must be in scope. +use std::sync::Arc; -use jni::{ - objects::{GlobalRef, JByteArray, JClass, JList, JObject, JString, JValue}, - sys::{jboolean, jbyteArray, jint, jlong, jobject}, - JNIEnv, -}; +use jni::{objects::JClass, JNIEnv}; use zenoh::{ config::Config, key_expr::KeyExpr, pubsub::{Publisher, Subscriber}, - query::{Querier, Query, Queryable, ReplyError, ReplyKeyExpr, Selector}, - sample::Sample, - session::{EntityGlobalId, Session, ZenohId}, - Wait, + query::{Querier, Queryable}, + session::Session, }; +#[cfg(feature = "zenoh-ext")] +use zenoh_ext::{AdvancedPublisher, AdvancedSubscriber}; -use crate::{ - errors::ZResult, key_expr::process_kotlin_key_expr, throw_exception, utils::*, zerror, -}; +include!(concat!(env!("OUT_DIR"), "/zenoh_flat_jni.rs")); -/// Open a Zenoh session via JNI. -/// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. -/// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `config_path`: Nullable path to the Zenoh config file. If null, the default configuration will be loaded. +/// Drop the `Arc` referenced by `session_ptr`. Must be called exactly +/// once per pointer obtained from session-opening JNI calls; pairs with the +/// `Arc::into_raw` performed by the generated wrapper for the open path. /// +/// Distinct from `closeSessionViaJNI`, which only deactivates the session +/// (network shutdown) without dropping the Rust handle. #[no_mangle] #[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_00024Companion_openSessionViaJNI( - mut env: JNIEnv, - _class: JClass, - config_ptr: *const Config, -) -> *const Session { - let session = open_session(config_ptr); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - null() - } - } -} - -/// Open a Zenoh session with the configuration pointed out by `config_path`. -/// -/// If the config path provided is null then the default configuration is loaded. -/// -unsafe fn open_session(config_ptr: *const Config) -> ZResult { - let config = Arc::from_raw(config_ptr); - let result = zenoh::open(config.as_ref().clone()) - .wait() - .map_err(|err| zerror!(err)); - mem::forget(config); - result -} - -/// Open a Zenoh session with a JSON configuration. -/// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. -/// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `json_config`: Configuration as a JSON string. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( - mut env: JNIEnv, - _class: JClass, - json_config: JString, -) -> *const Session { - let session = open_session_with_json_config(&mut env, json_config); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - null() - } - } -} - -/// Open a Zenoh session with the provided json configuration. -/// -fn open_session_with_json_config(env: &mut JNIEnv, json_config: JString) -> ZResult { - let json_config = decode_string(env, &json_config)?; - let mut deserializer = - json5::Deserializer::from_str(&json_config).map_err(|err| zerror!(err))?; - let config = Config::from_deserializer(&mut deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("JSON error: {}", e), - })?; - zenoh::open(config).wait().map_err(|err| zerror!(err)) -} - -/// Open a Zenoh session with a YAML configuration. -/// -/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. -/// -/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `yaml_config`: Configuration as a YAML string. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI( - mut env: JNIEnv, - _class: JClass, - yaml_config: JString, -) -> *const Session { - let session = open_session_with_yaml_config(&mut env, yaml_config); - match session { - Ok(session) => Arc::into_raw(Arc::new(session)), - Err(err) => { - tracing::error!("Unable to open session: {}", err); - throw_exception!(env, zerror!(err)); - null() - } - } -} - -/// Open a Zenoh session with the provided yaml configuration. -/// -fn open_session_with_yaml_config(env: &mut JNIEnv, yaml_config: JString) -> ZResult { - let yaml_config = decode_string(env, &yaml_config)?; - let deserializer = serde_yaml::Deserializer::from_str(&yaml_config); - let config = Config::from_deserializer(deserializer).map_err(|err| match err { - Ok(c) => zerror!("Invalid configuration: {}", c), - Err(e) => zerror!("YAML error: {}", e), - })?; - zenoh::open(config).wait().map_err(|err| zerror!(err)) -} - -/// Closes a Zenoh session via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `session_ptr`: The raw pointer to the Zenoh session. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// - After the session is closed, the provided pointer is no more valid. -/// -#[no_mangle] -#[allow(non_snake_case, unused)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( - mut env: JNIEnv, - _class: JClass, +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_freePtrViaJNI( + _env: JNIEnv, + _: JClass, session_ptr: *const Session, ) { Arc::from_raw(session_ptr); } -/// Declare a Zenoh publisher via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the publisher, may be null. -/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the publisher. -/// It is only considered when the key_expr_ptr parameter is null, meaning the function is -/// receiving a key expression that was not declared. -/// - `session_ptr`: Raw pointer to the Zenoh [Session] to be used for the publisher. -/// - `congestion_control`: The [zenoh::publisher::CongestionControl] configuration as an ordinal. -/// - `priority`: The [zenoh::core::Priority] configuration as an ordinal. -/// - `is_express`: The express config of the publisher (see [zenoh::prelude::QoSBuilderTrait]). -/// - `reliability`: The reliability value as an ordinal. -/// -/// # Returns: -/// - A raw pointer to the declared Zenoh publisher or null in case of failure. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The ownership of the session is not transferred, and the session pointer remains valid -/// after this function call so it is safe to use it after this call. -/// - The function may throw an exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( - mut env: JNIEnv, - _class: JClass, - 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 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 result = session - .declare_publisher(key_expr) - .congestion_control(congestion_control) - .priority(priority) - .express(is_express != 0) - .reliability(reliability) - .wait(); - match result { - Ok(publisher) => Ok(Arc::into_raw(Arc::new(publisher))), - Err(err) => Err(zerror!(err)), - } - }() - .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. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the operation, may be null. -/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the operation. -/// It is only considered when the key_expr_ptr parameter is null, meaning the function is -/// receiving a key expression that was not declared. -/// - `session_ptr`: Raw pointer to the [Session] to be used for the operation. -/// - `payload`: The payload to send through the network. -/// - `encoding_id`: The encoding id of the payload. -/// - `encoding_schema`: Optional encoding schema, may be null. -/// - `congestion_control`: The [CongestionControl] mechanism specified. -/// - `priority`: The [Priority] mechanism specified. -/// - `is_express`: The express flag. -/// - `attachment`: Optional attachment encoded into a byte array. May be null. -/// - `reliability`: The reliability value as an ordinal. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw an exception in case of failure, which should be handled by the Java/Kotlin caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( - mut env: JNIEnv, - _class: JClass, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - session_ptr: *const Session, - payload: JByteArray, - encoding_id: jint, - encoding_schema: JString, - congestion_control: jint, - priority: jint, - is_express: jboolean, - attachment: JByteArray, - reliability: jint, -) { - let session = Arc::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)?; - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let reliability = decode_reliability(reliability)?; - - let mut put_builder = session - .put(&key_expr, payload) - .congestion_control(congestion_control) - .encoding(encoding) - .express(is_express != 0) - .priority(priority) - .reliability(reliability); - - if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; - put_builder = put_builder.attachment(attachment) - } - - put_builder - .wait() - .map(|_| tracing::trace!("Put on '{key_expr}'")) - .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. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the operation, may be null. -/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the operation. -/// It is only considered when the key_expr_ptr parameter is null, meaning the function is -/// receiving a key expression that was not declared. -/// - `session_ptr`: Raw pointer to the [Session] to be used for the operation. -/// - `congestion_control`: The [CongestionControl] mechanism specified. -/// - `priority`: The [Priority] mechanism specified. -/// - `is_express`: The express flag. -/// - `attachment`: Optional attachment encoded into a byte array. May be null. -/// - `reliability`: The reliability value as an ordinal. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw a JNI exception or a Session exception in case of failure, which -/// should be handled by the Java/Kotlin caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( - mut env: JNIEnv, - _class: JClass, - 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 _ = || -> ZResult<()> { - 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 delete_builder = session - .delete(&key_expr) - .congestion_control(congestion_control) - .express(is_express != 0) - .priority(priority) - .reliability(reliability); - - if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; - delete_builder = delete_builder.attachment(attachment) - } - - delete_builder - .wait() - .map(|_| tracing::trace!("Delete on '{key_expr}'")) - .map_err(|err| zerror!(err)) - }() - .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); -} - -/// Declare a Zenoh subscriber via JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: The key expression pointer for the subscriber. May be null in case of using an -/// undeclared key expression. -/// - `key_expr_str`: String representation of the key expression to be used to declare the subscriber. -/// It won't be considered in case a key_expr_ptr to a declared key expression is provided. -/// - `session_ptr`: The raw pointer to the Zenoh session. -/// - `callback`: The callback function as an instance of the `JNISubscriberCallback` interface in Java/Kotlin. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the subscriber. -/// -/// Returns: -/// - A raw pointer to the declared Zenoh subscriber. In case of failure, an exception is thrown and null is returned. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The callback function passed as `callback` must be a valid instance of the `JNISubscriberCallback` interface -/// in Java/Kotlin, matching the specified signature. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( - mut env: JNIEnv, - _class: JClass, - 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); - || -> 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 - .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))?; - - tracing::debug!("Subscriber declared on '{}'.", key_expr); - std::mem::forget(session); - Ok(Arc::into_raw(Arc::new(subscriber))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - -/// Declare a Zenoh querier via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: A raw pointer to the [KeyExpr] to be used for the querier. May be null in case of using an -/// undeclared key expression. -/// - `key_expr_str`: String representation of the key expression to be used to declare the querier. -/// It won't be considered in case a key_expr_ptr to a declared key expression is provided. -/// - `target`: The ordinal value of the query target enum value. -/// - `consolidation`: The ordinal value of the consolidation enum value. -/// - `congestion_control`: The ordinal value of the congestion control enum value. -/// - `priority`: The ordinal value of the priority enum value. -/// - `is_express`: The boolean express value of the QoS provided. -/// - `timeout_ms`: The timeout in milliseconds. -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQuerierViaJNI( - mut env: JNIEnv, - _class: JClass, - key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, - key_expr_str: JString, - session_ptr: *const Session, - target: jint, - consolidation: jint, - congestion_control: jint, - priority: jint, - is_express: jboolean, - timeout_ms: jlong, - accept_replies: jint, -) -> *const Querier<'static> { - let session = Arc::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)?; - let consolidation = decode_consolidation(consolidation)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let timeout = Duration::from_millis(timeout_ms as u64); - let priority = decode_priority(priority)?; - let reply_key_expr = decode_reply_key_expr(accept_replies)?; - tracing::debug!("Declaring querier on '{}'...", key_expr); - - let querier = session - .declare_querier(key_expr.to_owned()) - .congestion_control(congestion_control) - .consolidation(consolidation) - .express(is_express != 0) - .target(query_target) - .priority(priority) - .timeout(timeout) - .accept_replies(reply_key_expr) - .wait() - .map_err(|err| zerror!(err))?; - - tracing::debug!("Querier declared on '{}'.", key_expr); - std::mem::forget(session); - Ok(Arc::into_raw(Arc::new(querier))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }) -} - -/// Declare a Zenoh queryable via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: A raw pointer to the [KeyExpr] to be used for the queryable. May be null in case of using an -/// undeclared key expression. -/// - `key_expr_str`: String representation of the key expression to be used to declare the queryable. -/// It won't be considered in case a key_expr_ptr to a declared key expression is provided. -/// - `session_ptr`: A raw pointer to the Zenoh [Session] to be used to declare the queryable. -/// - `callback`: The callback function as an instance of the `JNIQueryableCallback` interface in Java/Kotlin. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the queryable. -/// - `complete`: The completeness of the queryable. -/// -/// Returns: -/// - A raw pointer to the declared Zenoh queryable. In case of failure, an exception is thrown and null is returned. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The callback function passed as `callback` must be a valid instance of the `JNIQueryableCallback` interface -/// in Java/Kotlin, matching the specified signature. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( - mut env: JNIEnv, - _class: JClass, - 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 java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let complete = complete != 0; - let on_close = load_on_close(&java_vm, on_close_global_ref); - tracing::debug!("Declaring queryable through JNI on {}", key_expr); - let builder = session - .declare_queryable(key_expr) - .callback(move |query: Query| { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - let env = match java_vm.attach_current_thread_as_daemon() { - Ok(env) => env, - Err(err) => { - tracing::error!("Unable to attach thread for queryable callback: {}", err); - return; - } - }; - - tracing::debug!("Receiving query through JNI: {}", query.to_string()); - match on_query(env, query, &callback_global_ref) { - Ok(_) => tracing::debug!("Queryable callback called successfully."), - Err(err) => tracing::error!("Error calling queryable callback: {}", err), - } - }) - .complete(complete); - - let queryable = builder - .wait() - .map_err(|err| zerror!("Error declaring queryable: {}", err))?; - Ok(Arc::into_raw(Arc::new(queryable))) - }() - .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<()> { - let selector_params_jstr = env - .new_string(query.parameters().to_string()) - .map(|value| env.auto_local(value)) - .map_err(|err| { - zerror!( - "Could not create a JString through JNI for the Query key expression. {}", - err - ) - })?; - - let (payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() { - let encoding = query.encoding().unwrap(); //If there is payload, there is encoding. - let encoding_id = encoding.id() as jint; - let encoding_schema = encoding - .schema() - .map_or_else( - || Ok(JString::default()), - |schema| slice_to_java_string(&env, schema), - ) - .map(|value| env.auto_local(value))?; - let byte_array = bytes_to_java_array(&env, payload).map(|value| env.auto_local(value))?; - (byte_array, encoding_id, encoding_schema) - } else { - ( - env.auto_local(JByteArray::default()), - 0, - env.auto_local(JString::default()), - ) - }; - - let attachment_bytes = query - .attachment() - .map_or_else( - || Ok(JByteArray::default()), - |attachment| bytes_to_java_array(&env, attachment), - ) - .map(|value| env.auto_local(value)) - .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; - - let key_expr_str = env - .new_string(query.key_expr().to_string()) - .map(|key_expr| env.auto_local(key_expr)) - .map_err(|err| { - zerror!( - "Could not create a JString through JNI for the Query key expression: {}.", - err - ) - })?; - - let accepts_replies: jint = match query.accepts_replies() { - ReplyKeyExpr::MatchingQuery => 0, - ReplyKeyExpr::Any => 1, - }; - - let query_ptr = Arc::into_raw(Arc::new(query)); - - let result = env - .call_method( - callback_global_ref, - "run", - "(Ljava/lang/String;Ljava/lang/String;[BILjava/lang/String;[BJI)V", - &[ - JValue::from(&key_expr_str), - JValue::from(&selector_params_jstr), - JValue::from(&payload), - JValue::from(encoding_id), - JValue::from(&encoding_schema), - JValue::from(&attachment_bytes), - JValue::from(query_ptr as jlong), - JValue::from(accepts_replies), - ], - ) - .map(|_| ()) - .map_err(|err| { - // The callback could not be invoked, therefore the created kotlin query object won't be - // used. Since `query_ptr` as well as `key_expr_ptr` was created within this function - // and remains unaltered, it is safe to reclaim ownership of the memory by converting - // the raw pointers back into an `Arc` and freeing the memory. - unsafe { - Arc::from_raw(query_ptr); - }; - _ = env.exception_describe(); - zerror!(err) - }); - result -} - -/// Declare a [KeyExpr] through a [Session] via JNI. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `session_ptr`: A raw pointer to the Zenoh [Session] from which to declare the key expression. -/// - `key_expr_str`: A Java String with the intended key expression. -/// -/// # Returns: -/// - A raw pointer to the declared key expression. In case of failure, an exception is thrown and null is returned. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw an exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_str: JString, -) -> *const KeyExpr<'static> { - let session: Arc = Arc::from_raw(session_ptr); - let key_expr_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()) - .wait() - .map_err(|err| { - zerror!( - "Unable to declare key expression '{}': {}", - key_expr_str, - err - ) - })?; - Ok(Arc::into_raw(Arc::new(key_expr))) - }() - .unwrap_or_else(|err| { - throw_exception!(env, err); - null() - }); - mem::forget(session); - key_expr_ptr -} - -/// Undeclare a [KeyExpr] through a [Session] via JNI. -/// -/// The key expression must have been previously declared on the specified session, otherwise an -/// exception is thrown. -/// -/// This functions frees the key expression pointer provided. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `session_ptr`: A raw pointer to the Zenoh [Session] from which to undeclare the key expression. -/// - `key_expr_ptr`: A raw pointer to the [KeyExpr] to undeclare. -/// -/// # Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session and keyexpr pointers are valid and have not been modified or freed. -/// - The session pointer remains valid after this function call. -/// - The key expression pointer is voided after this function call. -/// - The function may throw an exception in case of failure, which should be handled by the caller. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, - key_expr_ptr: *const KeyExpr<'static>, -) { - let session = Arc::from_raw(session_ptr); - let key_expr = Arc::from_raw(key_expr_ptr); - let key_expr_clone = key_expr.deref().clone(); - match session.undeclare(key_expr_clone).wait() { - Ok(_) => {} - Err(err) => { - throw_exception!( - env, - zerror!("Unable to declare key expression '{}': {}", key_expr, err) - ); - } - } - std::mem::forget(session); - // `key_expr` is intentionally left to be freed by Rust -} - -/// Performs a `get` operation in the Zenoh session via JNI with Value. -/// -/// # Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to a declared [KeyExpr] to be used for the query. May be null in case -/// of using a non declared key expression, in which case the `key_expr_str` parameter will be used instead. -/// - `key_expr_str`: String representation of the key expression to be used to declare the query. It is not -/// considered if a `key_expr_ptr` is provided. -/// - `selector_params`: Optional parameters of the selector. -/// - `session_ptr`: A raw pointer to the Zenoh [Session]. -/// - `callback`: A Java/Kotlin callback to be called upon receiving a reply. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called when no more replies will be received. -/// - `timeout_ms`: The timeout in milliseconds. -/// - `target`: The query target as the ordinal of the enum. -/// - `consolidation`: The consolidation mode as the ordinal of the enum. -/// - `attachment`: An optional attachment encoded into a byte array. -/// - `payload`: Optional payload for the query. -/// - `encoding_id`: The encoding of the payload. -/// - `encoding_schema`: The encoding schema of the payload, may be null. -/// - `congestion_control`: The ordinal value of the congestion control enum value. -/// - `priority`: The ordinal value of the priority enum value. -/// - `is_express`: The boolean express value of the QoS provided. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// -/// Throws: -/// - An exception in case of failure handling the query. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( - mut env: JNIEnv, - _class: JClass, - 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, - target: jint, - consolidation: jint, - attachment: /*nullable*/ JByteArray, - payload: /*nullable*/ JByteArray, - encoding_id: jint, - encoding_schema: /*nullable*/ JString, - congestion_control: jint, - priority: jint, - is_express: jboolean, - accept_replies: jint, -) { - let session = Arc::from_raw(session_ptr); - let _ = || -> ZResult<()> { - let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; - let java_vm = Arc::new(get_java_vm(&mut env)?); - let callback_global_ref = get_callback_global_ref(&mut env, callback)?; - let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; - let query_target = decode_query_target(target)?; - let consolidation = decode_consolidation(consolidation)?; - let timeout = Duration::from_millis(timeout_ms as u64); - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let reply_key_expr = decode_reply_key_expr(accept_replies)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); - let selector_params = if selector_params.is_null() { - String::new() - } else { - decode_string(&mut env, &selector_params)? - }; - let selector = Selector::owned(&key_expr, selector_params); - let mut get_builder = session - .get(selector) - .congestion_control(congestion_control) - .priority(priority) - .express(is_express != 0) - .callback(move |reply| { - || -> ZResult<()> { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - tracing::debug!("Receiving reply through JNI: {:?}", reply); - let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { - zerror!("Unable to attach thread for GET query callback: {}.", err) - })?; - - match reply.result() { - Ok(sample) => on_reply_success( - &mut env, - reply.replier_id(), - sample, - &callback_global_ref, - ), - Err(error) => on_reply_error( - &mut env, - reply.replier_id(), - error, - &callback_global_ref, - ), - } - }() - .unwrap_or_else(|err| tracing::error!("Error on get callback: {err}")); - }) - .target(query_target) - .timeout(timeout) - .consolidation(consolidation) - .accept_replies(reply_key_expr); - - if !payload.is_null() { - let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; - get_builder = get_builder.encoding(encoding); - get_builder = get_builder.payload(decode_byte_array(&env, payload)?); - } - - if !attachment.is_null() { - let attachment = decode_byte_array(&env, attachment)?; - get_builder = get_builder.attachment::>(attachment); - } - - get_builder - .wait() - .map(|_| tracing::trace!("Performing get on '{key_expr}'.",)) - .map_err(|err| zerror!(err)) - }() - .map_err(|err| throw_exception!(env, err)); - std::mem::forget(session); -} - -pub(crate) fn on_reply_success( - env: &mut JNIEnv, - replier_id: Option, - sample: &Sample, - callback_global_ref: &GlobalRef, -) -> ZResult<()> { - let zenoh_id = replier_id - .map_or_else( - || Ok(JByteArray::default()), - |replier_id| { - env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) - .map_err(|err| zerror!(err)) - }, - ) - .map(|value| env.auto_local(value))?; - let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); - - let byte_array = - bytes_to_java_array(env, sample.payload()).map(|value| env.auto_local(value))?; - let encoding: jint = sample.encoding().id() as jint; - let encoding_schema = sample - .encoding() - .schema() - .map_or_else( - || Ok(JString::default()), - |schema| slice_to_java_string(env, schema), - ) - .map(|value| env.auto_local(value))?; - let kind = sample.kind() as jint; - - let (timestamp, is_valid) = sample - .timestamp() - .map(|timestamp| (timestamp.get_time().as_u64(), true)) - .unwrap_or((0, false)); - - let attachment_bytes = sample - .attachment() - .map_or_else( - || Ok(JByteArray::default()), - |attachment| bytes_to_java_array(env, attachment), - ) - .map(|value| env.auto_local(value)) - .map_err(|err| zerror!("Error processing attachment of reply: {}.", err))?; - - let key_expr_str = env - .new_string(sample.key_expr().to_string()) - .map(|value| env.auto_local(value)) - .map_err(|err| { - zerror!( - "Could not create a JString through JNI for the Sample key expression. {}", - err - ) - })?; - - let express = sample.express(); - let priority = sample.priority() as jint; - let cc = sample.congestion_control() as jint; - - let result = match env.call_method( - callback_global_ref, - "run", - "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", - &[ - JValue::from(&zenoh_id), - JValue::from(eid), - JValue::from(true), - JValue::from(&key_expr_str), - JValue::from(&byte_array), - JValue::from(encoding), - JValue::from(&encoding_schema), - JValue::from(kind), - JValue::from(timestamp as i64), - JValue::from(is_valid), - JValue::from(&attachment_bytes), - JValue::from(express), - JValue::from(priority), - JValue::from(cc), - ], - ) { - Ok(_) => Ok(()), - Err(err) => { - _ = env.exception_describe(); - Err(zerror!("On GET callback error: {}", err)) - } - }; - result -} - -pub(crate) fn on_reply_error( - env: &mut JNIEnv, - replier_id: Option, - reply_error: &ReplyError, - callback_global_ref: &GlobalRef, -) -> ZResult<()> { - let zenoh_id = replier_id - .map_or_else( - || Ok(JByteArray::default()), - |replier_id| { - env.byte_array_from_slice(&replier_id.zid().to_le_bytes()) - .map_err(|err| zerror!(err)) - }, - ) - .map(|value| env.auto_local(value))?; - let eid = replier_id.map_or_else(|| 0, |replier_id| replier_id.eid() as jint); - - let payload = - bytes_to_java_array(env, reply_error.payload()).map(|value| env.auto_local(value))?; - let encoding_id: jint = reply_error.encoding().id() as jint; - let encoding_schema = reply_error - .encoding() - .schema() - .map_or_else( - || Ok(JString::default()), - |schema| slice_to_java_string(env, schema), - ) - .map(|value| env.auto_local(value))?; - let result = match env.call_method( - callback_global_ref, - "run", - "([BIZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", - &[ - JValue::from(&zenoh_id), - JValue::from(eid), - JValue::from(false), - JValue::from(&JString::default()), - JValue::from(&payload), - JValue::from(encoding_id), - JValue::from(&encoding_schema), - // The remaining parameters aren't used in case of replying error, so we set them to default. - JValue::from(0 as jint), - JValue::from(0_i64), - JValue::from(false), - JValue::from(&JByteArray::default()), - JValue::from(false), - JValue::from(0 as jint), - JValue::from(0 as jint), - ], - ) { - Ok(_) => Ok(()), - Err(err) => { - _ = env.exception_describe(); - Err(zerror!("On GET callback error: {}", err)) - } - }; - result -} - -/// Returns a list of zenoh ids as byte arrays corresponding to the peers connected to the session provided. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getPeersZidViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, -) -> jobject { - let session = Arc::from_raw(session_ptr); - let ids = { - let peers_zid = session.info().peers_zid().wait(); - let ids = peers_zid.collect::>(); - ids_to_java_list(&mut env, ids).map_err(|err| zerror!(err)) - } - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::default().as_raw() - }); - std::mem::forget(session); - ids -} - -/// Returns a list of zenoh ids as byte arrays corresponding to the routers connected to the session provided. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getRoutersZidViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, -) -> jobject { - let session = Arc::from_raw(session_ptr); - let ids = { - let peers_zid = session.info().routers_zid().wait(); - let ids = peers_zid.collect::>(); - ids_to_java_list(&mut env, ids).map_err(|err| zerror!(err)) - } - .unwrap_or_else(|err| { - throw_exception!(env, err); - JObject::default().as_raw() - }); - std::mem::forget(session); - ids -} - -/// Returns the Zenoh ID as a byte array of the session. -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getZidViaJNI( - mut env: JNIEnv, - _class: JClass, - session_ptr: *const Session, -) -> jbyteArray { - let session = Arc::from_raw(session_ptr); - let ids = { - let zid = session.info().zid().wait(); - env.byte_array_from_slice(&zid.to_le_bytes()) - .map(|x| x.as_raw()) - .map_err(|err| zerror!(err)) - } - .unwrap_or_else(|err| { - throw_exception!(env, err); - JByteArray::default().as_raw() - }); - std::mem::forget(session); - ids -} - -fn ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> jni::errors::Result { - let array_list = env.new_object("java/util/ArrayList", "()V", &[])?; - let jlist = JList::from_env(env, &array_list)?; - for id in ids { - let value = &mut env.byte_array_from_slice(&id.to_le_bytes())?; - jlist.add(env, value)?; - } - Ok(array_list.as_raw()) -} diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index a2c6a9f4..6a0599c1 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -14,7 +14,7 @@ use std::sync::Arc; -use crate::{errors::ZResult, throw_exception, zerror}; +use crate::{errors::ZResult, throw_exception}; use jni::{ objects::{JByteArray, JObject, JString}, sys::jint, @@ -53,6 +53,21 @@ pub(crate) fn decode_encoding( Ok(Encoding::new(encoding_id, schema)) } +/// Decode a Kotlin `io.zenoh.jni.JNIEncoding` holder into a zenoh [`Encoding`]. +/// Fields: `id: Int`, `schema: String?`. +pub(crate) fn decode_jni_encoding(env: &mut JNIEnv, obj: &JObject) -> ZResult { + let id = env + .get_field(obj, "id", "I") + .and_then(|v| v.i()) + .map_err(|err| zerror!("JNIEncoding.id: {}", err))?; + let schema_obj = env + .get_field(obj, "schema", "Ljava/lang/String;") + .and_then(|v| v.l()) + .map_err(|err| zerror!("JNIEncoding.schema: {}", err))?; + let schema_js: JString = schema_obj.into(); + decode_encoding(env, id, &schema_js) +} + pub(crate) fn get_java_vm(env: &mut JNIEnv) -> ZResult { env.get_java_vm() .map_err(|err| zerror!("Unable to retrieve JVM reference: {}", err)) @@ -60,16 +75,16 @@ pub(crate) fn get_java_vm(env: &mut JNIEnv) -> ZResult { pub(crate) fn get_callback_global_ref( env: &mut JNIEnv, - callback: JObject, + callback: &JObject, ) -> crate::errors::ZResult { env.new_global_ref(callback) .map_err(|err| zerror!("Unable to get reference to the provided callback: {}", err)) } /// Helper function to convert a JByteArray into a Vec. -pub(crate) fn decode_byte_array(env: &JNIEnv<'_>, payload: JByteArray) -> ZResult> { +pub(crate) fn decode_byte_array(env: &JNIEnv, payload: &JByteArray) -> ZResult> { let payload_len = env - .get_array_length(&payload) + .get_array_length(payload) .map(|length| length as usize) .map_err(|err| zerror!(err))?; let mut buff = vec![0; payload_len]; @@ -160,6 +175,29 @@ impl Drop for CallOnDrop { } } +/// Wrap a decoded data callback so that the supplied Kotlin `on_close` +/// callback fires exactly once when the data closure is dropped (via +/// `CallOnDrop`). Used by hand-written JNI entry points whose Kotlin signatures +/// pair a callback with an `on_close: JNIOnCloseCallback`. Generated entry +/// points (via `JniMethodsConverter`) take `on_close` as a separate Rust parameter +/// instead and are wrapped at the zenoh-flat layer. +pub(crate) fn wrap_with_on_close( + env: &mut JNIEnv, + on_close: JObject, + cb: F, +) -> ZResult +where + F: Fn(T) + Send + Sync + 'static, +{ + let java_vm = Arc::new(get_java_vm(env)?); + let on_close_global_ref = get_callback_global_ref(env, &on_close)?; + let guard = load_on_close(&java_vm, on_close_global_ref); + Ok(move |t| { + guard.noop(); + cb(t); + }) +} + pub(crate) fn load_on_close( java_vm: &Arc, on_close_global_ref: jni::objects::GlobalRef, diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs index 627bb8b0..c09b60c6 100644 --- a/zenoh-jni/src/zbytes.rs +++ b/zenoh-jni/src/zbytes.rs @@ -24,7 +24,6 @@ use crate::{ errors::ZResult, throw_exception, utils::{bytes_to_java_array, decode_byte_array}, - zerror, }; enum JavaType { @@ -171,15 +170,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); @@ -257,7 +248,7 @@ fn serialize( } JavaType::ByteArray => { let jbyte_array = JByteArray::from(any); - let bytes = decode_byte_array(env, jbyte_array).map_err(|err| zerror!(err))?; + let bytes = decode_byte_array(env, &jbyte_array).map_err(|err| zerror!(err))?; serializer.serialize(bytes); } JavaType::List(kotlin_type) => { @@ -296,17 +287,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..6fba5b70 --- /dev/null +++ b/zenoh-jni/src/zbytes_kotlin.rs @@ -0,0 +1,575 @@ +// +// 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}, +}; + +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()) + } + } +} diff --git a/zenoh-jni/src/zenoh_id.rs b/zenoh-jni/src/zenoh_id.rs index 6647f86f..217b2339 100644 --- a/zenoh-jni/src/zenoh_id.rs +++ b/zenoh-jni/src/zenoh_id.rs @@ -12,14 +12,36 @@ // ZettaScale Zenoh Team, // -use crate::{errors::ZResult, throw_exception, utils::decode_byte_array, zerror}; +use crate::{errors::ZResult, throw_exception, utils::decode_byte_array}; use jni::{ - objects::{JByteArray, JClass, JString}, - sys::jstring, + objects::{JByteArray, JClass, JList, JString}, + sys::{jbyteArray, jobject, jstring}, JNIEnv, }; use zenoh::session::ZenohId; +/// Encode a single [`ZenohId`] as a Java `byte[]`. +pub(crate) fn zenoh_id_to_byte_array(env: &JNIEnv<'_>, zid: ZenohId) -> ZResult { + env.byte_array_from_slice(&zid.to_le_bytes()) + .map(|x| x.as_raw()) + .map_err(|err| zerror!(err)) +} + +/// Encode a `Vec` as a Java `ArrayList`. +pub(crate) fn zenoh_ids_to_java_list(env: &mut JNIEnv, ids: Vec) -> ZResult { + let array_list = env + .new_object("java/util/ArrayList", "()V", &[]) + .map_err(|err| zerror!(err))?; + let jlist = JList::from_env(env, &array_list).map_err(|err| zerror!(err))?; + for id in ids { + let value = &mut env + .byte_array_from_slice(&id.to_le_bytes()) + .map_err(|err| zerror!(err))?; + jlist.add(env, value).map_err(|err| zerror!(err))?; + } + Ok(array_list.as_raw()) +} + /// Returns the string representation of a ZenohID. #[no_mangle] #[allow(non_snake_case)] @@ -29,7 +51,7 @@ pub extern "C" fn Java_io_zenoh_jni_JNIZenohId_toStringViaJNI( zenoh_id: JByteArray, ) -> jstring { || -> ZResult { - let bytes = decode_byte_array(&env, zenoh_id)?; + let bytes = decode_byte_array(&env, &zenoh_id)?; let zenohid = ZenohId::try_from(bytes.as_slice()).map_err(|err| zerror!(err))?; env.new_string(zenohid.to_string()) .map_err(|err| zerror!(err))