OpenHarmony 源码解析之NAPI框架内部实现分析

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

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

前言

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

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

一、NAPI在系统中的位置

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

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

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

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

二、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框架需要解决哪些问题。

三、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。

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

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

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

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

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

四、NAPI模块注册功能的实现

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

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

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

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

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)。

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"方法引用并执行。

五. 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() 获取返回结果。

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对象
}

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;
}

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

异步工作流程

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

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

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,用完后再删除引用对象以便释放相关内存资源。(有点像给智能指针增加引用计数的效果,内部实际是如何实现的待进一步探究源码)。

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​​。

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-09-16 15:08:08

鸿蒙HarmonyOS应用

2021-12-08 15:07:51

鸿蒙HarmonyOS应用

2021-12-06 06:19:03

鸿蒙HarmonyOS应用

2024-01-03 15:41:49

2022-02-14 14:47:11

SystemUIOpenHarmon鸿蒙

2022-05-17 10:42:36

reboot源码解析

2022-10-11 15:04:28

NAPI开发鸿蒙

2022-05-12 14:42:17

项目开发Napi实现

2022-06-13 14:18:39

电源管理子系统耗电量服务

2022-02-17 20:57:07

OpenHarmon操作系统鸿蒙

2021-11-25 09:54:54

鸿蒙HarmonyOS应用

2022-07-05 16:03:29

电源管理子系统鸿蒙

2022-01-06 16:17:58

鸿蒙HarmonyOS应用

2021-09-18 14:40:37

鸿蒙HarmonyOS应用

2021-11-08 15:04:47

鸿蒙HarmonyOS应用

2021-12-17 16:42:09

鸿蒙HarmonyOS应用

2022-05-20 10:32:49

事件循环器事件队列鸿蒙

2022-01-20 14:33:29

openharmonwayland协议鸿蒙

2023-04-12 15:31:11

系统服务管理鸿蒙

2022-01-10 15:30:11

鸿蒙HarmonyOS应用
点赞
收藏

51CTO技术栈公众号