Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Zenoh native library loading refactor. #145

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions zenoh-kotlin/src/androidMain/kotlin/io.zenoh/Zenoh.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,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.
*/
internal actual class Zenoh private actual constructor() {

actual companion object {
private const val ZENOH_LIB_NAME = "zenoh_jni"
private const val ZENOH_LOGS_PROPERTY = "zenoh.logger"

private var instance: Zenoh? = null

actual fun load() {
instance ?: Zenoh().also { instance = it }
}
}
internal actual object ZenohLoad {
private const val ZENOH_LIB_NAME = "zenoh_jni"
private const val ZENOH_LOGS_PROPERTY = "zenoh.logger"

init {
System.loadLibrary(ZENOH_LIB_NAME)
Expand Down
4 changes: 0 additions & 4 deletions zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ class Session private constructor(private val config: Config) : AutoCloseable {
}
}

init {
Zenoh.load()
}

/** Close the session. */
override fun close() {
jniSession?.close()
Expand Down
6 changes: 1 addition & 5 deletions zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Zenoh.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,4 @@ 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.
*/
internal expect class Zenoh private constructor() {
companion object {
fun load()
}
}
internal expect object ZenohLoad
8 changes: 5 additions & 3 deletions zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@

package io.zenoh.jni

import io.zenoh.Zenoh
import io.zenoh.ZenohLoad
import io.zenoh.keyexpr.KeyExpr

internal class JNIKeyExpr(internal val ptr: Long) {

companion object {
init {
ZenohLoad
}

fun tryFrom(keyExpr: String): Result<KeyExpr> = runCatching {
Zenoh.load() // It may happen the zenoh library is not yet loaded when creating a key expression.
KeyExpr(tryFromViaJNI(keyExpr))
}

fun autocanonize(keyExpr: String): Result<KeyExpr> = runCatching {
Zenoh.load()
KeyExpr(autocanonizeViaJNI(keyExpr))
}

Expand Down
6 changes: 6 additions & 0 deletions zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ import java.util.concurrent.atomic.AtomicLong
/** Adapter class to handle the communication with the Zenoh JNI code for a [Session]. */
internal class JNISession {

companion object {
init {
ZenohLoad
}
}

/* Pointer to the underlying Rust zenoh session. */
private var sessionPtr: AtomicLong = AtomicLong(0)

Expand Down
4 changes: 0 additions & 4 deletions zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ import kotlin.test.*

class KeyExprTest {

init {
Zenoh.load()
}

@Test
fun creation_TryFromTest() {
// A couple of examples of valid and invalid key expressions.
Expand Down
4 changes: 0 additions & 4 deletions zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SelectorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import kotlin.test.assertFailsWith

class SelectorTest {

init {
Zenoh.load()
}

@Test
fun selectorFromStringTest() {
"a/b/c?arg1=val1".intoSelector().getOrThrow().use { selector: Selector ->
Expand Down
229 changes: 110 additions & 119 deletions zenoh-kotlin/src/jvmMain/kotlin/io/zenoh/Zenoh.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,155 +15,146 @@
package io.zenoh

import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.FileInputStream
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 class Zenoh private actual constructor() {

actual companion object {
private const val ZENOH_LIB_NAME = "zenoh_jni"
private const val ZENOH_LOGS_PROPERTY = "zenoh.logger"
internal actual object ZenohLoad {
private const val ZENOH_LIB_NAME = "zenoh_jni"
private const val ZENOH_LOGS_PROPERTY = "zenoh.logger"

private var instance: Zenoh? = null
init {
// Try first to load the local native library for cases in which the module was built locally,
// otherwise try to load from the JAR.
if (tryLoadingLocalLibrary().isFailure) {
val target = determineTarget().getOrThrow()
tryLoadingLibraryFromJarPackage(target).getOrThrow()
}

actual fun load() {
instance ?: Zenoh().also { instance = it }
val logLevel = System.getProperty(ZENOH_LOGS_PROPERTY)
if (logLevel != null) {
Logger.start(logLevel)
}
}

/**
* Determine target
*
* Determines the [Target] corresponding to the machine on top of which the native code will run.
*
* @return A result with the target.
*/
private fun determineTarget(): Result<Target> = runCatching {
val osName = System.getProperty("os.name").lowercase()
val osArch = System.getProperty("os.arch")

val target = when {
osName.contains("win") -> when {
osArch.contains("x86_64") || osArch.contains("amd64") -> Target.WINDOWS_X86_64_MSVC
else -> throw UnsupportedOperationException("Unsupported architecture: $osArch")
}

osName.contains("mac") -> when {
osArch.contains("x86_64") || osArch.contains("amd64") -> Target.APPLE_X86_64
osArch.contains("aarch64") -> Target.APPLE_AARCH64
else -> throw UnsupportedOperationException("Unsupported architecture: $osArch")
}

osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> when {
osArch.contains("x86_64") || osArch.contains("amd64") -> Target.LINUX_X86_64
osArch.contains("aarch64") -> Target.LINUX_AARCH64
else -> throw UnsupportedOperationException("Unsupported architecture: $osArch")
}

else -> throw UnsupportedOperationException("Unsupported platform: $osName")
/**
* Determine target
*
* Determines the [Target] corresponding to the machine on top of which the native code will run.
*
* @return A result with the target.
*/
private fun determineTarget(): Result<Target> = runCatching {
val osName = System.getProperty("os.name").lowercase()
val osArch = System.getProperty("os.arch")

val target = when {
osName.contains("win") -> when {
osArch.contains("x86_64") || osArch.contains("amd64") -> Target.WINDOWS_X86_64_MSVC
else -> throw UnsupportedOperationException("Unsupported architecture: $osArch")
}
return Result.success(target)
}

/**
* Unzip library.
*
* The Zenoh libraries are stored within the JAR as compressed ZIP files.
* The location of the zipped files is expected to be under target/target.zip.
* It is expected that the zip file only contains the compressed library.
*
* The uncompressed library will be stored temporarily and deleted on exit.
*
* @param compressedLib Input stream pointing to the compressed library.
* @return A result with the uncompressed library file.
*/
private fun unzipLibrary(compressedLib: InputStream): Result<File> = runCatching {
val zipInputStream = ZipInputStream(compressedLib)
val buffer = ByteArray(1024)
val zipEntry = zipInputStream.nextEntry

val library = File.createTempFile(zipEntry!!.name, ".tmp")
library.deleteOnExit()

val parent = library.parentFile
if (!parent.exists()) {
parent.mkdirs()
osName.contains("mac") -> when {
osArch.contains("x86_64") || osArch.contains("amd64") -> Target.APPLE_X86_64
osArch.contains("aarch64") -> Target.APPLE_AARCH64
else -> throw UnsupportedOperationException("Unsupported architecture: $osArch")
}

val fileOutputStream = FileOutputStream(library)
var len: Int
while (zipInputStream.read(buffer).also { len = it } > 0) {
fileOutputStream.write(buffer, 0, len)
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> when {
osArch.contains("x86_64") || osArch.contains("amd64") -> Target.LINUX_X86_64
osArch.contains("aarch64") -> Target.LINUX_AARCH64
else -> throw UnsupportedOperationException("Unsupported architecture: $osArch")
}
fileOutputStream.close()

zipInputStream.closeEntry()
zipInputStream.close()
return Result.success(library)
else -> throw UnsupportedOperationException("Unsupported platform: $osName")
}
return Result.success(target)
}

private fun loadLibraryAsInputStream(target: Target): Result<InputStream> = runCatching {
val libUrl = ClassLoader.getSystemClassLoader().getResourceAsStream("$target/$target.zip")!!
val uncompressedLibFile = unzipLibrary(libUrl)
return Result.success(FileInputStream(uncompressedLibFile.getOrThrow()))
/**
* Unzip library.
*
* The Zenoh libraries are stored within the JAR as compressed ZIP files.
* The location of the zipped files is expected to be under target/target.zip.
* It is expected that the zip file only contains the compressed library.
*
* The uncompressed library will be stored temporarily and deleted on exit.
*
* @param compressedLib Input stream pointing to the compressed library.
* @return A result with the uncompressed library file.
*/
private fun unzipLibrary(compressedLib: InputStream): Result<File> = runCatching {
val zipInputStream = ZipInputStream(compressedLib)
val buffer = ByteArray(1024)
val zipEntry = zipInputStream.nextEntry

val library = File.createTempFile(zipEntry!!.name, ".tmp")
library.deleteOnExit()

val parent = library.parentFile
if (!parent.exists()) {
parent.mkdirs()
}

@Suppress("UnsafeDynamicallyLoadedCode")
private fun loadZenohJNI(inputStream: InputStream) {
val tempLib = File.createTempFile("tempLib", ".tmp")
tempLib.deleteOnExit()
val fileOutputStream = FileOutputStream(library)
var len: Int
while (zipInputStream.read(buffer).also { len = it } > 0) {
fileOutputStream.write(buffer, 0, len)
}
fileOutputStream.close()

FileOutputStream(tempLib).use { output ->
inputStream.copyTo(output)
}
zipInputStream.closeEntry()
zipInputStream.close()
return Result.success(library)
}

System.load(tempLib.absolutePath)
}
private fun loadLibraryAsInputStream(target: Target): Result<InputStream> = runCatching {
val libUrl = ClassLoader.getSystemClassLoader().getResourceAsStream("$target/$target.zip")!!
val uncompressedLibFile = unzipLibrary(libUrl)
return Result.success(FileInputStream(uncompressedLibFile.getOrThrow()))
}

/**
* Load library from jar package.
*
* Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar.
*
* @param target
*/
private fun tryLoadingLibraryFromJarPackage(target: Target): Result<Unit> = runCatching {
val lib: Result<InputStream> = loadLibraryAsInputStream(target)
lib.onSuccess { loadZenohJNI(it) }.onFailure { throw Exception("Unable to load Zenoh JNI: $it") }
}
@Suppress("UnsafeDynamicallyLoadedCode")
private fun loadZenohJNI(inputStream: InputStream) {
val tempLib = File.createTempFile("tempLib", ".tmp")
tempLib.deleteOnExit()

/**
* Try loading local library.
*
* This function aims to load the default library that is usually included when building the zenoh kotlin library
* locally.
*/
private fun tryLoadingLocalLibrary(): Result<Unit> = runCatching {
val lib = ClassLoader.getSystemClassLoader().findLibraryStream(ZENOH_LIB_NAME)
if (lib != null) {
loadZenohJNI(lib)
} else {
throw Exception("Unable to load local Zenoh JNI.")
}
FileOutputStream(tempLib).use { output ->
inputStream.copyTo(output)
}

System.load(tempLib.absolutePath)
}

init {
// Try first to load the local native library for cases in which the module was built locally,
// otherwise try to load from the JAR.
if (tryLoadingLocalLibrary().isFailure) {
val target = determineTarget().getOrThrow()
tryLoadingLibraryFromJarPackage(target).getOrThrow()
}
/**
* Load library from jar package.
*
* Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar.
*
* @param target
*/
private fun tryLoadingLibraryFromJarPackage(target: Target): Result<Unit> = runCatching {
val lib: Result<InputStream> = loadLibraryAsInputStream(target)
lib.onSuccess { loadZenohJNI(it) }.onFailure { throw Exception("Unable to load Zenoh JNI: $it") }
}

val logLevel = System.getProperty(ZENOH_LOGS_PROPERTY)
if (logLevel != null) {
Logger.start(logLevel)
/**
* Try loading local library.
*
* This function aims to load the default library that is usually included when building the zenoh kotlin library
* locally.
*/
private fun tryLoadingLocalLibrary(): Result<Unit> = runCatching {
val lib = ClassLoader.getSystemClassLoader().findLibraryStream(ZENOH_LIB_NAME)
if (lib != null) {
loadZenohJNI(lib)
} else {
throw Exception("Unable to load local Zenoh JNI.")
}
}
}
Expand Down