# 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_);
}
1
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() 方法内部先调用 NativeModuleManagerLoadNativeModule() 方法加载动态库并返回一个 module,如果 module 非空,则判断 module 中的 jsABCCode 或者 jsCode 是否为空,如果有一个非空则条件成立进入 if 语句,那么 jsABCCode 或者 jsCode 什么时候非空呢?比如加载的是项目中的一个模块而非一个单纯的动态库时条件才成立或者在跨平台的场景需要加载 abc 时条件成立,本文的样例只是加载了一个 libentry.so,因此条件不成立,接着判断 moduleregisterCallback 是否为空,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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

napi_module_register() 方法内部把 mod 中配置的 nm_register_func 强制转换成 RegisterCallback 后赋值给了 NativeModuleregisterCallback,这里可以进行强制转换利用的是 C++ 的一个特性:

在 C++ 中,函数指针类型的转换需要满足源类型和目标类型的函数签名(参数类型和数量,以及返回类型)完全相同。本样例中 nm_register_funcRegisterCallback 类型定义分别如下所示:

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); 
}
1
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 数据只配置了 utf8namemethodattributes3 项,然后调用 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);
}
1
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 转化成 NapiPropertyDescriptorproperty 并调用 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;
}
1
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,然后判断 gettersetter是否为空,本样例中它们都是空,接着判断 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);
  })
1
2
3
4
5
6
7
8
9
10
11

笔者给 Text 添加了一个点击事件,当点击 Text 组件时执行了 testNapi.add(2, 3) 语句,JS 引擎解释执行到 testNapi.add() 方法时,就去查引擎内部维护的映射表,根据映射表可以找到 C++ 中定义的 Add() 方法,后续就是执行 C++ 中 Add() 方法的流程了……

请作者喝杯咖啡

津公网安备 12011402001367号

津ICP备2020008934号-2

中央网信办互联网违法和不良信息举报中心

天津市互联网违法和不良信息举报中心