From b84b0447fb9aa7ba86b368af8a5c652ed7264978 Mon Sep 17 00:00:00 2001 From: Ranjithkumar Ragavan <43761047+RanjithRagavan@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:26:39 -0700 Subject: [PATCH 1/2] Support pure JVM JAR compilation and packaging for desktop platforms (Linux/macOS/Windows) Add desktop JVM support (executorch_jvm) that compiles a standalone desktop JAR without altering ExecuTorch Android layout or adding runtime indirection. Shared Java/Kotlin sources are compiled directly via Gradle sourceSets. This PR was authored with Claude. --- extension/android/CMakeLists.txt | 157 +++++++++++------- extension/android/build.gradle | 8 +- .../android/executorch_android/build.gradle | 16 +- .../pytorch/executorch/ExecuTorchRuntime.kt | 5 +- .../main/java/org/pytorch/executorch/Log.kt | 30 ++++ .../java/org/pytorch/executorch/Tensor.kt | 1 - extension/android/gradle/libs.versions.toml | 1 + extension/android/jni/jni_layer.cpp | 2 +- extension/android/jni/log.cpp | 30 ++++ extension/android/settings.gradle | 11 +- extension/jvm/build.gradle | 96 +++++++++++ .../executorch/JvmNativeLoaderDelegate.kt | 34 ++++ .../main/java/org/pytorch/executorch/Log.kt | 28 ++++ .../pytorch/executorch/NativeLibraryLoader.kt | 88 ++++++++++ ...soloader.nativeloader.NativeLoaderDelegate | 1 + 15 files changed, 433 insertions(+), 75 deletions(-) create mode 100644 extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt create mode 100644 extension/jvm/build.gradle create mode 100644 extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt create mode 100644 extension/jvm/src/main/java/org/pytorch/executorch/Log.kt create mode 100644 extension/jvm/src/main/java/org/pytorch/executorch/NativeLibraryLoader.kt create mode 100644 extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index c94b95286d0..bc75b7cf083 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -12,65 +12,84 @@ if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) endif() -if(NOT ANDROID) - message(FATAL_ERROR "This directory is for Android build only") -endif() - set(EXECUTORCH_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake) -set(_common_compile_options - $<$:/wd4996> - $<$>:-Wno-deprecated-declarations -fPIC> -) -if(NOT ANDROID_PLATFORM) - set(ANDROID_PLATFORM android-30) -endif() -# We need to download fbjni library from maven, and use its "prefab" library and -# headers, and link executorch library against that fbjni library. We don't know -# which NDK is used to compile fbjni, and we need to link our executorch library -# to the version which Android APK links against for runtime to ensure the -# libc++ dependencies are consistent. WARNING # Users need to use the SAME fbjni -# version here and in app gradle dependency for runtime compatibility! -if(NOT FBJNI_VERSION) - set(FBJNI_VERSION 0.7.0) -endif() +if(NOT ANDROID) + find_package(JNI REQUIRED) + if(NOT FBJNI_HEADERS_DIR OR NOT FBJNI_LIBRARY) + message( + FATAL_ERROR + "For non-Android platforms, please specify -DFBJNI_HEADERS_DIR=/path/to/fbjni/include and -DFBJNI_LIBRARY=/path/to/libfbjni.so" + ) + endif() -set(FBJNI_AAR_URL - https://repo1.maven.org/maven2/com/facebook/fbjni/fbjni/${FBJNI_VERSION}/fbjni-${FBJNI_VERSION}.aar -) -set(FBJNI_DOWNLOAD_PATH ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/fbjni.aar) + add_library(fbjni SHARED IMPORTED) + set_target_properties(fbjni PROPERTIES IMPORTED_LOCATION "${FBJNI_LIBRARY}") + set(_common_compile_options + $<$:/wd4996> + $<$>:-Wno-deprecated-declarations -fPIC> + ) +else() + set(_common_compile_options + $<$:/wd4996> + $<$>:-Wno-deprecated-declarations -fPIC> + ) + if(NOT ANDROID_PLATFORM) + set(ANDROID_PLATFORM android-30) + endif() -if(NOT EXISTS "${FBJNI_DOWNLOAD_PATH}") - file(DOWNLOAD "${FBJNI_AAR_URL}" "${FBJNI_DOWNLOAD_PATH}") -endif() + # We need to download fbjni library from maven, and use its "prefab" library + # and headers, and link executorch library against that fbjni library. We + # don't know which NDK is used to compile fbjni, and we need to link our + # executorch library to the version which Android APK links against for + # runtime to ensure the libc++ dependencies are consistent. WARNING # Users + # need to use the SAME fbjni version here and in app gradle dependency for + # runtime compatibility! + if(NOT FBJNI_VERSION) + set(FBJNI_VERSION 0.7.0) + endif() -add_custom_command( - OUTPUT - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" - COMMAND unzip -o ${FBJNI_DOWNLOAD_PATH} -d - ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni - DEPENDS "${FBJNI_DOWNLOAD_PATH}" -) + set(FBJNI_AAR_URL + https://repo1.maven.org/maven2/com/facebook/fbjni/fbjni/${FBJNI_VERSION}/fbjni-${FBJNI_VERSION}.aar + ) + set(FBJNI_DOWNLOAD_PATH + ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/fbjni.aar + ) -add_custom_target( - fbjni_prefab - DEPENDS - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" -) + if(NOT EXISTS "${FBJNI_DOWNLOAD_PATH}") + file(DOWNLOAD "${FBJNI_AAR_URL}" "${FBJNI_DOWNLOAD_PATH}") + endif() -add_library(fbjni SHARED IMPORTED) -add_dependencies(fbjni fbjni_prefab) -set_target_properties( - fbjni - PROPERTIES - IMPORTED_LOCATION - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" -) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" + COMMAND unzip -o ${FBJNI_DOWNLOAD_PATH} -d + ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni + DEPENDS "${FBJNI_DOWNLOAD_PATH}" + ) + + add_custom_target( + fbjni_prefab + DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" + ) -executorch_target_link_options_shared_lib(executorch) + add_library(fbjni SHARED IMPORTED) + add_dependencies(fbjni fbjni_prefab) + set_target_properties( + fbjni + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/libs/android.${ANDROID_ABI}/libfbjni.so" + ) +endif() + +if(ANDROID) + executorch_target_link_options_shared_lib(executorch) +endif() add_library( executorch_jni SHARED jni/jni_layer.cpp jni/log.cpp jni/jni_layer_runtime.cpp @@ -233,19 +252,35 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/fb/extended_targets/CMakeLists.txt") include("${CMAKE_CURRENT_SOURCE_DIR}/fb/extended_targets/CMakeLists.txt") endif() -target_include_directories( - executorch_jni - PRIVATE - ${_common_include_directories} - "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" -) +if(ANDROID) + target_include_directories( + executorch_jni + PRIVATE + ${_common_include_directories} + "${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/prefab/modules/fbjni/include/" + ) +else() + target_include_directories( + executorch_jni + PRIVATE + ${_common_include_directories} + ${FBJNI_HEADERS_DIR} + ${JNI_INCLUDE_DIRS} + ) +endif() target_compile_options(executorch_jni PUBLIC ${_common_compile_options}) -target_link_options( - executorch_jni PRIVATE - "LINKER:--version-script,${CMAKE_CURRENT_SOURCE_DIR}/jni/version_script.txt" -) +if(NOT WIN32) + target_link_options( + executorch_jni PRIVATE + "LINKER:--version-script,${CMAKE_CURRENT_SOURCE_DIR}/jni/version_script.txt" + ) +endif() target_link_options_gc_sections(executorch_jni) -target_link_libraries(executorch_jni ${link_libraries} log) +if(ANDROID) + target_link_libraries(executorch_jni ${link_libraries} log) +else() + target_link_libraries(executorch_jni ${link_libraries} ${JNI_LIBRARIES}) +endif() diff --git a/extension/android/build.gradle b/extension/android/build.gradle index 78db3c0f6b5..b6e4f79032b 100644 --- a/extension/android/build.gradle +++ b/extension/android/build.gradle @@ -12,9 +12,15 @@ allprojects { dependencies { classpath 'com.android.tools.build:gradle:8.9.0' - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.34.0' + classpath 'com.vanniktech:gradle-maven-publish-plugin:0.31.0' + classpath 'com.diffplug.spotless:spotless-plugin-gradle:8.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23" } + } + ext { + fbjniJavaOnlyVersion = "0.7.0" + soLoaderNativeLoaderVersion = "0.10.5" } repositories { diff --git a/extension/android/executorch_android/build.gradle b/extension/android/executorch_android/build.gradle index 2dbe0e1fb5f..d45504a07c8 100644 --- a/extension/android/executorch_android/build.gradle +++ b/extension/android/executorch_android/build.gradle @@ -7,12 +7,10 @@ */ -plugins { - id "com.android.library" version "8.9.0" - id "com.vanniktech.maven.publish" version "0.31.0" - id 'com.diffplug.spotless' version '8.0.0' - alias(libs.plugins.jetbrains.kotlin.android) -} +apply plugin: 'com.android.library' +apply plugin: 'com.vanniktech.maven.publish' +apply plugin: 'com.diffplug.spotless' +apply plugin: 'kotlin-android' spotless { kotlin { @@ -37,8 +35,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } sourceSets { @@ -50,7 +48,7 @@ android { } } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" freeCompilerArgs += ["-Xjvm-default=all"] } } diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt index 52d846c5647..f309979533d 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt @@ -10,8 +10,10 @@ package org.pytorch.executorch import com.facebook.jni.annotations.DoNotStrip import com.facebook.soloader.nativeloader.NativeLoader +import com.facebook.soloader.nativeloader.NativeLoaderDelegate import com.facebook.soloader.nativeloader.SystemDelegate import java.io.File +import java.util.ServiceLoader /** Class for entire ExecuTorch Runtime related functions. */ class ExecuTorchRuntime private constructor() { @@ -19,7 +21,8 @@ class ExecuTorchRuntime private constructor() { companion object { init { if (!NativeLoader.isInitialized()) { - NativeLoader.init(SystemDelegate()) + val customDelegate = ServiceLoader.load(NativeLoaderDelegate::class.java).firstOrNull() + NativeLoader.init(customDelegate ?: SystemDelegate()) } // Loads libexecutorch.so from jniLibs NativeLoader.loadLibrary("executorch") diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt new file mode 100644 index 00000000000..df209a70460 --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch + +import android.util.Log as AndroidLog + +/** Android logging helper that forwards logs directly to android.util.Log. */ +internal object Log { + fun e(tag: String, msg: String) { + AndroidLog.e(tag, msg) + } + + fun w(tag: String, msg: String) { + AndroidLog.w(tag, msg) + } + + fun i(tag: String, msg: String) { + AndroidLog.i(tag, msg) + } + + fun d(tag: String, msg: String) { + AndroidLog.d(tag, msg) + } +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt index f2f3ebea214..433667ccae0 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt @@ -8,7 +8,6 @@ package org.pytorch.executorch -import android.util.Log import com.facebook.jni.HybridData import com.facebook.jni.annotations.DoNotStrip import java.nio.Buffer diff --git a/extension/android/gradle/libs.versions.toml b/extension/android/gradle/libs.versions.toml index fcd6a356536..d051c046a22 100644 --- a/extension/android/gradle/libs.versions.toml +++ b/extension/android/gradle/libs.versions.toml @@ -15,3 +15,4 @@ junit = { module = "junit:junit", version.ref = "junit" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } [plugins] jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 38ea80a9a81..3bed3330db1 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -561,7 +561,7 @@ class ExecuTorchJni : public facebook::jni::HybridClass { return ret; #else - return facebook::jni::JArrayClass::newArray(0); + return facebook::jni::JArrayClass::newArray(0); #endif } diff --git a/extension/android/jni/log.cpp b/extension/android/jni/log.cpp index 663198e1271..d8c5288d44e 100644 --- a/extension/android/jni/log.cpp +++ b/extension/android/jni/log.cpp @@ -66,4 +66,34 @@ void access_log_buffer(std::function&)> accessor) { } // namespace executorch::extension +#else + +#include + +namespace executorch::extension { + +void access_log_buffer(std::function&)> accessor) { + // No-op for non-Android + (void)accessor; +} + +} // namespace executorch::extension + +void et_pal_emit_log_message( + et_timestamp_t timestamp, + et_pal_log_level_t level, + const char* filename, + const char* function, + size_t line, + const char* message, + size_t length) { + (void)timestamp; + (void)filename; + (void)function; + (void)line; + (void)length; + // Fallback console log for JVM desktop + fprintf(stderr, "[ExecuTorch JNI %c] %s\n", level, message); +} + #endif diff --git a/extension/android/settings.gradle b/extension/android/settings.gradle index 95d46203058..195339f452e 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -21,4 +21,13 @@ plugins { rootProject.name = 'executorch' -include('executorch_android') +// Android module — only included when Android SDK is available +if (System.getenv("ANDROID_HOME") != null || System.getenv("ANDROID_SDK_ROOT") != null || new java.io.File(rootDir, "local.properties").exists()) { + include('executorch_android') +} else { + logger.warn("WARNING: ANDROID_HOME is not set, skipping Android subproject.") +} + +// Desktop JVM module +include(':executorch_jvm') +project(':executorch_jvm').projectDir = new File(rootDir, '../jvm') diff --git a/extension/jvm/build.gradle b/extension/jvm/build.gradle new file mode 100644 index 00000000000..e2cee45246a --- /dev/null +++ b/extension/jvm/build.gradle @@ -0,0 +1,96 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +apply plugin: 'kotlin' +apply plugin: 'java-library' +apply plugin: 'com.vanniktech.maven.publish' +apply plugin: 'com.diffplug.spotless' + +spotless { + kotlin { + target '**/*.kt' + ktfmt() + } +} + +def execuTorchVersion = System.properties['execuTorchVersion'] + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +sourceSets { + main { + kotlin { + srcDirs = ['../android/executorch_android/src/main/java', 'src/main/java'] + exclude { element -> + element.file.path.contains("executorch_android") && element.file.name == "Log.kt" + } + } + } + test { + kotlin { + srcDirs = ['../android/executorch_android/src/test/java', 'src/test/java'] + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation 'com.facebook.fbjni:fbjni-java-only:0.7.0' + implementation 'com.facebook.soloader:nativeloader:0.10.5' + implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.assertj:assertj-core:3.27.2' + testImplementation "org.jetbrains.kotlin:kotlin-test:${libs.versions.kotlin.get()}" +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs += ["-Xjvm-default=all"] + } +} + +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + coordinates("org.pytorch", "executorch-jvm", execuTorchVersion ? execuTorchVersion : "1.2.0-SNAPSHOT") + + pom { + name = "ExecuTorch JVM" + description = "ExecuTorch Java/Kotlin bindings for standard desktop JVM (Linux, macOS, Windows)" + inceptionYear = "2024" + url = "https://github.com/pytorch/executorch/" + licenses { + license { + name = "BSD 3-Clause" + url = "https://github.com/pytorch/executorch/blob/main/LICENSE" + distribution = "https://github.com/pytorch/executorch/blob/main/LICENSE" + } + } + developers { + developer { + id = "pytorch" + name = "pytorch" + url = "https://github.com/pytorch/executorch/" + } + } + scm { + url = "https://github.com/pytorch/executorch.git" + connection = "scm:git:https://github.com/pytorch/executorch" + developerConnection = "scm:git:git@github.com:pytorch/executorch.git" + } + } +} diff --git a/extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt b/extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt new file mode 100644 index 00000000000..07873739e8a --- /dev/null +++ b/extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch + +import com.facebook.soloader.nativeloader.NativeLoaderDelegate + +/** + * Desktop JVM-specific [NativeLoaderDelegate] that delegates library loading to + * [NativeLibraryLoader]. + * + * This implementation maps requests to load "executorch" to the actual JNI library "executorch_jni" + * built for desktop platforms. + */ +class JvmNativeLoaderDelegate : NativeLoaderDelegate { + override fun loadLibrary(shortName: String, flags: Int): Boolean { + val libraryToLoad = if (shortName == "executorch") "executorch_jni" else shortName + NativeLibraryLoader.load(libraryToLoad) + return true + } + + override fun getLibraryPath(libName: String): String? { + return null + } + + override fun getSoSourcesVersion(): Int { + return 0 + } +} diff --git a/extension/jvm/src/main/java/org/pytorch/executorch/Log.kt b/extension/jvm/src/main/java/org/pytorch/executorch/Log.kt new file mode 100644 index 00000000000..c5962e08e55 --- /dev/null +++ b/extension/jvm/src/main/java/org/pytorch/executorch/Log.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch + +/** Desktop JVM logging helper that prints warnings and errors to standard error/out. */ +internal object Log { + fun e(tag: String, msg: String) { + System.err.println("[$tag] ERROR: $msg") + } + + fun w(tag: String, msg: String) { + System.err.println("[$tag] WARNING: $msg") + } + + fun i(tag: String, msg: String) { + System.out.println("[$tag] INFO: $msg") + } + + fun d(tag: String, msg: String) { + System.out.println("[$tag] DEBUG: $msg") + } +} diff --git a/extension/jvm/src/main/java/org/pytorch/executorch/NativeLibraryLoader.kt b/extension/jvm/src/main/java/org/pytorch/executorch/NativeLibraryLoader.kt new file mode 100644 index 00000000000..6d46f8473f1 --- /dev/null +++ b/extension/jvm/src/main/java/org/pytorch/executorch/NativeLibraryLoader.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch + +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +/** + * Utility for loading platform-specific native libraries on desktop JVMs. + * + * Native libraries are expected to be bundled in the classpath as: + * ``` + * native/// + * ``` + * + * For example: + * - `native/linux/x86_64/libexecutorch_jni.so` + * - `native/macos/aarch64/libexecutorch_jni.dylib` + * - `native/windows/x86_64/executorch_jni.dll` + * + * The loader extracts the library to a temporary directory and loads it via [System.load]. The + * temporary file is deleted on JVM exit. + */ +object NativeLibraryLoader { + + private val loaded = mutableSetOf() + + /** + * Loads the given native library for the current OS and architecture. + * + * @param libraryName the platform-independent library name (e.g. "executorch_jni") + * @throws UnsatisfiedLinkError if the OS/arch is unsupported or the library is not found + */ + @Synchronized + fun load(libraryName: String) { + if (libraryName in loaded) return + + val osName = System.getProperty("os.name")?.lowercase() ?: "" + val arch = System.getProperty("os.arch")?.lowercase() ?: "" + + val osDir = + when { + "linux" in osName -> "linux" + "mac" in osName || "darwin" in osName -> "macos" + "win" in osName -> "windows" + else -> throw UnsatisfiedLinkError("Unsupported OS: $osName") + } + + val archDir = + when { + arch == "amd64" || arch == "x86_64" -> "x86_64" + arch == "aarch64" || arch == "arm64" -> "aarch64" + else -> throw UnsatisfiedLinkError("Unsupported architecture: $arch") + } + + val fileName = System.mapLibraryName(libraryName) + val resourcePath = "/native/$osDir/$archDir/$fileName" + + val inputStream = + NativeLibraryLoader::class.java.getResourceAsStream(resourcePath) + ?: throw UnsatisfiedLinkError( + "Native library not found on classpath: $resourcePath. " + + "Add the appropriate platform-specific JAR " + + "(e.g. executorch-jvm--$osDir-$archDir.jar) to your dependencies." + ) + + try { + val tempDir = File(System.getProperty("java.io.tmpdir"), "executorch-native") + tempDir.mkdirs() + val tempFile = File(tempDir, fileName) + tempFile.deleteOnExit() + + FileOutputStream(tempFile).use { output -> inputStream.use { input -> input.copyTo(output) } } + + System.load(tempFile.absolutePath) + loaded.add(libraryName) + } catch (e: IOException) { + throw UnsatisfiedLinkError("Failed to extract native library: ${e.message}") + } + } +} diff --git a/extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate b/extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate new file mode 100644 index 00000000000..614fe9eb2f6 --- /dev/null +++ b/extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate @@ -0,0 +1 @@ +org.pytorch.executorch.JvmNativeLoaderDelegate From 438dce37f953d4bcad05f2f847a445dea1f11736 Mon Sep 17 00:00:00 2001 From: Ranjithkumar Ragavan Date: Fri, 14 Aug 2026 23:53:36 -0700 Subject: [PATCH 2/2] Restructure per review: shared extension/java module with thin Android/JVM siblings Address reviewer feedback on #20761: 1. Extract platform-neutral API into extension/java (executorch-java). No android.* references in the shared module - enforced by classpath (JDK + fbjni-java-only + soloader only). Android and JVM modules are thin siblings that depend on it via api project(':executorch_java'). No relative srcDirs cross-compilation anymore. 2. Replace reflection/ServiceLoader-based wiring with compile-checked interfaces: Logger interface with AndroidLogger (installed via ExecuTorchInitProvider at process start) and ConsoleLogger (JVM default). Native loading is configured explicitly through ExecuTorchRuntime.configureNativeLoading(); the desktop entry point is ExecuTorchJvm.init(). The Android runtime path no longer contains any ServiceLoader lookup and keeps SystemDelegate + 'executorch' defaults. 3. Native library delivery for desktop: API jar + per-platform native jars with Maven classifiers (linux-x86_64, macos-aarch64, ...). jarNative* Gradle tasks package binaries staged under extension/cmake-out-jvm// into native/// paths that NativeLibraryLoader extracts at runtime. 4. Android artifact parity: package names unchanged, Android runtime defaults unchanged, manifest gains only the init provider. Unit tests (EValue/Tensor/Logger) now run on the desktop JVM in the shared module - 63 tests green. Buck targets keep their historical names in extension/android/BUCK and re-export the new //xplat/executorch/extension/java targets. --- extension/android/BUCK | 53 ++++----- extension/android/build.gradle | 2 +- .../android/executorch_android/build.gradle | 4 + .../src/main/AndroidManifest.xml | 4 + .../executorch/{Log.kt => AndroidLogger.kt} | 15 +-- .../executorch/ExecuTorchInitProvider.kt | 49 +++++++++ .../pytorch/executorch/ExecuTorchRuntime.kt | 65 ----------- extension/android/gradle/libs.versions.toml | 1 - extension/android/settings.gradle | 5 + extension/java/BUCK | 90 ++++++++++++++++ extension/java/build.gradle | 85 +++++++++++++++ .../pytorch/executorch/BackendOptionsMap.kt | 0 .../main/java/org/pytorch/executorch/DType.kt | 0 .../java/org/pytorch/executorch/EValue.kt | 0 .../pytorch/executorch/ExecuTorchRuntime.kt | 102 ++++++++++++++++++ .../executorch/ExecutorchRuntimeException.kt | 0 .../java/org/pytorch/executorch/Logger.kt | 89 +++++++++++++++ .../org/pytorch/executorch/MethodMetadata.kt | 0 .../java/org/pytorch/executorch/Module.kt | 7 +- .../java/org/pytorch/executorch/Tensor.kt | 0 .../executorch/annotations/Experimental.kt | 0 .../executorch/extension/asr/AsrCallback.kt | 0 .../executorch/extension/asr/AsrModule.kt | 3 +- .../extension/asr/AsrTranscribeConfig.kt | 0 .../executorch/extension/llm/LlmCallback.kt | 0 .../extension/llm/LlmGenerationConfig.kt | 0 .../executorch/extension/llm/LlmModule.kt | 0 .../extension/llm/LlmModuleConfig.kt | 0 .../org/pytorch/executorch/training/SGD.kt | 8 +- .../executorch/training/TrainingModule.kt | 7 +- .../java/org/pytorch/executorch/EValueTest.kt | 0 .../java/org/pytorch/executorch/LoggerTest.kt | 70 ++++++++++++ .../java/org/pytorch/executorch/TensorTest.kt | 0 extension/jvm/README.md | 53 +++++++++ extension/jvm/build.gradle | 91 +++++++++++----- .../org/pytorch/executorch/ExecuTorchJvm.kt | 41 +++++++ .../executorch/JvmNativeLoaderDelegate.kt | 11 +- .../main/java/org/pytorch/executorch/Log.kt | 28 ----- ...soloader.nativeloader.NativeLoaderDelegate | 1 - 39 files changed, 701 insertions(+), 183 deletions(-) rename extension/android/executorch_android/src/main/java/org/pytorch/executorch/{Log.kt => AndroidLogger.kt} (51%) create mode 100644 extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchInitProvider.kt delete mode 100644 extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt create mode 100644 extension/java/BUCK create mode 100644 extension/java/build.gradle rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/BackendOptionsMap.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/DType.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/EValue.kt (100%) create mode 100644 extension/java/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.kt (100%) create mode 100644 extension/java/src/main/java/org/pytorch/executorch/Logger.kt rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/MethodMetadata.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/Module.kt (97%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/Tensor.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/annotations/Experimental.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt (98%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/extension/llm/LlmCallback.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/extension/llm/LlmGenerationConfig.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/extension/llm/LlmModuleConfig.kt (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/training/SGD.kt (91%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/training/TrainingModule.kt (94%) rename extension/{android/executorch_android => java}/src/test/java/org/pytorch/executorch/EValueTest.kt (100%) create mode 100644 extension/java/src/test/java/org/pytorch/executorch/LoggerTest.kt rename extension/{android/executorch_android => java}/src/test/java/org/pytorch/executorch/TensorTest.kt (100%) create mode 100644 extension/jvm/README.md create mode 100644 extension/jvm/src/main/java/org/pytorch/executorch/ExecuTorchJvm.kt delete mode 100644 extension/jvm/src/main/java/org/pytorch/executorch/Log.kt delete mode 100644 extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate diff --git a/extension/android/BUCK b/extension/android/BUCK index d1672a2a45b..f61a441147e 100644 --- a/extension/android/BUCK +++ b/extension/android/BUCK @@ -3,64 +3,57 @@ load("@fbsource//tools/build_defs/android:fb_android_library.bzl", "fb_android_l oncall("executorch") +# Thin Android shell. The platform-neutral API sources moved to +# //xplat/executorch/extension/java; these targets keep their historical names +# and re-export the shared targets so existing dependents keep working. + non_fbcode_target(_kind = fb_android_library, name = "executorch", warnings_as_errors = False, required_for_source_only_abi = True, srcs = [ - "executorch_android/src/main/java/org/pytorch/executorch/BackendOptionsMap.kt", - "executorch_android/src/main/java/org/pytorch/executorch/DType.kt", - "executorch_android/src/main/java/org/pytorch/executorch/EValue.kt", - "executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt", - "executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.kt", - "executorch_android/src/main/java/org/pytorch/executorch/MethodMetadata.kt", - "executorch_android/src/main/java/org/pytorch/executorch/Module.kt", - "executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt", - "executorch_android/src/main/java/org/pytorch/executorch/annotations/Experimental.kt", + "executorch_android/src/main/java/org/pytorch/executorch/AndroidLogger.kt", + "executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchInitProvider.kt", ], autoglob = False, language = "KOTLIN", pure_kotlin = False, extra_kotlinc_arguments = ["-Xjvm-default=all"], + exported_deps = [ + "//xplat/executorch/extension/java:executorch", + ], deps = [ "//fbandroid/java/com/facebook/jni:jni", "//fbandroid/libraries/soloader/java/com/facebook/soloader/nativeloader:nativeloader", - ] + ([ - "//xplat/executorch/backends/vulkan:vulkan_backend_lib_static", - ] if read_config("executorch", "minimal_jni", "false") == "false" else []), + ], ) non_fbcode_target(_kind = fb_android_library, name = "executorch_training", warnings_as_errors = False, - srcs = [ - "executorch_android/src/main/java/org/pytorch/executorch/training/SGD.kt", - "executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.kt", - ], autoglob = False, language = "KOTLIN", - deps = [ - ":executorch", - "//fbandroid/java/com/facebook/jni:jni", - "//fbandroid/libraries/soloader/java/com/facebook/soloader/nativeloader:nativeloader", + exported_deps = [ + "//xplat/executorch/extension/java:executorch_training", ], ) non_fbcode_target(_kind = fb_android_library, name = "executorch_llama", warnings_as_errors = False, - srcs = [ - "executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmCallback.kt", - "executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmGenerationConfig.kt", - "executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.kt", - "executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModuleConfig.kt", + autoglob = False, + language = "KOTLIN", + exported_deps = [ + "//xplat/executorch/extension/java:executorch_llama", ], +) + +non_fbcode_target(_kind = fb_android_library, + name = "executorch_asr", + warnings_as_errors = False, autoglob = False, language = "KOTLIN", - extra_kotlinc_arguments = ["-Xjvm-default=all"], - deps = [ - ":executorch", - "//fbandroid/java/com/facebook/jni:jni", - "//fbandroid/libraries/soloader/java/com/facebook/soloader/nativeloader:nativeloader", + exported_deps = [ + "//xplat/executorch/extension/java:executorch_asr", ], ) diff --git a/extension/android/build.gradle b/extension/android/build.gradle index b6e4f79032b..c2113c7f196 100644 --- a/extension/android/build.gradle +++ b/extension/android/build.gradle @@ -12,7 +12,7 @@ allprojects { dependencies { classpath 'com.android.tools.build:gradle:8.9.0' - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.31.0' + classpath 'com.vanniktech:gradle-maven-publish-plugin:0.34.0' classpath 'com.diffplug.spotless:spotless-plugin-gradle:8.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23" } diff --git a/extension/android/executorch_android/build.gradle b/extension/android/executorch_android/build.gradle index d45504a07c8..18a78088a4e 100644 --- a/extension/android/executorch_android/build.gradle +++ b/extension/android/executorch_android/build.gradle @@ -58,6 +58,10 @@ task copyTestRes(type: Exec) { } dependencies { + // Platform-neutral API (Module, Tensor, EValue, ...) shared with the desktop JVM artifact. + // Declared as `api` so consumers of executorch-android resolve executorch-java transitively, + // exactly like the classes that used to ship inside the AAR. + api project(':executorch_java') implementation 'com.facebook.fbjni:fbjni:0.7.0' implementation 'com.facebook.soloader:nativeloader:0.10.5' implementation libs.core.ktx diff --git a/extension/android/executorch_android/src/main/AndroidManifest.xml b/extension/android/executorch_android/src/main/AndroidManifest.xml index cb2a2dcdc53..2a1a8faaa09 100644 --- a/extension/android/executorch_android/src/main/AndroidManifest.xml +++ b/extension/android/executorch_android/src/main/AndroidManifest.xml @@ -1,6 +1,10 @@ + ?, + selection: String?, + selectionArgs: Array?, + sortOrder: String?, + ): Cursor? = null + + override fun getType(uri: Uri): String? = null + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array?, + ): Int = 0 +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt deleted file mode 100644 index f309979533d..00000000000 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -package org.pytorch.executorch - -import com.facebook.jni.annotations.DoNotStrip -import com.facebook.soloader.nativeloader.NativeLoader -import com.facebook.soloader.nativeloader.NativeLoaderDelegate -import com.facebook.soloader.nativeloader.SystemDelegate -import java.io.File -import java.util.ServiceLoader - -/** Class for entire ExecuTorch Runtime related functions. */ -class ExecuTorchRuntime private constructor() { - - companion object { - init { - if (!NativeLoader.isInitialized()) { - val customDelegate = ServiceLoader.load(NativeLoaderDelegate::class.java).firstOrNull() - NativeLoader.init(customDelegate ?: SystemDelegate()) - } - // Loads libexecutorch.so from jniLibs - NativeLoader.loadLibrary("executorch") - } - - private val sInstance = ExecuTorchRuntime() - - /** Get the runtime instance. */ - @JvmStatic fun getRuntime(): ExecuTorchRuntime = sInstance - - /** - * Validates that the given path points to a readable file. - * - * @throws IllegalArgumentException if the path is null, does not exist, is not a file, or is - * not readable. - */ - @JvmStatic - fun validateFilePath(path: String?, description: String) { - if (path == null) { - throw IllegalArgumentException("Cannot load $description: path is null") - } - val file = File(path) - if (!file.exists()) { - throw IllegalArgumentException("Cannot load $description: path does not exist: $path") - } - if (!file.isFile) { - throw IllegalArgumentException("Cannot load $description: path is not a file: $path") - } - if (!file.canRead()) { - throw IllegalArgumentException("Cannot load $description: path is not readable: $path") - } - } - - /** Get all registered ops. */ - @DoNotStrip @JvmStatic external fun getRegisteredOps(): Array - - /** Get all registered backends. */ - @DoNotStrip @JvmStatic external fun getRegisteredBackends(): Array - } -} diff --git a/extension/android/gradle/libs.versions.toml b/extension/android/gradle/libs.versions.toml index d051c046a22..fcd6a356536 100644 --- a/extension/android/gradle/libs.versions.toml +++ b/extension/android/gradle/libs.versions.toml @@ -15,4 +15,3 @@ junit = { module = "junit:junit", version.ref = "junit" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } [plugins] jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/extension/android/settings.gradle b/extension/android/settings.gradle index 195339f452e..a4199c1a022 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -21,6 +21,11 @@ plugins { rootProject.name = 'executorch' +// Shared, platform-neutral Java API (no android.* — enforced by classpath). +// Both the Android AAR and the desktop JVM jar depend on this module. +include(':executorch_java') +project(':executorch_java').projectDir = new File(rootDir, '../java') + // Android module — only included when Android SDK is available if (System.getenv("ANDROID_HOME") != null || System.getenv("ANDROID_SDK_ROOT") != null || new java.io.File(rootDir, "local.properties").exists()) { include('executorch_android') diff --git a/extension/java/BUCK b/extension/java/BUCK new file mode 100644 index 00000000000..dd1b714651b --- /dev/null +++ b/extension/java/BUCK @@ -0,0 +1,90 @@ +load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target", "non_fbcode_target") +load("@fbsource//tools/build_defs/android:fb_android_library.bzl", "fb_android_library") + +oncall("executorch") + +# Shared, platform-neutral ExecuTorch Java API. These sources contain no +# android.* references; platform-specific behavior (logging sink, native +# library loading) is injected by the thin sibling modules under +# extension/android (AAR) and extension/jvm (desktop jar). + +non_fbcode_target(_kind = fb_android_library, + name = "executorch", + warnings_as_errors = False, + required_for_source_only_abi = True, + srcs = [ + "src/main/java/org/pytorch/executorch/BackendOptionsMap.kt", + "src/main/java/org/pytorch/executorch/DType.kt", + "src/main/java/org/pytorch/executorch/EValue.kt", + "src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt", + "src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.kt", + "src/main/java/org/pytorch/executorch/Logger.kt", + "src/main/java/org/pytorch/executorch/MethodMetadata.kt", + "src/main/java/org/pytorch/executorch/Module.kt", + "src/main/java/org/pytorch/executorch/Tensor.kt", + "src/main/java/org/pytorch/executorch/annotations/Experimental.kt", + ], + autoglob = False, + language = "KOTLIN", + pure_kotlin = False, + extra_kotlinc_arguments = ["-Xjvm-default=all"], + deps = [ + "//fbandroid/java/com/facebook/jni:jni", + "//fbandroid/libraries/soloader/java/com/facebook/soloader/nativeloader:nativeloader", + ] + ([ + "//xplat/executorch/backends/vulkan:vulkan_backend_lib_static", + ] if read_config("executorch", "minimal_jni", "false") == "false" else []), +) + +non_fbcode_target(_kind = fb_android_library, + name = "executorch_training", + warnings_as_errors = False, + srcs = [ + "src/main/java/org/pytorch/executorch/training/SGD.kt", + "src/main/java/org/pytorch/executorch/training/TrainingModule.kt", + ], + autoglob = False, + language = "KOTLIN", + deps = [ + ":executorch", + "//fbandroid/java/com/facebook/jni:jni", + "//fbandroid/libraries/soloader/java/com/facebook/soloader/nativeloader:nativeloader", + ], +) + +non_fbcode_target(_kind = fb_android_library, + name = "executorch_llama", + warnings_as_errors = False, + srcs = [ + "src/main/java/org/pytorch/executorch/extension/llm/LlmCallback.kt", + "src/main/java/org/pytorch/executorch/extension/llm/LlmGenerationConfig.kt", + "src/main/java/org/pytorch/executorch/extension/llm/LlmModule.kt", + "src/main/java/org/pytorch/executorch/extension/llm/LlmModuleConfig.kt", + ], + autoglob = False, + language = "KOTLIN", + extra_kotlinc_arguments = ["-Xjvm-default=all"], + deps = [ + ":executorch", + "//fbandroid/java/com/facebook/jni:jni", + "//fbandroid/libraries/soloader/java/com/facebook/soloader/nativeloader:nativeloader", + ], +) + +non_fbcode_target(_kind = fb_android_library, + name = "executorch_asr", + warnings_as_errors = False, + srcs = [ + "src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt", + "src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt", + "src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt", + ], + autoglob = False, + language = "KOTLIN", + extra_kotlinc_arguments = ["-Xjvm-default=all"], + deps = [ + ":executorch", + "//fbandroid/java/com/facebook/jni:jni", + "//fbandroid/libraries/soloader/java/com/facebook/soloader/nativeloader:nativeloader", + ], +) diff --git a/extension/java/build.gradle b/extension/java/build.gradle new file mode 100644 index 00000000000..f5912b9cd2f --- /dev/null +++ b/extension/java/build.gradle @@ -0,0 +1,85 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Shared, platform-neutral ExecuTorch Java API. +// +// This module MUST NOT depend on any Android APIs: the classpath only contains the +// JDK, fbjni-java-only, soloader nativeloader, and the Kotlin stdlib, so any +// android.* reference fails at compile time. Platform-specific behavior (logging +// sink, native library loading) is injected by the thin sibling modules +// extension/android (AAR) and extension/jvm (desktop jar) through the Logger and +// ExecuTorchRuntime.configureNativeLoading APIs. + +apply plugin: 'kotlin' +apply plugin: 'java-library' +apply plugin: 'com.vanniktech.maven.publish' +apply plugin: 'com.diffplug.spotless' + +spotless { + kotlin { + target '**/*.kt' + ktfmt() + } +} + +def execuTorchVersion = System.properties['execuTorchVersion'] + +java { + // fbjni-java-only is published for JVM 17+, so the shared API must target Java 17. + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + api "com.facebook.fbjni:fbjni-java-only:${fbjniJavaOnlyVersion}" + api "com.facebook.soloader:nativeloader:${soLoaderNativeLoaderVersion}" + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.23" + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.assertj:assertj-core:3.27.2' + testImplementation 'org.jetbrains.kotlin:kotlin-test:1.9.23' +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs += ["-Xjvm-default=all"] + } +} + +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + coordinates("org.pytorch", "executorch-java", execuTorchVersion ? execuTorchVersion : "1.2.0-SNAPSHOT") + + pom { + name = "ExecuTorch Java" + description = "Platform-neutral ExecuTorch Java/Kotlin API shared by the Android and desktop JVM artifacts" + inceptionYear = "2025" + url = "https://github.com/pytorch/executorch/" + licenses { + license { + name = "BSD 3-Clause" + url = "https://github.com/pytorch/executorch/blob/main/LICENSE" + distribution = "https://github.com/pytorch/executorch/blob/main/LICENSE" + } + } + developers { + developer { + id = "pytorch" + name = "pytorch" + url = "https://github.com/pytorch/executorch/" + } + } + scm { + url = "https://github.com/pytorch/executorch.git" + connection = "scm:git:git@github.com:pytorch/executorch.git" + developerConnection = "scm:git:git@github.com:pytorch/executorch.git" + } + } +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/BackendOptionsMap.kt b/extension/java/src/main/java/org/pytorch/executorch/BackendOptionsMap.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/BackendOptionsMap.kt rename to extension/java/src/main/java/org/pytorch/executorch/BackendOptionsMap.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/DType.kt b/extension/java/src/main/java/org/pytorch/executorch/DType.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/DType.kt rename to extension/java/src/main/java/org/pytorch/executorch/DType.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/EValue.kt b/extension/java/src/main/java/org/pytorch/executorch/EValue.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/EValue.kt rename to extension/java/src/main/java/org/pytorch/executorch/EValue.kt diff --git a/extension/java/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt b/extension/java/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt new file mode 100644 index 00000000000..5fce82d3fe7 --- /dev/null +++ b/extension/java/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch + +import com.facebook.jni.annotations.DoNotStrip +import com.facebook.soloader.nativeloader.NativeLoader +import com.facebook.soloader.nativeloader.NativeLoaderDelegate +import com.facebook.soloader.nativeloader.SystemDelegate +import java.io.File + +/** Class for entire ExecuTorch Runtime related functions. */ +class ExecuTorchRuntime private constructor() { + + companion object { + private val initLock = Any() + + // Defaults preserve the long-standing Android behavior: soloader's SystemDelegate and the + // "executorch" library name. Platform modules (e.g. the desktop JVM artifact) override these + // via configureNativeLoading() before any ExecuTorch API is touched. + @Volatile private var nativeLoaderDelegate: NativeLoaderDelegate = SystemDelegate() + @Volatile private var nativeLibraryName: String = "executorch" + + init { + ensureNativeLibraryLoaded() + } + + private val sInstance = ExecuTorchRuntime() + + /** Get the runtime instance. */ + @JvmStatic fun getRuntime(): ExecuTorchRuntime = sInstance + + /** + * Configures how the ExecuTorch native library is loaded. Intended for platform modules (e.g. + * the desktop JVM artifact, which extracts a packaged .so/.dylib/.dll from the classpath) — + * Android applications do not need to call this. + * + * Must be called before any other ExecuTorch API, i.e. before the native library has been + * loaded. + * + * @param delegate the soloader [NativeLoaderDelegate] used to resolve and load the library. + * @param libraryName the platform-independent library name passed to the delegate. + */ + @JvmStatic + fun configureNativeLoading(delegate: NativeLoaderDelegate, libraryName: String) { + synchronized(initLock) { + check(!NativeLoader.isInitialized()) { + "configureNativeLoading must be called before the ExecuTorch native library is loaded" + } + nativeLoaderDelegate = delegate + nativeLibraryName = libraryName + } + } + + /** Initializes soloader (once) and loads the ExecuTorch native library. Idempotent. */ + @JvmStatic + fun ensureNativeLibraryLoaded() { + synchronized(initLock) { + if (!NativeLoader.isInitialized()) { + NativeLoader.init(nativeLoaderDelegate) + } + // Loads libexecutorch.so from jniLibs on Android; delegated to the platform module's + // NativeLoaderDelegate elsewhere. + NativeLoader.loadLibrary(nativeLibraryName) + } + } + + /** + * Validates that the given path points to a readable file. + * + * @throws IllegalArgumentException if the path is null, does not exist, is not a file, or is + * not readable. + */ + @JvmStatic + fun validateFilePath(path: String?, description: String) { + if (path == null) { + throw IllegalArgumentException("Cannot load $description: path is null") + } + val file = File(path) + if (!file.exists()) { + throw IllegalArgumentException("Cannot load $description: path does not exist: $path") + } + if (!file.isFile) { + throw IllegalArgumentException("Cannot load $description: path is not a file: $path") + } + if (!file.canRead()) { + throw IllegalArgumentException("Cannot load $description: path is not readable: $path") + } + } + + /** Get all registered ops. */ + @DoNotStrip @JvmStatic external fun getRegisteredOps(): Array + + /** Get all registered backends. */ + @DoNotStrip @JvmStatic external fun getRegisteredBackends(): Array + } +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.kt b/extension/java/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.kt rename to extension/java/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.kt diff --git a/extension/java/src/main/java/org/pytorch/executorch/Logger.kt b/extension/java/src/main/java/org/pytorch/executorch/Logger.kt new file mode 100644 index 00000000000..67e1660a2c2 --- /dev/null +++ b/extension/java/src/main/java/org/pytorch/executorch/Logger.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch + +/** + * Logging sink for the ExecuTorch Java APIs. + * + * Platform modules provide an implementation: the Android artifact installs an + * `android.util.Log`-backed implementation at process start, while the desktop JVM artifact uses + * [ConsoleLogger]. Implementations are installed via [Log.install] before any other ExecuTorch API + * is used. + */ +interface Logger { + /** Log an error message. */ + fun e(tag: String, msg: String) + + /** Log a warning message. */ + fun w(tag: String, msg: String) + + /** Log an info message. */ + fun i(tag: String, msg: String) + + /** Log a debug message. */ + fun d(tag: String, msg: String) +} + +/** + * Default [Logger] that writes warnings and errors to stderr and info/debug to stdout. Works on any + * JVM and is the default sink unless a platform module installs another implementation. + */ +class ConsoleLogger : Logger { + override fun e(tag: String, msg: String) { + System.err.println("[$tag] ERROR: $msg") + } + + override fun w(tag: String, msg: String) { + System.err.println("[$tag] WARNING: $msg") + } + + override fun i(tag: String, msg: String) { + System.out.println("[$tag] INFO: $msg") + } + + override fun d(tag: String, msg: String) { + System.out.println("[$tag] DEBUG: $msg") + } +} + +/** + * Internal logging facade used by the ExecuTorch Java API. Delegates to the installed [Logger]. + * + * This is not part of the stable public API surface; applications should not call it directly. + * Platform modules (Android, desktop JVM) call [install] once during initialization. + */ +object Log { + @Volatile private var logger: Logger = ConsoleLogger() + + /** Installs the platform-specific [Logger]. Must be called before any logging occurs. */ + @JvmStatic + fun install(custom: Logger) { + logger = custom + } + + @JvmStatic + fun e(tag: String, msg: String) { + logger.e(tag, msg) + } + + @JvmStatic + fun w(tag: String, msg: String) { + logger.w(tag, msg) + } + + @JvmStatic + fun i(tag: String, msg: String) { + logger.i(tag, msg) + } + + @JvmStatic + fun d(tag: String, msg: String) { + logger.d(tag, msg) + } +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/MethodMetadata.kt b/extension/java/src/main/java/org/pytorch/executorch/MethodMetadata.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/MethodMetadata.kt rename to extension/java/src/main/java/org/pytorch/executorch/MethodMetadata.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.kt b/extension/java/src/main/java/org/pytorch/executorch/Module.kt similarity index 97% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.kt rename to extension/java/src/main/java/org/pytorch/executorch/Module.kt index bf63086fc5c..4f151424c56 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.kt +++ b/extension/java/src/main/java/org/pytorch/executorch/Module.kt @@ -10,8 +10,6 @@ package org.pytorch.executorch import com.facebook.jni.HybridData import com.facebook.jni.annotations.DoNotStrip -import com.facebook.soloader.nativeloader.NativeLoader -import com.facebook.soloader.nativeloader.SystemDelegate import java.io.Closeable import java.util.concurrent.locks.ReentrantLock import org.pytorch.executorch.annotations.Experimental @@ -239,10 +237,7 @@ private constructor( companion object { init { - if (!NativeLoader.isInitialized()) { - NativeLoader.init(SystemDelegate()) - } - NativeLoader.loadLibrary("executorch") + ExecuTorchRuntime.ensureNativeLibraryLoaded() } /** Load mode for the module. Load the whole file as a buffer. */ diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt b/extension/java/src/main/java/org/pytorch/executorch/Tensor.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt rename to extension/java/src/main/java/org/pytorch/executorch/Tensor.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/annotations/Experimental.kt b/extension/java/src/main/java/org/pytorch/executorch/annotations/Experimental.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/annotations/Experimental.kt rename to extension/java/src/main/java/org/pytorch/executorch/annotations/Experimental.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt b/extension/java/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt rename to extension/java/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/java/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt similarity index 98% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt rename to extension/java/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt index ab9099ba405..c8addc127fa 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt +++ b/extension/java/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -11,6 +11,7 @@ package org.pytorch.executorch.extension.asr import java.io.Closeable import java.io.File import java.util.concurrent.atomic.AtomicLong +import org.pytorch.executorch.ExecuTorchRuntime import org.pytorch.executorch.ExecutorchRuntimeException import org.pytorch.executorch.annotations.Experimental @@ -64,7 +65,7 @@ class AsrModule( companion object { init { - System.loadLibrary("executorch") + ExecuTorchRuntime.ensureNativeLibraryLoaded() } @JvmStatic diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt b/extension/java/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt rename to extension/java/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmCallback.kt b/extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmCallback.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmCallback.kt rename to extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmCallback.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmGenerationConfig.kt b/extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmGenerationConfig.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmGenerationConfig.kt rename to extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmGenerationConfig.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.kt b/extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.kt rename to extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModuleConfig.kt b/extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmModuleConfig.kt similarity index 100% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModuleConfig.kt rename to extension/java/src/main/java/org/pytorch/executorch/extension/llm/LlmModuleConfig.kt diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.kt b/extension/java/src/main/java/org/pytorch/executorch/training/SGD.kt similarity index 91% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.kt rename to extension/java/src/main/java/org/pytorch/executorch/training/SGD.kt index e4aa5373498..37830358ec0 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.kt +++ b/extension/java/src/main/java/org/pytorch/executorch/training/SGD.kt @@ -10,8 +10,7 @@ package org.pytorch.executorch.training import com.facebook.jni.HybridData import com.facebook.jni.annotations.DoNotStrip -import com.facebook.soloader.nativeloader.NativeLoader -import com.facebook.soloader.nativeloader.SystemDelegate +import org.pytorch.executorch.ExecuTorchRuntime import org.pytorch.executorch.Tensor import org.pytorch.executorch.annotations.Experimental @@ -48,10 +47,7 @@ private constructor( companion object { init { - if (!NativeLoader.isInitialized()) { - NativeLoader.init(SystemDelegate()) - } - NativeLoader.loadLibrary("executorch") + ExecuTorchRuntime.ensureNativeLibraryLoaded() } @DoNotStrip diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.kt b/extension/java/src/main/java/org/pytorch/executorch/training/TrainingModule.kt similarity index 94% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.kt rename to extension/java/src/main/java/org/pytorch/executorch/training/TrainingModule.kt index 5556b0c16c4..9b9c1c32cb9 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.kt +++ b/extension/java/src/main/java/org/pytorch/executorch/training/TrainingModule.kt @@ -10,8 +10,6 @@ package org.pytorch.executorch.training import com.facebook.jni.HybridData import com.facebook.jni.annotations.DoNotStrip -import com.facebook.soloader.nativeloader.NativeLoader -import com.facebook.soloader.nativeloader.SystemDelegate import java.io.Closeable import java.util.concurrent.locks.ReentrantLock import org.pytorch.executorch.EValue @@ -101,10 +99,7 @@ class TrainingModule private constructor(moduleAbsolutePath: String, dataAbsolut companion object { init { - if (!NativeLoader.isInitialized()) { - NativeLoader.init(SystemDelegate()) - } - NativeLoader.loadLibrary("executorch") + ExecuTorchRuntime.ensureNativeLibraryLoaded() } @DoNotStrip diff --git a/extension/android/executorch_android/src/test/java/org/pytorch/executorch/EValueTest.kt b/extension/java/src/test/java/org/pytorch/executorch/EValueTest.kt similarity index 100% rename from extension/android/executorch_android/src/test/java/org/pytorch/executorch/EValueTest.kt rename to extension/java/src/test/java/org/pytorch/executorch/EValueTest.kt diff --git a/extension/java/src/test/java/org/pytorch/executorch/LoggerTest.kt b/extension/java/src/test/java/org/pytorch/executorch/LoggerTest.kt new file mode 100644 index 00000000000..02afed5c81d --- /dev/null +++ b/extension/java/src/test/java/org/pytorch/executorch/LoggerTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +package org.pytorch.executorch + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** Unit tests for the [Logger] / [Log] wiring shared by all platform modules. */ +@RunWith(JUnit4::class) +class LoggerTest { + + private class RecordingLogger : Logger { + val events = mutableListOf>() + + override fun e(tag: String, msg: String) { + events.add(Triple("e", tag, msg)) + } + + override fun w(tag: String, msg: String) { + events.add(Triple("w", tag, msg)) + } + + override fun i(tag: String, msg: String) { + events.add(Triple("i", tag, msg)) + } + + override fun d(tag: String, msg: String) { + events.add(Triple("d", tag, msg)) + } + } + + @Test + fun installedLoggerReceivesAllLevels() { + val recorder = RecordingLogger() + Log.install(recorder) + + Log.e("tag", "error-message") + Log.w("tag", "warning-message") + Log.i("tag", "info-message") + Log.d("tag", "debug-message") + + assertEquals( + listOf( + Triple("e", "tag", "error-message"), + Triple("w", "tag", "warning-message"), + Triple("i", "tag", "info-message"), + Triple("d", "tag", "debug-message"), + ), + recorder.events, + ) + } + + @Test + fun consoleLoggerIsAValidDefault() { + // The shared module must work without any platform module installing a logger. + // ConsoleLogger writes to stdout/stderr and must not throw. + val logger = ConsoleLogger() + logger.e("tag", "message") + logger.w("tag", "message") + logger.i("tag", "message") + logger.d("tag", "message") + } +} diff --git a/extension/android/executorch_android/src/test/java/org/pytorch/executorch/TensorTest.kt b/extension/java/src/test/java/org/pytorch/executorch/TensorTest.kt similarity index 100% rename from extension/android/executorch_android/src/test/java/org/pytorch/executorch/TensorTest.kt rename to extension/java/src/test/java/org/pytorch/executorch/TensorTest.kt diff --git a/extension/jvm/README.md b/extension/jvm/README.md new file mode 100644 index 00000000000..7cdcd7c0af5 --- /dev/null +++ b/extension/jvm/README.md @@ -0,0 +1,53 @@ +# ExecuTorch JVM (Desktop) + +ExecuTorch Java/Kotlin bindings for standard desktop JVMs (Linux, macOS, Windows). + +This module is a thin sibling of the Android artifact: all platform-neutral API classes +(`Module`, `Tensor`, `EValue`, `DType`, …) come from the shared +[`extension/java`](../java) module (`org.pytorch:executorch-java`). This module only adds +desktop-specific behavior: classpath-based native library delivery and console logging. + +## Usage + +```kotlin +import org.pytorch.executorch.ExecuTorchJvm +import org.pytorch.executorch.Module + +fun main() { + // Install the desktop native loader before touching any other ExecuTorch API. + ExecuTorchJvm.init() + + val module = Module.load("/path/to/model.pte") + // ... +} +``` + +`ExecuTorchJvm.init()` is explicit and compile-checked — no reflection, no ServiceLoader. + +## Native library delivery + +The API jar contains no native binaries. Each desktop OS/arch ships as a classified +Maven artifact, following the standard API-jar + per-platform-native-jars pattern: + +``` +org.pytorch:executorch-jvm: (API jar) +org.pytorch:executorch-jvm::linux-x86_64 (native jar, Linux x86_64) +org.pytorch:executorch-jvm::linux-aarch64 +org.pytorch:executorch-jvm::macos-x86_64 +org.pytorch:executorch-jvm::macos-aarch64 +org.pytorch:executorch-jvm::windows-x86_64 +org.pytorch:executorch-jvm::windows-aarch64 +``` + +At runtime, `NativeLibraryLoader` extracts `native///libexecutorch_jni.{so,dylib,dll}` +from the classpath to a temp directory and loads it. At publish time, CI stages binaries under +`extension/cmake-out-jvm//` (built with `extension/android/CMakeLists.txt`, +non-Android branch) and the `jarNative*` Gradle tasks in this module package them into the +classified jars. + +Gradle dependency example (Linux x86_64 host): + +```groovy +implementation "org.pytorch:executorch-jvm:" +implementation "org.pytorch:executorch-jvm::linux-x86_64" +``` diff --git a/extension/jvm/build.gradle b/extension/jvm/build.gradle index e2cee45246a..755a7c5ca6f 100644 --- a/extension/jvm/build.gradle +++ b/extension/jvm/build.gradle @@ -6,6 +6,15 @@ * LICENSE file in the root directory of this source tree. */ +// Desktop JVM (Linux / macOS / Windows) artifact for ExecuTorch. +// +// Thin sibling of :executorch_android: all platform-neutral API classes come from +// :executorch_java. This module only adds the desktop-specific pieces: +// - ExecuTorchJvm.init(): installs the classpath-extracting native loader +// - JvmNativeLoaderDelegate / NativeLibraryLoader: per-OS native library delivery +// - Per-platform native jars published with Maven classifiers +// (executorch-jvm--linux-x86_64.jar, -macos-aarch64.jar, ...) + apply plugin: 'kotlin' apply plugin: 'java-library' apply plugin: 'com.vanniktech.maven.publish' @@ -25,34 +34,12 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -sourceSets { - main { - kotlin { - srcDirs = ['../android/executorch_android/src/main/java', 'src/main/java'] - exclude { element -> - element.file.path.contains("executorch_android") && element.file.name == "Log.kt" - } - } - } - test { - kotlin { - srcDirs = ['../android/executorch_android/src/test/java', 'src/test/java'] - } - } -} - -repositories { - google() - mavenCentral() -} - dependencies { - implementation 'com.facebook.fbjni:fbjni-java-only:0.7.0' - implementation 'com.facebook.soloader:nativeloader:0.10.5' - implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" + api project(':executorch_java') + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.23" testImplementation 'junit:junit:4.13.2' testImplementation 'org.assertj:assertj-core:3.27.2' - testImplementation "org.jetbrains.kotlin:kotlin-test:${libs.versions.kotlin.get()}" + testImplementation 'org.jetbrains.kotlin:kotlin-test:1.9.23' } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { @@ -62,6 +49,43 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { } } +// --------------------------------------------------------------------------- +// Per-platform native jars. +// +// CI builds the JNI library for each desktop OS/arch with CMake +// (extension/android/CMakeLists.txt, non-Android branch) and stages the binary as +// extension/cmake-out-jvm//libexecutorch_jni.{so,dylib} (or executorch_jni.dll) +// where is e.g. linux-x86_64. Each staged binary is packaged as +// executorch-jvm--.jar containing +// native/// +// which NativeLibraryLoader extracts and loads at runtime. Classifiers whose +// staging directory does not exist are skipped, so local builds publish the API +// jar only. +// --------------------------------------------------------------------------- +def nativeClassifiers = [ + 'linux-x86_64', 'linux-aarch64', + 'macos-x86_64', 'macos-aarch64', + 'windows-x86_64', 'windows-aarch64', +] + +def nativeJarTasks = [] +nativeClassifiers.each { classifier -> + def parts = classifier.split('-') + def osDir = parts[0] + def archDir = parts[1] + def taskName = 'jarNative' + classifier.split('-').collect { it.capitalize() }.join('') + def stagingDir = rootProject.file("../cmake-out-jvm/${classifier}") + def t = tasks.register(taskName, Jar) { + archiveClassifier.set(classifier) + destinationDirectory.set(layout.buildDirectory.dir("libs")) + from(stagingDir) { + into "native/${osDir}/${archDir}" + } + onlyIf { stagingDir.exists() } + } + nativeJarTasks.add([task: t, dir: stagingDir]) +} + mavenPublishing { publishToMavenCentral() signAllPublications() @@ -89,8 +113,23 @@ mavenPublishing { } scm { url = "https://github.com/pytorch/executorch.git" - connection = "scm:git:https://github.com/pytorch/executorch" + connection = "scm:git:git@github.com:pytorch/executorch.git" developerConnection = "scm:git:git@github.com:pytorch/executorch.git" } } } + +// Attach each staged per-platform native jar to the Maven publication as a +// classified artifact. Evaluated after the maven-publish plugin creates the +// publication; unstaged platforms are omitted entirely. +afterEvaluate { + publishing { + publications.withType(MavenPublication).configureEach { pub -> + nativeJarTasks.each { entry -> + if (entry.dir.exists()) { + pub.artifact(entry.task.get()) + } + } + } + } +} diff --git a/extension/jvm/src/main/java/org/pytorch/executorch/ExecuTorchJvm.kt b/extension/jvm/src/main/java/org/pytorch/executorch/ExecuTorchJvm.kt new file mode 100644 index 00000000000..50d4d4ddc0c --- /dev/null +++ b/extension/jvm/src/main/java/org/pytorch/executorch/ExecuTorchJvm.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch + +/** + * Entry point for using ExecuTorch on a desktop JVM (Linux, macOS, Windows). + * + * Call [init] once, before any other ExecuTorch API: + * ```kotlin + * ExecuTorchJvm.init() + * val module = Module.load("model.pte") + * ``` + * + * [init] installs the desktop [JvmNativeLoaderDelegate], which extracts the packaged + * `libexecutorch_jni` binary for the current OS/arch from the classpath (see [NativeLibraryLoader]) + * instead of relying on `java.library.path`. No reflection or ServiceLoader is involved — the + * wiring is compile-checked. + * + * The corresponding per-platform native artifact must be on the classpath, e.g. + * `org.pytorch:executorch-jvm::linux-x86_64`. + */ +object ExecuTorchJvm { + @Volatile private var initialized = false + + /** Installs the desktop JVM native loading behavior. Idempotent. */ + @JvmStatic + @Synchronized + fun init() { + if (initialized) { + return + } + ExecuTorchRuntime.configureNativeLoading(JvmNativeLoaderDelegate(), "executorch_jni") + initialized = true + } +} diff --git a/extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt b/extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt index 07873739e8a..df026cbc765 100644 --- a/extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt +++ b/extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.kt @@ -11,16 +11,15 @@ package org.pytorch.executorch import com.facebook.soloader.nativeloader.NativeLoaderDelegate /** - * Desktop JVM-specific [NativeLoaderDelegate] that delegates library loading to - * [NativeLibraryLoader]. + * Desktop JVM [NativeLoaderDelegate] that loads native libraries via [NativeLibraryLoader], which + * extracts the binary for the current OS/arch from the classpath. * - * This implementation maps requests to load "executorch" to the actual JNI library "executorch_jni" - * built for desktop platforms. + * Installed by [ExecuTorchJvm.init]; the shared runtime passes the configured library name + * ("executorch_jni") straight through. */ class JvmNativeLoaderDelegate : NativeLoaderDelegate { override fun loadLibrary(shortName: String, flags: Int): Boolean { - val libraryToLoad = if (shortName == "executorch") "executorch_jni" else shortName - NativeLibraryLoader.load(libraryToLoad) + NativeLibraryLoader.load(shortName) return true } diff --git a/extension/jvm/src/main/java/org/pytorch/executorch/Log.kt b/extension/jvm/src/main/java/org/pytorch/executorch/Log.kt deleted file mode 100644 index c5962e08e55..00000000000 --- a/extension/jvm/src/main/java/org/pytorch/executorch/Log.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -package org.pytorch.executorch - -/** Desktop JVM logging helper that prints warnings and errors to standard error/out. */ -internal object Log { - fun e(tag: String, msg: String) { - System.err.println("[$tag] ERROR: $msg") - } - - fun w(tag: String, msg: String) { - System.err.println("[$tag] WARNING: $msg") - } - - fun i(tag: String, msg: String) { - System.out.println("[$tag] INFO: $msg") - } - - fun d(tag: String, msg: String) { - System.out.println("[$tag] DEBUG: $msg") - } -} diff --git a/extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate b/extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate deleted file mode 100644 index 614fe9eb2f6..00000000000 --- a/extension/jvm/src/main/resources/META-INF/services/com.facebook.soloader.nativeloader.NativeLoaderDelegate +++ /dev/null @@ -1 +0,0 @@ -org.pytorch.executorch.JvmNativeLoaderDelegate