003_了解过鸿蒙的 Napi 吗?如果我需要编译三方库如何链接?

NAPI 框架简介

Node.js 的 N-API

  • NAPI 其实是最早应该是来自 node.js 中的一个拓展库(也可以说是一整套 API 接口),叫 Node-API ,叫做 N-API 。是用来构建本地插件的 API,将所有的 nodejs 底层数据结构黑盒化,封装成二进制接口,这样就可以实现不同版本的 Node.js 使用同样的接口,其目的是为了简化开发和维护。

NAPI (OpenHarmony)

  1. NAPI,全称 Native API ,是 OpenHarmony 系统中的一套原生模块拓展开发框架,基于 Nodejs 中的 N-API 开发,为开发者提供了 JS 与 C/C++不同语言模块之间的相互访问,交互的能力。它可以用于规范化封装 IO、OS 底层等,并可以提供相应的 JS 接口供开发者调用。当然。 N-API 也可以做到这一点。
  2. 区别于 N-API ,主要在于 NAPI 针对 OpenHarmony 系统做了 一些适配化和优化。但二者的目的都是为了简化和统一原生模块的开发和维护,提高跨平台和跨版本的兼容性

JS 和 C/C++互相访问实现原理(浅谈)

鄙人浅谈一下这个东西,欢迎各位斧正!

  1. 不同的语言的数据类型采用的是 napi_value 类型做封装和转换(计算机网络协议既视感),而像函数等接口则采用如 napi_create_function() 以及 napi_call_function() 等来进行创建和调用。
  2. 使用到了 V8 引擎,且对 V8 的接口做了 黑盒化 和 抽象化,使得更加稳定。

Code Question

主要是记录一下在读以及编写 Code 时的遇到的问题的以及自己积累的心得体会。
大多是一些代码中的接口的解释和个人结合相关资料后的一点理解

#ifdef __cplusplus extern “C”

  1. 这是一个在 cpp 中的宏命令,其表示的是如果在 cpp 文件中,我们需要调用一个 C 文件的接口
  2. 背景:
    a. C 和 C++ 对于函数名字处理的机制不同,众所周知,C++支持函数重载,因此在执行函数时会对名字有特殊处理,但是 C 不同,C 认为函数名只是一个名字。
    b. 如果需要使用到 C 中写好的接口,需要使用 C 方式的链接,因此在需要 extern “C” 来提示编译器在将 cpp 文件转为汇编时将该处对接口的调用方式由 Cpp 方式改为 C 方式,从而可以正确链接。
  3. 好处:就是方便了开发,使得 Cpp 对 C 的兼容性更强,对于已经写得很好的 C 接口,无需用 Cpp 再写一份。

_attribute_((constructor))

  1. 这是 GCC 一个特有的语法,用来修饰一个函数,从而让该函数在“main”之前执行,所以可以用来做初始化以及其他准备工作,比如初始化块变量或注册回调函数。可以避免一些依赖问题,提高性能。
  2. 相反, __attribute__((destructor)) 可以修饰函数,使得这个函数在共享库卸载或者程序退出时执行。
  3. 这两个都是 C++ 11 标准中引入的属性指定符序列中的一种,属性指定符序列是一种标准语法。
  4. 该语法还可以携带一个优先级参数,用于指定多个构造函数的执行顺序,优先级越低,执行越早。
  5. 区别 static
    a. static 变量是在全局变量初始化后,main 执行之前的,而 __attribute__((constructor)) 是在全局变量初始化之前执行,这样可以避免依赖问题。
    b. static 只能在当前文件中使用,而 __attribute__((constructor)) 可以在不同文件或者动态链接库中使用。

NAPI_CALL

  1. 是一个接口函数,用来调用 JS 中的函数,参数包括环境变量,接收对象,函数对象,参数个数,参数数组,返回值
  2. 使用场景
    a. 封装 IO、CPU 密集型、OS 底层能力,并将 JS 接口对外暴露。
    b. 实现 JS 与 C/C++代码的互相访问。
    c. 优先封装异步方法。
  3. 该函数与其他类型的接口函数的区别
    a. 这是一个宏,可以用来检测 NAPI 函数的返回值是否正确,其他类型的函数需要手动检测。
    b. 可以调用 JS 中任意函数,无论是全局还是对象的,其他接口只能调用特定类型以及特定范围的接口。
    c. 可以在任何地方需要回调的时候调用,不需要额外的参数以及 DS。
  4. 优势
    a. 简化 NAPI 函数的调用和错误处理,提高 Code 的可读性和可维护性。
    b. 可以方便调用 JS 中的接口,实现 C/C++和 JS 代码的互相访问
    c. 任意调用,无需额外的参数以及 DS
  5. 局限性
    a. 宏,不能作为函数指针传递给其他函数
    b. 不能直接处理异步操作,需要结合其他接口
    c. 存在兼容性和稳定性问题。

DELCARE_NAPI_FUCTION

  1. 这是 NAPI 的一个宏,看名字大家都知道这个是用来声明一个函数的,黄同学在很多使用 NAPI 的 Cpp 代码都能看到这个宏。
  2. 宏定义原型(参数),有两种形式
1
2
3
4
// 不传回调
#define DECLARE_NAPI_FUNCTION(modname,name)
// 传递回调
#define DECLARE_NAPI_FUNCTION_EX(modname, name, func)
  1. 两种形式
    a. 传递三个参数(模块名,函数名,回调函数),这种形式最常见,由开发者定义回调函数的逻辑和返回值。
    b. 传递两个参数(模块名,函数名),这种形式其实是一种简化写法,会自动使用一个默认的回调函数,将 JS 传递的参数转换为 C 的数据类型,并将 C 函数的返回值转化为 napi_value 类型返回给 JS。
    c. 在使用宏的时候,会根据传递参数的个数来执行,这种方式其实就是宏的条件编译,而不是函数重载。
  2. 实现原理
    a. 生成一个 napi_value 类型的函数,调用到 napi_create_function 函数,创建一个 JS 对象,并将回调函数作为 JS 对象的内部数据。
    b. 将生成的函数添加到一个全局数组中,用于存储所有的 NAPI 模块接口函数。
    c. 框架初始化的时候,遍历这个数组,将每个接口导出到 JS 中,方便调用。
    d. JS 调用接口时,NAPI 框架会调出对应的回调函数,并将 JS 的参数和返回值转化为 napi_value,实现 JS 和 C/C++间的交互。

实现原理的流程图,不包括框架初始化

napi_get_cb_info

  1. napi 的一个函数,用于获取回调函数的参数和其他信息下面是它的原形
1
2
3
napi_status napi_get_cb_info(napi_env env, napi_callback_info cbinfo,
size_t* argc, napi_value* argv,
napi_value* this_arg, void** data);
  • 参数解释:
    a. env:环境变量
    b. cbinfo:回调函数信息
    c. argc:接收参数个数的指针
    d. argv:存放参数值的数组
    e. this_arg 是 JS 中的 this 对象,data 是接收数据指针的指针。

NAPI 函数定义限制

这是一条使用框架编写一些 C/C++代码作为 JS 的接口的时需要注意的事情。
黄同学在做一个板子的 sample 的时候发现,某个 smaple 的样例源码无法跑通,除了一些简单的语法错误,最主要的是函数定义时的参数类型。

  • 在本身数据是没有 NAPI 类型的数据的,但是框架中很多接口的定义都是用 napi 类型,所以我们在定义的时候,传递参数可以用 void* ,即空类型传递,然后在函数体内再对应修改即可。否则调用时会不符合 NAPI 中接口的定义。

异步实现机制

以下内容只讨论计算机方面,不要和我听异步电机啥的,黄同学表示考完控制后,看到电机这个东西真的头很大。

什么是异步

  1. 异步操作不需要等待结果返回,而同步操作则需要等待。
  2. 优缺点取决于应用场景
    a. 异步会提高效率和响应速度,但会增加复杂度和难度。
    b. 同步比较简单直接,缺点是会造成阻塞和资源浪费。
  3. 异步和多线程:
    a. 首先,这是两个东西,虽然黄同学在很多时候也会把这两个东西弄混,但是确实是有区别。
    b. 区别在于,多线程编程是异步机制的常见实现方式之一,但并不是唯一,所以我们在看很多异步的操作认为是多线程其实是没什么问题的。
  • 在 NAPI 中,有两种实现异步操作:Calllback和 Promise 。

Callback

  1. 就是一般的回调函数机制,相对来说比 Promise 这种代码逻辑比较复杂,类似常见算法中递归的过程,相对来说,代码的可读性比较差,你只需回想一下你第一次看递归代码的时候大概就知道这种过程了。
  2. 优点
    a. 是 JS 的原生特性,无需额外的库或抽象层。
    b. 可以在完成异步操作后执行一些操作。
  3. 缺点
    a. 代码的可读性和可维护性差,当多个回调嵌套时,就会出现传说中的 回调地狱。
    b. 对错误处理变得复杂,因为每个回调都需要检查错误并传递给下一个回调。
    c. 不能返回多个参数,只能返回一个对象(有些资料对这个的解释是 Callback 是以参数的形式返回结果,但是并不准确,参考 JS 官方文档以及 javascript - How do you properly returnmultiple values from a Promise? - Stack Overflow 等内容,所谓的参数应该是一个参数,或者说是参数就是对象)
  4. Code(JS)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个异步的除法函数,接受两个数字和两个回调函数作为参数
function divisionAPI(number, divider, successCallback, errorCallback) {
if (divider == 0) {
return errorCallback(new Error('Division by zero'))
}
successCallback(number / divider)
}
// 调用异步的除法函数,传入两个数字和两个回调函数
divisionAPI(
10,
2,
function (result) {
// 成功的回调函数,打印结果
console.log('The result is ' + result)
},
function (error) {
// 失败的回调函数,打印错误
console.error('Something went wrong: ' + error.message)
}
)

Promise

  1. Promise 其实就是一种封装的异步操作结果的对象。
  2. 三种状态
    a. pending ,等待
    b. fulfilled ,已成功
    c. rejected ,已失败
  3. 优点:
    a. 可以采用同步的方式编写异步代码,因为异步操作结果已经被封装成了 Promise,这样可以避免回调地狱。
    b. Promise 对象本身是用链式而不是回调的方式调用,利用链式调用可以组合多个异步操作,并且可以用 then 或者 catch 操作来处理异步操作结果成功或者失败的情况。
    c. 比较灵活,可以用 all 方法等待所有异步操作的完成,也可以用 race 方法来获取最先完成的异步操作的结果。
  4. 缺点
    a. 因为做了一定程度的封装,用对象来存储异步操作的结果,其实就会消耗一些额外的内存和性能。
    b. 违背异步非阻塞 I/O 的原则,因为需要等待异步操作的完成(当从 pending 变到 fulfilled 或者 rejected 时,这个过程是不可逆的,使用 await 关键字在 async 函数中,等待一个 Promise 对象,实际过程就是再那个时间段代码调用到异步操作,此时 async 不执行,有点类似于同步操作,或者说我们一开始学习编程时最简单的函数调用,详见 asynchronous - Why use promise or async/await on child processes in Node.js? - Stack Overflow 和 How to use promises - Learn web development | MDN (mozilla.org))。
    c. 也是只能返回一个对象。
  5. Code( JS )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个异步的除法函数,返回一个Promise对象
function divisionAPI(number, divider) {
return new Promise(function (resolve, reject) {
if (divider == 0) {
return reject(new Error('Division by zero'))
}
resolve(number / divider)
})
}
// 调用异步的除法函数,返回一个Promise对象
divisionAPI(10, 2)
.then(function (result) {
// 成功的回调函数,打印结果
console.log('The result is ' + result)
})
.catch(function (error) {
// 失败的回调函数,打印错误
console.error('Something went wrong: ' + error.message)
})

NAPI 引入三方库

  1. 引言
  • 在 Node.js 环境中,我们经常需要使用第三方库来扩展我们的应用程序的功能。而在使用 Node.js 的 C 插件开发时,我们可以使用 NAPI(Node.js API)来实现对 C 库的引入和使用。NAPI 提供了一套稳定的 API,使得我们可以方便地在 Node.js 中使用 C++库。本文将介绍如何使用 NAPI 引入三方库,并提供示例代码。
  1. NAPI 简介
  • NAPI 是一组 C 语言的 API,用于构建 Node.js 的 C 插件。它提供了一套稳定的 API,使得我们可以直接在 C 代码中与 JavaScript 对象进行交互。使用 NAPI 可以提高插件的可移植性,并减少因 Node.js 版本变化而引起的代码修改。
  1. 引入三方库
  • 为了在 Node.js 中引入三方库,我们需要先将该库的头文件和库文件进行编译。以 CMake 为例,我们可以使用以下 CMakeLists.txt 文件来编译一个名为”mylib”的三方库:
1
2
3
4
5
cmake_minimum_required(VERSION 3.10)
project(mylib)
set(SOURCE_FILES mylib.cpp)
add_library(mylib SHARED ${SOURCE_FILES})
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
  • 在 CMakeLists.txt 文件中,我们首先指定了项目的名称为”mylib”,然后指定了源文件为”mylib.cpp”,这是我们自己编写的库代码。接着,我们使用 add_library 命令来构建一个动态链接库,其中 SHARED 关键字表示构建的是一个共享库。最后,我们使用 target_include_directories 命令将当前源文件目录添加到库的头文件搜索路径中。
  • 编译完成后,我们将得到一个名为”mylib.so”的共享库文件。
  1. NAPI 引入库
  • 在 Node.js 插件的 C++代码中,我们可以使用 NAPI 提供的函数来导入和使用三方库。首先,我们需要包含”napi.h”头文件,并根据需要引入其他头文件。我们还需要使用 NAPI_MODULE_INIT 宏来初始化模块。
  • 以下是一个使用 NAPI 引入”mylib”库的示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <napi.h>
#include "mylib.h"
Napi::Value MyFunction(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// 调用mylib库中的函数
int result = mylib_function();
// 将结果转换为JavaScript值
Napi::Number jsResult = Napi::Number::New(env, result);
return jsResult;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "myFunction"),
Napi::Function::New(env, MyFunction));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
  • 在以上代码中,我们首先包含了”Napi.h”和”mylib.h”头文件。然后,我们定义了一个名为 MyFunction 的函数,它接受一个 Napi::CallbackInfo 对象作为参数,该对象提供了关于调用函数的信息。在函数中,我们调用了 mylib 库中的函数,并将结果转换为 JavaScript 值。最后,我们使用 NAPI_MODULE 宏来初始化模块,将 MyFunction 函数导出为 JavaScript 函数。
  1. 使用 NAPI 插件
  • 要在 Node.js 中使用我们创建的 NAPI 插件,我们首先需要将插件编译为共享库。然后,我们可以使用 require 函数来引入插件并调用其中的函数。
  • 以下是一个使用我们创建的 NAPI 插件的 Node.js 代码示例:
1
2
3
const addon = require('./build/Release/mylib');
const result = addon.myFunction();
console.log(result); // 输出mylib库中函数的返回值
  • 在以上代码中,我们使用 require 函数引入了我们创建的插件。然后,我们调用了插件中导出的 myFunction 函数,并将结果打印到控制台。