各位码友们好!今天这篇干货主要聚焦实操细节,希望能帮大家少踩坑。
要是过程中遇到哪块没看懂、有疑问,或者你有更优的实现思路,评论区尽管聊!发现文档里有疏漏或错误也尽管指出来 ——
技术这东西就得互相挑刺才能越磨越精,咱们一起把这些知识点吃透~
前置学习
- 在【鸿蒙/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_END
static 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环境