鸿蒙应用开发笔记(1)——原生库移植踩坑

本文记录CoralReefPlayer这个项目在移植到HarmonyOS NEXT/OpenHarmony时遇到的各种问题. 为了避免读者不了解该项目, 从而不理解本文在说些什么, 这里先对该项目做一些简要介绍.

CoralReefPlayer 即珊瑚礁播放器,是一款使用 C++20 开发的跨平台流媒体播放器库,目前支持播放 RTSP 和 MJPEG over HTTP 流,可为基于网络进行视频流传输的机器人上位机提供可定制、高性能、低延迟的推拉流、编解码及录像能力。

该库为使用C++语言开发的原生库, 对外暴露C语言接口, 其他语言可利用其与C语言的互操作性来调用该库的接口, 并将这些调用封装为该语言的面向对象形式, 从而在其他语言中较为优雅地调用该原生库, 实现视频流解码等功能.

在适配鸿蒙操作系统之前, 该库已经实现了Node.js binding, 而鸿蒙的js具有两种与C语言互操作的方法, 其中一种为JSVM-API, 其类似于jni, 另一种即为Node-API, 考虑到我们已有基于Node-API开发的Node.js binding, 因此选择了Node-API方案, 理论上代码无需修改即可在鸿蒙上运行, 但是理论归理论, 实际是实际, 由于Node.js和鸿蒙在底层实现上的根本差异, 在整个移植的过程中还是踩了不少坑, 特此记录一下.

一. node-addon-api

鸿蒙装包只能用ohpm, 之前是能用npm的, 但是4.0开始不能了, 据说是安全问题, 防止供应链投毒吧, 那我欠的node-addon-api谁给我补呢(

好在就在我开始移植的几乎同一时间, 有个第三方维护的node-addon-api-ohos上ohpm了, 所以直接装就搞定了, 省着自己再移植了

二. Buffer不继承自Uint8Array

鸿蒙的Buffer不继承自Uint8Array, 所以对Buffer对象调用napi_get_typedarray_info会返回napi_invalid_arg异常
详见 gitee.com/openharmony/arkui_napi/blob/master/native_engine/native_api.cpp#L2738

我已经给维护者提了一个pr, 但是截至本文发布时尚未被合入, 如果有需要可以自己cherry pick下: fix: make Buffer extend Object directly by DawningW · Pull Request #2 · richerfu/node-addon-api-ohos

PS: 看来HarmonyOS NEXT真是基于OpenHarmony的了, 不是Android套壳了, 要不然log不可能和开源鸿蒙代码对的上, 而且我也不可能这么快发现问题

三. Buffer有大小限制

之前Node.js binding是采用Buffer传递视频/音频帧数据的, 虽然我忘了当初为啥要用Buffer不用ArrayBuffer了, 但是我决定继承, 结果在模拟器上一跑, 挂了, 一看日志, 说buffer大小超限制了…

然后去翻OpenHarmony的源码, 看到了这个判断, 在gitee.com/openharmony/arkui_napi/blob/master/native_engine/native_api.cpp#L2604

static constexpr size_t MAX_BYTE_LENGTH = 2097152;
static constexpr size_t ONEMIB_BYTE_SIZE = 1048576;
// ...
NAPI_EXTERN napi_status napi_create_external_buffer(napi_env env,
                                                    size_t length,
                                                    void* data,
                                                    napi_finalize finalize_cb,
                                                    void* finalize_hint,
                                                    napi_value* result)
{
    NAPI_PREAMBLE(env);
    CHECK_ARG(env, result);
    CHECK_ARG(env, data);
    RETURN_STATUS_IF_FALSE(env, length > 0, napi_invalid_arg);

    auto callback = reinterpret_cast<panda::NativePointerCallback>(finalize_cb);
    if (!data) {
        HILOG_ERROR("data is empty");
        return napi_set_last_error(env, napi_invalid_arg);
    }
    if (length > MAX_BYTE_LENGTH) {
        HILOG_ERROR("Creat failed, current size: %{public}2f MiB, limit size: %{public}2f MiB",
                    static_cast<float>(length) / static_cast<float>(ONEMIB_BYTE_SIZE),
                    static_cast<float>(MAX_BYTE_LENGTH) / static_cast<float>(ONEMIB_BYTE_SIZE));
        data = nullptr;
        return napi_set_last_error(env, napi_invalid_arg);
    }

    auto engine = reinterpret_cast<NativeEngine*>(env);
    auto vm = engine->GetEcmaVm();
    Local<panda::BufferRef> object = panda::BufferRef::New(vm, data, length, callback, finalize_hint);
    void* ptr = object->GetBuffer(vm);
    CHECK_ARG(env, ptr);

    *result = JsValueFromLocalValue(object);
    return GET_RETURN_STATUS(env);
}

从上面的代码中可以看出, Buffer最大居然只能是2MB, 最大只有2MB的Buffer有啥用啊…然后去翻ArrayBuffer的源码, 发现是没有这个大小限制的, 于是我决定用__OHOS__这个宏判一下, 为了避免依赖Node.js binding的存量代码出现问题, 在Node.js上还是用Buffer, 在鸿蒙上改用ArrayBuffer

PS: 看来HarmonyOS NEXT真的真的是基于OpenHarmony的了, 就连Buffer大小限制都是一样的, 但是为啥Buffer限大小ArrayBuffer不限, 这不是有毛病…

四. 为Android编译的原生库居然不需要重编

表面上看非常神奇, 但是其实是合理的, 因为架构一样, 都是x86/x86_64/armv7/aarch64, 然后C ABI又是稳定的, 所以不需要重编就能跑是很正常的, 并不能说明鸿蒙是安卓套壳

然后我还在OpenHarmony用的musl库里看到这样一段话, 嗯, 非常合理…

基于openharmony的需求,为musl新增的特性:
● 加载器地址随机化,RELRO共享机制
● 提供加载器namespace机制
● OHOS容器中能够运行依赖bionic的库
● musl全球化接口适配locale数据能力
● mallocng堆内存分配器安全增强,默认开启meta指针混淆。地址随机化通过MALLOC_SECURE_ALL宏开关。可在编译命令中增加--gn-args="musl_secure_level=3"开启
等。在新增特性基础上,也进行了对于musl接口功能的完善与错误的修复。

五. libc库差异导致的符号缺失问题

Android用的是自研bionic库, 鸿蒙用的是裁剪过的musl库, 虽然已经尽可能做了兼容, 但是难免还是有一些bionic库中的私有符号是在musl库中不存在的, 这时候就需要我们分析日志, 比对bionic库和鸿蒙用的musl库的源码, 去实现这些私有符号

1. __errno

bionic的errno和musl的errno都是宏, 只不过bionic定义成了__errno, musl定义成了__errno_location, 所以自己实现下__errno就行了

int* __errno(void) {
    return __errno_location();
}

2. __sF

这个问题比较棘手, 需要去bionic源码中搜:

// libc/include/stdio.h
struct __sFILE;
typedef struct __sFILE FILE;

#if __ANDROID_API__ >= 23
extern FILE* _Nonnull stdin __INTRODUCED_IN(23);
extern FILE* _Nonnull stdout __INTRODUCED_IN(23);
extern FILE* _Nonnull stderr __INTRODUCED_IN(23);

/* C99 and earlier plus current C++ standards say these must be macros. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#else
/* Before M the actual symbols for stdin and friends had different names. */
extern FILE __sF[] __REMOVED_IN(23, "Use stdin/stdout/stderr");

#define stdin (&__sF[0])
#define stdout (&__sF[1])
#define stderr (&__sF[2])
#endif

可以看到在Android API 23之前这三个变量并非和其他libc库一样是三个指针, 而是对一个数组中的三个元素取地址, 这样的话可移植性非常差, 所以在API 23之后改为了三个指针

之所以会出现这种问题, 是因为我使用的ffmpeg库的目标API版本很低, 我记得是21, 低于23, 所以引用了__sF这个变量. 最初我想自己构造__sF这个数组, 然后把stdout之类的值都拷进去, 结果发现musl的FILE结构体比bionic的要大得多, 所以作罢. 然后我又想出了一个临时解决方案, 目前只有ffmpeg是为Android平台预编译的库, 然后又只有log用到了stdout, 那么只要调用av_log_set_callback(nullptr)不让ffmpeg输出日志就好了, 然后代码就这么水灵灵地上库了, 我真机智(bushi

但是上述解决方案显然不够优雅, 而且不够一劳永逸, 这是ffmpeg提供了设置日志打印回调的接口, 如果别的库没提供呢, 所以正确的做法应该是以高于API23的级别重新编译ffmpeg, API24是Android7.0, 现在99%设备都应该是7.0以上版本, 所以我决定把目标API设为24再重新编译ffmpeg库, 从而从根本上解决这个问题

3. __register_atfork

bionic的pthread_atfork调的是__register_atfork, 然而musl的pthread_atfork直接就是实现, 所以会报找不到__register_atfork, 解决办法也很简单, 自己实现一个就行:

int __register_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void), void* dso) {
    (void)(dso);
    return pthread_atfork(prepare, parent, child);
}

六. 适配OpenHarmony

别急, 环境没有呢还

标题: 鸿蒙应用开发笔记(1)——原生库移植踩坑
作者: QingChenW
链接: https://dawncraft.cc/2024/11/571/
本文遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 许可
禁止商用, 非商业转载请注明作者及来源!
上一篇
隐藏