HarmonyOS NAPI入门

1,255 阅读2分钟

一个关于HarmonyOS应用研发的专栏,实践中见证你的实力提升

HarmonyOS 专栏

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

效果图

功能演示

  1. ets文件获取C++生成的JSON对象
  2. ets文件获取C++生成的数组
  3. 引入第三方源码,并且使用其API【这里引用的国密算法SM4的实现
  4. ets获取C++的运算结果
  5. ets实现回调方法, C++触发ets回调方法
Screenshot_20240312155256471.png

实践步骤

备注:以添加 源码 形式演示

简易目录结构

粉色为添加和改动部分

WX20240312-161812@2x.png

1. entry模块添加文件

  1. src/main 目录下创建“cpp”文件夹【名字必须为cpp】
  2. src/main 目录下创建主入口文件"main.cpp"【名字可以随意命名】
  3. 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文件引用,所以其文件名称无关紧要

这个文件主要分为四部分

  1. so api注册
  2. so模块说明
  3. napi接口描述
  4. 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.sotarget_link_libraries(customnapi PUBLIC libace_napi.z.so)

其它相关文章

  1. www.arkui.club/chapter16/1…
  2. juejin.cn/post/734090…
  3. www.51cto.com/article/751…
  4. laysent.com/blog/post/m…
  5. blog.csdn.net/qq_17432441…
  6. zhuanlan.zhihu.com/p/576370605

结尾

如果平时不太写C/C++的情况下,上手写NAPI,还是比较困难的,相当于HarmonyOS新手不知道如何展示一个文字一样。

这种开发模式在大多数公司都用不到,即使用到了,也不会采用全量源码编译接入方式,所以稍微看看就可以了。

祝好运!