Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,4 @@ However, you can specify a custom installation directory by setting it in your *
- [Java](./sample/java) - A java sample with 1 method
- [Kotlin](./sample/kotlin) - A kotlin sample with 1 method
- [Game](./sample/game) - A simple game without any engine like cocos2d, just Rust and Android
- [REST API](./sample/restapi) - A REST API client using Tokio and reqwest in pure Rust
8 changes: 8 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.1.0 (2026-02-06)
### Fixed
- Fixed rust path resolution in ReflectionJVM and ReflectionNative to resolve relative to project root instead of Gradle daemon directory

### Added
- Pass CC_<target> and AR_<target> environment variables to cargo build, enabling cross-compilation of Rust crates that depend on C code via cc-rs (e.g. ring, openssl-sys, libsqlite3-sys)
- New REST API sample (`sample/restapi`) demonstrating HTTP client in Rust using Tokio, reqwest, and serde with the Dog API

## 0.0.27 (2025-08-07)
### Fixed
- Fixed the support for Cargo checking on Windows
Expand Down
2 changes: 1 addition & 1 deletion gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repositories {
google()
}

version = "0.0.27"
version = "0.1.0"
group = "io.github.andrefigas.rustjni"

gradlePlugin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ class RustJNI : Plugin<Project> {

private fun runCargoCommand(arguments: List<String>,
dir: File = rustDir,
outputProcessor: ((String) -> Unit)? = null) {
runCommand("cargo", arguments, dir, outputProcessor)
outputProcessor: ((String) -> Unit)? = null,
extraEnv: Map<String, String> = emptyMap()) {
runCommand("cargo", arguments, dir, outputProcessor, extraEnv)
}

private fun runRustupCommand(arguments: List<String>,
Expand Down Expand Up @@ -341,8 +342,20 @@ class RustJNI : Plugin<Project> {
}

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
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -105,11 +105,12 @@ internal object ReflectionJVM {
}

private fun parseRustJniFunctions(
project: Project,
extension: RustJniExtension,
jniHost: String,
isKotlinFile: Boolean
): List<MethodSignature> {
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*\{""",
Expand Down Expand Up @@ -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}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -69,11 +70,12 @@ internal object ReflectionNative {
}

private fun parseRustJniFunctions(
project: Project,
extension: RustJniExtension,
jniHost: String,
isKotlinFile: Boolean
): List<MethodSignature> {
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*\{""",
Expand Down Expand Up @@ -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}")
}
Expand Down Expand Up @@ -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<String>,
Expand All @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion sample/game/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import io.github.andrefigas.rustjni.reflection.Visibility
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("io.github.andrefigas.rustjni") version "0.0.22"
id("io.github.andrefigas.rustjni") version "0.1.0"
}

rustJni{
Expand Down
2 changes: 1 addition & 1 deletion sample/java/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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"
id("io.github.andrefigas.rustjni") version "0.1.0"
}

rustJni{
Expand Down
2 changes: 1 addition & 1 deletion sample/kotlin/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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"
id("io.github.andrefigas.rustjni") version "0.1.0"
}

rustJni{
Expand Down
15 changes: 15 additions & 0 deletions sample/restapi/.gitignore
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions sample/restapi/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
64 changes: 64 additions & 0 deletions sample/restapi/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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.1.0"
}

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)
}
6 changes: 6 additions & 0 deletions sample/restapi/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions sample/restapi/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RestApi"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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() {

//<RustJNI>
// 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") }

//</RustJNI>


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val tvResult = findViewById<TextView>(R.id.tvResult)
val etBreed = findViewById<EditText>(R.id.etBreed)
val btnRandomDog = findViewById<Button>(R.id.btnRandomDog)
val btnListBreeds = findViewById<Button>(R.id.btnListBreeds)
val btnBreedImage = findViewById<Button>(R.id.btnBreedImage)

btnRandomDog.setOnClickListener {
callRust(tvResult) { getRandomDog() }
}

btnListBreeds.setOnClickListener {
callRust(tvResult) { listBreeds() }
}

btnBreedImage.setOnClickListener {
val breed = etBreed.text.toString().trim()
if (breed.isEmpty()) {
tvResult.text = "Please enter a breed name"
return@setOnClickListener
}
callRust(tvResult) { getBreedImage(breed) }
}
}

private fun callRust(tvResult: TextView, block: () -> String) {
tvResult.text = getString(R.string.loading)
Thread {
val result = block()
runOnUiThread {
tvResult.text = result
}
}.start()
}
}
Loading