All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.io.realm.kotlin.jvm.SoLoader.kt Maven / Gradle / Ivy

Go to download

Wrapper for interacting with Realm Kotlin native code. This artifact is not supposed to be consumed directly, but through 'io.realm.kotlin:gradle-plugin:3.0.0' instead.

There is a newer version: 3.0.0
Show newest version
/*
 * Copyright 2021 Realm Inc.
 *
 * 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
 *
 * http://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.
 */

package io.realm.kotlin.jvm

import java.io.File
import java.nio.file.Files
import java.security.MessageDigest
import java.util.Collections
import java.util.Enumeration
import java.util.LinkedList
import java.util.Locale
import java.util.Properties

/**
 * Load the C++ dynamic libraries from the fat Jar.
 * The fat Jar contains three platforms (Win, Linux and Mac) the loader detects the host platform
 * then extract and install the libraries in the same order specified in the 'dynamic_libraries.properties' file.
 *
 * Note: this class should be invoke dynamically using reflection so the classloader can have accesses
 * to the dynamic libraries files located inside the fat Jar.
 */
class SoLoader {
    private val platform: Platform = Platform.currentOS()
    private val libs: MutableList> = mutableListOf()

    init {
        readLibrariesHashes()
    }

    @Suppress("unused") // Called using reflection. See /packages/jni-swig-stub/realm.i
    fun load() {
        // First attempt to load the native library code using `System.loadLibrary()`. This is the
        // default way of shipping dependencies to JVM Desktop apps, but it does require that the
        // author of these apps have extracted the library from our JAR file and manually placed it
        // in the location defined by "java.libraries.path".
        //
        // If this fails, we will fallback to finding the native code inside the JAR file and
        // store it in the users cache directory.
        //
        // See https://github.com/realm/realm-kotlin/issues/1105 for more information.
        try {
            System.loadLibrary("realmc")
        } catch (ex: UnsatisfiedLinkError) {
            // Load the libraries in the order of dependency specified in 'dynamic_libraries.properties'
            for (lib in libs) {
                load(libraryName = lib.first, expectedHash = lib.second)
            }
        }
    }

    private fun load(libraryName: String, expectedHash: String) {
        // load the embedded .so file located inside the Jar file.
        // unpacking the file is skipped if the hash of the file is already installed.
        // instead, the on-disk file will be loaded.

        // for each SO file:
        // check if the library is already installed in the default platform location
        // path should be /io.realm.kotlin/hash/librealmffi.so
        // if the full path exists (and the on-disk hash matches) then load it otherwise unpack and load it.
        val libraryInstallationLocation: File = defaultAbsolutePath(libraryName, expectedHash)
        if (!libraryInstallationLocation.exists()) {
            unpackAndInstall(libraryName, libraryInstallationLocation, expectedHash)
        } else {
            // only double check the installed lib hash (in case it was tampered with locally)
            validHashOrThrow(libraryInstallationLocation, expectedHash)
        }
        @Suppress("UnsafeDynamicallyLoadedCode")
        // System.loadLibrary does not accept a full path to the lib (needs to be in the current Java paths)
        System.load(libraryInstallationLocation.absolutePath)
    }

    private fun readLibrariesHashes() {
        javaClass.getResourceAsStream("${platform.shortName}/dynamic_libraries.properties").use { props ->
            OrderedProperties().run {
                load(props)
                for (libName in keys()) {
                    libs.add(Pair(libName as String, get(libName) as String))
                }
            }
        }
    }

    private fun defaultAbsolutePath(libraryName: String, libraryHash: String): File {
        return File(
            platform.defaultSystemLocation + File.separator +
                libraryHash + File.separator +
                (platform.prefix + libraryName + "." + platform.suffix)
        )
    }

    private fun libPathInsideJar(libraryName: String) =
        "${platform.shortName}/${platform.prefix}$libraryName.${platform.suffix}"

    private fun unpackAndInstall(libraryName: String, absolutePath: File, expectedHash: String) {
        absolutePath.parentFile.mkdirs()
        javaClass.getResourceAsStream(libPathInsideJar(libraryName)).use { lib ->
            Files.newOutputStream(absolutePath.toPath()).use {
                lib.copyTo(it)
            }
        }
        // after unpacking make sure the hash is valid
        validHashOrThrow(absolutePath, expectedHash)
    }

    private fun validHashOrThrow(file: File, expectedHash: String, cleanup: Boolean = true) {
        if (!isValidHash(file, expectedHash)) {
            if (cleanup) {
                file.delete()
            }
            throw error("Corrupt or invalid hash for ${file.absolutePath} expected hash is $expectedHash")
        }
    }

    private fun isValidHash(file: File, expected: String): Boolean {
        val digest = MessageDigest.getInstance("SHA-1")
        Files.newInputStream(file.toPath()).use {
            val buf = ByteArray(BUFFER_SIZE)
            while (true) {
                val bytes = it.read(buf)
                if (bytes > 0) {
                    digest.update(buf, 0, bytes)
                } else {
                    break
                }
            }
            val hash = digest.digest().toHexString()
            return hash == expected
        }
    }

    private fun ByteArray.toHexString(): String =
        joinToString("", transform = { "%02x".format(it) })
}

private enum class Platform(
    val shortName: String,
    val prefix: String,
    val suffix: String,
    val defaultSystemLocation: String
) {
    MACOS(
        shortName = "/jni/macos",
        prefix = "lib",
        suffix = "dylib",
        defaultSystemLocation = "${System.getProperty("user.home")}/Library/Caches/io.realm.kotlin/"
    ),
    LINUX(
        shortName = "/jni/linux",
        prefix = "lib",
        suffix = "so",
        defaultSystemLocation = "${System.getProperty("user.home")}/.cache/io.realm.kotlin/"
    ),
    WINDOWS(
        shortName = "/jni/windows",
        prefix = "",
        suffix = "dll",
        defaultSystemLocation = (
            System.getenv("LOCALAPPDATA")
                ?: "${System.getProperty("user.home")}/AppData/Local"
            ) + "/io-realm-kotlin/"
    );

    companion object {
        fun currentOS(): Platform {
            val os = System.getProperty("os.name").lowercase(Locale.getDefault())
            return when {
                os.contains("win") -> {
                    WINDOWS
                }
                os.contains("nix") || os.contains("nux") || os.contains("aix") -> {
                    LINUX
                }
                os.contains("mac") -> {
                    MACOS
                }
                else -> error("Unsupported OS: $os")
            }
        }
    }
}

private const val BUFFER_SIZE = 16384 // 16k

// Preserve the insertion orders for the keys in order to load
// the dynamic libraries in the same order specified in the property file.
private class OrderedProperties : Properties() {
    private val orderedKeys = LinkedList()

    override fun put(key: Any?, value: Any?): Any? {
        orderedKeys.add(key!!)
        return super.put(key, value)
    }

    override fun keys(): Enumeration {
        return Collections.enumeration(orderedKeys)
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy