Android 使用 Rust 生成的动态库

Android NDK 可以使用一些第三方的动态库, 如何用 Rust 写个东西生成动态库, 给 Cpp 这边调用, 这边记录一下过程.

配置 Rust 工程

首先写个 Rust 工程, 搞出个动态库出来, 先是创建个项目, 这里取名叫 ffi-example

1cargo new ffi-example --lib

打开 Cargo.toml 文件, 里面的内容长这样

1[package] 2name = "ffi-example" 3version = "0.1.0" 4edition = "2021" 5 6# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7[lib] 8name = "ffi_example" 9crate-type = ["staticlib", "cdylib"] 10 11[dependencies] 12md5 = "0.7"

我们的初衷是为了把 Rust 生成的动态库给 Android 端使用, 这里就不添加 jni 相关的 crate 了, 如果要写很多 native 的代码, 建议补上这个 crate. 这个工程主要是使用到了一个 md5crate, 顺便把后续要生成的 crate 类型标注成 staticlib 跟 cdylib.

然后跳到工程中的 lib.rs 文件, 把里面的内容改成下面这些

1use md5::compute; 2use std::ffi::{CStr, CString}; 3use std::os::raw::{c_char, c_uchar}; 4 5#[no_mangle] 6extern "C" fn ll_md5(buf: *const c_char) -> *const c_uchar { 7 let buf = unsafe { CStr::from_ptr(buf) }.to_str().unwrap().as_bytes(); 8 let digest = format!("{:x}", compute(buf)); 9 CString::new(digest).unwrap().into_raw() as *const c_uchar 10}

代码可以加点自己的 lint, 可以补充个 rustfmt.toml 文件, 譬如我这里用得是两个空格的代码风格

1tab_spaces = 2

现在把我们的代码构建成动态库, 可以把对应 x86target 安装上, 安装对应的 target, 需要用 rustup 安装, 可以先搜索一下有哪些 target

1rustup target list

如果你用得是水果 M1 芯片的设备, 可以直接使用 ARM64 的 Android 仿真器, 下面这条命令就可以兼顾 Android 真机跟 M1 上的 Android 仿真器 (只要你 Android Studio 设置的仿真器是 ARM64 的)

1rustup target add aarch64-linux-android

假设你已经装好了必要的 target, 可以执行下面的命令打包

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

然后我们看到工程的 target 文件夹下生成了一个 aarch64-linux-android 文件夹, 里面的 release 文件夹下就有我们想要的 libffi_example.so 文件

如果编译出错

其实还有一个事情没讲, 那就是 Rust 编译 Android 可用的动态库, 需要配置 NDK standalone. 先把 ndk 装好, 直接在 Android Studio SDK Tools 的 NDK (Side by side) 选一个版本安装.

然后执行下面的命令, 具体目录根据自己的情况而定

1export ANDROID_HOME=$HOME/Library/Android/sdk 2export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/21.4.7075529 3cd ~/Library/Android/sdk/ndk 4python3 $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --api 28 --arch arm64 --install-dir ./arm64

上面只处理 arm64 的情况, 具体 ABI 根据自己的需要而定, 然后设置一下 .cargo/config 里面的内容

1[target.aarch64-linux-android] 2ar = "/Users/your name/Library/Android/sdk/ndk/arm64/bin/aarch64-linux-android-ar" 3linker = "/Users/your name/Library/Android/sdk/ndk/arm64/bin/aarch64-linux-android-clang" 4 5[target.armv7-linux-androideabi] 6ar = "/Users/your name/Library/Android/sdk/ndk/arm/bin/arm-linux-androideabi-ar" 7linker = "/Users/your name/Library/Android/sdk/ndk/arm/bin/arm-linux-androideabi-clang" 8 9[target.i686-linux-android] 10ar = "/Users/your name/Library/Android/sdk/ndk/x86/bin/i686-linux-android-ar" 11linker = "/Users/your name/Library/Android/sdk/ndk/x86/bin/i686-linux-android-clang"

其实就是根据你指定的 target 使用用对应平台的链接器, 这里建议使用 NDK 的版本是 21, 更高版本的我还没测试过能不能编译通过.

配置 Android 工程

现在用 Android Studio 来创建个 Android 的项目, 模板选择 Cpp 的那个, 语言不论 Kotlin 还是 Java 都可以, Minimum SDK 随便选一个, 我这里选得是 API 26 以上的.

接着要来改改配置, 找到项目中的 CMakeLists.txt 文件, 在 find_library 上面添加一些内容, 这里的 CMAKE_ANDROID_ARCH_ABI 对根据环境自动指定对应的文件夹(target_link_libraries 也要加入相应的动态库名字)

1add_library(ffi_example SHARED IMPORTED) 2set_target_properties(ffi_example PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/lib/${CMAKE_ANDROID_ARCH_ABI}/libffi_example.so) 3 4# ... 5 6target_link_libraries( # Specifies the target library. 7 ffidemo 8 # 你导入的动态库 9 ffi_example 10 11 # Links the target library to the log library 12 # included in the NDK. 13 ${log-lib})

然后把之前生成的动态库拷贝到 Android 项目中来, 直接放到 src/main/cpp/lib/arm64-v8a 目录下(如果你有其他的 ABI 的动态库, 你也可以加上对应的文件夹, 放入相应的动态库), 网上很多文章说要放到 libs 或者 jniLibs 之类的文件夹, 现在新版本不需要这样做了, 我们以官方的文档为准. 此外, build.gradle (:app) 可以把对应的 ndkVersion 加上

1android { 2 compileSdk 32 3 4 defaultConfig { 5 applicationId "wiki.mdzz.ffidemo" 6 minSdk 26 7 targetSdk 32 8 versionCode 1 9 versionName "1.0" 10 11 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 externalNativeBuild { 13 cmake { 14 cppFlags '' 15 } 16 } 17 } 18 19 buildTypes { 20 release { 21 minifyEnabled false 22 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 } 24 } 25 compileOptions { 26 sourceCompatibility JavaVersion.VERSION_1_8 27 targetCompatibility JavaVersion.VERSION_1_8 28 } 29 kotlinOptions { 30 jvmTarget = '1.8' 31 } 32 externalNativeBuild { 33 cmake { 34 path file('src/main/cpp/CMakeLists.txt') 35 version '3.18.1' 36 } 37 } 38 buildFeatures { 39 viewBinding true 40 } 41 ndkVersion '24.0.8215888' 42}

然后我们执行一下, 就会发现, 你的 Logcat 告诉你说

java.lang.UnsatisfiedLinkError: dlopen failed: library "~/FFIDemo/app/src/main/cpp/lib/arm64-v8a/libffi_example.so" not found

我们把 app-debug.apk 的文件拿出来, 副档名改成 zip, 然后解压, 找到里面的 libffi_example.so 文件, 用 readelf 命令读取一下文件看看, 再把另一个 so 文件用 readelf 读取一下看看内容

1readelf -d libffi_example.so 2 3Dynamic section at offset 0x3fc70 contains 25 entries: 4 Tag Type Name/Value 5 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 6 0x0000000000000001 (NEEDED) Shared library: [libc.so] 7 0x0000000000000001 (NEEDED) Shared library: [libm.so] 8 0x000000000000001a (FINI_ARRAY) 0x3eb90 9 0x000000000000001c (FINI_ARRAYSZ) 16 (bytes) 10 0x0000000000000004 (HASH) 0x1c8 11 0x000000006ffffef5 (GNU_HASH) 0x3a0 12 0x0000000000000005 (STRTAB) 0x7a0 13 0x0000000000000006 (SYMTAB) 0x3c8 14 0x000000000000000a (STRSZ) 412 (bytes) 15 0x000000000000000b (SYMENT) 24 (bytes) 16 0x0000000000000003 (PLTGOT) 0x40e40 17 0x0000000000000002 (PLTRELSZ) 864 (bytes) 18 0x0000000000000014 (PLTREL) RELA 19 0x0000000000000017 (JMPREL) 0x3688 20 0x0000000000000007 (RELA) 0x9d0 21 0x0000000000000008 (RELASZ) 11448 (bytes) 22 0x0000000000000009 (RELAENT) 24 (bytes) 23 0x000000000000001e (FLAGS) BIND_NOW 24 0x000000006ffffffb (FLAGS_1) Flags: NOW 25 0x000000006ffffffe (VERNEED) 0x990 26 0x000000006fffffff (VERNEEDNUM) 2 27 0x000000006ffffff0 (VERSYM) 0x93c 28 0x000000006ffffff9 (RELACOUNT) 476 29 0x0000000000000000 (NULL) 0x0
1readelf -d libffidemo.so 2 3Dynamic section at offset 0x32aa8 contains 27 entries: 4 Tag Type Name/Value 5 0x0000000000000001 (NEEDED) Shared library: [~/FFIDemo/app/src/main/cpp/lib/arm64-v8a/libffi_example.so] 6 0x0000000000000001 (NEEDED) Shared library: [liblog.so] 7 0x0000000000000001 (NEEDED) Shared library: [libm.so] 8 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 9 0x0000000000000001 (NEEDED) Shared library: [libc.so] 10 0x000000000000000e (SONAME) Library soname: [libffidemo.so] 11 0x000000000000001a (FINI_ARRAY) 0x30d50 12 0x000000000000001c (FINI_ARRAYSZ) 16 (bytes) 13 0x000000006ffffef5 (GNU_HASH) 0x228 14 0x0000000000000005 (STRTAB) 0x36a0 15 0x0000000000000006 (SYMTAB) 0xe68 16 0x000000000000000a (STRSZ) 8493 (bytes) 17 0x000000000000000b (SYMENT) 24 (bytes) 18 0x0000000000000003 (PLTGOT) 0x33c98 19 0x0000000000000002 (PLTRELSZ) 1848 (bytes) 20 0x0000000000000014 (PLTREL) RELA 21 0x0000000000000017 (JMPREL) 0xd8d8 22 0x0000000000000007 (RELA) 0x5b68 23 0x0000000000000008 (RELASZ) 32112 (bytes) 24 0x0000000000000009 (RELAENT) 24 (bytes) 25 0x000000000000001e (FLAGS) BIND_NOW 26 0x000000006ffffffb (FLAGS_1) Flags: NOW 27 0x000000006ffffffe (VERNEED) 0x5b28 28 0x000000006fffffff (VERNEEDNUM) 2 29 0x000000006ffffff0 (VERSYM) 0x57ce 30 0x000000006ffffff9 (RELACOUNT) 886 31 0x0000000000000000 (NULL) 0x0

然后你会发觉, Rust 生成的动态库, 没有 Library soname, 所以我们得再生成一个带 soname 的动态库. 回到 Rust 项目, 通过下面的命令构建一下, 可以先 cargo clean 清理一下 target 目录

1cargo clean 2 3RUSTFLAGS="-Clink-arg=-Wl,-soname=libffi_example.so" cargo build --target aarch64-linux-android --release

这里我们手动给动态库加上了 soname, 再把生成的动态库放到 Android 工程中. 在重新执行之前, 可以把项目中 app 目录下的 .cxx 跟 build 文件夹删一下, 防止出现奇怪的问题. 再次执行时, 我们的 App 已经可以跑起来. 接着把 native-lib 的 Cpp 代码 stringFromJNI, 修改一下, 用用看原生库的效果, 因为现在仿真器的屏幕上显示得还是 Hello from C++.

1#include <jni.h> 2#include <string> 3 4#include "llmd5.h" 5 6extern "C" JNIEXPORT jstring JNICALL 7Java_wiki_mdzz_ffidemo_MainActivity_stringFromJNI( 8 JNIEnv* env, 9 jobject /* this */) { 10 auto fooMD5 = ll_md5("foo"); 11 return env->NewStringUTF(fooMD5); 12}

我们还忘了把头文件加上, 头文件内容长这样

1#ifndef FFIDEMO_LLMD5_H 2#define FFIDEMO_LLMD5_H 3 4#if __cplusplus 5extern "C" { 6#endif 7 8const char *ll_md5(const char *buf); 9 10#if __cplusplus 11} 12#endif 13 14#endif //FFIDEMO_LLMD5_H

因为我们原生语言用得是 Cpp, 所以需要加上 extern "C".

然后再编译执行 App, 应该能看到仿真器的屏幕上显示一串字符串. 为了让这个函数更通用, 可以接收 Java/Kotlin 那边传过来的字符串, 再生成对应的 md5 字符串.

1#include <jni.h> 2#include <string> 3 4#include "llmd5.h" 5 6extern "C" JNIEXPORT jstring JNICALL 7Java_wiki_mdzz_ffidemo_MainActivity_stringFromJNI( 8 JNIEnv *env, 9 jobject /* this */, jstring buf) { 10 auto data = env->GetStringUTFChars(buf, nullptr); 11 auto result = ll_md5(data); 12 env->ReleaseStringUTFChars(buf, data); 13 return env->NewStringUTF(result); 14}

Kotlin/Java 的代码也可以改一下

1private external fun stringFromJNI(buf: String): String
1public native String stringFromJNI(String buf);

不想看文字, 可以直接看项目 https://e.coding.net/limitLiu/java/FFIDemo.git

点赞
收藏

评论区

加载中...

相关推荐

文盘Rust -- 本地库引发的依赖冲突

clickhouse的原生rust客户端目前比较好的有两个clickhousers 和 clickhouse.rs 。两个库在单独使用时没有任何问题,但是,在同一工程同时引用时会报错。本篇内容主要讲解如何用rust语言解决本地库引发的依赖冲突问题

Linux和Windows平台 动态库.so和.dll文件的生成

Linux动态库的生成1、纯cpp文件打包动态库将所有cpp文件和所需要的头文件放在同一文件夹,然后执行下面命令gccsharedfpic\.coxxx.so;gstdc17fpic\.cppoxxx.so;\C17标准,需要高版本gcc,本人采用gcc8.2.0\

QT开发——动态库(.so文件)的生成与调用

1.qmake方式生成和调用动态库链接:https://blog.csdn.net/lywzgzl/article/details/428059912.cmake方式生成和调用动态库2.1创建共享库项目Cmake新建一个QtCreator项目,在C

EasyExcel写入百万级数据到多sheet

EasyExcel是什么?快速、简单避免OOM的java处理Excel工具一、项目需求    从mongo库中查询数据,导出到excel文件中。但是动态导出的excel有多少列、列名是什么、有多少sheet页都需要动态获取。所以生成的excel也必须是动态生成,不能通过注解配置对象映射。而且写入的数据量,有可能达到100W级,使用传统的PO

Python 调用 C 库的实现

在linux开发的动态链接库需要被python调用,首先需要生成.so文件。生成动态链接库的方法网上有很多,这里就不用多说,主要就是首先根据源文件编译生成.o,然后链接这些.o文件shared生成.so。需要注意的是,在编译链接生成动态库的时候一定要加fPIC参数。而且如果在链接时需要链接其他库的话,必须保证其他库编译时也是加了fPIC参数

Java动态代理机制解析

动态代理是指在运行时动态生成代理类。不需要我们像静态代理那个去手动写一个个的代理类。生成动态代理类有很多方式:Java动态代理,CGLIB,Javassist,ASM库等。这里主要说一下Java动态代理的实现。Java动态代理InvocationHandler接口Java动态代理中,每一个