From 452e8bfd9dbd842756fdcf4741884f6ce4c823d8 Mon Sep 17 00:00:00 2001 From: andrefigas Date: Fri, 6 Feb 2026 18:40:55 +0000 Subject: [PATCH 1/4] Fix rust path resolution relative to project root ReflectionJVM and ReflectionNative used File(extension.rustPath) which resolved relative to the Gradle daemon working directory instead of the project root, causing "Could not find lib.rs" errors. Changed to FileUtils.getRustDir(project, extension) which correctly resolves relative to project.rootProject.projectDir. --- .../andrefigas/rustjni/reflection/ReflectionJVM.kt | 9 +++++---- .../rustjni/reflection/ReflectionNative.kt | 13 ++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionJVM.kt b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionJVM.kt index 6d8df5c..92846af 100644 --- a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionJVM.kt +++ b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionJVM.kt @@ -95,7 +95,7 @@ internal object ReflectionJVM { isKotlinFile: Boolean ): String { val jniHost = extension.jniHost.trim() - val methodsToGenerate = parseRustJniFunctions(extension, jniHost, isKotlinFile) + val methodsToGenerate = parseRustJniFunctions(project, extension, jniHost, isKotlinFile) if (methodsToGenerate.isEmpty()) { throw org.gradle.api.GradleException("No JNI methods found for class $jniHost in lib.rs") @@ -105,11 +105,12 @@ internal object ReflectionJVM { } private fun parseRustJniFunctions( + project: Project, extension: RustJniExtension, jniHost: String, isKotlinFile: Boolean ): List { - val rustLibContent = readRustJniFile(extension) + val rustLibContent = readRustJniFile(project, extension) val jniFunctionPattern = Regex( """(?s)#\s*\[\s*no_mangle\s*\]\s*pub\s+extern\s+"C"\s+fn\s+(Java_\w+)\s*\((.*?)\)\s*(->\s*[\w:]+)?\s*\{""", @@ -177,8 +178,8 @@ internal object ReflectionJVM { return MethodSignature(jniFunctionName, returnType, parameters) } - private fun readRustJniFile(extension: RustJniExtension): String { - val rustLibFile = FileUtils.getRustSrcFile(File(extension.rustPath)) + private fun readRustJniFile(project: Project, extension: RustJniExtension): String { + val rustLibFile = FileUtils.getRustSrcFile(FileUtils.getRustDir(project, extension)) if (!rustLibFile.exists()) { throw org.gradle.api.GradleException("Could not find '${rustLibFile.name}' file at ${rustLibFile.absolutePath}") } diff --git a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionNative.kt b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionNative.kt index ffca7a8..9d883ab 100644 --- a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionNative.kt +++ b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/reflection/ReflectionNative.kt @@ -51,10 +51,11 @@ internal object ReflectionNative { // Extract method signatures val kotlinMethodSignatures = ReflectionJVM.extractMethodSignaturesFromClass(fileContent, isKotlinFile) - val rustMethodSignatures = parseRustJniFunctions(extension, jniHost, isKotlinFile) + val rustMethodSignatures = parseRustJniFunctions(project, extension, jniHost, isKotlinFile) compareMethodSignatures(kotlinMethodSignatures, rustMethodSignatures, isKotlinFile).forEach { methodSignature -> updateRustFileIfMethodNotExists( + project, extension, methodSignature.methodName, methodSignature.parameters, @@ -69,11 +70,12 @@ internal object ReflectionNative { } private fun parseRustJniFunctions( + project: Project, extension: RustJniExtension, jniHost: String, isKotlinFile: Boolean ): List { - val rustLibContent = readRustJniFile(extension) + val rustLibContent = readRustJniFile(project, extension) val jniFunctionPattern = Regex( """(?s)#\s*\[\s*no_mangle\s*\]\s*pub\s+extern\s+"C"\s+fn\s+(Java_\w+)\s*\((.*?)\)\s*(->\s*[\w:]+)?\s*\{""", @@ -166,8 +168,8 @@ internal object ReflectionNative { return classNameParts.joinToString(".").replace('_', '.') } - private fun readRustJniFile(extension: RustJniExtension): String { - val rustLibFile = FileUtils.getRustSrcFile(File(extension.rustPath)) + private fun readRustJniFile(project: Project, extension: RustJniExtension): String { + val rustLibFile = FileUtils.getRustSrcFile(FileUtils.getRustDir(project, extension)) if (!rustLibFile.exists()) { throw org.gradle.api.GradleException("Could not find '${rustLibFile.name}' file at ${rustLibFile.absolutePath}") } @@ -206,6 +208,7 @@ internal object ReflectionNative { // Updates the Rust file if the corresponding method does not exist private fun updateRustFileIfMethodNotExists( + project: Project, extension: RustJniExtension, methodName: String, parameters: List, @@ -216,7 +219,7 @@ internal object ReflectionNative { if (RustJniExtension.shouldSkipAddingMethods(jniHost, extension)) return - val rustFilePath = FileUtils.getRustSrcFile(File(extension.rustPath)) + val rustFilePath = FileUtils.getRustSrcFile(FileUtils.getRustDir(project, extension)) addMethodToRust(rustFilePath, methodName, parameters, returnType, jniHost, isKotlinFile) } From 7a577f04a34e424466256a4675dc0ca173230e36 Mon Sep 17 00:00:00 2001 From: andrefigas Date: Fri, 6 Feb 2026 18:41:08 +0000 Subject: [PATCH 2/4] Add CC/AR environment variables for cross-compilation Pass CC_ and AR_ env vars to cargo build, pointing to the NDK clang and llvm-ar. This enables compilation of Rust crates that depend on C code via cc-rs (e.g. ring, openssl-sys, libsqlite3-sys) which was previously failing with "failed to find tool clang.exe". --- .../io/github/andrefigas/rustjni/RustJNI.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt index 69671fa..ffa8c52 100644 --- a/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt +++ b/gradle-plugin/src/main/kotlin/io/github/andrefigas/rustjni/RustJNI.kt @@ -44,8 +44,9 @@ class RustJNI : Plugin { private fun runCargoCommand(arguments: List, dir: File = rustDir, - outputProcessor: ((String) -> Unit)? = null) { - runCommand("cargo", arguments, dir, outputProcessor) + outputProcessor: ((String) -> Unit)? = null, + extraEnv: Map = emptyMap()) { + runCommand("cargo", arguments, dir, outputProcessor, extraEnv) } private fun runRustupCommand(arguments: List, @@ -341,8 +342,20 @@ class RustJNI : Plugin { } private fun buildRustForArchitectures() { + val prebuiltPath = getPrebuiltPath() extension.architecturesList.forEach { archConfig -> - runCargoCommand(listOf("build", "--target", archConfig.target, "--release", "--verbose")) + val targetEnvKey = archConfig.target.replace('-', '_') + val linker = OSHelper.addLinkerExtensionIfNeeded(archConfig.linker) + val ccPath = "$prebuiltPath$linker" + val arPath = "$prebuiltPath${archConfig.ar}" + val envVars = mapOf( + "CC_$targetEnvKey" to ccPath, + "AR_$targetEnvKey" to arPath + ) + runCargoCommand( + listOf("build", "--target", archConfig.target, "--release", "--verbose"), + extraEnv = envVars + ) } } From d6f7dfc95e875961375a497daa5b469ccfb13a88 Mon Sep 17 00:00:00 2001 From: andrefigas Date: Fri, 6 Feb 2026 18:41:22 +0000 Subject: [PATCH 3/4] Add REST API sample using Tokio and reqwest New sample demonstrating HTTP client implemented purely in Rust using Tokio runtime, reqwest with rustls-tls, and serde for JSON parsing. Calls the Dog API (dog.ceo) with three JNI functions: getRandomDog, listBreeds, and getBreedImage. This sample exercises the new CC/AR env var support since reqwest depends on ring (C/ASM via cc-rs). --- sample/restapi/.gitignore | 15 + sample/restapi/app/.gitignore | 1 + sample/restapi/app/build.gradle.kts | 64 + sample/restapi/app/proguard-rules.pro | 6 + .../restapi/app/src/main/AndroidManifest.xml | 25 + .../devfigas/rustjni/restapi/MainActivity.kt | 62 + .../res/drawable/ic_launcher_background.xml | 170 ++ .../res/drawable/ic_launcher_foreground.xml | 30 + .../app/src/main/res/layout/activity_main.xml | 68 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values-night/themes.xml | 6 + .../app/src/main/res/values/colors.xml | 5 + .../app/src/main/res/values/strings.xml | 9 + .../app/src/main/res/values/themes.xml | 6 + .../app/src/main/rust/.cargo/config.toml | 21 + sample/restapi/app/src/main/rust/Cargo.lock | 1525 +++++++++++++++++ sample/restapi/app/src/main/rust/Cargo.toml | 19 + sample/restapi/app/src/main/rust/src/lib.rs | 125 ++ sample/restapi/build.gradle.kts | 5 + sample/restapi/gradle.properties | 5 + sample/restapi/gradle/libs.versions.toml | 25 + .../restapi/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + sample/restapi/gradlew | 185 ++ sample/restapi/gradlew.bat | 89 + sample/restapi/settings.gradle.kts | 25 + 37 files changed, 2508 insertions(+) create mode 100644 sample/restapi/.gitignore create mode 100644 sample/restapi/app/.gitignore create mode 100644 sample/restapi/app/build.gradle.kts create mode 100644 sample/restapi/app/proguard-rules.pro create mode 100644 sample/restapi/app/src/main/AndroidManifest.xml create mode 100644 sample/restapi/app/src/main/java/com/devfigas/rustjni/restapi/MainActivity.kt create mode 100644 sample/restapi/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 sample/restapi/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 sample/restapi/app/src/main/res/layout/activity_main.xml create mode 100644 sample/restapi/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 sample/restapi/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 sample/restapi/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 sample/restapi/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 sample/restapi/app/src/main/res/values-night/themes.xml create mode 100644 sample/restapi/app/src/main/res/values/colors.xml create mode 100644 sample/restapi/app/src/main/res/values/strings.xml create mode 100644 sample/restapi/app/src/main/res/values/themes.xml create mode 100644 sample/restapi/app/src/main/rust/.cargo/config.toml create mode 100644 sample/restapi/app/src/main/rust/Cargo.lock create mode 100644 sample/restapi/app/src/main/rust/Cargo.toml create mode 100644 sample/restapi/app/src/main/rust/src/lib.rs create mode 100644 sample/restapi/build.gradle.kts create mode 100644 sample/restapi/gradle.properties create mode 100644 sample/restapi/gradle/libs.versions.toml create mode 100644 sample/restapi/gradle/wrapper/gradle-wrapper.jar create mode 100644 sample/restapi/gradle/wrapper/gradle-wrapper.properties create mode 100644 sample/restapi/gradlew create mode 100644 sample/restapi/gradlew.bat create mode 100644 sample/restapi/settings.gradle.kts diff --git a/sample/restapi/.gitignore b/sample/restapi/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/sample/restapi/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/sample/restapi/app/.gitignore b/sample/restapi/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/restapi/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/restapi/app/build.gradle.kts b/sample/restapi/app/build.gradle.kts new file mode 100644 index 0000000..b699aa0 --- /dev/null +++ b/sample/restapi/app/build.gradle.kts @@ -0,0 +1,64 @@ +import io.github.andrefigas.rustjni.reflection.Visibility + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + id("io.github.andrefigas.rustjni") version "0.0.27" +} + +rustJni { + rustPath = "./app/src/main/rust" + jniHost = "com.devfigas.rustjni.restapi.MainActivity" + jniMethodsVisibility = Visibility.PRIVATE + ndkVersion = "27.1.12297006" + architectures { + armv7_linux_androideabi("armv7a-linux-androideabi21-clang") + aarch64_linux_android("aarch64-linux-android21-clang") + i686_linux_android("i686-linux-android21-clang") + x86_64_linux_android("x86_64-linux-android21-clang") + } + rustVersion = ">=1.64.0" +} + +android { + namespace = "com.devfigas.rustjni.restapi" + compileSdk = 34 + + defaultConfig { + applicationId = "com.devfigas.rustjni.restapi" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} diff --git a/sample/restapi/app/proguard-rules.pro b/sample/restapi/app/proguard-rules.pro new file mode 100644 index 0000000..b19b309 --- /dev/null +++ b/sample/restapi/app/proguard-rules.pro @@ -0,0 +1,6 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html diff --git a/sample/restapi/app/src/main/AndroidManifest.xml b/sample/restapi/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..27b5c5d --- /dev/null +++ b/sample/restapi/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/sample/restapi/app/src/main/java/com/devfigas/rustjni/restapi/MainActivity.kt b/sample/restapi/app/src/main/java/com/devfigas/rustjni/restapi/MainActivity.kt new file mode 100644 index 0000000..87af9d4 --- /dev/null +++ b/sample/restapi/app/src/main/java/com/devfigas/rustjni/restapi/MainActivity.kt @@ -0,0 +1,62 @@ +package com.devfigas.rustjni.restapi + +import android.os.Bundle +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + + // + // auto-generated code + + private external fun getRandomDog(): String + + private external fun listBreeds(): String + + private external fun getBreedImage(breed: String): String + + init { System.loadLibrary("rust_rest_api") } + + // + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val tvResult = findViewById(R.id.tvResult) + val etBreed = findViewById(R.id.etBreed) + val btnRandomDog = findViewById