HarmonyOS NDK的JavaScript/TypeScript与C++交互机制
细解释这个调用流程:
整体架构流程
ArkTS/JavaScript ←→ .d.ts (类型定义) ←→ NAPI ←→ .cpp (C++实现)
- 文件结构和作用
项目结构示例:
MyHarmonyApp/
├── entry/src/main/ets/ # ArkTS应用代码
│ └── pages/Index.ets
├── entry/src/main/cpp/ # C++原生代码
│ ├── CMakeLists.txt
│ ├── hello.cpp # C++实现
│ └── types/libhello/ # 类型定义
│ └── index.d.ts # TypeScript类型声明
└── oh-package.json5
- .d.ts类型定义文件
.d.ts文件作用:
为C++函数提供TypeScript类型声明
定义JavaScript和C++之间的接口
IDE智能提示和类型检查
示例 - index.d.ts:
// entry/src/main/cpp/types/libhello/index.d.ts
export const add: (a: number, b: number) => number;
export const getString: () => string;
export const processArray: (arr: number[]) => number[];
// 复杂对象交互
export interface UserInfo {
name: string;
age: number;
scores: number[];
}
export const processUser: (user: UserInfo) => UserInfo;
export const asyncOperation: (callback: (result: string) => void) => void;
- C++实现文件
hello.cpp实现:
#include "napi/native_api.h"
#include <string>
#include <vector>
// 简单数值计算
static napi_value Add(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2] = {nullptr};
// 获取JavaScript传入的参数
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 从JavaScript值中提取number
double value1, value2;
napi_get_value_double(env, args[0], &value1);
napi_get_value_double(env, args[1], &value2);
// C++计算
double result = value1 + value2;
// 将C++结果转换为JavaScript值返回
napi_value jsResult;
napi_create_double(env, result, &jsResult);
return jsResult;
}
// 字符串处理
static napi_value GetString(napi_env env, napi_callback_info info) {
std::string cppString = "Hello from C++!";
napi_value jsString;
napi_create_string_utf8(env, cppString.c_str(), cppString.length(), &jsString);
return jsString;
}
// 数组处理
static napi_value ProcessArray(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 获取数组长度
uint32_t arrayLength;
napi_get_array_length(env, args[0], &arrayLength);
// 创建结果数组
napi_value resultArray;
napi_create_array_with_length(env, arrayLength, &resultArray);
// 处理每个元素
for (uint32_t i = 0; i < arrayLength; i++) {
napi_value element;
napi_get_element(env, args[0], i, &element);
double value;
napi_get_value_double(env, element, &value);
// C++处理逻辑:每个元素乘以2
double processedValue = value * 2;
napi_value processedElement;
napi_create_double(env, processedValue, &processedElement);
napi_set_element(env, resultArray, i, processedElement);
}
return resultArray;
}
// 复杂对象处理
static napi_value ProcessUser(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
napi_value userObj = args[0];
// 提取对象属性
napi_value nameValue, ageValue, scoresValue;
napi_get_named_property(env, userObj, "name", &nameValue);
napi_get_named_property(env, userObj, "age", &ageValue);
napi_get_named_property(env, userObj, "scores", &scoresValue);
// 转换为C++类型
size_t nameLength;
char name[100];
napi_get_value_string_utf8(env, nameValue, name, 100, &nameLength);
double age;
napi_get_value_double(env, ageValue, &age);
// C++处理逻辑
std::string processedName = "Processed: " + std::string(name);
double processedAge = age + 1;
// 创建返回对象
napi_value result;
napi_create_object(env, &result);
napi_value newName, newAge;
napi_create_string_utf8(env, processedName.c_str(), processedName.length(), &newName);
napi_create_double(env, processedAge, &newAge);
napi_set_named_property(env, result, "name", newName);
napi_set_named_property(env, result, "age", newAge);
napi_set_named_property(env, result, "scores", scoresValue); // 原样返回数组
return result;
}
// 模块初始化和函数注册
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
// 将C++函数注册为JavaScript可调用的函数
napi_property_descriptor desc[] = {
{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getString", nullptr, GetString, nullptr, nullptr, nullptr, napi_default, nullptr},
{"processArray", nullptr, ProcessArray, nullptr, nullptr, nullptr, napi_default, nullptr},
{"processUser", nullptr, ProcessUser, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
// 模块描述
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "hello", // 模块名称
.nm_priv = ((void*)0),
.reserved = { 0 },
};
// 注册模块
extern "C" __attribute__((constructor)) void RegisterHelloModule(void) {
napi_module_register(&demoModule);
}
- CMakeLists.txt配置
# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(hello)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(NAPI_ROOT_PATH ${NATIVERENDER_ROOT_PATH}/../../../../../../)
# 添加头文件路径
include_directories(${NAPI_ROOT_PATH}
${NAPI_ROOT_PATH}/third_party/node/src)
# 编译动态库
add_library(hello SHARED hello.cpp)
# 链接系统库
target_link_libraries(hello PUBLIC libace_napi.z.so libc++.a)
- ArkTS中调用C++函数
Index.ets使用示例:
// entry/src/main/ets/pages/Index.ets
import hello from 'libhello.so'; // 导入编译后的动态库
@Entry
@Component
struct Index {
@State result: number = 0;
@State message: string = '';
@State processedArray: number[] = [];
build() {
Column() {
// 简单函数调用
Button('调用C++加法')
.onClick(() => {
this.result = hello.add(10, 20); // 调用C++函数
console.log('C++计算结果:', this.result); // 30
})
Text(`计算结果: ${this.result}`)
// 字符串函数调用
Button('获取C++字符串')
.onClick(() => {
this.message = hello.getString();
})
Text(`C++消息: ${this.message}`)
// 数组处理
Button('处理数组')
.onClick(() => {
let inputArray = [1, 2, 3, 4, 5];
this.processedArray = hello.processArray(inputArray);
})
Text(`处理后数组: ${JSON.stringify(this.processedArray)}`)
// 复杂对象交互
Button('处理用户对象')
.onClick(() => {
let user: UserInfo = {
name: "张三",
age: 25,
scores: [85, 92, 78]
};
let processedUser = hello.processUser(user);
console.log('处理后的用户:', processedUser);
})
}
.width('100%')
.height('100%')
}
}
// 引入类型定义
interface UserInfo {
name: string;
age: number;
scores: number[];
}
- 编译和构建流程
构建命令:
# 1. 清理构建缓存
hvigor clean
# 2. 编译C++代码为动态库
hvigor assembleHap --mode module -p product=default
# 构建过程:
# .cpp → (CMake) → libhello.so → 打包到HAP → 运行时加载
- 调用时序图
用户点击按钮
↓
ArkTS: hello.add(10, 20)
↓
HarmonyOS Runtime 加载 libhello.so
↓
NAPI Bridge: 参数类型转换 (JS → C++)
↓
C++: Add函数执行计算
↓
NAPI Bridge: 返回值转换 (C++ → JS)
↓
ArkTS: 接收结果并更新UI
- 异步调用示例
C++异步函数:
#include <thread>
#include <chrono>
static napi_value AsyncOperation(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
napi_value callback = args[0];
// 创建异步工作
std::thread([env, callback]() {
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(2));
// 调用JavaScript回调
napi_value result;
napi_create_string_utf8(env, "异步操作完成!", 15, &result);
napi_value global;
napi_get_global(env, &global);
napi_call_function(env, global, callback, 1, &result, nullptr);
}).detach();
return nullptr;
}
ArkTS调用异步函数:
Button('异步操作')
.onClick(() => {
hello.asyncOperation((result: string) => {
console.log('异步结果:', result);
// 更新UI
});
})
这个交互机制让HarmonyOS应用能够充分利用C++的性能优势,同时保持ArkTS的开发便利性。通过NAPI桥接层,实现了类型安全的双向通信。
详细解析这个HarmonyOS NDK模块初始化和函数注册的核心机制:
- EXTERN_C_START/END 宏解析
宏定义原理:
// 在napi头文件中定义
#ifdef __cplusplus
#define EXTERN_C_START extern "C" {
#define EXTERN_C_END }
#else
#define EXTERN_C_START
#define EXTERN_C_END
#endif
作用机制:
// 展开后实际代码:
extern "C" {
static napi_value Init(napi_env env, napi_value exports) {
// 函数实现
}
}
// 原理说明:
// 1. 阻止C++名称修饰 (Name Mangling)
// 2. 确保函数名在符号表中保持原始形式
// 3. 让JavaScript运行时能够准确找到函数入口点
名称修饰对比:
// 没有extern "C"的情况:
// C++编译器生成: _Z4InitP8napi_envP10napi_value
// JavaScript运行时无法找到此符号
// 有extern "C"的情况:
// 编译器生成: Init
// JavaScript运行时可以直接找到"Init"符号
- napi_property_descriptor 结构详解
完整结构定义:
typedef struct {
const char* utf8name; // 属性名称
napi_value name; // 属性名称的napi_value版本
napi_callback method; // 函数指针
napi_callback getter; // getter函数
napi_callback setter; // setter函数
napi_value value; // 属性值
napi_property_attributes attributes; // 属性特性
void* data; // 用户数据指针
} napi_property_descriptor;
- 注册流程深入分析
Step 1: 内存布局准备
// 编译时,desc数组在栈上分配
napi_property_descriptor desc[4]; // 4个函数描述符
// 内存布局:
// [desc[0]: "add"函数描述]
// [desc[1]: "getString"函数描述]
// [desc[2]: "processArray"函数描述]
// [desc[3]: "processUser"函数描述]
Step 2: napi_define_properties 内部机制
// HarmonyOS运行时内部伪代码
napi_status napi_define_properties(napi_env env,
napi_value object,
size_t property_count,
const napi_property_descriptor* properties) {
for (size_t i = 0; i < property_count; i++) {
const napi_property_descriptor* prop = &properties[i];
// 1. 为每个C++函数创建JavaScript函数包装器
napi_value js_function;
napi_create_function(env,
prop->utf8name, // "add"
NAPI_AUTO_LENGTH,
prop->method, // Add函数指针
prop->data,
&js_function);
// 2. 将JavaScript函数添加到exports对象
napi_set_named_property(env, object, prop->utf8name, js_function);
}
return napi_ok;
}
Step 3: 函数包装器创建
// 运行时为每个C++函数创建包装器
// 伪代码表示包装器结构
struct FunctionWrapper {
napi_callback cpp_function; // 指向Add函数
napi_env environment; // 运行环境
void* user_data; // 用户数据
// JavaScript调用时的入口点
napi_value CallCppFunction(napi_env env, napi_callback_info info) {
// 1. 参数类型检查和转换
// 2. 调用实际的C++函数
return cpp_function(env, info);
// 3. 返回值类型转换
}
};
- exports对象机制
exports对象的本质:
// exports就是一个JavaScript对象
// 类似于:
var exports = {
add: function(a, b) {
return [调用C++ Add函数];
},
getString: function() {
return [调用C++ GetString函数];
},
processArray: function(arr) {
return [调用C++ ProcessArray函数];
},
processUser: function(user) {
return [调用C++ ProcessUser函数];
}
};
注册后的内存结构:
exports对象在V8堆中的结构:
{
"add": FunctionWrapper {
callback: &Add,
env: current_env,
data: nullptr
},
"getString": FunctionWrapper {
callback: &GetString,
env: current_env,
data: nullptr
},
// ... 其他函数
}
- 完整调用链路分析
JavaScript调用C++的完整流程:
// 1. ArkTS调用
hello.add(10, 20)
↓
// 2. V8引擎查找exports["add"]
JSFunction* js_func = exports.GetProperty("add");
↓
// 3. 调用函数包装器
FunctionWrapper* wrapper = js_func.GetWrapper();
↓
// 4. 参数类型转换 (JS → NAPI)
napi_value args[2];
args[0] = JSNumberToNapiValue(10);
args[1] = JSNumberToNapiValue(20);
↓
// 5. 调用实际C++函数
napi_value result = wrapper->cpp_function(env, callback_info);
↓
// 6. 返回值类型转换 (NAPI → JS)
JSValue js_result = NapiValueToJSNumber(result);
↓
// 7. 返回给ArkTS
return js_result; // 30
- 错误处理和类型安全
运行时类型检查:
// NAPI内部会进行类型验证
static napi_value Add(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2] = {nullptr};
// 获取参数
napi_status status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (status != napi_ok) {
// 抛出JavaScript异常
napi_throw_error(env, nullptr, "参数获取失败");
return nullptr;
}
// 类型检查
napi_valuetype type0, type1;
napi_typeof(env, args[0], &type0);
napi_typeof(env, args[1], &type1);
if (type0 != napi_number || type1 != napi_number) {
napi_throw_type_error(env, nullptr, "参数必须是数字类型");
return nullptr;
}
// 继续处理...
}
- 模块生命周期管理
初始化时机:
// 1. 动态库加载时 (dlopen)
extern "C" __attribute__((constructor)) void RegisterHelloModule(void) {
napi_module_register(&demoModule);
}
↓
// 2. HarmonyOS运行时调用Init函数
static napi_value Init(napi_env env, napi_value exports) {
// 注册所有函数到exports
return exports; // 返回填充后的exports对象
}
↓
// 3. ArkTS中可以使用 import hello from 'libhello.so'
内存管理:
// C++函数包装器的生命周期由V8 GC管理
// 当JavaScript中没有引用时,包装器会被垃圾回收
// 但C++动态库保持加载状态直到应用退出
这个注册机制的核心是建立JavaScript世界和C++世界之间的桥梁,通过NAPI提供的标准接口,实现类型安全的双向通信。每个注册的函数都会在JavaScript运行时中创建对应的包装器,确保调用的正确性和性能。