1、背景介绍
随着HarmonyOS Next版本系统推进,各大厂逐步开始适配HarmonyOS Next版本,公司虽然没有签约,但是也从年初开始了紧锣密鼓的适配。我们负责的即时通讯方向也同步开启了HarmonyOS化。我们的IM消息收取采用了推拉结合的模式,推送需要长连接通道,在Android和iOS端我们使用了微信开源的Mars长连接库,Mars使用C++开发,实现了Android、iOS、Mac、Windows、Linux系统的长连接通道,为保证一致性和开发成本考虑,HarmonyOS端我们也继续使用mars作为长连接通道。
微信已经很久没更新mars库,而且也没有适配HarmonyOS计划。使用mars作为长连接通道就涉及鸿蒙平台编译和迁移C++库的问题。C++相关知识官方提供了简单的使用示例,对于像mars这种比较复杂的库,很多场景找不到参考的实力踩了很多坑,本文分享napi各种场景的使用方式和napi开发适配相关的流程及遇到问题。
2、mars介绍及适配工作拆分
mars主要包含两大块:长连接信令网络stn与高效本地日志xlog。
mars没有直接使用c++系统标准库,而是使用自己编译的boost,boost库直接编译到stn动态库;xlog中日志加密模块依赖了openssl,编译框架使用了外部提供的openssl静态库,mars的适配工作整体分为四块:
- openssl HarmonyOS平台编译;
- mars源码HarmonyOS平台编译;
- C++与TS交互接口封装;
- TS层接口封装。
本文主要介绍HarmonyOS C++层适配,第四块TS层封装不多做介绍。
3、openssl HarmonyOS平台库编译
3.1 HarmonyOS C++编译器介绍
可以在OpenHarmony官网渠道下载最新版本SDK,如果有HarmonyOS 白名单权限的也可以下载HarmonyOS的SDK。下载完成后可以参考《MacOS上使用OpenHarmony SDK交叉编译指导》配置开发环境。《MacOS上使用OpenHarmony SDK交叉编译指导》中提供了编译cJSON库示例。
配置编译环境后可以指定交叉编译链,指定编译架构,执行:
cmake -DCMAKE_TOOLCHAIN_FILE=/Users/ohos/sdk/packages/ohos-sdk/darwin/native/build/cmake/ohos.toolchain.cmake -DCMAKE_INSTALL_PREFIX=/Users/ohos/Workspace/usr/cJSON -DOHOS_ARCH=arm64-v8a .. -L
make
编译成功后可以生成对应的库。
3.2 OpenHarmony三方库适配指导
OpenHarmony提供了一些已经编译适配过的开源库并且提供了已经适配OpenHarmony的C/C++三方库的适配脚本和OpenHarmony三方库适配指导文档、三方库适配相关的工具,在https://gitee.com/openharmony-sig/tpc_c_cplusplus中。
文档中详细介绍了OpenHarmony提供的三种三方库交叉编译方式:
- cmake
- configure
- make
具体可以查看链接中的文档。
3.3 lycium 交叉编译框架
lycium是一款协助开发者通过shell语言实现C/C++三方库快速交叉编译,并在OpenHarmony系统上快速验证的编译框架工具。开发者只需要设置对应C/C++三方库的编译方式以及编译参数,通过lycium就能快速的构建出能在OpenHarmony系统运行的二进制文件。
官方在https://gitee.com/openharmony-sig/tpc_c_cplusplus/tree/master...提供了openssl的编译脚本,但是只有1.0.2u的,我们用到的是1.0.2k,基于lycium工具对1.0.2k版本库进行编译。
这里先尝试编译官方提供的1.0.2u版本。
1. 安装依赖
#安装pkg-config
brew install pkg-config
#安装autoconf
brew install autoconf
#安装ninja
brew install ninja
#安装sha512sum
brew install coreutils
brew install sha512sum
2. 开始构建
./build.sh openssl_1_0_2u
3. 配置编辑1.0.2k版本
- thirdparty中拷贝1.0.2u一份
- 修改文件夹名称为对应的1.0.2k;
- 修改对应HPKBUILD文件中的pkgname和pkgver
- 修改SHA512SUM文件中的内容,使用
shasum -a 512 openssl-OpenSSL_1_0_2k.tar.gz
获取对应512SUM
4. 编译mars库源码
将编译完成的的openssl产物拷贝到mars/mars/openssl/openssl_lib_ohos
中。
使用交叉编译工具直接构建:
cd mars/mars
mkdir build
cd build
cmake -DOHOS_STL=c++_shared -DOHOS_ARCH=armeabi-v7a -DOHOS_PLATFORM=OHOS -DCMAKE_TOOLCHAIN_FILE=~/Library/Huawei/Sdk/openharmony/11/native/build/cmake/ohos.toolchain.cmake ..
cmake --build .
要编译对应产物需要在CMakelist.txt中针对HarmonyOS编译器增加配置,以xlog为例:
elseif(OHOS)
message("start build ohos")
if(NATIVE_CALLBACK)
message("common native callback")
add_definitions(-DNATIVE_CALLBACK)
endif()
find_library(hilog-lib hilog_ndk.z)
find_library(z-lib z)
link_directories(app baseevent xlog sdt stn comm boost zstd)
# marsxlog
set(SELF_LIB_NAME marsxlog)
file(GLOB SELF_SRC_FILES libraries/mars_xlog_sdk/jni/import.cc)
add_library(${SELF_LIB_NAME} SHARED ${SELF_SRC_FILES})
install(TARGETS ${SELF_LIB_NAME} LIBRARY DESTINATION ${SELF_LIBS_OUT} ARCHIVE DESTINATION ${SELF_LIBS_OUT})
#get_filename_component(EXPORT_XLOG_EXP_FILE libraries/mars_android_sdk/jni/export.exp ABSOLUTE)
set(SELF_XLOG_LINKER_FLAG "-Wl,--gc-sections -Wl,--version-script='${EXPORT_XLOG_EXP_FILE}'")
target_link_libraries(${SELF_LIB_NAME} "${SELF_XLOG_LINKER_FLAG}"
xlog
mars-boost
comm
libzstd_static
${hilog-lib}
${z-lib}
)
5、实现napi桥接层
5.1 napi介绍
HarmonyOS Node-API是基于Node.js的Node-API规范扩展开发的机制,为开发者提供了ArkTS/JS与C/C++模块之间的交互能力。
Node-API简称napi,是提供Nodejs与C++语言交互的一种手段,类似与Java中的JNI提供了Java和C++交互能力。
Node-API可以参考Node官网:https://nodejs.org/api/n-api.html
下面是一个官方提供的基础的NodeAPI交互流程图:
实际的开发场景会比这个图复杂很多,下面我们逐步拆解交互步骤和过程。
5.2 TS调用c++
5.2.1 准备工作
通过DevEco-Studio中创建Native C++项目,在src/main/cpp/types/entry/index.d.ts
中创建对应方法:
export const appenderOpen: (level: number, mode: number, cacheDir: string,logDir: string, nameprefix: string, cacheDays: number, pubkey: string) => void;
这个示例中我们传入7个参数,返回为空。
在ts模块中导入动态库:
import xlog from 'libmarsxlog.so';
导入后我们就可以直接使用xlog.appenderOpen
方法进行调用了。
TS层接口封装完,需要在C++中注册模块实现方法:
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "appenderOpen", nullptr, appenderOpen, 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);
}
接下来就是实现appenderOpen方法,解析参数处理逻辑了。
5.2.2 参数解析
方法实现:
static napi_value appenderOpen(napi_env env, napi_callback_info info)
{
}
这里传入两个参数,一个代表上下文环境和回调信息。
napi_env类似于JNI中的JNIEnv,跟JNIEnv不同的是JNIEnv可以使用JNI_OnLoad时传入的JavaVM创建,napi_env不支持,同时napi_env不支持在其他(这个其他后面具体解释)线程中使用。
方法声明的另一个不同是JNI中Java中声明几个参数,这里就有几个参数,但是napi中都封装到napi_callback_info中,参数都是从napi_callback_info中读取。
接下来就是解析传入的参数,在appenderOpen方法中我们传入七个参数:
size_t argc = 7;
napi_value args[7] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int level;
napi_get_value_int32(env, args[0], &level);
int mode;
napi_get_value_int32(env, args[1], &mode);
std::string cachedir;
NapiUtil::JsValueToString(env, args[2], cachedir);
std::string logDir;
NapiUtil::JsValueToString(env, args[3], logDir);
std::string nameprefix;
NapiUtil::JsValueToString(env, args[4], nameprefix);
int cachedays;
napi_get_value_int32(env, args[5], &cachedays);
std::string pubkey;
NapiUtil::JsValueToString(env, args[6], pubkey);
//调用业务逻辑
return nullptr;
- 首先创建7个napi_value的数组参数用于,用于接受ts层传入的参数;
- 通过 napi_get_cb_info 将napi_callback_info中的参数信息读到napi_value数组中;
- 通过napi_value和napi提供的转换函数将TS类型的数据转换成对应C/C++类型数据;
下面分别介绍解析不同类型参数的方法。
5.2.2.1 解析数值型参数
通过napi_get_value_int32将第一个数值类型的js对象转换成c++对象,int类型的都可以用该方法转换:
int intValue;
napi_get_value_int32(env, args[0], &intValue);
其他基本类型的都有对应参数,用法和napi_get_value_int32类似:
- 转换为布尔类型:
napi_get_value_bool
- 转换为int64:
napi_get_value_int64
- 转换为无符号32位:
napi_get_value_uint32
- 转换为double:
napi_get_value_double
- bitint 64位:
napi_get_value_bigint_int64
- bitint 无符号64位:
napi_get_value_bigint_uint64
5.2.2.2 解析字符串类型参数
通过napi_get_value_string_utf8将js的字符串对象转换为c++的std::string对象。但是获取字符串比数值型麻烦些,看napi_get_value_string_utf8函数说明:
napi_status napi_get_value_string_utf8(napi_env env,
napi_value value,
char* buf,
size_t bufsize,
size_t* result)
[in] env
: The environment that the API is invoked under.[in] value
:napi_value
representing JavaScript string.[in] buf
: Buffer to write the UTF8-encoded string into. IfNULL
is passed in, the length of the string in bytes and excluding the null terminator is returned inresult
.[in] bufsize
: Size of the destination buffer. When this value is insufficient, the returned string is truncated and null-terminated.[out] result
: Number of bytes copied into the buffer, excluding the null terminator.
Returns napi_ok
if the API succeeded. If a non-string
napi_value
is passed in it returns napi_string_expected
.
我们需要先创建一个char类型的空间去接收转换后的字符串,创建char空间需要指定大小,可以先调用一次napi_get_value_string_utf8,buf传入空获取到传入的数据大小,然后创建对应大小buf,再次调用napi_get_value_string_utf8获取转换后的字符串:
void JsValueToString(const napi_env &env, const napi_value &value, std::string &target) {
size_t result = 0;
napi_get_value_string_utf8(env, value, nullptr, 0, &result);
std::unique_ptr<char[]> buf(new char[result+1]);
if (buf.get() == nullptr) {
return;
}
(void)memset(buf.get(), 0, result+1);
napi_get_value_string_utf8(env, value, buf.get(), result+1, &result);
target = buf.get();
}
5.2.2.3 解析数组类型参数
和string类似,数组类型参数需要先通过napi_get_array_length
参数获取数组长度,再通过napi_get_element
获取对应每个napi_value对象,通过上述基本类型转换为对应C++对象:
napi_value MarsNapi::setBackupIPs(napi_env env, napi_callback_info info){
std::vector<std::string> backupip_list;
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
bool is_array;
//判断是否为数组
napi_status status = napi_is_array(env, args[0], &is_array);
if (!is_array) {
return nullptr;
}
napi_value array = args[0];
uint32_t length;
napi_get_array_length(env,array,&length);
for(int i = 0; i < length; i++){
napi_value element;
napi_get_element(env, array, i, &element); // 获取返回值数组的每个元素
std::string ipStr;
NapiUtil::JsValueToString(env, element, ipStr);
backupip_list.push_back(ipStr);
}
//调用业务逻辑
return nullptr;
}
5.2.2.4 解析object类型参数
解析object对象参数和前面参数一样,通过napi_get_cb_info
转换为napi_value对象后,通过napi_get_named_property
获取对象中的属性值:
//ts对象:
export class Task {
public taskID: number; //unique task identify
public channelSelect?: number; //short,long or both
//...
}
napi_value MarsNapi::startTask(napi_env env, napi_callback_info info){
size_t argc = 1;
napi_value js_cb;
napi_get_cb_info(env, info, &argc, &js_cb, nullptr, nullptr);
napi_value taskIdNapiValue;
napi_get_named_property(env, js_cb, "taskID", &taskIdNapiValue);
int32_t taskid;
napi_get_value_int32(env, taskIdNapiValue, &taskid);
napi_value channelSelectNapiValue;
napi_get_named_property(env, js_cb, "channelSelect", &channelSelectNapiValue);
int32_t channel_select;
napi_get_value_int32(env, channelSelectNapiValue, &channel_select);
//....
//调用业务逻辑
return nullptr;
}
5.2.2.5 解析ArrayBuffer类型参数
如果ts向C++传输二进制流,需要用到ArrayBuffer类型数据,在C++侧通过napi_get_arraybuffer_info
转换成C++字节流,接口说明:
napi_status napi_get_arraybuffer_info(napi_env env,
napi_value arraybuffer,
void** data,
size_t* byte_length)
[in] env
: The environment that the API is invoked under.[in] arraybuffer
:napi_value
representing theArrayBuffer
being queried.[out] data
: The underlying data buffer of theArrayBuffer
. If byte_length is0
, this may beNULL
or any other pointer value.[out] byte_length
: Length in bytes of the underlying data buffer.
Returnsnapi_ok
if the API succeeded.
This API is used to retrieve the underlying data buffer of anArrayBuffer
and its length.
第三个参数传入空时,只会获取字节流大小。第三个参数是指向指针的指针,我们不需要创建空间,函数内部会创建空间:
napi_value MarsNapi::setArrayBufferData(napi_env env, napi_callback_info info){
size_t argc = 1;
napi_value js_cb;
napi_get_cb_info(env, info, &argc, &js_cb, nullptr, nullptr);
// 获取 ArrayBuffer 对象的指针和长度
void* buffer;
size_t length;
napi_get_arraybuffer_info(env, arrayBuffer, &buffer, &length);
// 打印 ArrayBuffer 中的数据 ,也可以修改ArrayBuffer的值
uint32_t* data = (uint32_t*) buffer;
//调用业务逻辑
return nullptr;
}
5.2.3 返回值处理
返回值是返回表示js对象的napi_value,需要通过napi_value创建函数将c++对象转换为js对象:
//布尔类型示例:
napi_value MarsNapi::hasTask(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);
int _taskid;
napi_get_value_int32(env, args[0], &_taskid);
napi_value result;
bool hasTaskById = mars::stn::HasTask(_taskid);
//创建布尔型js对象
napi_get_boolean(env, hasTaskById, &result);
return result;
}
其他对应创建对象方式:
- napi_create_int32
- napi_create_uint32
- napi_create_int64
- napi_create_double
- napi_create_bigint_int64
- napi_create_bigint_uint64
- napi_create_bigint_words
- napi_create_string_latin1
- node_api_create_external_string_latin1
- napi_create_string_utf16
- node_api_create_external_string_utf16
- napi_create_string_utf8
5.3 c++调用ts
HarmonyOS中在C++层回调TS层与Android中C++层调用Java方法有个很大的区别是:在HarmonyOS中不管调用static方法还是对象方法,都必须把类或者对象传到C++层,而不像JNI中可以直接根据类名方法名直接调用Java的静态方法。所以之前调用Java静态方法的实现都要增加napi方法中设置一个类或者对象的方法。
下面先实现一个简单的回调流程。
5.3.1 TS调用C++的方法中直接回调TS
TS中定义回调函数:
export class Callback{
function add(a: number, b:number) {
return a + b;
}
}
将回调函数传递给C++后:
static napi_value RegisterCallback(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);
//1. 从回调对象中获取回调方法
napi_value add;
napi_get_named_property(env, args[0], "add", &add);
//2. 创建回调函数使用的方法
double value1 = 2;
double value2 = 3;
napi_value callbackArgs[2];
// 创建两个double,给callback调用
napi_create_double(env, value1, &callbackArgs[0]);
napi_create_double(env, value2, &callbackArgs[1]);
//3. 调用回调函数
napi_call_function(env, args[0], add, 2 , argv, &result);
return nullptr;
}
这是一个最简单的例子,TS给C++层传入回调对象,C++中通过napi_get_named_property
在对象中获取要调用的方法,并且为方法创建TS类型的参数,最后调用napi_call_function
回调TS方法。napi_call_function
函数说明:
NAPI_EXTERN napi_status napi_call_function(napi_env env,
napi_value recv,
napi_value func,
size_t argc,
const napi_value* argv,
napi_value* result);
[in] env
: The environment that the API is invoked under.[in] recv
: Thethis
value passed to the called function.[in] func
:napi_value
representing the JavaScript function to be invoked.[in] argc
: The count of elements in theargv
array.[in] argv
: Array ofnapi_values
representing JavaScript values passed in as arguments to the function.[out] result
:napi_value
representing the JavaScript object returned.
Returns napi_ok
if the API succeeded.
要注意第四个参数回调函数的参数个数和实际传的参数个数一致,否则有缺失的话TS层参数会变成undifined,方法体没有判断的话会导致crash。
最后一个参数用来接收TS函数的返回值。
这里是传入了一个TS对象,如果我们直接传入的是一个TS匿名方法:
testNapi.registerCallback((a: number, b: number) => {
return a + b;
})
在C++中则不需要通过napi_get_named_property
获取函数,napi_get_cb_info
获取到的就是方法。
5.3.2 C++逻辑层主线程调用ts
HarmonyOS目前不支持NAPI的napi_get_global
函数,所以我们无法全局对象中设置属性,上面是一种简单的场景在设置回调函数的方法中直接调用回调函数,但是大部分情况是我们设置了回调函数后应该缓存起来,在C++逻辑层的随意位置调用。
在缓存对象时需要使用napi_create_reference
创建引用后缓存,在使用的时候napi_get_reference_value
从应用获取对象:
//创建应用缓存到MarsNapiManager单例
napi_value js_cb;
napi_get_cb_info(env, info, &argc, &js_cb, nullptr, nullptr);
MarsNapiCallback *marsNapiCallback = MarsNapiManager::getInstance()->getMarsNapiCallback();
napi_create_reference(env, js_cb, 1, &marsNapiCallback->mMarsStnCallbackObj);
//获取引用中对象
napi_value callback = nullptr;
napi_get_reference_value(MarsNapiManager::getInstance()->mainEnv_, MarsNapiManager::getInstance()->getMarsNapiCallback()->mMarsStnCallbackObj, &callback);
有了上下文和对应对象就可以直接发起回调。
5.3.3 C++逻辑层子线程调用TS
官方提供了异步任务开发和线程安全开发的示例,通过napi_create_async_work
创建异步任务,通过napi_queue_async_work
将异步任务加入队列。但我们的场景是通过ptread创建的原生线程去回调TS,HarmonyOS不支持子线程调用TS,所以我们要基于napi_call_threadsafe_function
方式调用到主线程,然后在主线程中回调TS。
5.3.2.1 子线程调用到主线程实现回调TS
要使用napi_call_threadsafe_function
,需要先创建threadsafe function:
napi_value MarsNapi::setCallback(napi_env env, napi_callback_info info){
OH_LOG_Print(LOG_APP,LOG_WARN, LOG_DOMAIN, LOG_TAG, "setStnCallback");
size_t argc = 1;
napi_value js_cb;
napi_get_cb_info(env, info, &argc, &js_cb, nullptr, nullptr);
MarsNapiCallback *marsNapiCallback = MarsNapiManager::getInstance()->getMarsNapiCallback();
napi_create_reference(env, js_cb, 1, &marsNapiCallback->mMarsStnCallbackObj);
// 处理makesureAuthed
napi_value makesureAuthedWorkName;
napi_create_string_utf8(env, "makesureAuthedTh", NAPI_AUTO_LENGTH, &makesureAuthedWorkName);
napi_create_threadsafe_function(env, js_cb, nullptr, makesureAuthedWorkName, 0, 1, nullptr, nullptr,
nullptr, marsNapiCallback->makesureAuthedCallJs, &marsNapiCallback->makesureAuthedTsfn);
// 处理onNewDns
napi_value onNewDnsWorkName;
napi_create_string_utf8(env, "onNewDnsTh", NAPI_AUTO_LENGTH, &onNewDnsWorkName);
napi_create_threadsafe_function(env, js_cb, nullptr, onNewDnsWorkName, 0, 1, nullptr, nullptr,
nullptr, marsNapiCallback->onNewDnsCallJs, &marsNapiCallback->onNewDnsTsfn);
//...
}
xxxCallJs
方法中实现对TS的回调,以makesureAuthedCallJs
为例:
void MarsNapiCallback::makesureAuthedCallJs(napi_env env, napi_value jsCb, void *context, void *data){
xdebug2(TSF"makesureAuthedCallJs");
napi_value result;
napi_value callback = nullptr;
napi_get_reference_value(MarsNapiManager::getInstance()->mainEnv_, MarsNapiManager::getInstance()->getMarsNapiCallback()->mMarsStnCallbackObj, &callback);
napi_value makesureAuthedCallBack;
napi_get_named_property(env, callback, "makesureAuthed", &makesureAuthedCallBack);
napi_call_function(env, callback, makesureAuthedCallBack, 0, nullptr, &result);
bool resultIntValue;
napi_get_value_bool(env, result, &resultIntValue);
}
在原生逻辑子线程中可以:
MarsNapiCallback *marsNapiCallback = MarsNapiManager::getInstance()->getMarsNapiCallback();
napi_acquire_threadsafe_function(marsNapiCallback->makesureAuthedTsfn);
// 调用主线程函数,传入 Data
std::promise<bool> promise;
auto future = promise.get_future();
napi_call_threadsafe_function(marsNapiCallback->makesureAuthedTsfn, &promise, napi_tsfn_noblocking);
5.3.2.2子线程调用到主线程并等待主线程返回结果
上面的流程有个问题,调用TS的函数是想拿到返回值,主线程拿到返回值后怎么传到子线程呢?
这里面用到了promise机制。
子线程中创建std::promise
对象,get_future
获取Future,通过napi_call_threadsafe_function
传递给子线程,调用玩napi_call_threadsafe_function
后通过future.get()
等待主线程通知,主线程调用完TS方法后,通过set_value设置返回值,通知子线程继续执行。修改后的代码:
//子线程调用
bool (*MakesureAuthed)()
= []() -> bool {
xdebug2(TSF"MakesureAuthed");
MarsNapiCallback *marsNapiCallback = MarsNapiManager::getInstance()->getMarsNapiCallback();
napi_acquire_threadsafe_function(marsNapiCallback->makesureAuthedTsfn);
// 调用主线程函数,传入 Data
std::promise<bool> promise;
auto future = promise.get_future();
napi_call_threadsafe_function(marsNapiCallback->makesureAuthedTsfn, &promise, napi_tsfn_blocking);
bool result = future.get();
return result;
};
//主线程执行调用TS逻辑
void MarsNapiCallback::makesureAuthedCallJs(napi_env env, napi_value jsCb, void *context, void *data){
xdebug2(TSF"makesureAuthedCallJs");
std::promise<bool> *promise = (std::promise<bool> *)data;
napi_value result;
napi_value callback = nullptr;
napi_get_reference_value(MarsNapiManager::getInstance()->mainEnv_, MarsNapiManager::getInstance()->getMarsNapiCallback()->mMarsStnCallbackObj, &callback);
napi_value makesureAuthedCallBack;
napi_get_named_property(env, callback, "makesureAuthed", &makesureAuthedCallBack);
napi_call_function(env, callback, makesureAuthedCallBack, 0, nullptr, &result);
bool resultIntValue;
napi_get_value_bool(env, result, &resultIntValue);
promise->set_value(resultIntValue);
}
napi_call_threadsafe_function的第三个参数变成了阻塞方式:napi_tsfn_blocking。
注意:napi_call_threadsafe_function
只能在子线程中调用,如果在主线程调用会导致配置的回调方式不执行问题。
5.3.3 回调函数中传入复杂类型
有些场景需要C++向TS传入字节流或者数组,在TS层可以读写字节流和数组。
TS代码:
req2Buf(taskID: number, userContext: object, inBuffer: ArrayBuffer, errCode: number[], channelSelect: number): number {
Logg.d(this.TAG, "req2Buf ts");
const wrapper = this.mTaskWrapperMap.get(taskID);
errCode[0] = 0;
if (!wrapper) {
Logg.e(this.TAG, `invalid req2Buf for task, taskID=${taskID}`);
return MarsConstants.FAILED;
}
try {
const buffer = wrapper.req2buf();
errCode[0] = 1;
Logg.d(this.TAG, `req2Buf length :${buffer.length}`);
new Uint8Array(inBuffer).set(buffer);
return MarsConstants.SUCCESS;
} catch (error) {
Logg.e(this.TAG, "task wrapper req2buf failed for short, check your encode process");
return MarsConstants.FAILED;
}
}
getReq2BufLength(taskID: number):number{
Logg.d(this.TAG, "getReq2BufLength ts");
const wrapper = this.mTaskWrapperMap.get(taskID);
if (!wrapper) {
Logg.e(this.TAG, `invalid getReq2BufLength for task, taskID=${taskID}`);
return 0;
}
try {
const buffer = wrapper.req2buf();
Logg.d(this.TAG, `getReq2BufLength length :${buffer.length}`);
return buffer.length;
} catch (error) {
Logg.e(this.TAG, "task wrapper getReq2BufLength failed for short, check your encode process");
return 0;
}
}
void MarsNapiCallback::req2BufCallJs(napi_env env, napi_value jsCb, void *context, void *data){
xdebug2(TSF"req2BufCallJs");
struct Req2BufCallbackParamData *arg = (struct Req2BufCallbackParamData *)data;
napi_value callback = nullptr;
NAPI_CALL_RETURN_VOID(env,napi_get_reference_value(MarsNapiManager::getInstance()->mainEnv_, MarsNapiManager::getInstance()->getMarsNapiCallback()->mMarsStnCallbackObj, &callback));
napi_value getTaskLengthcallbackData;
NAPI_CALL_RETURN_VOID(env,napi_create_int32(env, arg->_taskid, &getTaskLengthcallbackData));
napi_value req2BufLengthCallBack;
napi_value req2BufLengthCallBackReturnData;
NAPI_CALL_RETURN_VOID(env,napi_get_named_property(env, callback, "getReq2BufLength", &req2BufLengthCallBack));
NAPI_CALL_RETURN_VOID(env,napi_call_function(env, callback, req2BufLengthCallBack, 1, &getTaskLengthcallbackData, &req2BufLengthCallBackReturnData));
int req2BufLength;
NAPI_CALL_RETURN_VOID(env,napi_get_value_int32(env, req2BufLengthCallBackReturnData, &req2BufLength));
napi_value callbackData[4];
NAPI_CALL_RETURN_VOID(env,napi_create_int32(env, arg->_taskid, &callbackData[0]));
NAPI_CALL_RETURN_VOID(env,napi_create_external(env, arg->_user_context, nullptr, nullptr, &callbackData[1]));
uint8_t* funcResultArrayData = new uint8_t[req2BufLength];
NAPI_CALL_RETURN_VOID(env,napi_create_external_arraybuffer(env, funcResultArrayData, req2BufLength, MarsNapiCallback::FinalizeArrayBuffer, nullptr, &callbackData[2]));
NAPI_CALL_RETURN_VOID(env,napi_create_array_with_length(env,2, &callbackData[3]));
NAPI_CALL_RETURN_VOID(env,napi_create_int32(env, arg->_channel_select, &callbackData[4]));
napi_value callbackFuncResultData;
napi_value req2BufCallBack;
NAPI_CALL_RETURN_VOID(env,napi_get_named_property(env, callback, "req2Buf", &req2BufCallBack));
NAPI_CALL_RETURN_VOID(env,napi_call_function(env, callback, req2BufCallBack, 5, callbackData, &callbackFuncResultData));
auto resultData = new Req2BufCallbackFuncResultData;
napi_value element;
NAPI_CALL_RETURN_VOID(env,napi_get_element(env, callbackData[3], 0, &element));
NAPI_CALL_RETURN_VOID(env,napi_get_value_int32(env, element, &resultData->errcode_array[0]));
NAPI_CALL_RETURN_VOID(env, napi_get_value_int32(env, callbackFuncResultData, &resultData->ret));
resultData->_body = new AutoBuffer(funcResultArrayData, req2BufLength, sizeof(uint8_t));//TODO 切回任务线程去写
xdebug2(TSF"req2BufCallJs req2BufLength :%_", req2BufLength);
delete []funcResultArrayData;
arg->promise->set_value(resultData);
}
这是一个复杂场景的示例,req2Buf函数需要传入五个参数,其中inBuffer是需要TS中写入二进制,errCode也需要TS中修改状态。
inBuffer需要在TS中写入数据,但是需要在C++层创建ArrayBuffer,创建ArrayBuffer需要指定大小,解决这种问题可以有两种方案:
- 创建指定大小buffer,如果不够TS层返回错误,C++层重新分配重新调用;
- TS实现一个获取buffer大小的方法,先调用该方法获取Buffer大小再创建buffer。
当然业务逻辑允许的条件下可以TS调用方法设置Buffer大小。最开始想着既然C++层无法知道buffer大小,那么就让TS层创建大小,以返回值的方式返回给C++ ArrayBuffer,实际情况是在C++中通过napi_get_arraybuffer_info
获取到的指针为空,所以改成在C++中创建。
这里面通过napi_create_external_arraybuffer
在C++层创建TS中的ArrayBuffer。
req2Buf方法第二个参数是object,这里通过napi_create_external
将C++指针装换位TS对象。
errCode是个数值型数组,通过napi_create_array_with_length
创建,通过napi_get_element
读取TS中修改的errCode数组内容。
之前我们的方法中都没有判断napi接口中的返回值,这里我们统一定义错误检查的宏NAPI_CALL_RETURN_VOID,在宏中统一处理了错误,并打印了日志。
6、线程模型
先看下Android平台的信令处理线程模型:
再根据上面的介绍对比下HarmonyOS的线程模型:
从图上可以看到这样可能会有问题,每次Socket线程收到消息都要等待主线程去处理回调,如果主线程比较忙时,Socket线程会被长时间阻塞,服务端会发生写入异常。针对这种情况如何优化呢?我们首先想到的还是多线程,这里面TS和C++交互设计到一个Env的问题,如果想要回调在子线程,那么TS应该也在子线程调用C++,这样C++中的Env就是在子线程的,napi_call_threadsafe_function方法调用后会切换到对应Env的子线程来处理。
HarmonyOS提供了两种多线程机制:TaskPool和Worker,TaskPool创建的线程不可控,Worker比较符合我们这种场景(Worker的缺陷是目前HarmonyOS对一个应用的Worker数量进行了限制)。使用Worker后的线程模型变为:
7、总结
本文通过一个三方开源C++库的鸿蒙适配案例介绍了HarmonyOS跨平台C++库编译的方式,以及HarmonyOS跨语言开发NAPI相关内容,从最基础的TS和C++互相调用、两种语言数据基础类型转换、复杂类型转换到复杂场景下TS与C++交互方式与实现思路,基本上覆盖了HarmonyOS C++开发相关的各种场景,最后基于性能问题介绍了基于Worker的多线程机制。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。