Rust JNI (生成可供安卓调用的so库)

一、项目构建

  1. 确认 Rust 环境已安装
    首先检查你的电脑是否安装了 Rust(如果没装,先执行下面的安装命令):
# 检查 Rust 版本(已安装则会显示版本号,未安装则执行下一步)
rustc --version

# 安装 Rust(Windows/macOS/Linux 通用)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装完成后,重启终端让环境变量生效。

  1. 添加安卓编译目标
    Rust 默认只编译本地架构,需要手动添加安卓的目标架构(这是编译安卓库的前提):
# 添加安卓常用的 3 种架构(复制整段执行)
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android

执行完成后,可通过 rustup target list | grep android 验证,显示 installed 即成功。

  1. 安装 cargo-ndk 工具
    cargo-ndk 是 Rust 编译安卓库的核心工具,能自动关联 Android NDK 并生成适配的 .so 文件:
# 安装 cargo-ndk
cargo install cargo-ndk

注意:如果是 Windows 系统,需先安装 Visual Studio 构建工具(勾选 “C++ 构建工具”),否则可能编译失败;macOS/Linux 需确保安装了 gcc/clang。

  1. 创建 Rust 项目(核心步骤)
    打开终端,进入你想存放项目的目录(比如 ~/code/rust-android),执行以下命令创建 Rust 库项目:
# 创建名为 rust-android-lib 的 Rust 库项目(名字可自定义,建议小写+连字符)
cargo new --lib rust-android-lib

# 进入项目目录(后续所有操作都在这个目录下)
cd rust-android-lib

执行完后,会生成一个默认的 Rust 库项目结构,初始结构如下:

rust-android-lib/
├── Cargo.toml  # 项目配置文件(依赖、编译规则)
└── src/
    └── lib.rs  # 代码入口文件
  1. 修改 Cargo.toml 配置
    打开项目中的 Cargo.toml 文件(用任意编辑器,比如 VS Code),替换默认内容为以下配置:
[package]
name = "rust-android-lib"  # 项目名(对应后续安卓加载的库名,建议和项目目录名一致)
version = "1.0.0"          # 版本号
edition = "2021"           # Rust 版本(2021 是目前的稳定版本)

# 核心配置:编译为安卓可识别的动态库(cdylib)
[lib]
crate-type = ["cdylib"]

# 添加 JNI 依赖(用于和安卓 JNI 交互)
[dependencies]
jni = { version = "0.21", features = ["invocation"] }

关键说明:crate-type = ["cdylib"] 是必须的,它告诉 Rust 编译器编译为 “C 风格的动态库”(.so 文件),而非 Rust 默认的静态库。

  1. 编写 Rust 核心代码
    打开 src/lib.rs 文件,删除默认的测试代码,替换为带 JNI 接口的代码(这是供安卓调用的核心逻辑):
use jni::EnvUnowned;
use jni::objects::{JClass, JString};

#[unsafe(no_mangle)]
pub extern "system" fn Java_com_example_hello<'caller>(
    mut unowned_env: EnvUnowned<'caller>,
    _class: JClass<'caller>,
    input: JString<'caller>)
    -> JString<'caller>
{
    let outcome = unowned_env.with_env(|env| -> Result<_, jni::errors::Error> {
        let input: String = input.to_string();
        JString::from_str(env, format!("Hello, {}!", input))
    });
    outcome.resolve::<jni::errors::ThrowRuntimeExAndDefault>()
}

二、打包成so库

核心思路
Rust 编译 Android .so 库的关键是:

  1. 配置 Rust 交叉编译工具链 + Android NDK;
  2. 用cargo-ndk简化编译(自动关联 NDK、设置编译参数);
  3. 编译出 arm64-v8a/armeabi-v7a/x86_64/x86 四种主流架构的 so 库。

一、前置环境准备(必做)

  1. 安装 Rust Android 交叉编译目标
    打开终端执行以下命令,添加 Android 各架构的编译目标:
# 添加Android核心架构(覆盖99%设备)
rustup target add aarch64-linux-android   # 对应arm64-v8a(64位ARM,主流手机)
rustup target add armv7-linux-androideabi # 对应armeabi-v7a(32位ARM,旧设备)
rustup target add x86_64-linux-android   # 对应x86_64(模拟器/少数平板)
rustup target add i686-linux-android     # 对应x86(旧模拟器)
  1. 安装 Android NDK 并配置环境变量
  • 步骤 1:在 Android Studio 中安装 NDK(推荐版本 25+):
    File → Settings → Appearance & Behavior → System Settings → Android SDK → SDK Tools,勾选NDK (Side by side),选择 25.2.9519653 或更高版本,点击 Apply 安装。
  • 步骤 2:找到 NDK 安装路径(示例):
    Windows:C:\Users\你的用户名\AppData\Local\Android\Sdk\ndk\25.2.9519653
    macOS:~/Library/Android/sdk/ndk/25.2.9519653
    Linux:~/Android/Sdk/ndk/25.2.9519653
  • 步骤 3:配置环境变量(终端临时生效,永久生效需改系统环境变量):
# Windows(PowerShell)
$env:ANDROID_NDK_HOME = "C:\Users\你的用户名\AppData\Local\Android\Sdk\ndk\25.2.9519653"

# macOS/Linux(bash/zsh)
export ANDROID_NDK_HOME=/Users/你的用户名/Library/Android/sdk/ndk/25.2.9519653
  1. 安装 cargo-ndk(简化编译工具)
    cargo-ndk是 Rust 社区工具,能自动关联 NDK、生成符合 Android 规范的 so 库,无需手动写复杂编译脚本:
cargo install cargo-ndk

二、配置 Rust 项目(Cargo.toml)
修改你的Cargo.toml,添加以下配置(关键:设置库类型为cdylib,生成 so 库):

[package]
name = "rustlib"  # so库名:编译后会生成librustlib.so,对应Java中System.loadLibrary("rustlib")
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]  # 核心:编译为C动态库(so)
name = "rustlib"         # 库名,和package.name保持一致

[dependencies]
jni = { version = "0.21.1", features = ["invocation"] }

三、编译生成 so 库(核心步骤)
进入你的 Rust 项目根目录,执行以下命令编译各架构的 so 库:

  1. 编译 arm64-v8a(主流 64 位架构)
cargo ndk -t arm64-v8a -o ./android_libs build --release
  1. 编译 armeabi-v7a(32 位 ARM 架构)
cargo ndk -t armeabi-v7a -o ./android_libs build --release
  1. 编译 x86_64(64 位模拟器)
cargo ndk -t x86_64 -o ./android_libs build --release
  1. 编译 x86(32 位模拟器)
cargo ndk -t x86 -o ./android_libs build --release

四、查看编译结果
执行完上述命令后,你的 Rust 项目根目录会生成android_libs文件夹,结构如下:

android_libs/
├── arm64-v8a/
│   └── librustlib.so  # 64位ARM设备用
├── armeabi-v7a/
│   └── librustlib.so  # 32位ARM设备用
├── x86/
│   └── librustlib.so  # 32位模拟器用
└── x86_64/
    └── librustlib.so  # 64位模拟器用

✅ 这些.so文件就是 Android 能直接使用的原生库!

关键参数解释

  • -t arm64-v8a:指定编译目标架构(target);
  • -o ./android_libs:指定 so 库输出目录;
  • build --release:编译 release 版本(优化性能,体积更小),调试时可去掉--release编译 debug 版本。

常见问题解决

  • 报错 “NDK not found”:
    检查ANDROID_NDK_HOME环境变量是否配置正确,或 NDK 版本是否≥21。
  • 报错 “target not installed”:
    重新执行rustup target add命令,确认目标架构已安装。
    so 库生成后 Java 调用找不到:
    确保 so 库名和System.loadLibrary("rustlib")中的名称一致(libxxx.so → 写 xxx);
    确保 Java 方法签名和 Rust 函数名完全一致(如Java_com_example_rustlib_RustBridge_helloFromRust)。

总结

  1. 编译 Rust 为 Android so 库的核心是配置crate-type = ["cdylib"],并安装交叉编译工具链;
  2. 用cargo ndk可一键编译多架构 so 库,无需手动配置 NDK 编译参数;
  3. 编译结果在android_libs目录下,各架构 so 库可直接拷贝到 Android 项目的jniLibs目录使用。

调用示例
lib.rs代码

use std::ffi::CStr;

use jni::errors::Error;
use jni::objects::{JClass, JString};
use jni::strings::JNIStr;
use jni::{Env, EnvUnowned, NativeMethod};

#[unsafe(no_mangle)]
pub extern "system" fn Java_com_example_hello_MainActivityKt_sayHello<'caller>(
    mut unowned_env: EnvUnowned<'caller>,
    _class: JClass<'caller>,
    input: JString<'caller>,
) -> JString<'caller> {
    let outcome = unowned_env.with_env(|env: &mut Env<'caller>| -> Result<_, jni::errors::Error> {
        let input: String = input.to_string();
        JString::from_str(env, format!("Hello, {}!", input))
    });
    outcome.resolve::<jni::errors::ThrowRuntimeExAndDefault>()
}

use jni::sys::{JNI_VERSION_1_6, JNIEnv as RawJNIEnv, jint};

pub extern "C" fn add_numbers(_env: *mut RawJNIEnv, _class: JClass, a: jint, b: jint) -> jint {
    a + b
}

pub extern "C" fn concat_strings<'caller>(
    env: *mut RawJNIEnv,
    _class: JClass<'caller>,
    str1: JString<'caller>,
    str2: JString<'caller>,
) -> JString<'caller> {
    let mut env_unowned: EnvUnowned<'caller> = unsafe { EnvUnowned::from_raw(env) };
    let output = env_unowned.with_env(|env1| -> Result<_, jni::errors::Error> {
        let str1 = JString::mutf8_chars(&str1, &env1)?.to_string();
        let str2 = JString::mutf8_chars(&str2, &env1)?.to_string();
        let result = format!("{}{}", str1, str2);
        JString::from_str(env1, result)
    });
    output.resolve::<jni::errors::ThrowRuntimeExAndDefault>()
}

// JNI 加载时自动注册方法
#[unsafe(no_mangle)]
pub extern "C" fn JNI_OnLoad(vm: *mut jni::sys::JavaVM, _reserved: *mut ()) -> jint {
    let vm1 = unsafe { jni::JavaVM::from_raw(vm) };
    vm1.attach_current_thread(|env| -> Result<(), Error> {
        // 定义要注册的方法列表
        let native_methods = unsafe {
            [
                NativeMethod::from_raw_parts(
                    &(JNIStr::from_cstr_unchecked(CStr::from_bytes_with_nul_unchecked(
                        b"addNumbers\0",
                    ))),
                    &(JNIStr::from_cstr_unchecked(CStr::from_bytes_with_nul_unchecked(b"(II)I\0"))),
                    add_numbers as _,
                ),
                NativeMethod::from_raw_parts(
                    &(JNIStr::from_cstr_unchecked(CStr::from_bytes_with_nul_unchecked(
                        b"concatStrings\0",
                    ))),
                    &(JNIStr::from_cstr_unchecked(CStr::from_bytes_with_nul_unchecked(
                        b"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;\0",
                    ))),
                    concat_strings as _,
                ),
            ]
        };
        // 注册到任意 Java 类(类名通过字符串指定,无需硬编码)
        let class_name = unsafe {
            JNIStr::from_cstr_unchecked(CStr::from_bytes_with_nul_unchecked(
                b"com/example/hello/MainActivityKt\0",
            ))
        };
        let class = env.find_class(class_name.as_ref())?;
        unsafe { env.register_native_methods(class, &native_methods) }
    })
    .unwrap();
    JNI_VERSION_1_6
}

android代码

package com.example.hello

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.hello.ui.theme.TestRustJniTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TestRustJniTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
        Log.e("测试Rust", sayHello("你好啊!"))
        Log.e("测试Rust", concatStrings("中国", "你好啊!"))
        Log.e("测试Rust", addNumbers(3, 3).toString())
    }

    companion object {

        init {
            System.loadLibrary("rust_android_lib")
        }

    }

}

external fun sayHello(input: String): String

external fun addNumbers(a: Int, b: Int): Int

external fun concatStrings(a: String, b: String): String

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    TestRustJniTheme {
        Greeting("Android")
    }
}

注意事项
如果代码中的调用方法是类成员方法,rust对应方法的第二个参数类型应该是jobject, 如果是静态方法的,则第二个参数类型应该是jclass。

三、16KB Pages 对齐

核心背景
Android 系统的内存页大小通常是 16KB(16384 字节),对 so 库做 16KB 对齐能提升加载性能,也是 Google Play 等应用市场的上架要求。对齐的核心是让 so 文件的大小是 16384 的整数倍,不足部分用 0 填充。

方法一:用 Android 官方工具 zipalign(推荐)
zipalign 是 Android SDK 自带的工具,专门用于对齐 APK/AAB 或单个 so 文件,无需手动计算,简单可靠。

  1. 找到 zipalign 工具路径
    zipalign 位于 Android SDK 的build-tools目录下,示例路径:
    Windows:C:\Users\你的用户名\AppData\Local\Android\Sdk\build-tools\34.0.0\zipalign.exe
    macOS/Linux:~/Library/Android/sdk/build-tools/34.0.0/zipalign
  2. 执行 16KB 对齐命令
    打开终端,切换到 so 库所在目录(如android_libs/arm64-v8a),执行以下命令:
# 通用格式:zipalign -f -p 16 原so文件 对齐后的so文件
# Windows示例
C:\Users\你的用户名\AppData\Local\Android\Sdk\build-tools\34.0.0\zipalign.exe -f -p 16 librustlib.so librustlib_aligned.so

# macOS/Linux示例
~/Library/Android/sdk/build-tools/34.0.0/zipalign -f -p 16 librustlib.so librustlib_aligned.so
  1. 命令参数解释
    -f:强制覆盖已存在的对齐后文件;
    -p:按页面大小对齐(16 表示 16KB,对应 16384 字节);
    16:对齐粒度(固定写 16,代表 16KB);
    librustlib.so:原始未对齐的 so 文件;
    librustlib_aligned.so:对齐后的新 so 文件。
  2. 验证对齐结果
    执行以下命令检查是否对齐成功:
# Windows
C:\Users\你的用户名\AppData\Local\Android\Sdk\build-tools\34.0.0\zipalign.exe -c -v 16 librustlib_aligned.so

# macOS/Linux
~/Library/Android/sdk/build-tools/34.0.0/zipalign -c -v 16 librustlib_aligned.so
  • 输出 Verification successful → 对齐成功;
  • 输出 Verification failed → 重新执行对齐命令。

方法二:手动对齐(适用于无 zipalign 的场景)
如果无法使用 zipalign,可通过脚本手动计算文件大小并填充 0,实现 16KB 对齐(以 Linux/macOS 为例,Windows 可借助 PowerShell)。
1. 手动对齐脚本(bash)
创建align_so.sh文件,放入 so 库目录,内容如下:

#!/bin/bash
# 16KB对齐脚本,参数:待对齐的so文件路径
SO_FILE=$1
if [ ! -f "$SO_FILE" ]; then
    echo "文件不存在:$SO_FILE"
    exit 1
fi

# 16KB = 16384字节
ALIGN_SIZE=16384
# 获取当前文件大小(字节)
FILE_SIZE=$(stat -c %s "$SO_FILE")
# 计算需要填充的字节数
PAD_SIZE=$(( (ALIGN_SIZE - (FILE_SIZE % ALIGN_SIZE)) % ALIGN_SIZE ))

if [ $PAD_SIZE -gt 0 ]; then
    echo "原始大小:$FILE_SIZE 字节,需要填充 $PAD_SIZE 字节"
    # 填充0字节到文件末尾
    dd if=/dev/zero bs=1 count=$PAD_SIZE >> "$SO_FILE"
    echo "对齐完成,新大小:$(stat -c %s "$SO_FILE") 字节"
else
    echo "文件已16KB对齐,无需处理"
fi
  1. 执行脚本
# 赋予执行权限
chmod +x align_so.sh
# 对齐单个so文件
./align_so.sh librustlib.so
  1. Windows PowerShell 手动对齐脚本
# 16KB对齐PowerShell脚本
$soFile = "librustlib.so"
$alignSize = 16384
$fileInfo = Get-Item $soFile
$fileSize = $fileInfo.Length
$padSize = ($alignSize - ($fileSize % $alignSize)) % $alignSize

if ($padSize -gt 0) {
    Write-Host "原始大小:$fileSize 字节,需要填充 $padSize 字节"
    # 生成0字节并追加到文件
    $nullBytes = New-Object byte[] $padSize
    [System.IO.File]::AppendAllBytes($soFile, $nullBytes)
    Write-Host "对齐完成,新大小:$(Get-Item $soFile).Length 字节"
} else {
    Write-Host "文件已16KB对齐,无需处理"
}

批量对齐所有架构的 so 库
如果需要对齐多个架构(arm64-v8a/armeabi-v7a 等)的 so 文件,可写批量脚本:

#!/bin/bash
# 批量对齐android_libs下所有so文件
ALIGN_TOOL="~/Library/Android/sdk/build-tools/34.0.0/zipalign" # 替换为你的zipalign路径
SO_DIR="./android_libs"

# 遍历所有架构目录
for ARCH in $(ls $SO_DIR); do
    SO_PATH="$SO_DIR/$ARCH/librustlib.so"
    if [ -f "$SO_PATH" ]; then
        echo "对齐 $ARCH 架构的so文件..."
        $ALIGN_TOOL -f -p 16 $SO_PATH $SO_PATH.tmp
        mv $SO_PATH.tmp $SO_PATH # 替换原文件
        echo "$ARCH 对齐完成"
    fi
done
echo "所有so文件16KB对齐完成!"

关键注意事项

  1. 对齐时机:建议在编译完 so 库后、拷贝到 Android 项目前做对齐;
  2. 验证优先:对齐后一定要用zipalign -c -v 16验证,避免对齐失败;
  3. 不要重复对齐:多次对齐会导致 so 文件体积异常增大,只需对齐一次;
  4. APK 整体对齐:如果是打包 APK,也可以先不单独对齐 so,最后用zipalign对齐整个 APK(效果一致):
zipalign -f -p 16 app-debug.apk app-debug-aligned.apk

总结

  1. 16KB 对齐的核心工具是 Android 官方的zipalign,参数固定为-f -p 16,简单且不易出错;
  2. 手动对齐需计算文件大小与 16384 的差值,用 0 填充,适合无 SDK 环境的场景;
  3. 对齐后必须验证,确保 so 文件大小是 16384 的整数倍,满足 Android 上架和性能要求。

对齐后的 so 库可直接放入 Android 项目使用,打包 APK/AAB 时不会出现 “未对齐” 的审核问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容