HarmonyOS:使用Node-API实现ArkTS与C/C++跨语言交互
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤GitHub地址:https://github.com/strengthen
➤如果链接不是为敢技术的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
(1)、系统可以将框架层丰富的模块功能通过ArkTS/JS接口开放给上层应用。
(2)、应用开发者也可以选择将一些对性能、底层系统调用有要求的核心功能用C/C++封装实现,再通过ArkTS/JS接口使用,提高应用本身的执行效率。
在日常开发中,经常会有一些网络通信、串口访问、多媒体解码、传感器数据收集等模块,这些模块大部分都是用C++接口实现的,ArkTS侧如果想使用这些能力,就需要用Node-API这这样一套接口去桥接C++代码。下面这张图是Node-API整体的架构图。
1、ArkTS Native Module:是提供给开发者使用Node-API开发的模块,主要用于实现Native侧业务逻辑。
2、Node-API层:定义ArkTS与C/C++交互的逻辑。Node-API这部分接口是基于node.js的一个扩展,所以我们平常在开发中也可以参考node.js官网,像接口实现的功能,入参等都是类似的。
3、中间三个模块是Node-API的功能模块。
(1)、ModuleManager: 是管理对象的模块,这是比较重要的,当ArkTS侧调用C++时,会加载Native侧的模块到ModuleManager,并转化成ArkTS对象返回上层。
(2)、ScopeManager:用于管理 napi-value 生命周期,napi-value是Node-API独特的数据类型,类似于ArkTS中的number、string等各种数据类型的统一表示形式,在Native侧代码开发中不需要感知不同的数据类型,统一都是napi_value。
(3)、ReferenceManager:用于管理引用,开发时经常会有一些跨线程的场景,这个时候就需要创建引用(即napi_ref),否则就会被GC垃圾回收掉。napi_ref用于指向 napi_value,允许用户管理 napi_value 值的生命周期。
4、Native Engine:作用主要是统一ArkTS引擎在Node-API层的接口行为。Native引擎主要用于支撑Node-API接口定义实现,以及封装方舟运行时暴露的接口,以便Node-API不感知方舟运行时的差异变化。
5、ArkCompiler ArkTS Runtime:方舟运行时,也就是ArkTS引擎,整个Node-API模块都是跑在方舟运行时的。
(2)、napi_value:Node-API独特的数据类型,在Native侧代码中,表示一个ArkTS类型值,类似于ArkTS中的number、string等各种数据类型的统一表示形式。在Native侧代码开发中不需要感知不同的数据类型,统一都是napi_value。
举例说明:声明一个napi_value类型的变量napiResult,然后通过调用napi_create_double函数,给napiResult赋值。
napi_value napiResult; // 给napi_ value 类型参数赋值,这个赋了一个double类型数值2.0 napi_create_double(env,2.0, &napiResult);
(3)、napi_create_string_utf8:通过UTF8编码的C字符串数据创建ArkTS侧string类型的数据。下图示例中所示,先声明了一个string类型的字符串str,然后声明一个napi_value类型对象sum,用于存储转换后的ArkTS侧string类型数据,最后通过napi_create_string_utf8接口进行数据的转换,并将转换结果存到sum中,这个sum可以用于返回到ArkTS侧使用。napi_create_string_utf8的第三个参数传入的是str.length,表示字符串最大转换长度,当被转换的字符串长度超过这个最大转换长度时,字符串将会被截断。
五、使用Node-API实现跨语言交互开发流程
1、通过一个案例了解Node-API实现跨语言交互的开发流程。如下图,左边展示的是案例的界面显示,右侧是该案例的交互流程图,
案例介绍:用户输入两个number类型的数值X、Y。点击计算结果,按钮会在结果栏下显示一个计算结果。针对本案例,具体交互流程,如下图右侧所示:
当用户点击计算结果按钮时,会触发Native侧函数的调用。传递参数信息到Native侧,就是刚刚提到的用户输入的两个参数,Native侧获取到ArkTS侧传递过来的参数后,通过Node-API接口将参数转换成C++类型的参数,接着调用对应的C++业务代码进行数据处理,计算出结果,然后将结果转化为ArkTS侧类型的参数,最终将结果传递回ArkTS侧显示。
2、在DevEco Studio中File >New > Create Project,选择Native C++模板,点击Next,选择API版本,设置好工程名称,点击Finish,创建得到新工程。创建工程后工程结构可以分两部分,cpp目录部分和ets目录部分,具体可见下文的工程目录介绍。刚开始接触Node-API的时候,Native C++模版可以作为一个很好的切入点,一些开发Node-API所需要关注的点,或者说整体的代码框架已经帮开发者搭建好了。所需要的就是往里面填充业务逻辑。
3、构建一个工程并做介绍:
(1)、在DevEco Studio中File >New > Create Project。
(2)、选择Native C++模板,点击Next。
(3)、选择API版本,设置好工程名称,点击Finish。
(4)、创建工程后,可以看到工程结构可以分为两个部分:一部分在cpp目录下,一部分在ets目录下。
(5)、cpp目录里面,types目录下的文件介绍。
index.d.ets文件:里面定义了C++侧需要暴露在Ark TS侧的接口,后续调用C++侧函数时,其实就是调用这个里面定义的接口。
oh-package.json5文件:描述这个index.d.ts文件的。
CMakeList.txt文件:C++编译的配置文件,里面定义了编译的模块名,依赖的文件,包括最终生成的是静态库还是动态库,都是在CMakeList.txt文件里面配置的。
napi_init文件:最重要的文件,开发者要在里面做Native模块的注册、类型的转换,以及大部分业务逻辑,都是写在napi_init里面的。
(6)、ets目录,会默认创建一个index.ets的文件,这个文件里面会有一个ArkTS调用C++的示例。ArkTS侧的代码主要就在这个文件里面。
(7)、模块级build-profile.json5文件:主要是配置构建信息,其实主要就是配置了CMakeList文件的路径,构建的时候能找到CMakeList文件。
(8)、模块级的oh-package.json5文件:主要配置了一些模块本身的信息和依赖的工程。
4、如何逐步开发Node-API工程
(1)、基于Node-API开发业务功能:Native侧方法功能的实现,一般分为这几个步骤:参数提取,把ArkTS侧可用的对象类型napi_value转化为C++侧类型,比如number、string。然后执行C++侧业务逻辑,最后再把C++类型转化成ArkTS可用的anpi_value对象返回出去。
// hello.cpp
// 首先看一下参数,env表示Node-API执行时的上下文,info里面存储了ArkTS侧传递过来的参数信息。
static napi_value MyHypot(napi_env env, napi_callback_info info)
{
if ((nullptr == env) || (nullptr == info)) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "MyHypot", "env or exports is null");
return nullptr;
}
// Number of parameters.
// 案例是计算两个数的平方和的平方根,ArkTS侧需要传递两个参数到Native侧,所以这里需要接收两个参数,argc代表参数个数。
size_t argc = PARAMETER_COUNT;
// Declare parameter array.
// args[]数组是用于存储ArkTS参数的参数数组,
napi_value args[PARAMETER_COUNT] = { nullptr };
// Gets the arguments passed in and puts them in the argument array.
// 1、首先通过napi_get_cb_info函数,将info里面的参数信息获取到args[]数组中,
if (napi_ok != napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "MyHypot", "api_get_cb_info failed");
return nullptr;
}
// Converts arguments passed in to type double.
// 2、然后定义两个double类型的参数valueX、valueY
double valueX = 0.0;
double valueY = 0.0;
// 3、使用napi_get_value_double函数,将获取到args[]数组中的参数信息,转换成C++侧的double类型,并存储到valueX、valueY中。
if (napi_ok != napi_get_value_double(env, args[0], &valueX) ||
napi_ok != napi_get_value_double(env, args[1], &valueY)) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "MyHypot", "napi_get_value_double failed");
return nullptr;
}
// The hypot method of the C standard library is called to perform the calculation.
// 4、接着调用C标准库中的hypot()方法来执行计算。
double result = hypot(valueX, valueY);
napi_value napiResult;
// 5、最终通过napi_create_double函数,将计算结果存储到napi_value类型的参数napiResult中,
if (napi_ok != napi_create_double(env, result, &napiResult)) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "MyHypot", "napi_create_double failed");
return nullptr;
}
// 6、最后通过return napiResult,将结果返回到ArkTS侧使用。
return napiResult;
}
(2)、接口映射:ArkTS接口与C/C++接口的绑定和映射,这一过程是在初始化函数里面完成的,
EXTERN_C_START
// 针对本案例就是Init方法
static napi_value Init(napi_env env, napi_value exports)
{
if ((nullptr == env) || (nullptr == exports)) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Init", "env or exports is null");
return exports;
}
// 当前只定义了一个方法myHypot,这里其实是做了一个绑定,
napi_property_descriptor desc[] = {
// 第一个参数:"myHypot"是在ArkTS侧暴露的接口名,是小写myHypot,基于命名规范命名。
// 第三个参数是在C++侧对应的方法,是大写MyHypot,基于命名规范命名。
//本案例中只定义了一个myHypot方法,实际开发时可以在这里定义更多的方法映射,以便在ArkTS侧可以调用更多的C++侧方法。
{ "myHypot", nullptr, MyHypot, nullptr, nullptr, nullptr, napi_default, nullptr }
};
if (napi_ok != napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc)) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Init", "napi_define_properties failed");
return nullptr;
}
return exports;
}
EXTERN_C_END
(3)、模块注册:ArkTS侧导入模块时,会加载其对应的so,在加载so时,首先会调用napi_module_register方法,将Node-API模块注册到系统中,并调用模块初始化函数,即对应刚刚定义的Init方法。
static napi_module demoModule = {
.nm_version = 1, // 版本
.nm_flags = 0,
.nm_filename = nullptr,
// 重要,里面定义了模块初始化函数,这里定义的初始化函数就是上方的Init函数,初始化时会进行接口映射。
.nm_register_func = Init,
// 重要,里面定义模块的名称,也就是ArkTS侧引入的so库的名称。模块系统会根据此名称来区分不同的so.
// 例如此处定义的模块名称为hello。
// 相对应需要导入的so库名称将会是libhello.so
.nm_modname = "hello",
.nm_priv = ((void *)0),
.reserved = { 0 }
};
extern "C" __attribute__((constructor)) void RegisterModule(void)
{
// 在调用napi_module_register方法时,会传入一个demoModule对象,就是上面定义的demoModule结构体
napi_module_register(&demoModule);
}
(4)、模块构建配置:需要在CMakeList文件中配置一下编译信息。即在CMakeList.txt文件中配置CMake打包参数。CMakeList是标准的CMake构建配置项。在比较复杂的项目中,CMakeList要配置的项就会比较多。
# the minimum version of CMake.
# CMake最低版本
cmake_minimum_required(VERSION 3.4.1)
# project名称
project(NativeTemplateDemo)
# 就是把后面这个变量的值赋值给前面,就相当于起了个别名。
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# 需要放整个项目编译所用到的头文件目录。一般用默认配置即可。
# 会配置整个cpp目录和cpp下的include目录。
include_directories(
${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include
)
# find_library是查找hilog_ndk.z库的位置。会将其信息存储到变量hilog-lib中。以便后续添加依赖模块时使用。
# hilog-lib命令适用于开发者不清楚需要的依赖库的具体路径时
find_library(
# Sets the name of the path variable.
hilog-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
hilog_ndk.z
)
# add_library命令,构建编译产物。
# 第一个参数是编译的模块名称,这里写了hello,最后编译出来的就叫做libhello.so。
# 第二个参数SHARED表示是动态库。
# 第三个参数需要配置参与编译的所有cpp文件。
# 也支持编译静态库,如果把SHARED改成STSTIC, 编译出来的就是静态库文件。
add_library(hello SHARED hello.cpp)
# target_link_libraries定义开发者需要编译当前模块需要依赖哪些模块。
# 比如当前案例需要依赖NAPI的能力和一些底层C++能力。就需要制定NAPI的库和libc++的库。
target_link_libraries(hello PUBLIC ${hilog-lib} libace_napi.z.so libc++.a)
上面已经配置了CMake参数信息,为什么构建工程的时候会编译C/C++模块呢?原因是在module级别的build-profile.json5文件中,指定了CMakeList文件的位置。也就是说CMakeList文件可以被放在任何目录。只要我们在build-profile.json5文件中配置的路径能够被找到即可。
(5)、导出Native接口:
// index.d.ts
// 标准的TS接口定义文件,定义C/C++侧暴露给ArkTS侧的接口,并将其导出。
// 本案例定义了一个myHypot方法,即上文中映射到ArkTS侧的方法。
// 该方法需要传递两个number类型的参数,并且最终会返回一个number类型的数据。
export const myHypot: (a: number, b: number) => number;
接口声明完成后,需要在模块级oh-package.json5文件中指定dependencies。就是ArkTS侧所依赖的C++侧的index.d.ts文件的路径。这里一般不需要开发者操作,在创建Native工程时会自动生成。
5、回到ArkTS侧,首选需要import一个so。
import libHello from 'libhello.so';
需要强调的是,'libhello.so'的这个so的名称,和上述Init方法里面的名称,以及CMakeList里面addlibrary的名称,这三处需要保持一致,否则会出现一些不可避免的问题。导入完so库,就可以调用C++侧的代码了。通过libHello调用myHypot方法,该方法已经做过与Native侧的MyHpot方法的映射,所以可以直接调用到Native侧的MyHpot方法进行结果的计算。
// entry/src/main/ets/pages/Index.ets // 通过import的方式,引入Native能力。 import nativeModule from 'libentry.so' @Entry @Component struct Index { @State message: string = 'Test Node-API callNative result: '; @State message2: string = 'Test Node-API nativeCallArkTS result: '; build() { Row() { Column() { // 第一个按钮,调用add方法,对应到Native侧的CallNative方法,进行两数相加。 Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick(() => { this.message += nativeModule.callNative(2, 3); }) // 第二个按钮,调用nativeCallArkTS方法,对应到Native的NativeCallArkTS,在Native调用ArkTS function。 Text(this.message2) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick(() => { this.message2 += nativeModule.nativeCallArkTS((a: number)=> { return a * 2; }); }) } .width('100%') } .height('100%') } }
结果演示,源码下载:NativeTemplate.zip

HarmonyOS:使用Node-API实现ArkTS与C/C++跨语言交互更多相关文章
随机推荐
2024/11/1日 日志 关于JavaScript简介&引入方式 以及基础语法的学习
舍得-时间-工作是人的一生最重要的事情-自己要有私房钱-人的一生最重要的事情是书写自己的人生
ubuntu 24.04 部署 mysql 8.4.3 LTS
国标GB28181公网平台LiteGBS国标GB28181视频平台建筑工地无线视频联网监控系统方案
BFS(Breath First Search 广度优先搜索)
Visual Studio Code(VSCode)中设置中文界面
winform用Dev的TreeList滚动到指定节点的位置