11
头图

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的适配工作整体分为四块:

  1. openssl HarmonyOS平台编译;
  2. mars源码HarmonyOS平台编译;
  3. C++与TS交互接口封装;
  4. 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版本

  1. thirdparty中拷贝1.0.2u一份
  2. 修改文件夹名称为对应的1.0.2k;
  3. 修改对应HPKBUILD文件中的pkgname和pkgver
  4. 修改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交互流程图:

HarmonyOS NEXT实战之开源长连接库Mars适配.png

实际的开发场景会比这个图复杂很多,下面我们逐步拆解交互步骤和过程。

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;
  1. 首先创建7个napi_value的数组参数用于,用于接受ts层传入的参数;
  2. 通过 napi_get_cb_info 将napi_callback_info中的参数信息读到napi_value数组中;
  3. 通过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] valuenapi_value representing JavaScript string.
  • [in] buf: Buffer to write the UTF8-encoded string into. If NULL is passed in, the length of the string in bytes and excluding the null terminator is returned in result.
  • [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] arraybuffernapi_value representing the ArrayBuffer being queried.
  • [out] data: The underlying data buffer of the ArrayBuffer. If byte_length is 0, this may be NULL or any other pointer value.
  • [out] byte_length: Length in bytes of the underlying data buffer.
    Returns napi_ok if the API succeeded.
    This API is used to retrieve the underlying data buffer of an ArrayBuffer 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: The this value passed to the called function.
  • [in] funcnapi_value representing the JavaScript function to be invoked.
  • [in] argc: The count of elements in the argv array.
  • [in] argv: Array of napi_values representing JavaScript values passed in as arguments to the function.
  • [out] resultnapi_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需要指定大小,解决这种问题可以有两种方案:

  1. 创建指定大小buffer,如果不够TS层返回错误,C++层重新分配重新调用;
  2. 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 NEXT实战之开源长连接库Mars适配-1.png

再根据上面的介绍对比下HarmonyOS的线程模型:

HarmonyOS NEXT实战之开源长连接库Mars适配-2.png

从图上可以看到这样可能会有问题,每次Socket线程收到消息都要等待主线程去处理回调,如果主线程比较忙时,Socket线程会被长时间阻塞,服务端会发生写入异常。针对这种情况如何优化呢?我们首先想到的还是多线程,这里面TS和C++交互设计到一个Env的问题,如果想要回调在子线程,那么TS应该也在子线程调用C++,这样C++中的Env就是在子线程的,napi_call_threadsafe_function方法调用后会切换到对应Env的子线程来处理。

HarmonyOS提供了两种多线程机制:TaskPool和Worker,TaskPool创建的线程不可控,Worker比较符合我们这种场景(Worker的缺陷是目前HarmonyOS对一个应用的Worker数量进行了限制)。使用Worker后的线程模型变为:

HarmonyOS NEXT实战之开源长连接库Mars适配-3.png

7、总结

本文通过一个三方开源C++库的鸿蒙适配案例介绍了HarmonyOS跨平台C++库编译的方式,以及HarmonyOS跨语言开发NAPI相关内容,从最基础的TS和C++互相调用、两种语言数据基础类型转换、复杂类型转换到复杂场景下TS与C++交互方式与实现思路,基本上覆盖了HarmonyOS C++开发相关的各种场景,最后基于性能问题介绍了基于Worker的多线程机制。


轻口味
16.9k 声望3.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei


下一篇 »
2024全面拥抱AI