3【鸿蒙/OpenHarmony/NDK】如何在鸿蒙应用中使用NDK?
各位码友们好!今天这篇干货主要聚焦实操细节,希望能帮大家少踩坑。
要是过程中遇到哪块没看懂、有疑问,或者你有更优的实现思路,评论区尽管聊!发现文档里有疏漏或错误也尽管指出来 ——
技术这东西就得互相挑刺才能越磨越精,咱们一起把这些知识点吃透~
前置学习
- 在【鸿蒙/OpenHarmony/NDK】C/C++开发教程之环境搭建我们学习了如何搭建NDK开发环境,同时我们运行了我们的第一个使用了NDK的应用。
- 在【鸿蒙/OpenHarmony/NDK】什么是NDK? 为啥要用NDK?我们知道了NDK的概念和用途。
- 这篇文章我们详细来看下我们之前运行的第一个使用了NDK的应用到底是如何实现的,我们一行一行代码来学习。
Hellow World详细解读
我们从上到下来看下NDK是如何被引入到应用中的。
import { hilog } from '@kit.PerformanceAnalysisKit';
import testNapi from 'libentry.so';const DOMAIN = 0x0000;@Entry
@Component
struct Index {@State message: string = 'Hello World';build() {Row() {Column() {Text(this.message).fontSize($r('app.float.page_text_font_size')).fontWeight(FontWeight.Bold).onClick(() => {this.message = 'Welcome';hilog.info(DOMAIN, 'testTag', 'Test NAPI 2 + 3 = %{public}d', testNapi.add(2, 3));})}.width('100%')}.height('100%')}
}
在ets中调用通过NDK实现的js接口
我们来看,如何在ets侧调用通过NDK实现的js接口。
- 首先
import testNapi from 'libentry.so';
这句是将我们js接口所在的so导进来,方便后续调用里面的接口。 testNapi.add(2, 3)
这句就是调用我们通过NDK实现的js接口add
,add
方法的返回值是number类型,返回2加3的结果5。
如何调用NDK实现js接口的我们已经讨论清楚了。下面我们在进一步看下,DevEco是如何知道我们的libentry.so中到底包含了哪些方法的?
- 如图1,我们在DevEco里面输入
testNapi.
的时候它能够自动联想出add方法,DevEco到底是如何知道libentry.so
中有add
方法的呢?
图1自动联想:
- 如图2,我们发现有一个
src/main/cpp/types/libentry/Index.d.ts
文件,它里面声明了add
方法。那么DevEco是怎么知道我们libentry.so
中的方法声明是在src/main/cpp/types/libentry/Index.d.ts
文件中的呢?
图2方法声明文件:
- 如图3,在项目的配置文件
oh-package.json5
里面,声明了一个依赖,说我这个项目依赖libentry.so
,和libentry.so
相关的配置在./src/main/cpp/types/libentry
目录下。 - 在
./src/main/cpp/types/libentry
目录下存在同名的一个配置文件oh-package.json5
,里面定义了libentry.so
的信息,其中就包括so对应的类型声明文件Index.d.ts
图3项目配置文件:
总结一下,DevEco通过项目的配置文件oh-package.json5
知道了这个项目依赖./src/main/cpp/types/libentry
目录下的libentry.so
。然后通过./src/main/cpp/types/libentry
目录下的oh-package.json5
配置文件知道了libentry.so
相关的接口声明是放在Index.d.ts
文件中的。这样DevEco读取Index.d.ts
文件就能够知道libentry.so
里面包含哪些接口了,就能够做自动联想了。
使用C++实现js接口
前面我们知道了如何在ets侧调用js接口,接下来我们看下被调用的add
方法在C++侧是如何实现的?
#include "napi/native_api.h"static napi_value Add(napi_env env, napi_callback_info info)
{size_t argc = 2;napi_value args[2] = {nullptr};napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);napi_valuetype valuetype0;napi_typeof(env, args[0], &valuetype0);napi_valuetype valuetype1;napi_typeof(env, args[1], &valuetype1);double value0;napi_get_value_double(env, args[0], &value0);double value1;napi_get_value_double(env, args[1], &value1);napi_value sum;napi_create_double(env, value0 + value1, &sum);return sum;}EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{napi_property_descriptor desc[] = {{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr }};napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);return exports;
}
EXTERN_C_ENDstatic napi_module demoModule = {.nm_version = 1,.nm_flags = 0,.nm_filename = nullptr,.nm_register_func = Init,.nm_modname = "entry",.nm_priv = ((void*)0),.reserved = { 0 },
};extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{napi_module_register(&demoModule);
}
注册napi模块
RegisterEntryModule
函数作为so的构造函数,在so被加载成功后被调用。- 函数里面调用了
napi_module_register
方法,这是告诉系统说我这里有一个napi模块,模块信息在demoModule
里面。 - 通过
demoModule
的定义我们可以看到,这个模块的名字叫做entry
,它的初始化函数是Init
方法。 Init
方法里面,首先定义了一个napi_property_descriptor
类型的变量desc
,里面有一个"add"
字符串,这个代表js侧的方法名字。还有一个Add
符号,这个就是定义在这个文件中的napi_value Add(napi_env env, napi_callback_info info)
函数。这个意思是说,在调用js侧的add
方法的时候,实际会调用到c++侧的napi_value Add(napi_env env, napi_callback_info info)
方法。- 然后通过
napi_define_properties
将上述信息注册到js环境中。
解析js接口的参数
接着我们看C++侧的Add
方法的实现,他是如何解析js侧传过来的参数,又是如何将计算结果传回到js侧的。
参数数解析
通过napi_get_cb_info
获取JavaScript传入的参数信息:
- 声明
argc=2
表示预期接收两个参数 args
数组用于存储实际传入的参数值- 后两个
nullptr
表示不关心this
对象和函数本身
类型检查
使用napi_typeof
检查参数类型:
- 分别对
args[0]
和args[1]
进行类型判断 - 结果存储在
valuetype0
和valuetype1
变量中
数值转换
通过napi_get_value_double
提取数值:
- 将JavaScript的Number类型转换为C的double类型
- 转换后的值存储在
value0
和value1
变量中
运算与返回
通过napi_create_double
创建结果:
- 将两个
double
值相加 - 构造新的
napi_value
类型结果 - 最终返回给JavaScript环境