6

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区-51CTO.CO...

 2 years ago
source link: https://ost.51cto.com/posts/14691
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文正在参加星光计划3.0–夏日挑战赛

作者:张志成

NAPI(Native API)是OpenHarmony系统中的一套原生模块扩展开发框架,它基于Node.js N-API规范开发,为开发者提供了JavaScript与C/C++模块之间相互调用的交互能力。这套机制对于鸿蒙系统开发的价值有两方面:

  1. 鸿蒙系统可以将框架层丰富的模块功能通过js接口开放给上层应用使用。
  2. 应用开发者也可以选择将一些对性能、底层系统调用有要求的核心功能用C/C++封装实现,再通过js接口使用,提高应用本身的执行效率。

1. NAPI在系统中的位置

NAPI在OpenHarmony中属于UI框架的一部分。

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区
#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区

实现一个NAPI模块,开发者需要完成模块注册、定义接口映射、实现回调方法等工作,这些工作在NAPI框架内部是怎么起作用的,为了实现Js与C++的交互,框架又做了哪些事情?今天我们就来看一看NAPI框架的内部实现。

2. NAPI框架代码目录结构

NAIP框架代码在 foundation\arkui\napi\ 路径下。总体上可分为interface、native_engine 和 xxxManager 三部分。

interface 目录为NAPI开发者提供了各种常用功能的API接口及宏定义。

native_engine 目录是NAPI框架的核心部分,interface目录提供的接口对应的功能都在这里实现。C++与Js之间的调用交互最终是要依托JS引擎来完成的,针对系统支持的不同JS引擎,在impl目录中也有对应的4种实现(ark, jerryscript, quickjs, v8)。

此外,还有几个Manager目录,分别负责模块注册、引用对象管理、作用域管理等专项功能。

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区

我们知道,一个模块被设计成什么样,往往是由它面临的问题决定的。为了了解这些目录的各个组成部分发挥的作用,我们先来看看JS调用C++的过程中,NAPI框架需要解决哪些问题。

3. NAPI框架完成的主要工作

假设我们在框架层用C/C++实现了一个myapp模块,这个模块可以为应用提供系统访问次数的统计。为了让应用层的JS代码能够使用这项能力,我们为应用开发者提供了如下的JS接口:

@ohos.myapp.d.ts
declare namespace myapp {
    // 同步方法
    function getVisitCountSync(key: string, defaultValue?: string): string;

    // 异步方法
    function getVisitCountAsync(key: string, callback: AsyncCallback<string>): void; // callback回调方式
    function getVisitCountAsync(key: string, defaultValue?: string): Promise<string>; // Promise方式
 }

App应用开发者的JS代码简单导入一下模块就可以直接调用了。

import myapp1 from "@ohos.myapp"
var result = myapp1.getVisitCountSync("www.mywebsite.com");

为了实现这样的调用,NAPI框架需要解决以下问题,各个子模块在其中都发挥了相关作用。

1)模块注册(import的模块名称”myapp”,是怎么对应到实际的c++lib库的?) — Module Manager

2)方法名映射(js调用的”getVisitCountSync”等方法,是怎么对应到native的C/C++的方法的?) — Native Engine

3)数据传递与转换(js传入的入参、得到的返回结果,需要转换成C/C++代码可以操作的数据类型)— NativeValue

而对于稍微再复杂一点的异步调用:

/** Promise 方式的异步调用 */
var  promiseObj = myapp1.getVisitCountAsync("www.mywebsite.com").then(data => {
    ......
}).catch(error => {
    ......
});
/** call back 方式的异步调用 */
myapp1.getVisitCountAsync("www.mywebsite.com", function (err, ret) {
    ......
});

NAPI框架还需要解决以下问题:

4)异步执行(js的调用立刻得到返回,native的业务处理另起线程单独执行)— NativeEngine – AsnycWork

5)Callback实现(C/C++怎么回调js提供的callback方法,返回结果怎么在异步线程中传递)— Native Reference

6)Promise实现(NodeJs promise语法特性的实现)— Native Deferred

3.1 NAPI框架背后依托的是JavaScript引擎

通过代码目录结构我们看到NAPI框架针对JerryScirpt、V8、QuickJS和鸿蒙自己的Ark引擎都单独实现了一套native_engin impl。这是因为C++到Js的调用最终是要依托JS引擎提供的能力实现的。

例如,当NAPI的开发者需要创建一个能被js代码识别的big int数值对象,创建的过程如下图所示:

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区

可以看到,最终创建这个数值对象的工作是由JS引擎去完成的,引擎从自己的GlobalStorage中创建了一个新的GlobalHandle来保存这个数值。

4. NAPI模块注册功能的实现

开发一个NAPI模块,首先需要按照NAPI框架的机制要求实现注册相关动作,通过注册告诉鸿蒙系统你开发的这个lib库的名称,提供了哪些native方法,以及它们对应的js接口名称是什么。

下图为一个NAPI接口“add()"的实现,C代码中定义了lib库对应的module名称,并在注册回调方法中定义了js方法和C方法的名称映射关系。

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区

图左侧的JS应用代码比较简单,import一个C的so库,然后直接调用add()方法就可以了。我们比较关心的是,图右侧的C代码实际是如何起作用的?

4.1 注册模块

最先被执行的是RegisterModule方法

extern "C" __attribute__((constructor)) void RegisterModule(void)
{
    napi_module_register(&demoModule);
}

这里,NAPI开发者只需要调用一下napi_module_register()方法即可完成注册,进一步看它的内部实现,ModuleManager登场了,它有一个内部链表(firstNativeModule_,lastNativeModule),开发者传入的demoModule注册信息最终是保存到了链表尾部。

到这里RegisterModule()的操作就结束了。感觉好像什么事都没干啊?没错,这里仅仅是做了个”登记“,真正加载动态库、映射方法名称的操作,要等到这个登记的module被js程序真正用到的时候。(通过import from xxx 或 requireNapi(“xxx”) 加载module)

4.2 加载模块

模块被注册到ModuleManager后,什么时候被加载使用?我们看一下Native_module_manager.h的定义

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区

这个ModuleManager类只提供了寥寥几个对外接口,外部程序想获取到它链表中的module对象,只能通过LoadNativeModule()方法,应该是它没错了。

在鸿蒙框架代码中四处寻觅LoadNativeModule()之后,我们在各个NativeEngine的构造函数中,都发现了它的踪迹。用法大体相同。

这里以ArkNativeEngine实现为例,继续上代码:

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区

ArkNativeEngine在自己的构造函数中,定义了一个回调方法"requireNapi",当js应用程序调用requireNapi(“xxxModule”)时,上图这段回调代码将会被执行。(在鸿蒙框架的js代码中搜requireNapi会发现很多这样的调用)。

我们注意到回调方法里做了2件事:

  1. loadNativeModule() — 加载模块(读取动态库)
  2. registerCallback() — 执行开发者定义的注册回调函数 (保存js和c++方法名称映射关系)

先看loadNativeModule() 内部实现,最终执行到NativeModuleManager::LoadModuleLibrary() 方法中,执行系统调用LoadLibrary(path)加载动态库。

其中path的定义可以参见 NativeModuleManager::GetNativeModulePath() 方法

#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析-开源基础软件社区

这就是鸿蒙的各种Native C++动态库最终部署在目标设备上的位置。

module被加载后,接着就是回调开发者自定义的注册函数,在本例中就是开发者实现的 Init() 方法。这里面调用了napi_define_properties(),把js方法"add"和C++方法"Add"的映射信息以属性的形式保存到JS Runtime运行时中。

后续当APP应用程序调用js接口"add"时,JS Runtime就能通过映射关系属性找到C++的"Add"方法引用并执行。

5. NAPI 方法实现

说完了模块注册流程,我们再来看看C++ Native方法的实现。

还是以前面提到的这组接口为例:

@ohos.myapp.d.ts
declare namespace myapp {
    // 同步方法
    function getVisitCountSync(key: string, defaultValue?: string): string;

    // 异步方法
    function getVisitCountAsync(key: string, callback: AsyncCallback<string>): void; // callback回调方式
    function getVisitCountAsync(key: string, defaultValue?: string): Promise<string>; // Promise方式
 }

这组接口为JS应用提供了一个获取访问次数的功能,并提供了同步、异步两种方法。其中异步方法的异步回调提供了callback和promise两种方式,

callback方式是由用户自定义一个回调函数,NAPI将执行结果通过回调函数的入参返回给用户;promise方式是NAPI返回一个promise对象给用户,后续用户可以通过调用promise.then() 获取返回结果。

5.1 同步方法的实现

先看同步方法的C实现。这个比较简单,C开发者做好数据的转换工作就可以了。

Js调用传递的参数对象、函数对象都是以napi_value这样一个抽象的类型提供给C的,开发者需要将它们转换为C数据类型进行计算,再将计算结果转为napi_value类型返回就可以了。NAPI框架提供了各种api接口为用户完成这些转换。前面我们提到过,这些转换工作背后是依赖JS引擎去实现的。

static napi_value GetVisitCountSync(napi_env env, napi_callback_info info) {    
  /* 根据环境变量获取参数 */
  size_t argc = 2; //参数个数
  napi_value argv[2] = { 0 }; //参数定义

  /* 入参变量获取 */
  napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
  // 获取入参的类型
  napi_valuetype valueType = napi_undefined;
  napi_typeof(env, argv[0], &valueType);    

  // 入参值转换为C/C++可以操作的数据类型
  char value[VALUE_BUFFER_SIZE] = { 0 };
  size_t valueLen = 0;
  napi_get_value_string_utf8(env, argv[0], value, VALUE_BUFFER_SIZE, &valueLen);

  // ...... 省略若干业务流程计算步骤

  /* C/C++数据类型转换为JS数据类型并返回 */
  napi_value result = nullptr; // JS字符串对象
  std::string resultStr = "Visit Count = 65535";
  napi_create_string_utf8(env, resultStr.c_str(), resultStr.length(), &result);

  return result; //返回JS对象
}

5.2 异步方法的实现

C++实现异步方法需要做这三件事:

1)立即返回一个临时结果给js调用者

2)另起线程完成异步计算工作

3)通过callback或promise返回正真的计算结果

下面代码给出了异步方法实现主体部分的注释说明:

//异步方法需要在不同线程中传递各种业务数据,定义一个结构体保存这些被传递的信息
struct MyAsyncContext {
    napi_env env = nullptr; // napi运行环境
    napi_async_work work = nullptr; // 异步工作对象
    napi_deferred deferred = nullptr; // 延迟执行对象(用于promise方式返回计算结果)
    napi_ref callbackRef = nullptr; // js callback function的引用对象 (用于callback方式返回计算结果)
};

static napi_value GetVisitCountAsync(napi_env env, napi_callback_info info)
{
    ...... // 省略部分前置代码
    // 首先还是读取JS入参
    napi_value argv[2] = { 0 };
    napi_get_cb_info(env, info, &argc, argv, &thisVar, &data);
    
    auto asyncContext = new MyAsyncContext(); //创建结构体用于保存各种需要在异步线程中传递的数据信息
    asyncContext->env = env;

    // callback回调方式和promise回调在ts接口文件中体现为2个独立的接口方法,但它们的接口名称相同,在C++侧是由同一个方法来实现的。
    // 这里通过判断JS传入的第二个参数是不是function类型,来判定用户调用的是callback回调接口,还是promise回调接口。
    napi_valuetype valueType = napi_undefined; 
    napi_typeof(env, argv[1], &valueType);

    // 为异步方法创建临时返回值。根据我们的ts接口文件定义,callback接口返回一个void就行, promise接口返回一个promise对象
    napi_value tmpRet = nullptr;
    if (valueType == napi_function) { // Js调用的是callback接口
        // 为js调用者传入的js fuction创建一个napi引用并保存到asyncContext中,以便后续在C++异步线程中能够回调该js fuction
        napi_create_reference(env, argv[1], 1, &asyncContext->callbackRef);    
        // callback接口返回参数为void,构造一个undefined的返回值即可。
        napi_get_undefined(env, &tmpRet);
    } else { // Js调用的是promise接口
        // 创建promise对象。tmpRet用于返回promise对象给js调用者, asyncContext->deferred用于后续在C++的异步线程中返回正真的计算结果
        napi_create_promise(env, &asyncContext->deferred, &tmpRet);
    }

    napi_value resource = nullptr;
    // 创建异步工作  (内部实际是使用了libuv组件的异步处理能力,需要开发者自定义两个callback方法)
    napi_create_async_work(
        env, nullptr, resource,
        [](napi_env env, void* data) { // 1)execute_callback 方法,该方法会在libuv新开的独立线程中被执行
            MyAsyncContext* innerAsyncContext = (MyAsyncContext*)data;
            // 需要异步处理的业务逻辑都放在这个execute_callback方法中,运算需要的数据可以通过data入参传进来。
            innerAsyncContext->status = 0;
            // ......
        },
        [](napi_env env, napi_status status, void* data) { // 2)complete_callback方法,在js应用的主线程中运行
            MyAsyncContext* innerAsyncContext = (MyAsyncContext*)data;
            napi_value asyncResult;
            // complete_callback回到了主线程,一般用于返回异步计算结果。execute_callback和complete_callback之间可以通过data传递数据信息
            // 计算结果一般是从data中获取的,这里略过直接硬编码
            napi_create_string_utf8(env, "Visit Count = 65535", NAPI_AUTO_LENGTH, &asyncResult);
            if (innerAsyncContext->deferred) {
                // promise 方式的回调
                // innerAsyncContext->deferred是前面步骤中创建的promise延迟执行对象(此时js调用者已经拿到了该promise对象)
                napi_resolve_deferred(env, innerAsyncContext->deferred, asyncResult);
            } else {
                // callback 函数方式的回调
                napi_value callback = nullptr;
                // 通过napi_ref获取之间js调用者传入的js function,并调用它返回计算结果
                napi_get_reference_value(env, innerAsyncContext->callbackRef, &callback);
                napi_call_function(env, nullptr, callback, 1, &asyncResult, nullptr);
                napi_delete_reference(env, innerAsyncContext->callbackRef);
            }
            // 在异步调用的结尾释放async_work和相关业务数据的内存
            napi_delete_async_work(env, innerAsyncContext->work);
            delete innerAsyncContext; 
        },
    (void*)asyncContext, &asyncContext->work);

  // 执行异步工作
  napi_queue_async_work(env, asyncContext->work);
  // 返回临时结果给js调用 (callback接口返回的是undefined, promise接口返回的是promise对象)
  return tmpRet;
}

异步实现的主体代码通过注释应该能够理解了。这里再说两点:

5.2.1 异步工作流程

libuv是一个基于事件驱动的异步io库,NAPI用它来实现了异步工作处理流程。 napi_create_async_work()创建异步工作时要求传入的execute_callback()和complete_callback(),也是沿用了libuv内部uv_queue_work的工作方式(见OpenHarmony\third_party\libuv\src\threadpool.c)。

其中第一个回调方法,也就是execute_callback,会在独立的线程中运行。

5.2.2 napi_value与napi_ref

在callback回调方式的处理流程中,用到了这3个与napi_ref相关的方法:

  • napi_create_reference() : 将napi_value包装成napi_ref引用对象
  • napi_get_reference_value() : 从napi_ref引用对象中取得napi_value
  • napi_delete_reference() :删除napi_ref引用对象

当我们需要跨作用域传递napi_value时,往往需要用到上面这组方法把napi_value变成napi_ref。这是因为napi_value本质上只是一个指针,指向某种类型的napi数据对象。NAPI框架希望通过这种方式为开发者屏蔽各种napi数据对象的类型细节,类似于void* ptr的作用 。既然是指针,使用时就需要考虑它指向的对象的生命周期。

在我们的例子中,我们通过GetVisitCountAsync()方法的入参得到了js应用传递给C++的 callback function,存放在napi_value argv[1]中。但我们不能在complete_callback()方法中直接通过这个argv[1]去回调callback function(通过data对象传递也不行)。这时因为当代码执行到complete_callback()方法时,原先的主方法GetVisitCountAsync()早已执行结束, napi_value argv[1]指向的内存可能已经被释放另作他用了。

NAPI框架给出的解决方案是让开发者通过napi_value创建一个napi_ref,这个napi_ref是可以跨作用域传递的,然后在需要用到的地方再将napi_ref还原为napi_value,用完后再删除引用对象以便释放相关内存资源。(有点像给智能指针增加引用计数的效果,内部实际是如何实现的待进一步探究源码)

更多原创内容请关注:深开鸿技术团队

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK