☰
c-ares 鸿蒙 PC 交叉编译移植与异步 DNS 解析实践
2026/10/21 19:40:39 网站建设 项目流程

1. 为什么要把 c-ares 搬进鸿蒙 PC

1.1 异步 DNS 解析库的选型逻辑

前段时间要给鸿蒙 PC 端的一个命令行工具补上 DNS 解析能力。业务场景不复杂:工具启动后要异步地解析几个服务域名,拿到地址再发起连接,中间不能因为网络抖动把主流程卡死。第一反应是直接调系统接口,但转念一想,这个工具的底层网络栈本来就是跨平台复用的,其他平台都统一用 c-ares 做异步解析,鸿蒙 PC 端如果单独开一条 getaddrinfo 同步分支,后续维护就是双倍成本。

c-ares 属于那种平时不起眼、但真到要用 DNS 时大家都会第一时间想起的库。它和系统自带的解析接口相比,最大的差异在"异步"两个字。getaddrinfo 这种接口是同步阻塞的,线程里一调下去,少则几十毫秒多则几秒,遇到 DNS 服务器超时更是直接挂在那一动不动。而 c-ares 是事件驱动的,它把整个 DNS 协议栈都实现在库内部,应用只需要把 socket 描述符交给自己的事件循环,靠 select/poll/epoll 去驱动查询进度,同一时刻可以并发发起几十个域名解析,非常适合命令行工具里那种"要解析一堆域名又不能等"的场景。

另一个选型理由是可移植性。c-ares 本身对 POSIX 接口的依赖非常克制,文件描述符、socket、select 这些基础 API 在鸿蒙 PC 的原生环境里都有对应实现。这一点很重要——移植一个库,最怕遇到系统差异巨大的场景,比如 Windows 那种把 socket 当句柄、又有 WSAStartup 的整套逻辑。c-ares 在类 Unix 环境下几乎是开箱即用,主要工作量都集中在交叉编译工具链的调通上,而不是改代码逻辑。

1.2 鸿蒙 PC 原生开发与交叉编译的现状

鸿蒙 PC 端的原生程序可以直接用 C/C++ 编写,通过 Native API 和系统交互,这一点和移动端上使用 Native 模块的方式一致。对做命令行适配的人来说,这意味着一件事:凡是能在 Linux 上用 CMake 编出来的 C/C++ 库,理论上在鸿蒙 PC 上都有移植路径,差别只在于工具链和系统库的替换。

我这里的做法是典型的交叉编译:在一台常规开发机上准备鸿蒙 Native SDK,用 SDK 自带的 clang 交叉编译器配合对应的 sysroot,生成目标设备上能跑的库和可执行文件。整个体系里最核心的就是三样东西:编译器、sysroot、CMake 工具链文件。编译器负责把源码变成目标架构的机器码;sysroot 负责在编译和链接时提供目标系统的头文件与库文件;工具链文件则把这些路径和规则告诉 CMake,让它以目标系统的方式去探测环境和生成构建系统。

理解了这个三角关系,后面所有的配置工作就都是往这三个地方填路径。官方 SDK 通常在 IDE 的 SDK 管理器里提供下载,也可以单独解包放在指定目录。我第一次配置时直接把 SDK 目录塞到环境变量里,后面所有脚本都从这个变量读取,好处是换一台机器、换一个 SDK 版本时,只需要改一个变量,不用到处翻命令行参数。

2. 交叉编译环境准备与工具链落位

2.1 获取 Native SDK 并确认目录结构

先从官方 IDE 的 SDK 管理器里找到 Native SDK 装到本地。装完后建议看一眼目录结构,这一步能省掉后面很多 debug 时间。一般来说 SDK 顶层会有几个按平台区分的子目录,真正用到的是 native 这一层,比如:

/path/to/ohos-sdk/<platform>/native ├── build-tools ├── llvm │ ├── bin │ └── lib ├── sysroot │ ├── include │ └── usr │ ├── include │ └── lib └── oh-uni-package.json

这个 native 目录几乎就是整个交叉编译的弹药库。llvm/bin 下面有 clang、clang++、llvm-nm、llvm-ar 这些工具;sysroot/usr/include 是鸿蒙侧的头文件;sysroot/usr/lib 里是 libc.so、libm.so 这类库的链接入口。我习惯先把 SDK 根目录设成环境变量:

export OHOS_SDK_HOME=/path/to/ohos-sdk/<platform>

后面所有 CMake 配置都直接用$ENV{OHOS_SDK_HOME}去拼路径,既清楚又好维护。SDK 版本的命名规则不同平台可能不一样,不用纠结,重点是 native 这一层的位置和里面的 llvm、sysroot 两个目录一定要能找到。

2.2 编写自定义 CMake 工具链文件

工具链文件是整个移植的灵魂。CMake 之所以能在各种环境下构建,就是因为它允许先通过工具链文件告诉编译器、链接器、目标系统都是什么样子,之后的探测和编译都基于这一套设定。我写了一个最小可用的 ohos.toolchain.cmake,实测在多个版本的 SDK 上都工作正常:

set(OHOS_SDK_ROOT "$ENV{OHOS_SDK_HOME}" CACHE PATH "OHOS SDK root") if(NOT OHOS_SDK_ROOT) message(FATAL_ERROR "Please set OHOS_SDK_HOME first") endif() set(CMAKE_SYSTEM_NAME OHOS) set(CMAKE_SYSTEM_VERSION 1) set(TARGET_ARCH "aarch64-linux-ohos") set(CMAKE_C_COMPILER "${OHOS_SDK_ROOT}/native/llvm/bin/clang") set(CMAKE_CXX_COMPILER "${OHOS_SDK_ROOT}/native/llvm/bin/clang++") set(CMAKE_SYSROOT "${OHOS_SDK_ROOT}/native/sysroot") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --target=${TARGET_ARCH}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --target=${TARGET_ARCH}") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --target=${TARGET_ARCH}") set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

几个关键点拆开说。CMAKE_SYSTEM_NAME OHOS这一行非常重要,它告诉 CMake 当前不是宿主系统,而是交叉编译到目标系统。CMAKE_C_FLAGS里的--target=aarch64-linux-ohos是 clang 认识的目标三元组,指明 CPU 架构和操作系统 ABI。如果没有这一行,clang 会默认按宿主机的架构去解析头文件,结果就是 sysroot 里的头文件因为架构不匹配被拒绝。CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY是用来绕开交叉编译中一个经典陷阱的,后面踩坑部分会详细说。CMAKE_FIND_ROOT_PATH_MODE_*设置为 ONLY,是为了防止 CMake 去宿主机的 /usr 目录里找库和头文件,避免把宿主环境的 glibc 头文件混进来。

2.3 验证工具链能否编译 Hello World

工具链文件写完之后,不要急着编 c-ares,先写一个最简单的 C 程序验证整个链路。这一点很朴素也很重要,它能帮你把问题切成两块:如果 Hello World 都过不了,那是工具链的问题;如果 Hello World 能过只有 c-ares 编译失败,那是库构建配置的问题。我当时的验证方式:

mkdir -p hello && cd hello cat > main.c <<'EOF' int main(void) { return 0; } EOF cmake -B build \ -DCMAKE_TOOLCHAIN_FILE=../ohos.toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release cmake --build build -j8 file build/hello

file输出只要显示目标架构是 aarch64、且链接到的是 libc.so 而不是 x86_64 的库,就说明工具链基本工作了。这里有个细节:Hello World 编译通过只代表编译器和 sysroot 头文件匹配,不代表最终链接没问题。静态库的链接问题是到应用打包那一步才会暴露的,所以后面编完 c-ares 还要专门做一次链接验证。

3. c-ares 的交叉编译配置与完整步骤

3.1 关键的 CMake 参数与取舍

c-ares 的官方仓库里维护着完整的 CMake 构建逻辑,这对交叉编译非常友好,因为它不像 autotools 那样依赖在宿主机上跑 configure 脚本做一堆运行时探测。下载 v1.28.1 版本的源码包,解压后直接进入目录配置即可。

我在配置时用的参数和使用理由如下表:

CMake 参数取值实际作用
CMAKE_TOOLCHAIN_FILEohos.toolchain.cmake指定交叉编译工具链
CMAKE_BUILD_TYPERelease开启优化,控制产物体积
CARES_STATICON产出静态库,方便命令行工具直接整包链接
CARES_SHAREDOFF不产出动态库,避免在目标设备上多一个 .so 依赖
CARES_BUILD_TOOLSOFF不需要 adig、acountry 这些命令行示例
CARES_BUILD_TESTSOFF测试代码交叉编译后也跑不了,编了纯浪费时间
CMAKE_POSITION_INDEPENDENT_CODEON生成位置无关的静态库,便于链接进 PIE 可执行文件
CMAKE_INSTALL_PREFIX$(pwd)/dist产物统一装到本地目录,方便后续拷贝
CARES_SYMBOL_HIDINGON做符号裁剪,只导出公开 API,减小库体积

静态库和动态库的选择值得多说一句。命令行工具最终要部署到鸿蒙 PC 上,动态库虽然体积小,但是要额外处理库文件路径和依赖关系,部署成本高。静态库直接把代码并进可执行文件里,运行时不依赖任何外部 .so,对命令行生态最友好。c-ares 的静态库在没有特殊依赖的情况下可能有三四百 KB,对现代设备来说完全可以接受。

3.2 一次跑通编译、安装、打包

进入源码目录后执行的完整命令如下:

cmake -B build \ -DCMAKE_TOOLCHAIN_FILE=$(pwd)/ohos.toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DCARES_STATIC=ON \ -DCARES_SHARED=OFF \ -DCARES_BUILD_TOOLS=OFF \ -DCARES_BUILD_TESTS=OFF \ -DCARES_SYMBOL_HIDING=ON \ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ -DCMAKE_INSTALL_PREFIX=$(pwd)/dist cmake --build build -j$(nproc) cmake --install build

这里把工具链文件放在源码目录外面是为了隔离源码和解耦,后面再移植其他库时可以复用同一个文件。编译过程一般一两分钟内能完成,不会有太多意外。安装完成后,dist 目录下会生成 include、lib 两个子目录,include 里是 ares.h 和相关头文件,lib 里是 libcares.a。

3.3 检查产物的头文件和链接信息

编译完成不等于万事大吉,需要确认产物的架构和符号信息是正确的。这一步容易被新手跳过,但它能提前暴露很多集成期才出现的问题。检查命令:

file dist/lib/libcares.a # 期望输出里包含 aarch64 相关架构信息 ${OHOS_SDK_HOME}/native/llvm/bin/llvm-nm dist/lib/libcares.a | head -30 # 或者用 ${OHOS_SDK_HOME}/native/llvm/bin/llvm-ar t 只看成员名

file会显示静态库的架构信息,如果显示的是 x86_64,说明编译器忘了传--target,target 那一步配置有问题。另外可以检查头文件的 API 版本和库的版本标记,c-ares 的 ares.h 里会有版本宏,比如ARES_VERSION_MAJOR,确保你的源码和库是一起拉取的同一次发布,避免 API 签名对不上。

4. 移植路上的坑与排查思路

4.1 try_compile 次次失败,原来卡在可执行链接

第一次配置 c-ares 时,我遇到的报错非常统一:CMake 在读取工具链文件后检查 C 编译器是否可用,结果永远失败。控制台只给了一句 C compiler cannot be used,没有更多细节,当时第一反应是编译器路径写错了,反复检查路径、环境变量,折腾了半小时也没解决。

后来打开 CMakeError.log 才看到真实原因:CMake 默认的 try_compile 会尝试生成一个完整的可执行文件,而交叉编译环境下 clang 链接可执行文件时,需要把--target和 sysroot 都传完整,只要有一项缺失,链接阶段就会报一堆找不到链接的符号。问题不在于编译器本身不能用,而是"尝试编译"这个动作默认生成可执行文件,在交叉环境里天然容易失败。

解决办法就是工具链文件里的那一行set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)。它的作用是把 CMake 的探测试探目标从可执行文件改成静态库,静态库的生成不需要链接具体系统库,一步就能通过。这个配置几乎是所有 clang 交叉编译场景的必选项,不只是鸿蒙,其他嵌入式环境也一样踩。

4.2 链接阶段找不到 libc 与符号

工具链通过后,c-ares 编译非常顺利,但在把它集成到真正的命令行工具里、链接成最终可执行文件时,又出现了新的报错。错误信息里明确说找不到以__cxa_atexit、__aeabi_memcpy为代表的数个 C/C++ 运行时符号。这些符号看上去熟悉又陌生,因为它们在 glibc 的环境里是自动提供的,而鸿蒙 PC 的 sysroot 里虽然也有 libc,但它用的实现和符号导出的方式与 glibc 不完全一样。

排查过程比较曲折。我先检查了链接命令,发现 CMake 虽然指定了CMAKE_SYSROOT,但传给链接器的搜索路径里并没有包含 sysroot 的 lib 目录。原因是我的工具链文件只设置了CMAKE_SYSROOT,却没有强制 CMake 把 sysroot 的 lib 目录加入链接搜索路径。后来在链接参数里显式追加:

set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --target=${TARGET_ARCH} -L${OHOS_SDK_ROOT}/native/sysroot/usr/lib")

再重新链接,符号错误立刻消失。这个坑的教训是:交叉编译时,CMAKE_SYSROOT影响的主要是头文件搜索路径,链接库路径有时候必须手动加上。不同版本的 CMake 对 sysroot 的 lib 目录处理行为不完全一致,显式指定是最稳妥的方式。

4.3 DNS 查询总是超时,问题出在轮询不推进

工具链通了、库也链接上了,接下来就是把 DNS 查询真正跑起来。我写了一个简单的测试程序,把 c-ares 初始化后发起一次gethostbyname查询,等回调执行。程序每次运行时都能正常启动,也正常退出,但回调永远不触发,直到整个事件循环超时结束,DNS 查询始终拿不到结果。

一开始怀疑是文件描述符被防火墙挡了,又怀疑是 DNS 服务器配置问题。后来仔细读代码,发现问题出在事件循环本身。我犯了最经典的错误:只调用了ares_fds去获取 socket 文件描述符并交给 select,却忽略了ares_timeout的返回值。select 的最后一个参数被传成了 NULL,表示无限等待。于是一旦 DNS 响应稍微慢一点,程序就一直在 select 里干等,c-ares 内部的重传逻辑根本没机会执行。

修正后的循环逻辑很简单:每次迭代都用ares_timeout(channel, NULL, &tv)获取当前最接近的超时时间,把它传给 select;select 返回后再调用ares_process让 c-ares 处理读写事件。只要按这个顺序来,异步查询就能正常推进。这个坑看起来很基础,但在写 Demo 时特别容易犯,尤其是之前写惯同步代码的人,总会下意识地把 select 的超时参数写成 NULL。

5. 写一个可跑的解析 Demo 并验证

5.1 最小事件循环驱动 c-ares 异步请求

为了让整个移植链路有一个可复现的验证入口,我把测试程序整理成了一个最小 Demo。它做的事只有三件:初始化库和 channel、发起一条 A 记录查询、用一个 select 事件循环驱动查询直到回调返回。完整代码如下:

#include <stdio.h> #include <string.h> #include <sys/select.h> #include <sys/time.h> #include <arpa/inet.h> #include <netinet/in.h> #include <ares.h> static volatile int g_done = 0; static void dns_callback(void *arg, int status, int timeouts, struct hostent *host) { (void)arg; (void)timeouts; if (status != ARES_SUCCESS) { fprintf(stderr, "DNS query failed: %s\n", ares_strerror(status)); g_done = 1; return; } printf("resolved name: %s\n", host->h_name); for (int i = 0; host->h_addr_list[i] != NULL; i++) { char buf[INET6_ADDRSTRLEN] = {0}; inet_ntop(host->h_addrtype, host->h_addr_list[i], buf, sizeof(buf)); printf(" address[%d]: %s\n", i, buf); } g_done = 1; } int main(int argc, char **argv) { ares_library_init(ARES_LIB_INIT_ALL); ares_channel_t *channel = NULL; if (ares_init(&channel) != ARES_SUCCESS) { fprintf(stderr, "ares_init failed\n"); return 1; } const char *name = (argc > 1) ? argv[1] : "www.example.com"; ares_gethostbyname(channel, name, AF_INET, dns_callback, NULL); while (!g_done) { fd_set rfds, wfds; struct timeval tv, *tvp = NULL; FD_ZERO(&rfds); FD_ZERO(&wfds); int nfds = ares_fds(channel, &rfds, &wfds); if (nfds == 0) { break; } tvp = ares_timeout(channel, NULL, &tv); int r = select(nfds, &rfds, &wfds, NULL, tvp); if (r < 0) { break; } ares_process(channel, &rfds, &wfds); } ares_destroy(channel); ares_library_cleanup(); return 0; }

这段代码用nfds == 0作为 loop 的安全出口,避免查询已经失败时无限空转。它没有依赖任何第三方事件循环库,select 是 POSIX 基础接口,在鸿蒙 PC 上可以直接编译运行。实测解析www.example.com能正确回调,地址列表能正常打印出来,说明 c-ares 的协议栈在目标环境下工作正常。

5.2 把它接进鸿蒙 PC 应用的构建里

Demo 跑通之后,就要把它接到真实的命令行工具工程里去了。如果你的鸿蒙 PC 应用用 CMake 组织原生代码,接入方式非常直接:

add_executable(my_cli src/main.c) target_include_directories(my_cli PRIVATE path/to/c-ares/dist/include) target_link_libraries(my_cli PRIVATE path/to/c-ares/dist/lib/libcares.a)

把头文件路径指向编译产物里的 include 目录,再把静态库路径放进 target_link_libraries。因为前面编译时开了CARE_SYMBOL_HIDING和 Release 优化,链接后的最终可执行文件里只保留 c-ares 的公开 API,不会把一堆内部符号带进来。

如果程序跑在鸿蒙应用的沙箱环境里,还要记得在 module.json5 中声明网络权限。不加权限的话,DNS 请求会在系统侧被拦掉,表现就是每次解析都超时。这一项很容易被忽略,因为命令行工具在自己的宿主环境下跑时通常已经有网络权限了。

"requestPermissions": [ { "name": "ohos.permission.INTERNET" } ]

6. 沉淀下来的移植经验

整个 c-ares 移植过程前后花了两天时间,真正被卡住的基本都在工具链和事件循环这两个点上。工具链的问题解决之后,c-ares 本身的编译几乎是一气呵成,这得益于它干净的平台封装和成熟的 CMake 支持。如果有读者也要在鸿蒙 PC 或者类似的 Clang/LLVM 系交叉环境里移植 C/C++ 库,这套流程可以直接按序复用。

我个人的体会是,交叉编译的难点其实不在"库本身能不能编译"上。c-ares 在几十个平台上都有构建支持,它的 CMake 脚本里已经处理了多数平台差异。难点更多的是在"如何让构建系统完整地看到目标环境"上:工具链文件是不是面面俱到、sysroot 路径是不是真的传给了链接器、CMake 的探测试探有没有被交叉环境干扰。把这三件事做扎实,移植绝大多数 POSIX 风格的库都会很顺。

最后分享一个肉眼可见的收益:因为选了静态库加 Release 裁剪,最终命令行工具的产物体积和启动速度都让人满意。如果后续还想继续瘦身,可以在构建命令里追加-ffunction-sections -fdata-sections,链接时配合-Wl,--gc-sections,c-ares 里没用到的函数会被链接器直接丢弃。我试过之后,静态库产物又缩小了一百多 KB,对于命令行工具来说很划算。把这套工具链模板和参数固化下来,下次再遇到其他依赖库的移植,就只需要改个库名重跑一遍了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询