1. 为什么我们需要在HarmonyOS中调用自定义SO库
在HarmonyOS应用开发中,我们经常会遇到需要保护敏感数据或核心算法的场景。比如你可能需要:
- 存储API密钥、加密凭证等敏感信息
- 实现核心加密算法(如AES、RSA等)
- 包含专有业务逻辑的计算模块
- 需要高性能处理的复杂运算
这些场景下,如果直接将代码写在ArkTS/JS层,会面临两个主要问题:
- 代码可读性高,容易被反编译获取核心逻辑
- JavaScript执行效率不如原生代码
这时候,使用C++编写核心逻辑并编译为SO动态链接库就是最佳选择。SO库经过编译后是二进制文件,相比脚本语言更难逆向,同时还能获得接近硬件层的执行效率。
提示:SO(Shared Object)库是Linux/Unix系统中的动态链接库,类似于Windows中的DLL文件。在HarmonyOS中,它同样采用这种机制来实现代码共享和模块化。
2. 环境准备与工程创建
2.1 开发环境要求
在开始前,请确保你的开发环境满足以下条件:
- DevEco Studio 3.1或更高版本
- HarmonyOS SDK API 9+
- Node.js 16+(用于NAPI开发)
- CMake 3.10.2+
2.2 创建Native C++工程
在DevEco Studio中创建Native C++工程的步骤如下:
- 打开DevEco Studio
- 选择File → New → Create Project
- 在模板选择界面,找到并选择"Native C++"模板
- 配置项目基本信息:
- Project Name: 你的项目名称(如native_lib_demo)
- Bundle Name: 应用包名(如com.example.nativelib)
- Save Location: 项目保存路径
- Compile API Version: 选择API 9或更高
- 点击Finish完成创建
创建完成后,项目结构应该如下:
code复制├── entry
│ ├── src
│ │ ├── main
│ │ │ ├── cpp
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── hello.cpp
│ │ │ │ └── napi_init.cpp
│ │ │ ├── resources
│ │ │ └── ets
│ │ └── oh-package.json5
3. SO库开发核心步骤
3.1 理解关键文件作用
在cpp目录下,有几个关键文件需要特别注意:
- CMakeLists.txt:构建配置文件,定义如何编译你的C++代码
- napi_init.cpp:NAPI模块初始化文件,包含模块注册逻辑
- Index.d.ts:类型声明文件,定义ArkTS可调用的接口
- oh-package.json5:模块配置文件,定义SO库名称和依赖
3.2 编写C++核心逻辑
让我们以一个简单的字符串加密函数为例,展示如何编写C++代码:
cpp复制// hello.cpp
#include <string>
#include "napi/native_api.h"
// 简单的XOR加密算法
std::string xor_encrypt(const std::string &input, char key) {
std::string output = input;
for (size_t i = 0; i < input.size(); ++i) {
output[i] = input[i] ^ key;
}
return output;
}
// NAPI导出函数
napi_value EncryptString(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 解析第一个参数(字符串)
size_t str_length;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &str_length);
char* str = new char[str_length + 1];
napi_get_value_string_utf8(env, args[0], str, str_length + 1, &str_length);
// 解析第二个参数(密钥)
int32_t key;
napi_get_value_int32(env, args[1], &key);
// 执行加密
std::string encrypted = xor_encrypt(str, static_cast<char>(key));
// 返回结果
napi_value result;
napi_create_string_utf8(env, encrypted.c_str(), encrypted.length(), &result);
delete[] str;
return result;
}
3.3 注册NAPI模块
在napi_init.cpp中注册我们的模块和函数:
cpp复制#include "napi/native_api.h"
#include "hello.cpp"
extern "C" __attribute__((visibility("default"))) void NAPI_hello_GetJSCode(const char** buf, int* bufLen) {
if (buf != nullptr) {
*buf = "This is hello module";
}
if (bufLen != nullptr) {
*bufLen = 0;
}
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"encryptString", nullptr, EncryptString, nullptr, nullptr, nullptr, napi_default, nullptr}
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
extern "C" __attribute__((constructor)) void RegisterModule() {
napi_module module = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry", // 必须与你的模块名一致
.nm_priv = nullptr,
.reserved = {0},
};
napi_module_register(&module);
}
注意:nm_modname必须与你的模块目录名一致,否则会导致加载失败。
3.4 配置声明文件
在Index.d.ts中声明我们的接口:
typescript复制export const encryptString: (input: string, key: number) => string;
4. 构建与打包SO库
4.1 配置CMake构建
确保CMakeLists.txt包含所有必要的源文件:
cmake复制cmake_minimum_required(VERSION 3.10.2)
project(hello)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${NATIVERENDER_ROOT_PATH})
include_directories(${CMAKE_SOURCE_DIR}/../../../common/napi/include)
file(GLOB SOURCES "*.cpp")
add_library(hello SHARED ${SOURCES})
target_link_libraries(hello PUBLIC libace_napi.z.so)
4.2 构建SO库
在DevEco Studio中:
- 点击菜单栏的Build → Build Module(s)
- 构建完成后,SO库会生成在:
code复制(路径可能因架构不同而变化)build/intermediates/libs/default/arm64-v8a/libhello.so
4.3 多架构支持
为了支持不同CPU架构,需要在build-profile.json5中配置:
json复制"buildOption": {
"artifactType": {
"rule": {
"abiFilters": ["armeabi-v7a", "arm64-v8a"]
}
}
}
5. 在ArkTS项目中集成SO库
5.1 导入SO库到项目
- 在你的ArkTS项目中创建libs目录:
code复制entry/src/main/libs/arm64-v8a/ - 将生成的libhello.so复制到对应架构目录下
5.2 配置依赖
在oh-package.json5中添加依赖:
json复制"dependencies": {
"hello": "file:../libs/hello"
}
5.3 调用SO库函数
在ArkTS中调用我们的加密函数:
typescript复制import { encryptString } from 'hello'
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
// 调用SO库函数
let encrypted = encryptString("SensitiveData", 0x55)
console.log("Encrypted: " + encrypted)
// 解密演示(实际应用中应该在SO库中完成)
let decrypted = encryptString(encrypted, 0x55)
console.log("Decrypted: " + decrypted)
})
}
.width('100%')
}
.height('100%')
}
}
6. 常见问题与解决方案
6.1 SO库加载失败
问题现象:
code复制Error: Cannot find module 'hello'
可能原因:
- SO库路径不正确
- oh-package.json5配置错误
- 架构不匹配(如设备是arm64但只有armeabi-v7a的库)
解决方案:
- 检查SO库是否放在正确的架构目录下
- 确认oh-package.json5中的路径配置正确
- 确保构建了所有需要的架构版本
6.2 函数调用崩溃
问题现象:
应用在调用SO函数时崩溃
可能原因:
- NAPI参数处理错误
- 内存访问越界
- 类型不匹配
调试方法:
- 使用DevEco Studio的Native Debug功能
- 在C++代码中添加日志:
cpp复制#include <hilog/log.h> OH_LOG_Print(LOG_APP, LOG_INFO, 0xD000F00, "TAG", "Debug message"); - 检查NDK堆栈信息
6.3 性能优化建议
- 减少JS/NAPI边界调用:每次跨语言调用都有开销,尽量批量处理数据
- 使用TypedArray:对于大数据传输,使用ArrayBuffer/TypedArray比普通JS数组高效
- 线程安全:如果SO库中使用多线程,确保正确处理线程同步
7. 高级应用场景
7.1 复杂数据类型处理
处理对象等复杂数据类型示例:
cpp复制napi_value ProcessObject(napi_env env, napi_callback_info info) {
napi_value obj;
size_t argc = 1;
napi_get_cb_info(env, info, &argc, &obj, nullptr, nullptr);
// 获取属性
napi_value prop;
napi_get_named_property(env, obj, "username", &prop);
// 处理属性...
// 返回新对象
napi_value result;
napi_create_object(env, &result);
napi_set_named_property(env, result, "processed", prop);
return result;
}
对应的TypeScript声明:
typescript复制export interface User {
username: string
age: number
}
export const processObject: (user: User) => { processed: string }
7.2 异步操作实现
实现异步操作的示例:
cpp复制struct AsyncData {
napi_async_work work;
napi_deferred deferred;
std::string input;
std::string result;
char key;
};
void ExecuteWork(napi_env env, void* data) {
AsyncData* asyncData = static_cast<AsyncData*>(data);
asyncData->result = xor_encrypt(asyncData->input, asyncData->key);
}
void CompleteWork(napi_env env, napi_status status, void* data) {
AsyncData* asyncData = static_cast<AsyncData*>(data);
napi_value result;
napi_create_string_utf8(env, asyncData->result.c_str(), asyncData->result.length(), &result);
napi_resolve_deferred(env, asyncData->deferred, result);
napi_delete_async_work(env, asyncData->work);
delete asyncData;
}
napi_value AsyncEncrypt(napi_env env, napi_callback_info info) {
// 解析参数...
AsyncData* asyncData = new AsyncData();
asyncData->input = str;
asyncData->key = static_cast<char>(key);
napi_value promise;
napi_create_promise(env, &asyncData->deferred, &promise);
napi_value work_name;
napi_create_string_utf8(env, "AsyncEncryptWork", NAPI_AUTO_LENGTH, &work_name);
napi_create_async_work(env, nullptr, work_name, ExecuteWork, CompleteWork, asyncData, &asyncData->work);
napi_queue_async_work(env, asyncData->work);
return promise;
}
TypeScript中使用:
typescript复制const result = await asyncEncrypt("data", 0x55)
7.3 加密算法最佳实践
在实际项目中,建议:
- 使用成熟的加密库(如OpenSSL)
- 实现密钥安全管理:
cpp复制// 从安全存储获取密钥 std::string getEncryptionKey() { // 实际项目中应该从安全存储获取 return "secure_key_placeholder"; } - 添加完整性校验:
cpp复制std::string addHMAC(const std::string &data) { // 实现HMAC校验 return data + "_HMAC"; }
8. 安全注意事项
- 不要硬编码密钥:即使是SO库,硬编码的密钥也能被逆向工程提取
- 使用白盒加密:考虑使用白盒加密技术保护密钥
- 混淆保护:对SO库进行混淆处理,增加逆向难度
- 完整性校验:检查SO库是否被篡改
- 最小权限原则:只暴露必要的接口
实现简单的完整性校验示例:
cpp复制bool verifyChecksum() {
// 实现简单的校验和验证
return true;
}
napi_value SafeEncrypt(napi_env env, napi_callback_info info) {
if (!verifyChecksum()) {
napi_throw_error(env, nullptr, "Integrity check failed");
return nullptr;
}
// ...正常加密逻辑
}
在实际项目中,我通常会结合多种保护措施。比如将核心算法拆分到多个SO库,运行时动态加载;或者使用JNI_OnLoad进行初始化检查。这些技巧虽然不能完全防止破解,但能显著提高攻击者的逆向成本。