鸿蒙 NAPI 注册流程分析

968 阅读6分钟

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

image.png

对比可以发现,我们在 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;
}

其中 GetRequireNativeModuleFuncAPP_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_registernapi_module 相关信息添加到 NativeModuleManager 内的链表尾部。此后再调用 napi_module 内的 nm_register_func 函数来触发 napi_define_properties 进而绑定相关的 NAPI,使得 ArkTS 侧可以通过相关的函数名调用 NAPI。