关于《关于<Android 使用 Rust 生成的动态库>的补充》的补充

361 阅读3分钟

说明

之前写过一篇水文《关于 《Android 使用 Rust 生成的动态库》的补充
为啥又来补充了?没完没了是吧!主要原因是,最近对接公司的算法过程中发觉 Cpp 调 Rust 构建的动态库比直接调动态库麻烦得多,同时也没必要,我都用 Rust 啦,那当然是要一镜到底Rewrite it in Rust

JNI crate 登场

什么年代啦还在用传统 Cpp,当然是要用 Modern Cpp Rust 来写 JNI 相关的代码啦! 具体代码在这里 limitliu.coding.net/public/java… 我已经改成全用 Rust 版本的啦。

查看一下里面的 rust 文件夹,里面有一个 ffi-example 的 Rust 项目,一个是之前的转 md5 的功能,另一个调用动态库的函数是把浮点数组里面的值乘以 2,顺便我也给出了出异常如何处理的方式,通过 throw 抛出,Java/Kotlin 端只要 try/catch 就可以。主要是利用 JNI 这个 crate 做一些事情,现在全程只要用 Rust 就能完成之前得上 Cpp 才能完成的工作,开发体验大幅上升。

use jni::{
  objects::{JClass, JFloatArray, JString},
  sys::{jfloatArray, jint, jstring},
  JNIEnv,
};
use md5::compute;

#[no_mangle]
pub extern "system" fn Java_rust_ffi_Demo_md5(mut env: JNIEnv, _: JClass, buf: JString) -> jstring {
  let input: String = match env.get_string(&buf) {
    Ok(x) => x.into(),
    Err(err) => {
      env.exception_clear().expect("clear");
      env
        .throw_new("java/lang/Exception", format!("{err:?}"))
        .expect("throw");
      return std::ptr::null_mut();
    }
  };
  let output = env
    .new_string(format!("{:x}", compute(input)))
    .expect("Failed to new string");
  output.into_raw()
}

#[no_mangle]
pub extern "system" fn Java_rust_ffi_Demo_transform(
  env: JNIEnv,
  _: JClass,
  array: JFloatArray,
) -> jfloatArray {
  let len = env.get_array_length(&array).unwrap_or(0);
  let mut v = vec![0f32; len as usize];
  env
    .get_float_array_region(array, 0, &mut v)
    .expect("Failed to copy array to vec");

  let v: Vec<f32> = v.into_iter().map(|x| x * 2.0).collect();
  let output = env
    .new_float_array(v.len() as jint)
    .expect("Failed to create new float array.");

  env
    .set_float_array_region(&output, 0, &v)
    .expect("Failed to set float array.");

  output.into_raw()
}

然后是项目配置问题,删掉之前的 app/src/main/cpp 文件夹,Cpp 这玩意早该屠屠啦,删完在 java 同级目录建一个 jniLibs 文件夹,里面放对应 CPU 架构的文件夹(譬如 arm64-v8a),然后构建一下 ffi-example,完事就是直接把生成的动态库(*.so)文件放里头。还是同样的配方

cargo build --target aarch64-linux-android --release

总之来看看目录结构吧

.
├── LICENSE
├── README.md
├── app
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src
│       ├── androidTest
│       │   └── java
│       │       └── wiki
│       │           └── mdzz
│       ├── main
│       │   ├── AndroidManifest.xml
│       │   ├── java
│       │   │   └── wiki
│       │   │       └── mdzz
│       │   ├── jniLibs
│               └── arm64-v8a
│                   └── libffi_example.so
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── rust
│   └── ffi-example
│       ├── Cargo.lock
│       ├── Cargo.toml
│       ├── src
│       └── target
├── rustfmt.toml
└── settings.gradle

现在把之前跟 Cpp 相关的配置给删掉,主要是在 app/build.gradle 这边。

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    defaultConfig {
        compileSdk 34
        buildToolsVersion = '34.0.0'
        applicationId "wiki.mdzz.ffidemo"
        minSdk 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        - externalNativeBuild {
        -    cmake {
        -        cppFlags ''
        -    }
        -}
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = '17'
    }
    -externalNativeBuild {
    -    cmake {
    -        path file('src/main/cpp/CMakeLists.txt')
    -        version '3.22.1'
    -    }
    -}
    buildFeatures {
        viewBinding true
    }
    ndkVersion '26.3.11579264'
    namespace 'wiki.mdzz.ffidemo'
}

dependencies {
    implementation 'androidx.core:core-ktx:1.13.1'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.12.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

Java/Kotlin 代码调整

因为我们在上面的代码里,写得函数名是 Java_rust_ffi_Demo_md5/Java_rust_ffi_Demo_transform,所以我们建一个文件 app/src/main/java/rust/ffi/Demo.kt,里面的内容是

package rust.ffi

class Demo {
    external fun md5(buf: String): String

    external fun transform(array: FloatArray): FloatArray

    companion object {
        init {
            System.loadLibrary("ffi_example")
        }
    }
}

在 Java/Kotlin 端的感知上跟之前 Cpp 的体验基本没区别,甚至由于 Rust 非常容易写出比较安全的代码,可能实际开发中,没 Cpp 那么多问题。