#夏日挑战赛#OpenHarmony 源码解析之NAPI框架内部实现分析 原创 精华
作者:张志成
前言
NAPI(Native API)是OpenHarmony系统中的一套原生模块扩展开发框架,它基于Node.js N-API规范开发,为开发者提供了JavaScript与C/C++模块之间相互调用的交互能力。这套机制对于鸿蒙系统开发的价值有两方面:
- 鸿蒙系统可以将框架层丰富的模块功能通过js接口开放给上层应用使用。
- 应用开发者也可以选择将一些对性能、底层系统调用有要求的核心功能用C/C++封装实现,再通过js接口使用,提高应用本身的执行效率。
1. NAPI在系统中的位置
NAPI在OpenHarmony中属于UI框架的一部分。
实现一个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目录,分别负责模块注册、引用对象管理、作用域管理等专项功能。
我们知道,一个模块被设计成什么样,往往是由它面临的问题决定的。为了了解这些目录的各个组成部分发挥的作用,我们先来看看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数值对象,创建的过程如下图所示:
可以看到,最终创建这个数值对象的工作是由JS引擎去完成的,引擎从自己的GlobalStorage中创建了一个新的GlobalHandle来保存这个数值。
4. NAPI模块注册功能的实现
开发一个NAPI模块,首先需要按照NAPI框架的机制要求实现注册相关动作,通过注册告诉鸿蒙系统你开发的这个lib库的名称,提供了哪些native方法,以及它们对应的js接口名称是什么。
下图为一个NAPI接口“add()"的实现,C++代码中定义了lib库对应的module名称,并在注册回调方法中定义了js方法和C++方法的名称映射关系。
图左侧的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的定义
这个ModuleManager类只提供了寥寥几个对外接口,外部程序想获取到它链表中的module对象,只能通过LoadNativeModule()方法,应该是它没错了。
在鸿蒙框架代码中四处寻觅LoadNativeModule()之后,我们在各个NativeEngine的构造函数中,都发现了它的踪迹。用法大体相同。
这里以ArkNativeEngine实现为例,继续上代码:
ArkNativeEngine在自己的构造函数中,定义了一个回调方法"requireNapi",当js应用程序调用requireNapi(“xxxModule”)时,上图这段回调代码将会被执行。(在鸿蒙框架的js代码中搜requireNapi会发现很多这样的调用)。
我们注意到回调方法里做了2件事:
- loadNativeModule() — 加载模块(读取动态库)
- registerCallback() — 执行开发者定义的注册回调函数 (保存js和c++方法名称映射关系)
先看loadNativeModule() 内部实现,最终执行到NativeModuleManager::LoadModuleLibrary() 方法中,执行系统调用LoadLibrary(path)加载动态库。
其中path的定义可以参见 NativeModuleManager::GetNativeModulePath() 方法
这就是鸿蒙的各种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开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
感谢老师的分享,老师在解读源码后对开发比较有帮助的地方方便分享一下吗?