如何适配C/C++的SDK到鸿蒙应用

601 阅读8分钟

本文主要讲解如何在鸿蒙原生应用开发中,将目前已有的社区或者自研能力适配到鸿蒙生态中。

在开始之前,请确保你有以下经验以获得最佳的阅读体验:

  1. 有 C/C++ 开发经验,简单了解开发套路,最好简单了解 CMake 工具。
  2. 对鸿蒙开发有一定的了解或者经验,知道基本套路。

本文将从笔者到目前为止适配过的一些仓库来给出一些适配的经验,可能不是绝对的正确或者完美,但是也希望能够对你的鸿蒙适配有一定的帮助。

本文涉及的所有代码在 cross-compiler

背景

在鸿蒙原生应用开发中,官方除去对 ArkTS 的上层开发语言的支持,同时也提供了使用 C/C++ 开发原生模块并且暴露给上层 ArkTS 调用的方法。

在一般的情况下,我们可能使用 ArkTS 就能够满足一般的开发需求。但是对于部分场景(这部分场景可能比 Node.js 更加常见),我们仍然需要使用 C/C++ 来调用更加底层的能力给上层提供更加强大的功能,比如:

  1. 使用 SIMD 指令加速计算过程
  2. 使用 pthread 封装更加底层和功能丰富的线程池
  3. 网络编程
  4. ···

不过除去 C/C++ 本身的标准库之外,为了更加方便我们的需求迭代,我们仍然需要社区已经实现或者在原有多端设备上实现的一些底层能力,从而实现复用。基于这些需求和场景,我们就需要将原有的 C/C++ 进行一些适配从而实现在鸿蒙上调用这些能力。

开始

本文主要基于如下思维导图的方式来简单介绍鸿蒙中的 C/C++ 适配。

CC++适配.png

通过Clang/Clang++构建

这部分的内容对于有着 C/C++ 经验的读者来说可能非常简单。不过仍然需要做一些简单的讲解,并且引入一些注意事项,这些注意事项在后面的部分中可能是仍然适用的。

llvm构建工具

同大部分传统的 C/C++ 项目类似,我们可以通过类似于 gcc/llvm 等构建工具来实现代码的最终构建。

比如我们在使用 gcc 进行构建的时候,通常我们可能会执行如下的命令:

gcc main.c -o main

这样我们就为当前系统通过 gcc 从main.c文件中构建了一个名为main的可执行文件。

那么类似的,在鸿蒙中也提供了对应架构的编译构建工具。而鸿蒙上整体编译工具都是基于 llvm 适配而来,因此我们可以在我们以前设置的OHOS_NDK_HOME下看到如下的路径。

$OHOS_NDK_HOME/native/llvm

17172956349703.jpg

这里面就提供了 llvm 相关的内容,包括各种编译和优化分析工具等。

系统基础能力

在 native 的目录下还有另一个文件夹需要我们额外注意,那就是:

$OHOS_NDK_HOME/native/sysroot

当我们在进行各种代码开发的时候,我们通常会使用到各种类型的标准库,比如stdiostring等等。那么为了简化我们的开发程序大小,系统通常会预置一些基础库在系统中作为最基础的能力提供。

比如我们常见的 glibcmusl等,每个基础库之间可能存在差异性,并且互不兼容。这就导致了我们出现一个场景,比如说在 Ubuntu 系统使用 gcc 构建的程序无法在 alpine 系统中运行。因为程序在构建时会将基础库使用系统的动态链接库,而最终运行时找不到对应合法的动态链接库导致程序运行失败。

那么类似,当我们构建在鸿蒙系统上面使用的程序或者动态/静态链接库的时候,就需要指定在鸿蒙的系统动态链接库/静态链接库,从而确保最终的产物符合预期。

这也是交叉编译的核心思路:如何在不同的系统中,使用最终产物系统需要的构建器以及标准库构建。

鸿蒙目前是基于 musl 进行了一些特殊的裁剪和适配,因此有些读者可能在构建的时候使用 musl 基础库的 linux 目标产物时似乎好像也能够运行,但是实际上这个是不正确的操作,不排除有隐藏 bug 的风险。

构建

现在我们尝试使用这个方式来构建程序。如下是一个最简单的 C 语言程序。

#include "stdio.h"

int main() {
    printf("hello harmony");
    return 0;
}

这里我们就不再使用错误的方式去构建,感兴趣的读者可以自行尝试。我们直接给出正确的构建脚本如下所示:

#!/bin/sh

$OHOS_NDK_HOME/native/llvm/bin/clang hello.c -o main -target aarch64-linux-ohos --sysroot=${OHOS_NDK_HOME}/native/sysroot -D__MUSL__

执行构建即可看到产物,发送到设备上即可运行。

这里有一个非常核心的知识点:可以看到我们并没有直接使用本机的 gcc/llvm 命令进行构建,而是使用的 NDK 中的 llvm 工具进行构建,同时在构建的时候增加了部分参数,这部分参数时必不可少的。

// 对于 arm64-v8a 架构
-target aarch64-linux-ohos --sysroot=${OHOS_NDK_HOME}/native/sysroot -D__MUSL__

// 对于 armeabi-v7a 架构
-target arm-linux-ohos --sysroot=${OHOS_NDK_HOME}/native/sysroot -D__MUSL__ -march=armv7-a -mfloat-abi=softfp -mtune=generic-armv7-a -mthumb

// 对于 x86_64 架构
-target x86_64-linux-ohos --sysroot=${OHOS_NDK_HOME}/native/sysroot -D__MUSL__

当我们构建不同的架构产物时,其参数都需要使用对应架构的参数确保构建产物的完整性。

不出意外的话,我们就能够正常的运行我们构建的最终产物了。

17172972841514.jpg

通过CMake构建

第二种方案也是比较常见的方案,就是通过 CMake 或者 ninja 等构建工具先编译出对应架构下的产物,在开发 Native 模块的时候通过 link 等手段实现最终的调用。

这是理解起来最简单也是最常用的一种手段,我们在很多库上都可以使用这种方法。比如我们之前讲到的 OpenSSL,还有其他诸如:cronet、libcurl等等。

官方提供了构建脚本

这种一般都是基于对应库官方提供的一些预购建脚本来适配,这里我们以 OpenSSL 作为例子来讲解。

对于 OpenSSL 来讲,官方提供了基于 PERL 实现的构建脚本,我们只需要像上面的使用 C/C++ 构建的方式一样提供给构建脚本一定的构建环境即可。

核心的逻辑就是:提供 CC/CFLAGS 等标准环境变量,让构建流程使用我们自定义的构建器以及环境变量。

比如我们想要构建 arm64-v8a 架构的 OpenSSL,首先我们创建一个构建脚本如下所示:

#!/bin/sh

export CC=$OHOS_NDK_HOME/native/llvm/bin/clang \
export CXX=$OHOS_NDK_HOME/native/llvm/bin/clang++ \
export AR=$OHOS_NDK_HOME/native/llvm/bin/llvm-ar \
export AS=$OHOS_NDK_HOME/native/llvm/bin/llvm-as \
export LD=$OHOS_NDK_HOME/native/llvm/bin/ld.lld \
export STRIP=$OHOS_NDK_HOME/native/llvm/bin/llvm-strip \
export RANLIB=$OHOS_NDK_HOME/native/llvm/bin/llvm-ranlib \
export OBJDUMP=$OHOS_NDK_HOME/native/llvm/bin/llvm-objdump \
export OBJCOPY=$OHOS_NDK_HOME/native/llvm/bin/llvm-objcopy \
export NM=$OHOS_NDK_HOME/native/llvm/bin/llvm-nm \
export CFLAGS="-target aarch64-linux-ohos --sysroot=${OHOS_NDK_HOME}/native/sysroot -D__MUSL__" \
export CXXFLAGS="-target aarch64-linux-ohos --sysroot=${OHOS_NDK_HOME}/native/sysroot -D__MUSL__" \

我们可以看到我们定了非常多的环境变量,在构建执行我们需要将这些环境变量读写到我们的构建环境中即可,如下所示:

source ./arm64-v8a.sh && ./Configure linux-aarch64 

这样 OpenSSL 的构建脚本就在会构建的过程中使用我们提供的构建器以及各种工具和环境变量。最终构建出符合鸿蒙 arm64-v8a 架构的产物了。

对于全部的三架构构建完整示例,可以参考 ohos-openssl

这里提供的脚本预设环境变量从而改变构建逻辑是一个非常重要的手段对于我们的日常适配工作来说,我们需要着重学习这种方式。

自定义仓库

这种方式对于一些带有源码的项目非常友好,并且改动量可以非常少。只需要将我们的 CMakeLists.txt 做一些简单的修改适配即可。

这里我们通过两个例子来简单演示如何适配。

示例一:mmkv

MMKV 作为一个非常常用的移动端 KV 存储能力,在鸿蒙上也是完全支持的。目前 MMKV 已经提供了官方的鸿蒙适配包,不过我们依旧可以通过这个项目来简单演示我们如何在鸿蒙中使用 CMake 来适配项目。

在创建好一个 native 项目之后,我们将 mmkv 引入到项目中。对于 MMKV 来说适配相对简单,我们只需要做简单修改即可引入代码。

# the minimum version of CMake.
cmake_minimum_required(VERSION 3.4.1)
project(mmkv)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

# 新增mmkv内部构建
add_subdirectory(mmkv/Core)

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

add_library(entry SHARED hello.cpp)
# 产物依赖于 mmkv 最终构建的 core 
target_link_libraries(entry PUBLIC libace_napi.z.so core)

这样简单的修改即可在项目中开始使用 MMKV,效果如下所示:

17173316892345.jpg

示例二

对于 MMKV 这种其配置已经非常标准化我们可以开箱即用的来说,配置相对简单。但是某些时候其项目可能并不如我们所预期的那样,为了简化这些不太标准化的 CMake 配置在鸿蒙的构建逻辑这时候我们可能需要额外进行一些处理逻辑。

这时候我们介绍另一个重要的路径以及文件

$OHOS_NDK_HOME/native/build/cmake/ohos.toolchain.cmake

这个文件可以简单的理解成为一个鸿蒙系统下通过 CMake 构建的预设文件,绝大部分不支持鸿蒙系统的库在使用该文件构建的情况下都能够完成鸿蒙系统的适配。

在 CMakeLists.txt 文件中加入如下代码即可:

include($ENV{OHOS_NDK_HOME}/native/build/cmake/ohos.toolchain.cmake)

这种方式对于我们使用其他编辑器开发鸿蒙模块非常有用,比如我们新建一个模块基于 CMake 构建并且编辑器使用 Clion,当我们修改该配置之后,我们就可以使用 Clion 作为我们的开发者工具来进行 Cpp 模块的编写。

17173325845951.jpg

而有些场景我们则可以通过环境变量来实现 CMake 构建过程引入该文件的逻辑。

export CMAKE_TOOLCHAIN_FILE=$OHOS_NDK_HOME/native/build/cmake/ohos.toolchain.cmake

这个在某些 SDK 的构建脚本中非常有用,一个具体的例子可以参考这个 Pull Request

额外的一些东西

到这里基本上所有的基于已有系统的 C/C++ 模块在鸿蒙上面的适配操作基本上就简单介绍完了。当然这是笔者目前有过的一些操作,还有很多其他操作可以进行。

更多适配方案

比如 ohos-rs 团队另一位成员的作品:corrosion,该库能够让我们在 CMake 中直接使用 Rust 的包。

如果你还有更好的方法欢迎一起交流学习~

一些额外的注意项

在上面的内容我们,我们并没有讲的面面俱到,还有一些小的知识点在笔者的一些适配工作中有用到,这里也简单讲一下,以作备忘。

  1. 对于 CPU 指令集的适配
    在部分场景下,我们代码中可能使用到了类似于 SIMD 等 CPU 指令集,对于 arm64-v8a 架构因为默认开启支持,所以不需要额外进行处理。
    但是对于另外两个架构则需要进行一些额外的处理:

    • armeabi-v7a 则需要在CFLAGS参数中增加 -mfpu=neon -DHAVE_NEON
    • x86_64 则可以使用 SSE 来实现对应指令的支持 -msse4.1 -DHAVE_NEON_X86 -DHAVE_NEON
  2. armeabi-v7a
    该架构与市场上的其他系统或者 CPU 不太一样,鸿蒙的 armeabi-v7a 支持atomic操作,但是不需要在构建参数中额外新增-latomic参数,因为在 NDK 中并没有提供该动态链接库,如果新增该参数会导致该架构构建失败。
    一个真实的例子可以参考该 Pull Request

本文主要是简单讲解了下如何基于现行的鸿蒙 NDK 实现对已有的 C/C++ 代码进行适配到鸿蒙端,文中涉及到的方案基本上都是笔者自己亲自使用过的方案,因此在可行性上基本上没有太大问题,而易用性就有待商榷了。

不过本文也只是简单的进行抛砖引玉,如果社区或者读者有更加完美并且简单的适配方案可以一起学习交流,希望能够在这个过程中学到更多的知识,也希望本文对你有所帮助~