From 4de6ecb5eb3fe124124ebdfb4b7c4b5cd21bf426 Mon Sep 17 00:00:00 2001 From: Ranjithkumar Ragavan <43761047+RanjithRagavan@users.noreply.github.com> Date: Tue, 7 Jul 2026 05:50:54 -0700 Subject: [PATCH 1/3] Support pure JVM JAR compilation and packaging for desktop platforms (Linux/macOS/Windows) --- extension/android/CMakeLists.txt | 122 ++++++++++-------- .../main/java/org/pytorch/executorch/Log.kt | 43 ++++++ .../java/org/pytorch/executorch/Tensor.kt | 1 - extension/android/executorch_jvm/build.gradle | 81 ++++++++++++ extension/android/gradle/libs.versions.toml | 1 + extension/android/jni/jni_layer.cpp | 2 +- extension/android/jni/log.cpp | 30 +++++ extension/android/settings.gradle | 7 +- 8 files changed, 233 insertions(+), 54 deletions(-) create mode 100644 extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt create mode 100644 extension/android/executorch_jvm/build.gradle diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index c94b95286d0..304806d27cf 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -13,64 +13,80 @@ if(NOT CMAKE_CXX_STANDARD) endif() if(NOT ANDROID) - message(FATAL_ERROR "This directory is for Android build only") -endif() + 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(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() + 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(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() + # 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() -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) + 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) -if(NOT EXISTS "${FBJNI_DOWNLOAD_PATH}") - file(DOWNLOAD "${FBJNI_AAR_URL}" "${FBJNI_DOWNLOAD_PATH}") -endif() + if(NOT EXISTS "${FBJNI_DOWNLOAD_PATH}") + file(DOWNLOAD "${FBJNI_AAR_URL}" "${FBJNI_DOWNLOAD_PATH}") + 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}" -) + 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" -) + 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" + ) -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_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() -executorch_target_link_options_shared_lib(executorch) +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 @@ -248,4 +264,8 @@ target_link_options( ) 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/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..6353ee050bc --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt @@ -0,0 +1,43 @@ +/* + * 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.lang.reflect.Method + +/** + * Platform-agnostic logging helper that forwards logs to android.util.Log when running + * on Android, and falls back to standard error console logging when running on desktop JVMs. + */ +internal object Log { + private var androidLogMethod: Method? = null + private var lookupFailed = false + + fun e(tag: String, msg: String) { + if (!lookupFailed && androidLogMethod == null) { + try { + val logClass = Class.forName("android.util.Log") + androidLogMethod = logClass.getMethod("e", String::class.java, String::class.java) + } catch (e: Exception) { + lookupFailed = true + } + } + + val method = androidLogMethod + if (method != null) { + try { + method.invoke(null, tag, msg) + return + } catch (e: Exception) { + // Fallback to console printing if invocation fails + } + } + + System.err.println("[$tag] ERROR: $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/executorch_jvm/build.gradle b/extension/android/executorch_jvm/build.gradle new file mode 100644 index 00000000000..b88b53174b7 --- /dev/null +++ b/extension/android/executorch_jvm/build.gradle @@ -0,0 +1,81 @@ +/* + * 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. + */ + +plugins { + alias(libs.plugins.jetbrains.kotlin.jvm) + id 'java-library' + id "com.vanniktech.maven.publish" version "0.31.0" +} + +def execuTorchVersion = System.properties['execuTorchVersion'] + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" + implementation "com.facebook.fbjni:fbjni-java-only:${fbjniJavaOnlyVersion}" + implementation "com.facebook.soloader:nativeloader:${soLoaderNativeLoaderVersion}" +} + +sourceSets { + main { + kotlin { + srcDirs = ['../executorch_android/src/main/java'] + exclude '**/androidTest/**' + exclude '**/test/**' + } + } +} + +compileKotlin { + 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 = "2026" + 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/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 2459746df0d..8adaee00afe 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -477,7 +477,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..83a62397671 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -21,4 +21,9 @@ plugins { rootProject.name = 'executorch' -include('executorch_android') +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.") +} +include('executorch_jvm') From 692c600ea8ec9f1536fc39d1a259d4ebcf2ff742 Mon Sep 17 00:00:00 2001 From: Ranjithkumar Ragavan <43761047+RanjithRagavan@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:24:11 -0700 Subject: [PATCH 2/3] Fix CMakeLists.txt formatting and apply plugins via root buildscript classpath to resolve Kotlin duplicate loading and classloader issues --- extension/android/CMakeLists.txt | 28 ++++++++++--------- extension/android/build.gradle | 5 ++-- .../android/executorch_android/build.gradle | 10 +++---- .../main/java/org/pytorch/executorch/Log.kt | 4 +-- extension/android/executorch_jvm/build.gradle | 14 ++++------ 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index 304806d27cf..fba3eba82e5 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -15,15 +15,14 @@ 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") + message( + FATAL_ERROR + "For non-Android platforms, please specify -DFBJNI_HEADERS_DIR=/path/to/fbjni/include and -DFBJNI_LIBRARY=/path/to/libfbjni.so" + ) endif() add_library(fbjni SHARED IMPORTED) - set_target_properties( - fbjni - PROPERTIES - IMPORTED_LOCATION "${FBJNI_LIBRARY}" - ) + set_target_properties(fbjni PROPERTIES IMPORTED_LOCATION "${FBJNI_LIBRARY}") set(_common_compile_options $<$:/wd4996> $<$>:-Wno-deprecated-declarations -fPIC> @@ -39,12 +38,13 @@ else() 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! + # 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() @@ -52,7 +52,9 @@ else() 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) + set(FBJNI_DOWNLOAD_PATH + ${CMAKE_CURRENT_BINARY_DIR}/third-party/fbjni/fbjni.aar + ) if(NOT EXISTS "${FBJNI_DOWNLOAD_PATH}") file(DOWNLOAD "${FBJNI_AAR_URL}" "${FBJNI_DOWNLOAD_PATH}") diff --git a/extension/android/build.gradle b/extension/android/build.gradle index 78db3c0f6b5..128f949ce9a 100644 --- a/extension/android/build.gradle +++ b/extension/android/build.gradle @@ -12,9 +12,10 @@ 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" } - } repositories { diff --git a/extension/android/executorch_android/build.gradle b/extension/android/executorch_android/build.gradle index 2dbe0e1fb5f..b0e4dbaf041 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 { 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 index 6353ee050bc..83eb780716e 100644 --- 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 @@ -11,8 +11,8 @@ package org.pytorch.executorch import java.lang.reflect.Method /** - * Platform-agnostic logging helper that forwards logs to android.util.Log when running - * on Android, and falls back to standard error console logging when running on desktop JVMs. + * Platform-agnostic logging helper that forwards logs to android.util.Log when running on Android, + * and falls back to standard error console logging when running on desktop JVMs. */ internal object Log { private var androidLogMethod: Method? = null diff --git a/extension/android/executorch_jvm/build.gradle b/extension/android/executorch_jvm/build.gradle index b88b53174b7..394ad4078bc 100644 --- a/extension/android/executorch_jvm/build.gradle +++ b/extension/android/executorch_jvm/build.gradle @@ -6,17 +6,15 @@ * LICENSE file in the root directory of this source tree. */ -plugins { - alias(libs.plugins.jetbrains.kotlin.jvm) - id 'java-library' - id "com.vanniktech.maven.publish" version "0.31.0" -} +apply plugin: 'kotlin' +apply plugin: 'java-library' +apply plugin: 'com.vanniktech.maven.publish' def execuTorchVersion = System.properties['execuTorchVersion'] java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } repositories { @@ -42,7 +40,7 @@ sourceSets { compileKotlin { kotlinOptions { - jvmTarget = "17" + jvmTarget = "11" freeCompilerArgs += ["-Xjvm-default=all"] } } From 50fcff98e6a82485435ce4c27a150ee578fe603f Mon Sep 17 00:00:00 2001 From: Ranjithkumar Ragavan <43761047+RanjithRagavan@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:13:18 -0700 Subject: [PATCH 3/3] Redesign ExecuTorch JVM support to extract shared Java API - Extract platform-neutral API into shared 'extension/java/' module - Replace reflection-based logging with ServiceLoader Logger SPI - Create thin sibling platform modules ('extension/android/' and 'extension/jvm/') - Implement per-OS NativeLibraryLoader and NativeLoader JvmDelegate on desktop JVM - Relocate unit tests to shared module for fast JVM test execution --- extension/android/CMakeLists.txt | 37 +++++--- extension/android/build.gradle | 5 ++ .../android/executorch_android/build.gradle | 7 +- .../org/pytorch/executorch/AndroidLogger.kt | 35 ++++++++ .../main/java/org/pytorch/executorch/Log.kt | 43 --------- .../services/org.pytorch.executorch.Logger | 1 + extension/android/settings.gradle | 10 ++- extension/java/build.gradle | 80 +++++++++++++++++ .../main/java/org/pytorch/executorch/DType.kt | 0 .../java/org/pytorch/executorch/EValue.kt | 0 .../pytorch/executorch/ExecuTorchRuntime.kt | 5 +- .../executorch/ExecutorchRuntimeException.kt | 0 .../main/java/org/pytorch/executorch/Log.kt | 59 +++++++++++++ .../org/pytorch/executorch/MethodMetadata.kt | 0 .../java/org/pytorch/executorch/Module.kt | 0 .../java/org/pytorch/executorch/Tensor.kt | 0 .../executorch/annotations/Experimental.kt | 0 .../executorch/extension/asr/AsrCallback.kt | 0 .../executorch/extension/asr/AsrModule.kt | 0 .../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 | 0 .../executorch/training/TrainingModule.kt | 0 .../java/org/pytorch/executorch/EValueTest.kt | 0 .../java/org/pytorch/executorch/LoggerTest.kt | 25 ++++++ .../java/org/pytorch/executorch/TensorTest.kt | 0 .../executorch_jvm => jvm}/build.gradle | 31 +++---- .../org/pytorch/executorch/ConsoleLogger.kt | 25 ++++++ .../executorch/JvmNativeLoaderDelegate.kt | 34 +++++++ .../pytorch/executorch/NativeLibraryLoader.kt | 88 +++++++++++++++++++ ...soloader.nativeloader.NativeLoaderDelegate | 1 + .../services/org.pytorch.executorch.Logger | 1 + 35 files changed, 410 insertions(+), 77 deletions(-) create mode 100644 extension/android/executorch_android/src/main/java/org/pytorch/executorch/AndroidLogger.kt delete mode 100644 extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt create mode 100644 extension/android/executorch_android/src/main/resources/META-INF/services/org.pytorch.executorch.Logger create mode 100644 extension/java/build.gradle 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%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt (88%) 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/Log.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 (100%) 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 (100%) 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 (100%) rename extension/{android/executorch_android => java}/src/main/java/org/pytorch/executorch/training/TrainingModule.kt (100%) 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%) rename extension/{android/executorch_jvm => jvm}/build.gradle (76%) create mode 100644 extension/jvm/src/main/java/org/pytorch/executorch/ConsoleLogger.kt create mode 100644 extension/jvm/src/main/java/org/pytorch/executorch/JvmNativeLoaderDelegate.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 create mode 100644 extension/jvm/src/main/resources/META-INF/services/org.pytorch.executorch.Logger diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index fba3eba82e5..bc75b7cf083 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -12,6 +12,9 @@ if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) endif() +set(EXECUTORCH_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") +include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake) + if(NOT ANDROID) find_package(JNI REQUIRED) if(NOT FBJNI_HEADERS_DIR OR NOT FBJNI_LIBRARY) @@ -28,8 +31,6 @@ if(NOT ANDROID) $<$>:-Wno-deprecated-declarations -fPIC> ) else() - set(EXECUTORCH_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") - include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake) set(_common_compile_options $<$:/wd4996> $<$>:-Wno-deprecated-declarations -fPIC> @@ -251,19 +252,31 @@ 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) if(ANDROID) diff --git a/extension/android/build.gradle b/extension/android/build.gradle index 128f949ce9a..b6e4f79032b 100644 --- a/extension/android/build.gradle +++ b/extension/android/build.gradle @@ -18,6 +18,11 @@ allprojects { } } + ext { + fbjniJavaOnlyVersion = "0.7.0" + soLoaderNativeLoaderVersion = "0.10.5" + } + repositories { google() mavenCentral() diff --git a/extension/android/executorch_android/build.gradle b/extension/android/executorch_android/build.gradle index b0e4dbaf041..2d8a7f5ae7e 100644 --- a/extension/android/executorch_android/build.gradle +++ b/extension/android/executorch_android/build.gradle @@ -35,8 +35,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } sourceSets { @@ -48,7 +48,7 @@ android { } } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" freeCompilerArgs += ["-Xjvm-default=all"] } } @@ -58,6 +58,7 @@ task copyTestRes(type: Exec) { } dependencies { + 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/java/org/pytorch/executorch/AndroidLogger.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/AndroidLogger.kt new file mode 100644 index 00000000000..b21b38e6b1b --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/AndroidLogger.kt @@ -0,0 +1,35 @@ +/* + * 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-specific [Logger] implementation that delegates to [android.util.Log]. + * + * Discovered automatically via [java.util.ServiceLoader] when the executorch-android AAR is on the + * classpath. + */ +class AndroidLogger : Logger { + override fun e(tag: String, msg: String) { + AndroidLog.e(tag, msg) + } + + override fun w(tag: String, msg: String) { + AndroidLog.w(tag, msg) + } + + override fun i(tag: String, msg: String) { + AndroidLog.i(tag, msg) + } + + override fun d(tag: String, msg: String) { + AndroidLog.d(tag, msg) + } +} 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 deleted file mode 100644 index 83eb780716e..00000000000 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Log.kt +++ /dev/null @@ -1,43 +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 java.lang.reflect.Method - -/** - * Platform-agnostic logging helper that forwards logs to android.util.Log when running on Android, - * and falls back to standard error console logging when running on desktop JVMs. - */ -internal object Log { - private var androidLogMethod: Method? = null - private var lookupFailed = false - - fun e(tag: String, msg: String) { - if (!lookupFailed && androidLogMethod == null) { - try { - val logClass = Class.forName("android.util.Log") - androidLogMethod = logClass.getMethod("e", String::class.java, String::class.java) - } catch (e: Exception) { - lookupFailed = true - } - } - - val method = androidLogMethod - if (method != null) { - try { - method.invoke(null, tag, msg) - return - } catch (e: Exception) { - // Fallback to console printing if invocation fails - } - } - - System.err.println("[$tag] ERROR: $msg") - } -} diff --git a/extension/android/executorch_android/src/main/resources/META-INF/services/org.pytorch.executorch.Logger b/extension/android/executorch_android/src/main/resources/META-INF/services/org.pytorch.executorch.Logger new file mode 100644 index 00000000000..3d4ed5cc8a2 --- /dev/null +++ b/extension/android/executorch_android/src/main/resources/META-INF/services/org.pytorch.executorch.Logger @@ -0,0 +1 @@ +org.pytorch.executorch.AndroidLogger diff --git a/extension/android/settings.gradle b/extension/android/settings.gradle index 83a62397671..2936b3af36f 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -21,9 +21,17 @@ plugins { rootProject.name = 'executorch' +// Shared platform-neutral API module (no android.* dependency) +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') } else { logger.warn("WARNING: ANDROID_HOME is not set, skipping Android subproject.") } -include('executorch_jvm') + +// Desktop JVM module +include(':executorch_jvm') +project(':executorch_jvm').projectDir = new File(rootDir, '../jvm') diff --git a/extension/java/build.gradle b/extension/java/build.gradle new file mode 100644 index 00000000000..c07a52b0fc3 --- /dev/null +++ b/extension/java/build.gradle @@ -0,0 +1,80 @@ +/* + * 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 +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" + api "com.facebook.fbjni:fbjni-java-only:${fbjniJavaOnlyVersion}" + api "com.facebook.soloader:nativeloader:${soLoaderNativeLoaderVersion}" + 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 = "ExecuTorch platform-neutral Java/Kotlin API (shared by Android and desktop JVM modules)" + 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/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/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt b/extension/java/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt similarity index 88% rename from extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.kt rename to extension/java/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/java/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/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/Log.kt b/extension/java/src/main/java/org/pytorch/executorch/Log.kt new file mode 100644 index 00000000000..44d323bcc6d --- /dev/null +++ b/extension/java/src/main/java/org/pytorch/executorch/Log.kt @@ -0,0 +1,59 @@ +/* + * 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.util.ServiceLoader + +/** + * Platform-agnostic logging interface. + * + * Platform-specific modules (Android, JVM desktop) provide implementations via [ServiceLoader] SPI. + * If no implementation is found on the classpath, a built-in [FallbackLogger] that writes to + * stderr/stdout is used. + */ +interface Logger { + fun e(tag: String, msg: String) + + fun w(tag: String, msg: String) + + fun i(tag: String, msg: String) + + fun d(tag: String, msg: String) +} + +/** + * Internal logging facade used throughout the ExecuTorch Java API. + * + * Delegates to a [Logger] implementation discovered via [ServiceLoader], falling back to a console + * logger if none is available. + */ +internal object Log { + private val delegate: Logger by lazy { + ServiceLoader.load(Logger::class.java).firstOrNull() ?: FallbackLogger() + } + + fun e(tag: String, msg: String) = delegate.e(tag, msg) + + fun w(tag: String, msg: String) = delegate.w(tag, msg) + + fun i(tag: String, msg: String) = delegate.i(tag, msg) + + fun d(tag: String, msg: String) = delegate.d(tag, msg) +} + +/** Default fallback if no platform-specific [Logger] is on the classpath. */ +private class FallbackLogger : 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] WARN: $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") +} 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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..cdb35502cf1 --- /dev/null +++ b/extension/java/src/test/java/org/pytorch/executorch/LoggerTest.kt @@ -0,0 +1,25 @@ +/* + * 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.Test + +/** Basic sanity test for [Log] facade. */ +class LoggerTest { + + @Test + fun testFallbackLogging() { + // Under :executorch_java tests, there is no platform Logger registered + // in META-INF/services, so it should fall back to Console fallback. + Log.d("LoggerTest", "Test debug log") + Log.i("LoggerTest", "Test info log") + Log.w("LoggerTest", "Test warn log") + Log.e("LoggerTest", "Test error log") + } +} 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/android/executorch_jvm/build.gradle b/extension/jvm/build.gradle similarity index 76% rename from extension/android/executorch_jvm/build.gradle rename to extension/jvm/build.gradle index 394ad4078bc..26f56fda89a 100644 --- a/extension/android/executorch_jvm/build.gradle +++ b/extension/jvm/build.gradle @@ -9,12 +9,20 @@ 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_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } repositories { @@ -23,24 +31,13 @@ repositories { } dependencies { + api project(':executorch_java') implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" - implementation "com.facebook.fbjni:fbjni-java-only:${fbjniJavaOnlyVersion}" - implementation "com.facebook.soloader:nativeloader:${soLoaderNativeLoaderVersion}" -} - -sourceSets { - main { - kotlin { - srcDirs = ['../executorch_android/src/main/java'] - exclude '**/androidTest/**' - exclude '**/test/**' - } - } } -compileKotlin { +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" freeCompilerArgs += ["-Xjvm-default=all"] } } @@ -54,7 +51,7 @@ mavenPublishing { pom { name = "ExecuTorch JVM" description = "ExecuTorch Java/Kotlin bindings for standard desktop JVM (Linux, macOS, Windows)" - inceptionYear = "2026" + inceptionYear = "2024" url = "https://github.com/pytorch/executorch/" licenses { license { diff --git a/extension/jvm/src/main/java/org/pytorch/executorch/ConsoleLogger.kt b/extension/jvm/src/main/java/org/pytorch/executorch/ConsoleLogger.kt new file mode 100644 index 00000000000..05b92b8a204 --- /dev/null +++ b/extension/jvm/src/main/java/org/pytorch/executorch/ConsoleLogger.kt @@ -0,0 +1,25 @@ +/* + * 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 [Logger] implementation that writes to standard output/error streams. + * + * Discovered automatically via [java.util.ServiceLoader] when the executorch-jvm JAR is on the + * classpath. + */ +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] WARN: $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") +} 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/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 diff --git a/extension/jvm/src/main/resources/META-INF/services/org.pytorch.executorch.Logger b/extension/jvm/src/main/resources/META-INF/services/org.pytorch.executorch.Logger new file mode 100644 index 00000000000..46b6a6687a8 --- /dev/null +++ b/extension/jvm/src/main/resources/META-INF/services/org.pytorch.executorch.Logger @@ -0,0 +1 @@ +org.pytorch.executorch.ConsoleLogger