# 16.8:NAPI 加载原理(中)
上一节笔者给大家讲解了 JS 引擎解释执行到 import
语句的加载流程,总结起来就是利用 dlopen() 方法的加载特性向 NativeModuleManager
内部的链接尾部添加一个 NativeModule,没有阅读过上节文章的小伙伴,笔者强烈建议阅读一下,本节笔者继续给大家讲解 JS 调用 C++ 方法的实现过程。
# 16.8.1:回看requireNapi方法
根据上节课的讲解,napi_module_register() 方法只是通过 demoModule
的配置创建一个 NativeModule 后并把它加入到 NativeModuleManager 内部的链表尾部,当在 JS 侧调用 C++ 的对应方法时,如何能精准调用到对应方法的呢?我们再回头看下 ArkNativeEngine 构造方法中注册的 requireNapi() 方法内的执行过程,省略部分源码如下所示:
ArkNativeEngine::ArkNativeEngine(EcmaVM* vm, void* jsEngine, bool isLimitedWorker) : NativeEngine(jsEngine), vm_(vm), topScope_(vm), isLimitedWorker_(isLimitedWorker) {
// 省略部分代码……
void* requireData = static_cast<void*>(this);
// 创建一个requireNapi()方法
Local<FunctionRef> requireNapi =
FunctionRef::New(
vm,
[](JsiRuntimeCallInfo *info) -> Local<JSValueRef> {
NativeModuleManager* moduleManager = NativeModuleManager::GetInstance();
NativeModule* module = nullptr;
// 调用NativeModuleManager的LoadNativeModule方法加载
module = moduleManager->LoadNativeModule();
if (module != nullptr) {
// 先判断 module 的 jsABCCode 或者 jsCode 是否为空则
if (module->jsABCCode != nullptr || module->jsCode != nullptr) {
// 省略部分代码……
} else if (module->registerCallback != nullptr) {
// 如果 module 的 registerCallback 不为空,则执行registerCallback() 方法
module->registerCallback(reinterpret_cast<napi_env>(arkNativeEngine), JsValueFromLocalValue(exportObj));
} else {
HILOG_ERROR("init module failed");
return scope.Escape(exports);
}
}
return scope.Escape(exports);
},
nullptr,
requireData);
Local<ObjectRef> global = panda::JSNApi::GetGlobalObject(vm);
Local<StringRef> requireName = StringRef::NewFromUtf8(vm, "requireNapi");
// 注入 requireNapi 方法
global->Set(vm, requireName, requireNapi);
Init();
panda::JSNApi::SetLoop(vm, loop_);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
requireNapi() 方法内部先调用 NativeModuleManager
的 LoadNativeModule() 方法加载动态库并返回一个 module
,如果 module
非空,则判断 module
中的 jsABCCode
或者 jsCode
是否为空,如果有一个非空则条件成立进入 if 语句,那么 jsABCCode
或者 jsCode
什么时候非空呢?比如加载的是项目中的一个模块而非一个单纯的动态库时条件才成立或者在跨平台的场景需要加载 abc 时条件成立,本文的样例只是加载了一个 libentry.so
,因此条件不成立,接着判断 module
的 registerCallback
是否为空,registerCallback
是什么时机赋值的呢?笔者在上一节讲 napi_module_register() 中讲到过赋值,源码如下所示:
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;
// registerCallback 是 mod 中配置的nm_register_func方法
module.registerCallback = (RegisterCallback)mod->nm_register_func;
moduleManager->Register(&module);
}
2
3
4
5
6
7
8
9
10
11
12
13
在 napi_module_register() 方法内部把 mod
中配置的 nm_register_func
强制转换成 RegisterCallback
后赋值给了 NativeModule
的 registerCallback
,这里可以进行强制转换利用的是 C++ 的一个特性:
在 C++ 中,函数指针类型的转换需要满足源类型和目标类型的函数签名(参数类型和数量,以及返回类型)完全相同。本样例中 nm_register_func 和 RegisterCallback 类型定义分别如下所示:
typedef napi_value (*napi_addon_register_func)(napi_env env, napi_value exports); typedef napi_value (*RegisterCallback)(napi_env, napi_value);
1
2
3它们都接收两个参数:一个
napi_env
类型的env
和一个napi_value
类型的exports
,并返回一个napi_value
类型的值,所以它们的函数签名是完全相同的,因此一个napi_addon_register_func
类型的函数指针可以被强制转换为RegisterCallback
类型的函数指针。
nm_register_func 就是在 hello.cpp
中配置的 Init() 方法,hello.cpp
的源码如下:
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
// 创建一个napi_property_descriptor数组,napi_property_descriptor的每一项只配置了
napi_property_descriptor desc[] = {
{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getMd5Sync", nullptr, GetMd5Sync, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getMd5", nullptr, GetMd5, nullptr, nullptr, nullptr, napi_default, nullptr},
};
// 调用napi_define_properties方法
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_register_func被配置为 Init 方法
.nm_modname = "entry",
.nm_priv = ((void *)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
综上所述,在 requireNapi() 方法内执行 module->registerCallback() 方法时就是执行的 hello.cpp
中配置的 Init() 方法,在 Init() 方法中先创建一个 napi_property_descriptor
类型的数组 desc
,每一个 napi_property_descriptor
数据只配置了 utf8name,method 和 attributes 这 3 项,然后调用 napi_define_properties() 方法,napi_define_properties() 方法源码如下所示:
NAPI_EXTERN 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++) {
NapiPropertyDescriptor property;
// 有值
property.utf8name = properties[i].utf8name;
// 无值
property.name = properties[i].name;
// 有值
property.method = reinterpret_cast<NapiNativeCallback>(properties[i].method);
// 无值
property.getter = reinterpret_cast<NapiNativeCallback>(properties[i].getter);
// 无值
property.setter = reinterpret_cast<NapiNativeCallback>(properties[i].setter);
// 无值
property.value = properties[i].value;
// 有值且值为0
property.attributes = (uint32_t)properties[i].attributes;
// 无值
property.data = properties[i].data;
// 调用NapiDefineProperty方法
NapiDefineProperty(env, nativeObject, property);
}
return napi_clear_last_error(env);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
为了便于后续分析源码,笔者加上了详细的注释,napi_define_properties() 方法内部循环遍历传递进来的每一个 napi_property_descriptor
,把每一个 napi_property_descriptor
转化成 NapiPropertyDescriptor
的 property
并调用 NapiDefineProperty() 方法完成 JS 方法和 C++方法的映射,NapiDefineProperty() 方法源码如下所示:
bool NapiDefineProperty(napi_env env, Local<panda::ObjectRef> &obj, NapiPropertyDescriptor propertyDescriptor)
{
auto engine = reinterpret_cast<NativeEngine*>(env);
auto vm = engine->GetEcmaVm();
bool result = false;
// 根据utf8name的名字创建一个JS引擎侧的字符串值赋值给propertyName
Local<panda::StringRef> propertyName = panda::StringRef::NewFromUtf8(vm, propertyDescriptor.utf8name);
// 校验attributes是否有设置其它值,本样例中attributes默认设置的是0,因此writable,enumable和configable都是false
// writable: 属性是否可读可修改,enumable:属性是否允许遍历,configable:属性是否允许删除
bool writable = (propertyDescriptor.attributes & NATIVE_WRITABLE) != 0;
bool enumable = (propertyDescriptor.attributes & NATIVE_ENUMERABLE) != 0;
bool configable = (propertyDescriptor.attributes & NATIVE_CONFIGURABLE) != 0;
std::string fullName("");
// 本样例中getter和setter都是为null
if (propertyDescriptor.getter != nullptr || propertyDescriptor.setter != nullptr) {
// 省略部分代码……
} else if (propertyDescriptor.method != nullptr) { // 本样例中method非空,配置的是C++端对应的方法名
fullName += propertyDescriptor.utf8name;
// 调用 NapiNativeCreateFunction方法创建一个 JS 引擎侧的方法cbObj
Local<panda::JSValueRef> cbObj = NapiNativeCreateFunction(env, fullName.c_str(), propertyDescriptor.method, propertyDescriptor.data);
// 创建一个PropertyAttribute类型的attr实例
PropertyAttribute attr(cbObj, writable, enumable, configable);
// 调用JS引擎侧的JSObject对象的DefineProperty()方法完成对vm添加额外的属性操作
result = obj->DefineProperty(vm, propertyName, attr);
} else {
Local<panda::JSValueRef> val = LocalValueFromJsValue(propertyDescriptor.value);
PropertyAttribute attr(val, writable, enumable, configable);
result = obj->DefineProperty(vm, propertyName, attr);
}
Local<panda::ObjectRef> excep = panda::JSNApi::GetUncaughtException(vm);
if (!excep.IsNull()) {
HILOG_ERROR("ArkNativeObject::DefineProperty occur Exception");
panda::JSNApi::GetAndClearUncaughtException(vm);
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
NapiDefineProperty() 方法的内注释的比较清楚,主要是先根据 utf8name
创建一个 JS 引擎侧的方法名 propertyName
,然后判断 getter
和 setter
是否为空,本样例中它们都是空,接着判断 method
是否是空, 因为method
是我们在 hello.cpp
中定义的本地方法,所以条件成立进入当前分支语句中,fullName
表示 JS 侧的方法名,接着调用 NapiNativeCreateFunction() 方法创建一个 JS 引擎侧实例 cbObj
,然后创建一个 PropertyAttribute
类型的 attr
实例,最后调用 JS 引擎侧的 JSObject 对象的 DefineProperty() 方法完成对 vm
添加额外的属性操作,也就是说代码分析到这里, JS 引擎内部已经保存了 JS 侧的方法名 和 C++ 侧的方法的映射关系。
好了,到目前为止,JS 侧的方法和 C++ 方法的关联我们已经清楚了,接下来看如何调用到 C++ 的方法……
# 16.8.2:JS调用C++方法
目前已经清楚了 JS 引擎已经保存了 JS 侧的方法名 和 C++ 侧的方法的映射关系,当 JS 侧需要调用 C++ 方法时,代码如下所示:
import testNapi from 'libentry.so'
Text(this.message)
.fontSize(25)
.fontWeight(FontWeight.Bold)
.backgroundColor(Color.Pink)
.onClick(() => {
var result = testNapi.add(2, 3);
this.message = "OpenHarmony, value: " + result;
console.log(this.message);
})
2
3
4
5
6
7
8
9
10
11
笔者给 Text
添加了一个点击事件,当点击 Text
组件时执行了 testNapi.add(2, 3) 语句,JS 引擎解释执行到 testNapi.add() 方法时,就去查引擎内部维护的映射表,根据映射表可以找到 C++ 中定义的 Add() 方法,后续就是执行 C++ 中 Add() 方法的流程了……