Android 使用 Rust 生成的动态库

limit
• 阅读 838

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

配置 Rust 工程

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

cargo new ffi-example --lib

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

[package]
name = "ffi-example"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "ffi_example"
crate-type = ["staticlib", "cdylib"]

[dependencies]
md5 = "0.7"

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

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

use md5::compute;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_uchar};

#[no_mangle]
extern "C" fn ll_md5(buf: *const c_char) -> *const c_uchar {
  let buf = unsafe { CStr::from_ptr(buf) }.to_str().unwrap().as_bytes();
  let digest = format!("{:x}", compute(buf));
  CString::new(digest).unwrap().into_raw() as *const c_uchar
}

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

tab_spaces = 2

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

rustup target list

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

rustup target add aarch64-linux-android

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

cargo 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) 选一个版本安装.

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

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

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

[target.aarch64-linux-android]
ar = "/Users/your name/Library/Android/sdk/ndk/arm64/bin/aarch64-linux-android-ar"
linker = "/Users/your name/Library/Android/sdk/ndk/arm64/bin/aarch64-linux-android-clang"

[target.armv7-linux-androideabi]
ar = "/Users/your name/Library/Android/sdk/ndk/arm/bin/arm-linux-androideabi-ar"
linker = "/Users/your name/Library/Android/sdk/ndk/arm/bin/arm-linux-androideabi-clang"

[target.i686-linux-android]
ar = "/Users/your name/Library/Android/sdk/ndk/x86/bin/i686-linux-android-ar"
linker = "/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 也要加入相应的动态库名字)

add_library(ffi_example SHARED IMPORTED)
set_target_properties(ffi_example PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/lib/${CMAKE_ANDROID_ARCH_ABI}/libffi_example.so)

# ...

target_link_libraries( # Specifies the target library.
        ffidemo
        # 你导入的动态库
        ffi_example

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

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

android {
    compileSdk 32

    defaultConfig {
        applicationId "wiki.mdzz.ffidemo"
        minSdk 26
        targetSdk 32
        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_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    externalNativeBuild {
        cmake {
            path file('src/main/cpp/CMakeLists.txt')
            version '3.18.1'
        }
    }
    buildFeatures {
        viewBinding true
    }
    ndkVersion '24.0.8215888'
}

然后我们执行一下, 就会发现, 你的 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 读取一下看看内容

readelf -d libffi_example.so

Dynamic section at offset 0x3fc70 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x000000000000001a (FINI_ARRAY)         0x3eb90
 0x000000000000001c (FINI_ARRAYSZ)       16 (bytes)
 0x0000000000000004 (HASH)               0x1c8
 0x000000006ffffef5 (GNU_HASH)           0x3a0
 0x0000000000000005 (STRTAB)             0x7a0
 0x0000000000000006 (SYMTAB)             0x3c8
 0x000000000000000a (STRSZ)              412 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000003 (PLTGOT)             0x40e40
 0x0000000000000002 (PLTRELSZ)           864 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x3688
 0x0000000000000007 (RELA)               0x9d0
 0x0000000000000008 (RELASZ)             11448 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000000000001e (FLAGS)              BIND_NOW
 0x000000006ffffffb (FLAGS_1)            Flags: NOW
 0x000000006ffffffe (VERNEED)            0x990
 0x000000006fffffff (VERNEEDNUM)         2
 0x000000006ffffff0 (VERSYM)             0x93c
 0x000000006ffffff9 (RELACOUNT)          476
 0x0000000000000000 (NULL)               0x0
readelf -d libffidemo.so

Dynamic section at offset 0x32aa8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [~/FFIDemo/app/src/main/cpp/lib/arm64-v8a/libffi_example.so]
 0x0000000000000001 (NEEDED)             Shared library: [liblog.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x000000000000000e (SONAME)             Library soname: [libffidemo.so]
 0x000000000000001a (FINI_ARRAY)         0x30d50
 0x000000000000001c (FINI_ARRAYSZ)       16 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x228
 0x0000000000000005 (STRTAB)             0x36a0
 0x0000000000000006 (SYMTAB)             0xe68
 0x000000000000000a (STRSZ)              8493 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000003 (PLTGOT)             0x33c98
 0x0000000000000002 (PLTRELSZ)           1848 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0xd8d8
 0x0000000000000007 (RELA)               0x5b68
 0x0000000000000008 (RELASZ)             32112 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000000000001e (FLAGS)              BIND_NOW
 0x000000006ffffffb (FLAGS_1)            Flags: NOW
 0x000000006ffffffe (VERNEED)            0x5b28
 0x000000006fffffff (VERNEEDNUM)         2
 0x000000006ffffff0 (VERSYM)             0x57ce
 0x000000006ffffff9 (RELACOUNT)          886
 0x0000000000000000 (NULL)               0x0

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

cargo clean

RUSTFLAGS="-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++.

#include <jni.h>
#include <string>

#include "llmd5.h"

extern "C" JNIEXPORT jstring JNICALL
Java_wiki_mdzz_ffidemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    auto fooMD5 = ll_md5("foo");
    return env->NewStringUTF(fooMD5);
}

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

#ifndef FFIDEMO_LLMD5_H
#define FFIDEMO_LLMD5_H

#if __cplusplus
extern "C" {
#endif

const char *ll_md5(const char *buf);

#if __cplusplus
}
#endif

#endif //FFIDEMO_LLMD5_H

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

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

#include <jni.h>
#include <string>

#include "llmd5.h"

extern "C" JNIEXPORT jstring JNICALL
Java_wiki_mdzz_ffidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */, jstring buf) {
    auto data = env->GetStringUTFChars(buf, nullptr);
    auto result = ll_md5(data);
    env->ReleaseStringUTFChars(buf, data);
    return env->NewStringUTF(result);
}

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

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

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

点赞
收藏
评论区
推荐文章
文盘Rust -- 本地库引发的依赖冲突
clickhouse的原生rust客户端目前比较好的有两个clickhousers 和 clickhouse.rs 。两个库在单独使用时没有任何问题,但是,在同一工程同时引用时会报错。本篇内容主要讲解如何用rust语言解决本地库引发的依赖冲突问题
Stella981 Stella981
3年前
Linux和Windows平台 动态库.so和.dll文件的生成
Linux动态库的生成1、纯cpp文件打包动态库将所有cpp文件和所需要的头文件放在同一文件夹,然后执行下面命令gccsharedfpic\.coxxx.so;gstdc17fpic\.cppoxxx.so;\C17标准,需要高版本gcc,本人采用gcc8.2.0\
Stella981 Stella981
3年前
QT开发——动态库(.so文件)的生成与调用
1.qmake方式生成和调用动态库链接:https://blog.csdn.net/lywzgzl/article/details/428059912.cmake方式生成和调用动态库2.1创建共享库项目Cmake新建一个QtCreator项目,在C
Stella981 Stella981
3年前
EasyExcel写入百万级数据到多sheet
EasyExcel是什么?快速、简单避免OOM的java处理Excel工具一、项目需求    从mongo库中查询数据,导出到excel文件中。但是动态导出的excel有多少列、列名是什么、有多少sheet页都需要动态获取。所以生成的excel也必须是动态生成,不能通过注解配置对象映射。而且写入的数据量,有可能达到100W级,使用传统的PO
Stella981 Stella981
3年前
Python 调用 C 库的实现
在linux开发的动态链接库需要被python调用,首先需要生成.so文件。生成动态链接库的方法网上有很多,这里就不用多说,主要就是首先根据源文件编译生成.o,然后链接这些.o文件shared生成.so。需要注意的是,在编译链接生成动态库的时候一定要加fPIC参数。而且如果在链接时需要链接其他库的话,必须保证其他库编译时也是加了fPIC参数
Wesley13 Wesley13
3年前
Java动态代理机制解析
动态代理是指在运行时动态生成代理类。不需要我们像静态代理那个去手动写一个个的代理类。生成动态代理类有很多方式:Java动态代理,CGLIB,Javassist,ASM库等。这里主要说一下Java动态代理的实现。Java动态代理InvocationHandler接口Java动态代理中,每一个
Wesley13 Wesley13
3年前
MongoDB 2.0发布
2.0终于发布了,赶快试一下。 http://fastdl.mongodb.org/linux/mongodblinuxx86\_642.0.0.tgz试了一下,和1.8.3安装配置一样,参考我之前的文章,略微修改一下脚本文件里面的路径即可。但是C的客户端代码无法编译动态库,原因是少了一个cpp文件,可以暂时使用1.8.3的
非凸科技 非凸科技
2年前
用 Rust 重写网站,性能提升了18倍!
对于构建中小型网站/个人博客来说,Hakyll是一个不错的静态网站生成器库,9年前的JonasHietala正是选择了Hakyll编写博客网站。但随着时间的推移,网站出现各种问题,诸如速度越来越慢,许多外部依赖性,设置问题等,JonasHietala决定用Rust重写。选择Rust的原因是?1.JonasHietala很喜欢Rus
京东云开发者 京东云开发者
7个月前
OSS_PIPE:Rust编写的大规模文件迁移工具| 京东云技术团队
文盘rust好久没有更新了。这段时间笔者用rust写了个小东西,跟各位分享一下背景随着业务的发展,文件数量和文件大小会急剧增加,文件迁移的数量和难度不断攀升。osspipe是rust编写的文件迁移工具,旨在支撑大规模的文件迁移场景。编写osspipe的初衷
京东云开发者 京东云开发者
1个月前
JavaScript 与 Rust 和 WebAssembly 集成
作者:京东物流梁瑞乐偶然一次机会,接触了Rust的代码。当时想给团队小伙伴做演示,发现自己并不能在移动端按照文档生成演示demo。我就想,要是Rust代码能转化成JavaScript就好了。结果一搜,还真有。下面整理成文档,分享给大家。为大家解决问题,多提
limit
limit
Lv1
临渊羡鱼,不如天天摸鱼。
文章
3
粉丝
1
获赞
3