一个关于HarmonyOS应用研发的专栏,实践中见证你的实力提升
背景
公司内部已经有现成的MQTT动态库,想在HarmonyOS平台上共享使用。查找官方指导后,发现可以通过NAPI方式,将MQTT C++库导入进来,然后封装一层ArkTS接口就可直接使用。
本篇内容是在按照官方指导下,自己做的一些调研性质的实践。
阅读完成后
将学会如何在已有的HarmonyOS应用工程中完成C++文件的添加以及TS与C++的交互
希望大家不用碰到类似的项目需求
NAPI简介
NAPI(Native API)组件是一套对外接口基于Node.js N-API规范开发的原生模块扩展开发框架
在HarmonyOS中,C API中的N-API接口可以实现ArkTS/TS/JS与C/C++之间的交互。N-API提供的接口名与三方Node.js一致,目前支持部分接口。
关于详细HarmonyOS 中的N-API开发教程,详见官方指导
关于Node.js的Node-API开发教程,详情官方指导
环境条件
- DevEco Studio 3.1.1 Release
- HarmonyOS SDK API 9
效果图
功能演示
- ets文件获取C++生成的JSON对象
- ets文件获取C++生成的数组
- 引入第三方源码,并且使用其API【这里引用的国密算法SM4的实现】
- ets获取C++的运算结果
- ets实现回调方法, C++触发ets回调方法
实践步骤
备注:以添加 源码 形式演示
简易目录结构
粉色为添加和改动部分
1. entry模块添加文件
- src/main 目录下创建“cpp”文件夹【名字必须为cpp】
- src/main 目录下创建主入口文件"main.cpp"【名字可以随意命名】
- src/main 目录下创建"CMakeLists.txt"文件【内容见 “片段1”】
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.4.1)
project(HarmonyLearn)
# 设置源码文件目录
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# 设置 .h 文件目录
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
# 设置自己的动态名称 和 对应的编译文件
add_library(customnapi SHARED main.cpp
sm4.cpp
util.cpp)
# 设置链接自己的动态库与其它动态库
target_link_libraries(customnapi PUBLIC libace_napi.z.so)
2. 配置entry模块
entry模块中的build-profile.json5文件增加native选项【内容见 “片段2”】
...
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
},
...
}
...
3. ets引用so
在需要使用到"customnapi"库的文件中,通过import方式来进行引用
注意 虽然我们自己定义的库名称为"customnapi", 但是引用的时候应该为"libcustomnapi.so", 注意是以"lib"为前缀
import customnapi from 'libcustomnapi.so'
...
customnapi.passStr(...)
...
经过这三步,即可完成C++源码在HarmonyOS应用工程中的添加使用
示例解读
main.cpp
这个文件在本示例中用来注册C++源码模块使用的,文件名称可任意修改,因为其最终会被CMakeLists.txt文件引用,所以其文件名称无关紧要
这个文件主要分为四部分
- so api注册
- so模块说明
- napi接口描述
- C++函数定义
so注册,即在ets文件被加载时,触发import动作,然后会通过系统机制自动执行 napi_module_register 代码
so模块说明,即对so 模块的描述,包含NAPI接口描述入口,so模块名称,版本等
so api注册, 即ets函数名称和NAPI 函数名称的映射关系。比如,ets中调用add,因为有这里的映射,所以执行的是C++文件中的Add函数。
C++函数定义,即so库对外暴露的接口实现,比如 static napi_value Add(napi_env env, napi_callback_info info)
......
static napi_value Add(napi_env env, napi_callback_info info) {
......
}
......
// Init将在exports上挂上Add/NativeCallArkTS这些native方法,此处的exports就是开发者import之后获取到的ArkTS对象。
static napi_value Init(napi_env env, napi_value exports) {
// 函数描述结构体,以Add为例,第三个参数"Add"为上述的native方法,
// 第一个参数"add"为ArkTS侧对应方法的名称。
napi_property_descriptor desc[] = {
{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
......
return exports;
}
// 准备模块加载相关信息,将上述Init函数与本模块名等信息记录下来。
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "customnapi",
.nm_priv = ((void *)0),
.reserved = {0},
};
// 打开so时,该函数将自动被调用,使用上述demoModule模块信息,进行模块注册相关动作。
extern "C" __attribute__((constructor)) void RegisterModule(void) {
napi_module_register(&demoModule);
}
C++解析TS方法的参数
例如:Add函数
// 期望从ArkTS侧获取的参数的数量,napi_value可理解为ArkTS value在native方法中的表现形式。
//TS函数有2个参数
size_t argc = 2;
//创建一个包含2个元素的TS参数值数组
napi_value args[2] = {nullptr};
// 从info中,拿到从ArkTS侧传递过来的参数,此处获取了两个ArkTS参数,即arg[0]和arg[1]。
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 将获取的ArkTS参数转换为native信息,此处ArkTS侧传入了两个number,这里将其转换为native侧可以操作的double类型。
//解析TS函数中的第一个参数
double value0;
//声明于 js_native_api.h 文件,属于HarmonyOS SDK里边的一部分
napi_get_value_double(env, args[0], &value0);
//解析TS函数中的第二个参数
double value1;
//声明于 js_native_api.h 文件,属于HarmonyOS SDK里边的一部分
napi_get_value_double(env, args[1], &value1);
C++获取TS方法中的字符串类型参数长度
例如:passStr函数
static napi_value passStr(napi_env env, napi_callback_info info) {
//TS函数有1一个参数
size_t argc = 1;
//创建一个包含1个元素的TS参数值数组
napi_value args[1] = {nullptr};
// 从info中,拿到从ArkTS侧传递过来的参数,此处获取了一个ArkTS参数,即arg[0]。
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
//获取字符串参数的长度
size_t typeLen = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &typeLen);
//创建存储字符串的数组,数组大小为“字符串长度+1”, 1代表字符串结束符
char *extContent = new char[typeLen + 1];
//解析TS的字符串参数,将内容赋值到char数组中
napi_get_value_string_utf8(env, args[0], extContent, typeLen + 1, &typeLen);
napi_value result;
//将字符串赋值给napi_value
napi_create_string_utf8(env, extContent, typeLen + 1, &result);
//释放数组空间
delete[] extContent;
//返回结果
return result;
}
C++返回数组类型
例如:generatorMockArray函数
static napi_value generatorMockArray(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
//解析TS参数值,这里的值代表需要初始化的数组容量
int result;
napi_get_value_int32(env, args[0], &result);
napi_value ret;
// 创建返回类型为数组的对象
napi_create_array(env, &ret);
const int arrayLength = 4;
const char *target[arrayLength] = {"Blue", "Red", "Orange", "Yellow"};
// 根据TS入参设置数组的初始值
for (int i = 0; i < result && i < arrayLength; i++) {
napi_value tempValue;
//创建字符串类型的tempValue对象,把字符串赋值给tempValue
napi_create_string_utf8(env, target[i], strlen(target[i]),&tempValue);
// 数组中添加元素
napi_set_element(env, ret, i, tempValue);
}
return ret;
}
C++返回Object类型对象
例如:getCustomObject函数
/**
* 获取mock对象
*
* 格式
* {
* "testNumber" : 123,
* "testString" : "welcome",
* "testNested" : { "child" : "welcome"}
* }
*
* @param env
* @param info
* @return
*/
static napi_value getCustomObject(napi_env env, napi_callback_info info) {
//创建一个数字类型的对象: value_number
napi_value value_number;
napi_create_int32(env, 123, &value_number);
//创建一个字符串类型的对象: value_string
char *contentStr = "welcome";
napi_value value_string;
napi_create_string_utf8(env, contentStr, strlen(contentStr), &value_string);
//创建一个Object类型的对象:childObj
napi_value childObj;
napi_create_object(env, &childObj);
//childObj对象添加一个名为“child”的属性,其值为value_string
napi_set_named_property(env, childObj, "child", value_string);
//创建一个返回类型为Object的对象: obj
napi_value obj;
napi_create_object(env, &obj);
//obj对象添加一个名为“testNumber”的属性,其值为value_number
napi_set_named_property(env, obj, "testNumber", value_number);
//obj对象添加一个名为“testString”的属性,其值为value_string
napi_set_named_property(env, obj, "testString", value_string);
//obj对象添加一个名为“testNested”的属性,其值为childObj
napi_set_named_property(env, obj, "testNested", childObj);
return obj;
}
CMakeLists.txt
注释来源:www.51cto.com/article/751…
Cmake教程: cmake.org/cmake/help/…
# cmake 最小版本
cmake_minimum_required(VERSION 3.4.1)
# 工程名称:HarmonyLearn
project(HarmonyLearn)
# set命令,格式为set(key value),表示设置key的值为value,其中value可以是路径,也可以是许多文件。
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# 添加项目编译所需要的头文件的目录
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
# 生成目标库文件customnapi.so,customnapi表示最终的库名称,SHARED表示生成的是动态链接库,
# main.cpp, sm4.cpp, util.cpp 表示最终生成的libcustomnapi.so中所包含的源码
# 如果要生成静态链接库,把SHARED该成STATIC即可
add_library(customnapi SHARED main.cpp
sm4.cpp
util.cpp)
# 把libcustomnapi.so链接到libace_napi.z.so上
target_link_libraries(customnapi PUBLIC libace_napi.z.so)
其它相关文章
- www.arkui.club/chapter16/1…
- juejin.cn/post/734090…
- www.51cto.com/article/751…
- laysent.com/blog/post/m…
- blog.csdn.net/qq_17432441…
- zhuanlan.zhihu.com/p/576370605
结尾
如果平时不太写C/C++的情况下,上手写NAPI,还是比较困难的,相当于HarmonyOS新手不知道如何展示一个文字一样。
这种开发模式在大多数公司都用不到,即使用到了,也不会采用全量源码编译接入方式,所以稍微看看就可以了。
祝好运!