Spaces:
Running
Running
| /* | |
| * Copyright 2025 Google LLC. | |
| * | |
| * 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 com.google.ai.edge.litertlm | |
| import java.io.File | |
| import java.io.FileOutputStream | |
| import java.io.IOException | |
| import java.io.InputStream | |
| /** Helper class for loading the LiteRT-LM native library. */ | |
| internal object NativeLibraryLoader { | |
| private const val JNI_LIBNAME = "litertlm_jni" | |
| private val DEBUG = | |
| System.getProperty("com.google.ai.edge.litertlm.NativeLibraryLoader.DEBUG") != null | |
| fun load() { | |
| // 0. Skip loading if loaded already. | |
| if (isLoaded()) { | |
| log("Skip loading as the native library is loaded already.") | |
| return | |
| } | |
| // 1. Try loading from library path. (e.g., for Android) | |
| if (tryLoadLibrary(JNI_LIBNAME)) { | |
| log("Loaded $JNI_LIBNAME from library path.") | |
| return | |
| } | |
| // For simplicity, the native library extension is ".so" instead of the default ".dylib" on | |
| // MacOS since it is the the default cc_binary output for MacOS. | |
| val jniLibName = System.mapLibraryName(JNI_LIBNAME).replace(".dylib", ".so") | |
| // 2. Try extracting from JAR (generic path). (e.g., for bazel) | |
| val genericResourcePath = "com/google/ai/edge/litertlm/jni/$jniLibName" | |
| if (tryExtractAndLoad(genericResourcePath, jniLibName)) { | |
| log("Loaded $JNI_LIBNAME from JAR: $genericResourcePath") | |
| return | |
| } | |
| // 3. Try extracting from JAR (OS-Arch specific path). (e.g., for multi-platform Maven packages) | |
| val osArchResourcePath = "com/google/ai/edge/litertlm/jni/${os()}-${architecture()}/$jniLibName" | |
| if (tryExtractAndLoad(osArchResourcePath, jniLibName)) { | |
| log("Loaded $JNI_LIBNAME from JAR: $osArchResourcePath") | |
| return | |
| } | |
| throw UnsatisfiedLinkError( | |
| "Failed to load native library $JNI_LIBNAME. Tried system path, $genericResourcePath, and $osArchResourcePath" | |
| ) | |
| } | |
| private fun isLoaded(): Boolean = | |
| try { | |
| nativeCheckLoaded() | |
| true | |
| } catch (e: UnsatisfiedLinkError) { | |
| false | |
| } | |
| private fun tryLoadLibrary(libName: String): Boolean = | |
| try { | |
| System.loadLibrary(libName) | |
| true | |
| } catch (e: UnsatisfiedLinkError) { | |
| log("System.loadLibrary($libName) failed: ${e.message}") | |
| false | |
| } | |
| private fun tryExtractAndLoad(resourcePath: String, libName: String): Boolean { | |
| log("Attempting to extract from: $resourcePath") | |
| val jniResource = NativeLibraryLoader::class.java.classLoader?.getResourceAsStream(resourcePath) | |
| if (jniResource == null) { | |
| log("Resource not found: $resourcePath") | |
| return false | |
| } | |
| return try { | |
| val tempPath = createTemporaryDirectory() | |
| tempPath.deleteOnExit() | |
| val tempDirectory = tempPath.canonicalPath | |
| val extractedLibraryPath = extractResource(jniResource, libName, tempDirectory) | |
| System.load(extractedLibraryPath) | |
| true | |
| } catch (e: IOException) { | |
| log("Failed to extract $resourcePath: $e") | |
| false | |
| } catch (e: UnsatisfiedLinkError) { | |
| log("Failed to load extracted library from $resourcePath: $e") | |
| false | |
| } | |
| } | |
| private fun extractResource( | |
| resource: InputStream, | |
| resourceName: String, | |
| extractToDirectory: String, | |
| ): String { | |
| val dst = File(extractToDirectory, resourceName) | |
| dst.deleteOnExit() | |
| val dstPath = dst.toString() | |
| log("extracting native library to: $dstPath") | |
| val nbytes = copy(resource, dst) | |
| log("copied $nbytes bytes to $dstPath") | |
| return dstPath | |
| } | |
| private fun os(): String { | |
| val p = System.getProperty("os.name", "")!!.lowercase() | |
| return when { | |
| p.contains("linux") -> "linux" | |
| p.contains("os x") || p.contains("darwin") -> "darwin" | |
| p.contains("windows") -> "windows" | |
| else -> p.replace("\\s".toRegex(), "") // os.name with all whitespace removed. | |
| } | |
| } | |
| private fun architecture(): String { | |
| val arch = System.getProperty("os.arch", "")!!.lowercase() | |
| return if (arch == "amd64") "x86_64" else arch | |
| } | |
| private fun copy(src: InputStream, dstFile: File): Long { | |
| FileOutputStream(dstFile).use { dst -> | |
| val buffer = ByteArray(1 shl 20) // 1MB | |
| var ret = 0L | |
| var n = src.read(buffer) | |
| while (n >= 0) { | |
| dst.write(buffer, 0, n) | |
| ret += n | |
| n = src.read(buffer) | |
| } | |
| return ret | |
| } | |
| } | |
| private fun createTemporaryDirectory(): File { | |
| val baseDirectory = File(System.getProperty("java.io.tmpdir")!!) | |
| val directoryName = "litertlm_native_libraries-${System.currentTimeMillis()}-" | |
| for (attempt in 0 until 1000) { | |
| val temporaryDirectory = File(baseDirectory, directoryName + attempt) | |
| if (temporaryDirectory.mkdir()) { | |
| return temporaryDirectory | |
| } | |
| } | |
| throw IllegalStateException( | |
| "Could not create a temporary directory (tried to make $directoryName*) to extract LiteRT-LM native libraries." | |
| ) | |
| } | |
| private fun log(msg: String) { | |
| if (DEBUG) { | |
| System.err.println("com.google.ai.edge.litertlm.NativeLibraryLoader: $msg") | |
| } | |
| } | |
| /** Native function to check if `JNI_LIBNAME` is loaded. Throws [UnsatisfiedLinkError] if not. */ | |
| external fun nativeCheckLoaded() | |
| } | |