#夏日挑战赛#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接口:
App应用开发者的JS代码简单导入一下模块就可以直接调用了。
为了实现这样的调用,NAPI框架需要解决以下问题,各个子模块在其中都发挥了相关作用。
1)模块注册(import的模块名称”myapp”,是怎么对应到实际的c++lib库的?) — Module Manager
2)方法名映射(js调用的”getVisitCountSync”等方法,是怎么对应到native的C/C++的方法的?) — Native Engine
3)数据传递与转换(js传入的入参、得到的返回结果,需要转换成C/C++代码可以操作的数据类型)— NativeValue
而对于稍微再复杂一点的异步调用:
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方法
这里,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方法的实现。
还是以前面提到的这组接口为例:
这组接口为JS应用提供了一个获取访问次数的功能,并提供了同步、异步两种方法。其中异步方法的异步回调提供了callback和promise两种方式,
callback方式是由用户自定义一个回调函数,NAPI将执行结果通过回调函数的入参返回给用户;promise方式是NAPI返回一个promise对象给用户,后续用户可以通过调用promise.then() 获取返回结果。
5.1 同步方法的实现
先看同步方法的C实现。这个比较简单,C开发者做好数据的转换工作就可以了。
Js调用传递的参数对象、函数对象都是以napi_value这样一个抽象的类型提供给C的,开发者需要将它们转换为C数据类型进行计算,再将计算结果转为napi_value类型返回就可以了。NAPI框架提供了各种api接口为用户完成这些转换。前面我们提到过,这些转换工作背后是依赖JS引擎去实现的。
5.2 异步方法的实现
C++实现异步方法需要做这三件事:
1)立即返回一个临时结果给js调用者
2)另起线程完成异步计算工作
3)通过callback或promise返回正真的计算结果
下面代码给出了异步方法实现主体部分的注释说明:
异步实现的主体代码通过注释应该能够理解了。这里再说两点:
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开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
感谢老师的分享,老师在解读源码后对开发比较有帮助的地方方便分享一下吗?