NAPI Demo
以官方的 Native C++ Demo 为例,一个完整的 NAPI 分为 ArkTS 和 C/C++ 两部分。此处为了后面的代码更好分析,将动态库名调整为 just_for_test,ArkTS 部分代码如下:
import { hilog } from '@kit.PerformanceAnalysisKit';
import testNapi from 'libjust_for_test.so';
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
hilog.info(0x0000, 'testTag', 'Test NAPI 2 + 3 = %{public}d', testNapi.add(2, 3));
})
}
.width('100%')
}
.height('100%')
}
}
其关联的 C/C++ 代码如下:
#include "napi/native_api.h"
static napi_value Add(napi_env env, napi_callback_info info) {
size_t requireArgc = 2;
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);
double value0;
napi_get_value_double(env, args[0], &value0);
double value1;
napi_get_value_double(env, args[1], &value1);
napi_value sum;
napi_create_double(env, value0 + value1, &sum);
return sum;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{ "add", nullptr, Add, 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 = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
动态库加载
将项目编译后,可以在 build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages 目录下找到 Index.ets 对应的中间产物 Index.ts。
对比可以发现,我们在 ArkTS 中编写的 import 代码如下:
import testNapi from 'libjust_for_test.so'
编译后变成了下面的代码:
import testNapi from "@app:com.example.nativeexample/entry/just_for_test";
而这行代码在经过 EcmaVM 解释执行时,会触发如下逻辑:
// arkcompiler/ets_runtime/ecmascript/builtins/builtins_promise_job.cpp
JSTaggedValue BuiltinsPromiseJob::DynamicImportJob(EcmaRuntimeCallInfo *argv) {
// ... @app 形式对应 isNative 为 true,且 moduleType 为 MODULE_APP
auto [isNative, moduleType] = SourceTextModule::CheckNativeModule(requestPath);
ModuleManager *moduleManager = thread->GetCurrentEcmaContext()->GetModuleManager();
if (isNative) {
return DynamicImport::ExecuteNativeOrJsonModule(thread, specifierString, moduleType, resolve, reject);
}
// ...
}
在 DynamicImport 的流程中会调用 SourceTextModule::LoadNativeModule 以加载动态库:
// arkcompiler/ets_runtime/ecmascript/module/js_module_source_text.cpp
bool SourceTextModule::LoadNativeModule(JSThread *thread, const JSHandle<SourceTextModule> &requiredModule,
ModuleTypes moduleType) {
//...
// 从 EcmaVM 中找出 APP_MODULE 对应的 "RequireNapi" 函数
auto maybeFuncRef = GetRequireNativeModuleFunc(vm, moduleType);
// ...
Local<FunctionRef> funcRef = maybeFuncRef;
// 调用 RequireNapi
auto exportObject = funcRef->Call(vm, JSValueRef::Undefined(vm), arguments.data(), arguments.size());
// ...
requiredModule->StoreModuleValue(thread, 0, JSNApiHelper::ToJSHandle(exportObject));
// ...
return true;
}
其中 GetRequireNativeModuleFunc 在 APP_MODULE 的情况下会从 EcmaVM 中获取名为 RequireNapi 的函数并返回,具体逻辑如下:
// arkcompiler/ets_runtime/ecmascript/module/js_module_source_text.cpp
Local<JSValueRef> SourceTextModule::GetRequireNativeModuleFunc(EcmaVM *vm, ModuleTypes moduleType) {
Local<ObjectRef> globalObject = JSNApi::GetGlobalObject(vm);
auto globalConstants = vm->GetJSThread()->GlobalConstants();
auto funcName = (moduleType == ModuleTypes::NATIVE_MODULE) ?
globalConstants->GetHandledRequireNativeModuleString() :
// @app 形式对应的是 APP_MODULE,所以返回的是 "requireNapi"
globalConstants->GetHandledRequireNapiString();
return globalObject->Get(vm, JSNApiHelper::ToLocal<StringRef>(funcName));
}
// arkcompiler/ets_runtime/ecmascript/global_env_constants.h
V(RequireNapiString, REQUIRE_NAPI_FUNC_INDEX, "requireNapi")
RequireNapi
上述逻辑中的 RequireNapi 是在 ArkNativeEngine 构造的流程中通过 EcmaVM 添加的,具体逻辑如下:
// foundation/arkui/napi/native_engine/impl/ark/ark_native_engine.cpp
ArkNativeEngine::ArkNativeEngine(EcmaVM* vm, void* jsEngine, bool isLimitedWorker) : NativeEngine(jsEngine), vm_(vm), topScope_(vm), isLimitedWorker_(isLimitedWorker) {
// ...
Local<FunctionRef> requireNapi =
FunctionRef::New(
vm,
[](JsiRuntimeCallInfo *info) -> Local<JSValueRef> {
// ...
// 前面的流程调用传参为 ["just_for_test", true, "com.example.nativeexample/entry"]
NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
if (info->GetArgsNumber() == 3) { // 3:Determine if the number of parameters is equal to 3
Local<StringRef> path(info->GetCallArgRef(2)); // 2:Take the second parameter
module =
moduleManager->LoadNativeModule(moduleName->ToString().c_str(), path->ToString().c_str(),
isAppModule, errInfo, false, "", arkNativeEngine->isLimitedWorker_);
}
// ... 注册相关的 NAPI 函数
return scope.Escape(exports);
},
nullptr,
requireData);
// ...
Local<ObjectRef> global = panda::JSNApi::GetGlobalObject(vm);
Local<StringRef> requireName = StringRef::NewFromUtf8(vm, "requireNapi");
// 在 EcmaVM 中添加 requireNapi 函数
global->Set(vm, requireName, requireNapi);
// ...
Init();// 初始化 NativeEngine
// ...
}
在 RequireNapi 的流程里会调用 NativeModuleManager::LoadNativeModule,其内部会调用 dlopen 来加载动态库。
dlopen() 加载动态库时会执行该库中的所有全局构造函数(亦称初始化函数)
此时 dlopen 就会执行前面 Demo 中的 RegisterEntryModule 并调用 napi_module_register 注册相关的 NAPI 信息:
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
// foundation/arkui/napi/native_engine/native_node_api.cpp
NAPI_EXTERN void napi_module_register(napi_module* mod) {
// ...
NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
NativeModule module;
module.version = mod->nm_version;
module.fileName = mod->nm_filename;
module.name = mod->nm_modname;
module.flags = mod->nm_flags;
// 对应 Demo 中 cpp 里的 Init 函数
module.registerCallback = (RegisterCallback)mod->nm_register_func;
moduleManager->Register(&module);
}
// foundation/arkui/napi/module_manager/native_module_manager.cpp
void NativeModuleManager::Register(NativeModule* nativeModule) {
// ... 省略逻辑主要是在链尾新增节点
lastNativeModule_->version = nativeModule->version;
lastNativeModule_->fileName = nativeModule->fileName;
lastNativeModule_->isAppModule = isAppModule_;
lastNativeModule_->name = moduleName;
lastNativeModule_->moduleName = nullptr; /* we update moduleName latter */
lastNativeModule_->refCount = nativeModule->refCount;
// 对应 Demo 中 cpp 里的 Init 函数
lastNativeModule_->registerCallback = nativeModule->registerCallback;
lastNativeModule_->getJSCode = nativeModule->getJSCode;
lastNativeModule_->getABCCode = nativeModule->getABCCode;
lastNativeModule_->next = nullptr;
lastNativeModule_->moduleLoaded = true;
lastNativeModule_->systemFilePath = "";
}
到这里我们就了解到了 ArkTS import NAPI 到动态库加载的主要流程,接下来再看 ArkTS 调用 NAPI 函数的主要实现流程。
绑定 NAPI 函数
在前面 RequireNapi 的介绍中为了凸显动态库的加载流程,删除了动态库加载完后的处理代码。实际在通过 NativeModuleManager::LoadNativeModule 加载完动态库后,还会用它返回的 module 变量(对应前文 napi_module_register 注册的 module)进行注册:
// foundation/arkui/napi/native_engine/impl/ark/ark_native_engine.cpp
ArkNativeEngine::ArkNativeEngine(EcmaVM* vm, void* jsEngine, bool isLimitedWorker) : NativeEngine(jsEngine), vm_(vm), topScope_(vm), isLimitedWorker_(isLimitedWorker) {
// ...
Local<FunctionRef> requireNapi =
FunctionRef::New(
vm,
[](JsiRuntimeCallInfo *info) -> Local<JSValueRef> {
// 省略内容:动态库加载流程,下文的 module 变量是 LoadNativeModule 返回的结果
Local<JSValueRef> exports(JSValueRef::Undefined(ecmaVm));
if (module != nullptr) {
// ...
if (module->jsABCCode != nullptr || module->jsCode != nullptr) {
// ...
} else if (module->registerCallback != nullptr) {
Local<ObjectRef> exportObj = ObjectRef::New(ecmaVm);
arkNativeEngine->SetModuleName(exportObj, module->name);
// 调用前面 napi_module_register 提供的 napi_module 内的 nm_register_func
module->registerCallback(reinterpret_cast<napi_env>(arkNativeEngine),
JsValueFromLocalValue(exportObj));
// ...
exports = exportObj;
arkNativeEngine->loadedModules_[module] = Global<JSValueRef>(ecmaVm, exports);
}
}
return scope.Escape(exports);
},
nullptr,
requireData);
// ...
}
上面代码中 module->registerCallback 实际对应 demoModule 的 nm_register_func,所以 module->registerCallback 实际就是调用 Demo 中的 Init 函数:
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
static napi_value Add(napi_env env, napi_callback_info info) {
// ...
return sum;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr }
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
Init 函数中先创建了一个 napi_property_descriptor 数组,再调用了 napi_define_properties,从这上下文中不难猜出这个函数是用来注册相关函数的,类似 JNI 的 RegisterNatives 函数。
napi_define_properties 的代码如下,从 requireNapi 的代码可以知道 env 是 ArkNativeEngine,而 object 则是提供给 ArkTS 调用 NAPI 的对象:
// foundation/arkui/napi/native_engine/native_api.cpp
NAPI_EXTERN napi_status napi_define_properties(napi_env env, napi_value object, size_t property_count, const napi_property_descriptor* properties) {
// ...
auto nativeProperties = reinterpret_cast<const NapiPropertyDescriptor*>(properties);
for (size_t i = 0; i < property_count; i++) {
// ... nativeObject 对应 object
NapiDefineProperty(env, nativeObject, nativeProperties[i]);
}
return GET_RETURN_STATUS(env);
}
// foundation/arkui/napi/native_engine/impl/ark/ark_native_engine.cpp
bool NapiDefineProperty(napi_env env, Local<panda::ObjectRef> &obj, NapiPropertyDescriptor propertyDescriptor) {
// ...
if (obj->IsJSShared(vm)) {
NativeSendable::NapiDefineSendabledProperty(env, obj, propertyDescriptor, propertyName, result);
} else {
NapiDefinePropertyInner(env, obj, propertyDescriptor, propertyName, result);
}
// ...
return result;
}
在 NapiDefineProperty 的逻辑中,我们的 Demo 内的均为非共享函数,所以主要关注 NapiDefinePropertyInner 部分:
// foundation/arkui/napi/native_engine/impl/ark/ark_native_engine.cpp
void NapiDefinePropertyInner(napi_env env, Local<panda::ObjectRef> &obj, NapiPropertyDescriptor &propertyDescriptor, Local<panda::JSValueRef> &propertyName, bool &result) {
// ...
if (propertyDescriptor.getter != nullptr || propertyDescriptor.setter != nullptr) {
// ...
} else if (propertyDescriptor.method != nullptr) {
// ...
// 利用 EcmaVM 创建 ArkTS 侧的函数 Obj,fullName 对应前面提供的 "add"
Local<panda::JSValueRef> cbObj = NapiNativeCreateFunction(env, fullName.c_str(),
PropertyAttribute attr(cbObj, writable, enumable, configable);
// 将函数名和函数对应的 attr 进行映射,后续 ArkTS 侧就可以通过函数名进行调用
result = obj->DefineProperty(vm, propertyName, attr);
} else {
// ...
}
}
在 NapiDefinePropertyInner 内,由于 Demo 中 napi_property_descriptor 数组内提供的 getter、setter 都是 nullptr,而 method 则对应 Add 的函数指针,所以我们只需要关注 method 分支内的内容。该部分逻辑注释得比较清楚,主要是通过 napi_property_descriptor 数组中提供的信息来创建对应的 ArkTS 侧相关的映射,从而使得后续 ArkTS 可以通过函数名调用 NAPI。
总结
以上就是 NAPI 整体的流程,最后我们再来回顾一下整体流程:EcmaVM 在解析到 import 语句时,会调用 ArkNativeEngine 内的 RequireNapi,并触发 NativeModuleManager 加载动态库。此时会调用动态库内的全局构造函数,触发 napi_module_register 将 napi_module 相关信息添加到 NativeModuleManager 内的链表尾部。此后再调用 napi_module 内的 nm_register_func 函数来触发 napi_define_properties 进而绑定相关的 NAPI,使得 ArkTS 侧可以通过相关的函数名调用 NAPI。