简介
👋NAPI是什么?
NAPI是Node-API曾用名。是HarmonyOS中提供ArkTS/JS与C/C++跨语言调用的接口,是NDK接口中的一部分。
NDK(Native Development Kit)是HarmonyOS SDK提供的Native API、相应编译脚本和编译工具链的集合,方便开发者使用C或C++语言实现应用的关键功能。
💡 简而言之,NAPI提供了ArkTs/js调用C/C++代码和C/C++调用ArkTs/js代码的能力。
为什么要用NAPI调用C++代码?
- 性能敏感的场景,如游戏、物理模拟等计算密集型场景。
- 需要复用已有C或C++库的场景。
- 需要针对CPU特性进行专项定制库的场景,如Neon加速。
NAPI怎么用?
对于ArkTs侧来说,主要应该学习以下的四个部分:
- napi_init.cpp
- index.d.ts
- CMakeLists.txt
- 特殊的,图像绘制:XComponent对于napi的使用
用法剖析
📌 由一个小例子切入
🍒 1. 一个简单的C++类
这里只有声明没有实现。 C++的类,一般分为两部分,.h文件(头文件)负责声明,.cpp文件负责实现。
// calculator.h
class Calculator {
public:
int Add(int a, int b);
int Sub(int a, int c);
int Mul(int a, int c);
};
🍒 2. C++插件类
对外暴露出api方法,既封装上述的
Calculator类中需要对外暴露出来的方法。
🔑
这个类是ArkTs侧需要最看懂的C++类
其实,上述的C++类都是C++侧定义实现好的,其中的逻辑不需要过度关注。
一般为单例类
// plugin_calculator.h
class PluginCalculator:
public:
// 下面三个方法是对Calculator类三个方法的封装
static napi_value NapiAdd(napi_env env, napi_callback_info info);
static napi_value NapiSub(napi_env env, napi_callback_info info);
static napi_value NapiMul(napi_env env, napi_callback_info info);
// 在C++的插件类里面,一般都有一个Export方法。
// 这个方法会定义需要暴露给napi的方法
static void Export(napi_env env, napi_value exports);
⭐ 简要分析一个NapiAdd这个函数的写法。其他更复杂的函数写法可以举一反三,触类旁通。
其中,第4步是逻辑核心~这里调用了对象持有的Calaulator实例的方法
// plugin_calculator.cpp
napi_value PluginCalculator::NapiAdd(napi_env env, napi_callback_info info) {
size_t requireArgc = 2;
size_t argc = 2;
// 1. 定义参数列表
napi_value args[2] = {nullptr};
// 2. 校验参数个数是否正确
if (napi_get_cb_info(env, info, &argc, args, nullptr, nullptr) != napi_ok) {
return nullptr;
}
// 3. 获取a,b参数的值
int valueA, valueB;
napi_get_value_int32(env, args[0], &valueA);
napi_get_value_int32(env, args[1], &valueB);
// 4. 调用api的方法计算结果,这里调用对象中的Calaulator实例的Add方法
int res = this->calculator.Add(valueA, valueB);
// 5. 定义返回值,并赋值
napi_value sum;
napi_create_int32(env, res, &sum);
// 6. 返回结果
return sum;
}
👉 再主要分析一个Export函数:
核心就是下面的一个数组的定义。
// plugin_calculator.cpp
void PluginCalculator::Export(napi_env env, napi_value value) {
// ...
// 描述需要暴露出来的api函数,并定义在ArkTs侧的函数名。
napi_property_descriptor desc[] = {
// 第一个参数是ArkTs中的函数名
// 第三个参数是实际调用的C++的函数
// 其他参数照写
{"add", nullptr, PluginCalculator::NapiAdd, nullptr, nullptr, nullptr, napi_default, nullptr},
{"sub", nullptr, PluginCalculator::NapiSub, nullptr, nullptr, nullptr, napi_default, nullptr},
{"mul", nullptr, PluginCalculator::NapiMul, nullptr, nullptr, nullptr, napi_default, nullptr},
};
// ...
}
🍒 3. 在napi_init文件中声明
将上述的定义的api接口暴露给ArkTs侧。
🔔 这里的napi_init.cpp文件名是自己自由定义的。只要写入CMakeLists.txt中即可。关于CMakeLists.txt的书写在随后会有详解。
📌 示例的napi_init.cpp
核心也只是
Init函数的一行代码。
// napi_int.cpp
// ...
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {/*...*/};
if (napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc) != napi_ok) {
OH_LOG_ERROR(LOG_APP, "Init", "napi_define_properties failed");
return nullptr;
}
// ...
PluginCalculator::Export(env, exports); // 关键也就是这一行
// 把刚才写好的Export方法调用一下就行了
// p->EXport()
// ...
return exports;
}
EXTERN_C_END
// ...
⭐ 其实很简单,把刚才写好的Export方法调用一下就行了~
🍒 4. 编写index.d.ts文件——关键的一步
👋
刚才我们把CPP接口定义实现,并且暴露出来了。
但是ArkTs怎么调用呢?这是关键的一步——ArkTs函数定义
在ArkTs侧直接调用的接口,就是在这一步声明的。
其实很简单~在cpp/types/index.d.ts文件中定义
// index.d.ts
export const add: (a: number, b: number) => number;
export const sub: (a: number, b: number) => number;
export const mul: (a: number, b: number) => number;
❗需要注意的是: 这里定义的函数名,比如
add,sub等,必须是在上面napi_init文件中定义过的
在我们这个例子里面,就是在PluginCalculator::Export函数的列表中定义过的函数。
⚠️ 函数名一定要完全一样。参数类型和参数个数,以及返回值类型一定要对应上。否则会编译失败。
🍒 5. ArkTs使用
⚠️ 注意,cpp/types/.../目录下面(即index.d.ts的同一级目录)的oh-package.json5
{
"name": "libcal.so",
"types": "./index.d.ts",
"version": "",
"description": "Please describe the basic information."
}
👉 定义了:
-
ArkTs类型定义文件,既index.d.ts
-
编译后的so库名。比如这里叫libcal.so
📌 使用方法:
import { add, sub, mul } from 'libcal.so'; // 导入
// ...
const res1 = add(1, 2); // 然后直接使用即可
const res2 = sub(2, 1);
const res3 = mul(2, 2);
// ...
💡 深入思考
📌 在napi中我们需要暴露出C++的函数作为ArkTs调用的接口。
那么这个函数就有以下几种情况:
- 普通函数
- 类实例方法
- 类静态方法
- 异步函数?
考虑一下更加复杂的情况,函数的参数类型、返回值类型为:
- 基本类型
- 对象
- 函数
- 异步函数
方法function
普通方法和类静态方法
👋关于普通方法和类静态方法,转换调用都是一样的
// 普通方法
void foo(string str);
// 类静态方法
class FooClass:
public:
static void Foo(string str);
👉在上述小例子的第2步的Export方法中,更改为
napi_property_descriptor desc[] = {
// ...
// 普通方法
{"foo1", nullptr, foo, nullptr, nullptr, nullptr, napi_default, nullptr},
// 类静态方法
{"foo2", nullptr, FooClass::Foo, nullptr, nullptr, nullptr, napi_default, nullptr},
};
📌 XComponent
🔔 是什么?
XComponent组件是绘制的载体。通常用于满足开发者较为复杂的自定义绘制需求,例如相机预览流的显示和游戏画面的绘制。
Native XComponent是XComponent组件提供在Native层的实例,可作为JS层和Native层XComponent绑定的桥梁。XComponent所提供的的NDK接口都依赖于该实例。
怎么用?
ArkTs 做什么?
- 定义XComponent组件UI。
- 在合适的时机,通过napi调用其他C++的接口。
C++ 做什么?
-
注册XComponent的生命周期回调、注册XComponent的触摸、鼠标、按键等事件回调。
-
在这些回调中进行初始化环境、获取当前状态、响应各类事件的开发。
-
利用Native Window和EGL接口开发自定义绘制内容以及申请和提交Buffer到图形队列。
🎉 ArkTs侧
📌 ArkTs侧关于XComponent的使用很简单~
因为核心绘制的逻辑都在C++侧实现了!
我们需要做的事情就只是定义好napi接口,定义XComponent组件的UI,在合适的时机调用C++接口即可。极大地简化了前端的开发,前端甚至可以不用管绘制、手势之类的操作。
ArkTs侧在定义XComponent组件时,只需要定义一个id,指定正确的C++的so库名称即可。
只要C++侧代码没有错误,绘制等操作就不会有问题。
👇 下面是一个简单的在XComponent上绘制图形的例子,ArkTs侧:
@Component
export struct DrawingDemo {
// ...
@State xComponentContext: XComponentContext | undefined = undefined;
build() {
// ...
XComponent({
id: 'xcomponentId1', // 自定义的id
type: 'surface',
libraryname: 'librender', // Native XComponent所在的模块编译出的so库名
})
.onLoad((xComponentContext: XComponentContext) => {
this.xComponentContext = xComponentContext
})
.onDestroy(() => {})
// ...
Button('绘制')
.onClick(() => {
this.xComponentContext?.drawPattern() // cpp编写好的,只管调用,无需关心其逻辑
})
// ...
}
}
关于XComponentContext接口
📌 XComponent提供了调用napi接口更加简便的一种方法
不需要在index.d.ts中定义导出的函数
可以直接定义一个ts的接口。在XComponent在onLoad时会自动在so库中寻找定义的函数
⚠️ 如果so库中不存在context接口定义的函数名或者函数的参数或返回值不对,都会导致编译时错误。这个请参考用法剖析的小例子。
👇 示例的接口定义如下:
export interface XComponentContext {
drawPattern: () => void; // 对应napi导出的函数,与第2章例子index.d.ts中类似
}
C++侧
💡
C++侧的代码不多阐述了
这里只简要解释一下关于XComponent的生命周期和事件的代码。
在Native XComponent中,即C++侧关于XComponent的代码中,会定义多个生命周期函数以及各类事件监听
// 定义事件和监听
void OnSurfaceCreatedCB(OH_NativeXComponent *component, void *window) {
// ...
}
void OnSurfaceChangedCB(OH_NativeXComponent *component, void *window) {
// ...
}
void OnSurfaceDestroyedCB(OH_NativeXComponent *component, void *window) {
// ...
}
void DispatchTouchEventCB(OH_NativeXComponent *component, void *window) {
// ...
}
void DispatchMouseEventCB(OH_NativeXComponent *component, void *window) {
// ...
}
void DispatchHoverEventCB(OH_NativeXComponent *component, bool isHover) {
// ...
}
void OnFocusEventCB(OH_NativeXComponent *component, void *window) {
// ...
}
void OnBlurEventCB(OH_NativeXComponent *component, void *window) {
// ...
}
void OnKeyEventCB(OH_NativeXComponent *component, void *window) {
// ...
}
// 注册事件和监听
void KMPPluginManager::RegisterCallback(OH_NativeXComponent * nativeXComponent) {
mRenderCallback.OnSurfaceCreated = OnSurfaceCreatedCB;
mRenderCallback.OnSurfaceChanged = OnSurfaceChangedCB;
mRenderCallback.OnSurfaceDestroyed = OnSurfaceDestroyedCB;
mRenderCallback.DispatchTouchEvent = DispatchTouchEventCB;
OH_NativeXComponent_RegisterCallback(nativeXComponent, &mRenderCallback);
mMouseCallback.DispatchMouseEvent = DispatchMouseEventCB;
mMouseCallback.DispatchHoverEvent = DispatchHoverEventCB;
OH_NativeXComponent_RegisterMouseEventCallback(nativeXComponent, &mMouseCallback);
OH_NativeXComponent_RegisterFocusEventCallback(nativeXComponent, OnFocusEventCB);
OH_NativeXComponent_RegisterKeyEventCallback(nativeXComponent, OnKeyEventCB);
OH_NativeXComponent_RegisterBlurEventCallback(nativeXComponent, OnBlurEventCB);
}
🔔
这些生命周期函数和事件监听函数,其内部实现我们无需关注
只需要知道,在每个XComponet组件onLoad时,会自动执行对应的so库的api_init.cpp文件,并且注册这些函数。
此后的事情都是在C++代码里面发生的,我们只需管好前端的UI就好。
CMake
是什么?
💡
CMake是什么?
CMake是C++一个跨平台的构建工具,用于管理和构建软件项目。
类似前端的webpack和vite,Java的Maven和Gradle。
CMakeLists.txt是什么?
CMakeList是CMake构建系统中的文件,用于描述项目的结构和构建规则。
它通过编写一系列的命令和指令来定义项目的构建过程,实现项目的模块化管理和可扩展构建。
💡 怎么用?
👋
怎么用?写CMakeLists.txt就行了~
CMakeLists.txt 的一个例子
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.4.1)
project(harmony_demo_napi_drawing)
# 路径别名
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(WPS_CODING_DIR "D:/HarmonyWPS_Core_Code/wps/Coding")
# 指定头文件目录
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include
${NATIVERENDER_ROOT_PATH}/plugin)
# 本地cpp源码
add_library(drawing SHARED
napi_init.cpp
plugin/plugin_manager.cpp
plugin/plugin_render.cpp
plugin/sample_bitMap.cpp
)
# harmony官方库
target_link_libraries(drawing PUBLIC
libace_napi.z.so # 都是官方NDK的so库
libace_ndk.z.so
libnative_window.so
libnative_drawing.so
hilog_ndk.z.so
)
👻 链接so库
⭐ 官方so库
👋
如果需要打印日志,要使用这个函数
OH_LOG_Print那就要添加鸿蒙的官方的 so库了~
先添加hilog这个so库,一共有3个步骤。
👇步骤如下:
👉 1. 直接在CMakeList.txt文件中的target_link_libraries添加
# harmony官方库
target_link_libraries(drawing PUBLIC # 注意这个drawing是你自己命名的库名,要保持上下文一致
# ...
hilog_ndk.z.so # 在官方文档中查询,需要这个so库,直接添加之~
# ...
)
或者,先起个别名 , 再链接
# ...
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 # 在官方文档中查询,需要这个so库,直接添加之,注意后缀去掉.so
)
# ...
# harmony官方库
target_link_libraries(drawing PUBLIC
# ...
${hilog-lib} # 添加这个别名
# ...
)
👉 2. 检查是否导入了官方的头文件目录,即是否有下面的两行
# ...
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# ...
include_directories(${NATIVERENDER_ROOT_PATH}
# ...
)
👉 3. 在需要使用的地方,添加hilog的头文件
#include <hilog/log.h>
🎉 就可以使用打印日志函数了~
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, TAG, "OnSurfaceCreated");
⭐ 本地so库
💡
如果别人写了C++代码,编译好后给你一个so库,你拷贝到你项目目录下
怎么使用这个so库里面的函数呢?
📢 首先,
假设你把这个so库放在这里 ~/Project/lib/mylib.so
假设你把头文件放在~/Project/include/mylib下面
📌 写一个CMakeList.txt的例子:
# ...
# 1. 给本地so库的目录和头文件目录起个别名
set(MY_LIB "~/Project/lib")
set(MY_INCLUDE "~/Project/include")
# ...
# 2. 指定头文件目录
include_directories(
# ...
${MY_INCLUDE}
# ...
)
# add_library(drawing SHARED init.cpp)
# 3. 链接到这个so库的目录
target_link_directories(drawing PUBLIC
# ...
${MY_LIB}
# ...
)
# ...
# 4. 链接到这个库
target_link_libraries(drawing PUBLIC
# ...
mylib.so # 那么程序会在你链接的目录下自动寻找这个库
# ...
)
🎉 然后就可以使用这个so库里面的方法了~
#include<mylib/api.h>
Foo(); // 假设是api.h里面的一个函数
👻 编译本地Cpp代码
📌
怎么用CMake编译你写好的cpp代码呢?
👇步骤如下:
📢 首先
假设你的鸿蒙项目cpp代码放在目录 -> src/main/cpp/plugin
而CMakeList.txt文件在 -> src/main/cpp/CMakeList.txt
📌 编写CMakeList.txt文件
# ...
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# ...
# 1. 首先,把你的.h头文件目录加进来
include_directories(
# ...
${NATIVERENDER_ROOT_PATH}/plugin # 头文件所在的目录
# ...
)
# ...
# 2. 把cpp文件加进来编译,哪些要用就加哪些
add_library(drawing SHARED # 打包后叫做libdrawing.so
napi_init.cpp # 这是napi的入口文件,名字不用管
plugin/plugin_manager.cpp # 假设是plugin下面的cpp文件
plugin/plugin_render.cpp # 同上
)
# 3. 最后记得把代码里面所用到的so库,链接进来(包括官方so库和本地so库)
target_link_libraries(drawing PUBLIC # 保持drawing名字与上面一样
libace_napi.z.so
libnative_window.so
# ...
)
🎉 现在就可以编译了~
编译完就会有一个libdrawing.so文件,.so为linux下的动态链接库
windows下的动态链接库后缀为
.ddl