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..2d8a7f5ae7e 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"] } } @@ -60,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/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/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..2936b3af36f 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -21,4 +21,17 @@ plugins { rootProject.name = 'executorch' -include('executorch_android') +// 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.") +} + +// 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 99% 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 index f2f3ebea214..433667ccae0 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/Tensor.kt +++ b/extension/java/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_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/jvm/build.gradle b/extension/jvm/build.gradle new file mode 100644 index 00000000000..26f56fda89a --- /dev/null +++ b/extension/jvm/build.gradle @@ -0,0 +1,76 @@ +/* + * 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 { + api project(':executorch_java') + implementation "org.jetbrains.kotlin:kotlin-stdlib:${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/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