资讯动态

鸿蒙PC上交叉编译c-ares:工具链配置与异步DNS解析实战

发布时间:2026/10/21 10:06:42 来源:尧图企业网站定制
做鸿蒙 PC 方向的网络组件适配时第一块硬骨头往往不是界面也不是沙箱而是像 c-ares 这种“又小又挑剔”的 C 库。c-ares 用纯 C 实现了完整的异步 DNS 解析能力几乎只跟 socket、标准 C 库打交道却承担了 curl、Node.js 等大量开源项目底层的域名解析职责。想在鸿蒙 PC 上跑起命令行网络工具或者复用已有的 C/C 网络栈把 c-ares 交叉编译成“鸿蒙户口”是绕不开的第一步。这篇文章是一份可复现的实战记录使用 OpenHarmony SDK 自带的 clang 工具链通过 CMake 的 ohos.toolchain.cmake 把 c-ares 编译成鸿蒙 PCx86_64可用的静态库再写一个最小异步解析程序推送到鸿蒙 PC 上跑通。整个过程我踩了四五次坑包括工具链 target 配错、resolv.conf 读不到、静态库和动态库选择失误等等。这篇文章会把能绕开的坑先给你标出来适合正在做鸿蒙原生库移植的开发者也适合想彻底弄懂交叉编译原理的技术爱好者。1. 为什么要在鸿蒙 PC 上移植 c-ares1.1 c-ares 在网络栈里的角色传统系统调用getaddrinfo是同步阻塞的在网络抖动时会卡住整个调用方。c-ares 的思路是把 DNS 查询拆成 socket 事件放进一个事件循环里逐步处理查询没完成时立刻返回控制权从机制上解决了阻塞问题。它还支持一次并发发起多个查询、UDP 失败自动切 TCP、EDNS0 扩展以及较新版本里的 DoHDNS over HTTPS能力。生态上的地位同样重要。curl 编译时可以直接选用 c-ares 作为域名解析后端配置项就是--enable-aresNode.js 的dns.resolve系列 API 底层也大量依赖 c-ares 的认证逻辑。换句话说这个库不只是一段“解析域名的代码”它已经是很多基础设施项目里默认的异步解析实现。想往鸿蒙 PC 上迁命令行网络工具大概率迟早要跟它碰面。从移植角度看c-ares 是典型的“麻烦库”它不依赖任何第三方动态库不用 OpenSSL不用 JSON全部源码清清爽爽但偏偏要处理操作系统相关的 socket 行为、文件描述符、resolv.conf 格式和线程安全。这种库交叉编译起来最能暴露工具链配置里的各种隐性差异。1.2 移植方案的取舍为什么必须交叉编译最直觉的“拷贝大法”不建议尝试。把开发机上基于 glibc 编译出来的 libcares.so 直接丢到鸿蒙 PC 上大概率会碰到一连串问题鸿蒙 PC 的 C 库是 musl 定制版跟 glibc 在头文件声明和 ABI 细节上有差异动态加载器的搜索路径和格式也不一样。想真正跑起来必须拥有一个面向鸿蒙目标环境的编译工具链。可行的路线有三条。第一条是在鸿蒙 PC 上本地安装编译器直接编问题是设备端通常没有现成的完整编译环境装工具链本身就非常折腾。第二条是在 Linux 开发机上用 OpenHarmony SDK 提供的 NDK 工具链做交叉编译这也是官方硬件厂商和开源社区普遍采用的路线。第三条是手工敲 clang 参数完全不用 CMake 封装适合用最小例子理解原理但不适合批量移植。最后在库形态上我推荐优先编译静态库。鸿蒙 PC 还没有像传统 Linux 发行版那样成熟的包管理器动态库的部署、签名、版本管理都需要额外考虑。静态库直接链接进可执行文件整个命令行工具就只有一个二进制推送到设备上就能跑省掉很多运行时依赖问题。代价是可执行文件体积会多出几百 KB这对一次性的系统命令行工具来说完全值得。2. 编译环境准备把鸿蒙工具链理顺2.1 SDK 获取与三个关键目录交叉编译 c-ares 之前先把鸿蒙 Native 开发套件准备好。开发 IDE 自带的 SDK 目录里已经包含了编译所需的全部组件也可以用开源社区分发的 SDK 包。安装完成后建议先用环境变量统一指向 SDK 根目录后面所有命令都复用这个变量减少拼错路径的概率。export OHOS_SDK/your/actual/ohos-sdk/path设置好后重点确认三个目录是否齐全。第一个是编译器目录$OHOS_SDK/native/llvm/bin里面应该有 clang、clang、llvm-ar 等工具第二个是系统头文件与库的根目录$OHOS_SDK/native/sysroot交叉编译时找头文件、找 C 库全依赖它第三个是官方 CMake 工具链文件$OHOS_SDK/native/build/cmake/ohos.toolchain.cmake后面 CMake 方案直接用它即可。ls $OHOS_SDK/native/build/cmake/ ls $OHOS_SDK/native/llvm/bin/ | head -20 ls $OHOS_SDK/native/sysroot/如果这三个目录都有内容说明 SDK 基本可用。版本上建议用新一点的 SDK 版本旧版 NDK 的 sysroot 在某些网络头文件上不完整容易在编译期报稀奇古怪的错误。工具链一旦选定了后面整条编译链都用同一个 SDK不要中间混用新旧版本。2.2 ohos.toolchain.cmake 内部做了什么很多第一次接触的人会好奇为什么不直接给 CMake 传-DCMAKE_C_COMPILERclang就完事了因为交叉编译不是只换一个编译器那么简单。同一个 clang 在不同--target参数下产生的代码是完全不同的它还必须有 sysroot 隔离系统头文件和系统库避免误用开发机上的 glibc 头文件。ohos.toolchain.cmake把这堆杂事一次性封装好了。它会把编译器指向 SDK 内的 clang把 sysroot 指向native/sysroot并根据你传入的OHOS_ARCH参数设置对应的 target 三元组。比如x86_64会映射成x86_64-linux-ohosarm64-v8a会映射成aarch64-linux-ohos。此外它还设置了查找根路径保证 CMake 在 find_package 时不会跑到宿主机目录里去瞎找库。理解这一层之后遇到“编译出来怎么是 x86_64 Linux 的可执行文件”“怎么链接到宿主机 libc 了”这类问题时你就知道多半是 sysroot 没生效或者 target 没传对而不是工具链坏了。2.3 目标架构与设备连接确认鸿蒙 PC 常见目标架构是 x86_64不过也有基于 ARM 处理器的主机形态。动手编译前先确认设备到底是什么架构可以省掉一次白干的时间。通过 hdc华为设备连接工具的通用命令连上设备后直接执行hdc list targets hdc shell uname -muname -m输出x86_64时CMake 里的OHOS_ARCH就填x86_64输出aarch64时填arm64-v8a。如果 hdc 命令不在 PATH 里去 SDK 的toolchains目录下找或者把该目录加进 PATH。这一步同时也验证了设备连接是通的。后面推送可执行文件、查看运行输出全靠 hdc如果这里就卡住先排查开发者模式的 USB 调试开关再排查 hdc 版本与设备系统版本的匹配情况。3. c-ares 交叉编译全流程3.1 源码获取与版本管理c-ares 的源码在托管平台上有现成仓库直接克隆即可。为了可复现我强烈建议锁定到一个具体的 release tag而不是直接用 master 分支。master 上随时可能改构建脚本今天能过跑的配置明天可能就挂排错时会非常头疼。git clone https://github.com/c-ares/c-ares.git cd c-ares git checkout v1.28.1如果更习惯用 release 压缩包下载 tar 包后也请留意包内是否自带 configure 和 CMakeLists.txt。官方 release 包一般都会带上这两个省去执行 buildconf 的步骤。从 git 克隆的代码则必须先跑./buildconf生成 configure这个差异会造成“明明照着教程敲为什么说找不到 configure”的困惑。3.2 CMake 交叉编译推荐路线CMake 方案是目前最顺手的路线因为官方 toolchain 文件已经把交叉编译的细节处理好了。下面这组命令可以直接复制使用注意把OHOS_SDK换成你自己的路径。export OHOS_SDK/your/actual/ohos-sdk/path export TARGET_ARCHx86_64 rm -rf build-cross install cmake -S . -B build-cross \ -DCMAKE_TOOLCHAIN_FILE$OHOS_SDK/native/build/cmake/ohos.toolchain.cmake \ -DOHOS_ARCH$TARGET_ARCH \ -DCMAKE_INSTALL_PREFIX$PWD/install \ -DCMAKE_BUILD_TYPERelease \ -DCARES_STATICON \ -DCARES_SHAREDOFF \ -DCARES_BUILD_TOOLSON \ -DCARES_BUILD_TESTSOFF cmake --build build-cross -j$(nproc) cmake --install build-cross几个关键选项的含义值得细说。-DOHOS_ARCHx86_64决定了最终代码运行在什么处理器上前面确认设备架构就是为了这一步。CARES_STATICON与CARES_SHAREDOFF组合起来就是只要静态库不要动态库。CARES_BUILD_TOOLSON会额外生成 adig、ahost、acountry 三个命令行小工具这三兄弟既是现成的验证工具也是理解 c-ares API 的活教材。CARES_BUILD_TESTSOFF主要是为了省事交叉编译环境里跑宿主机测试程序意义不大还会制造一推潜在的环境依赖。有些教程里还会加-DOHOS_STLc_shared这个选项是给 C 运行时准备的c-ares 本身是纯 C 库不加也没问题。如果以后要在 c-ares 上层接 C 组件再统一考虑 STL 的运行时形态即可。3.3 configure 交叉编译备选路线有些开源项目的构建体系还是 autotools没有 CMakeLists或者你需要在老版本 c-ares 上做验证那就必须走 configure 路线。core 的反直觉点在于你必须手动把--target和 sysroot 消息喂给 clang否则它会默认按宿主机编译。cd c-ares ./buildconf # 如果是 git 源码需要生成 configurerelease 包可跳过 export OHOS_SDK/your/actual/ohos-sdk/path export CC$OHOS_SDK/native/llvm/bin/clang --targetx86_64-linux-ohos export CFLAGS--sysroot$OHOS_SDK/native/sysroot -O2 export LDFLAGS--sysroot$OHOS_SDK/native/sysroot ./configure --hostx86_64-linux-ohos \ --buildx86_64-pc-linux-gnu \ --disable-shared \ --enable-static \ --prefix$PWD/install make -j$(nproc) make install这里最容易搞混的是--host和--build。--build是指当前这台开发机的环境--host是指编译出来的程序将要运行的环境。我们要让程序跑在鸿蒙 PC 上所以--hostx86_64-linux-ohos编译动作发生在 Linux 开发机上所以--buildx86_64-pc-linux-gnu。--target在这种场景下一般不用管它是为“编译器要生成哪个目标平台的代码”这种交叉编译器本身而设计的参数普通库编译用不到。autotools 在 configure 阶段会执行一系列诡异的小测试程序以此推断系统能力。这些测试程序是编译出来在当前机器上运行的所以 configure 本身要能在这个开发机上执行真正生成的库代码才是给鸿蒙用的。这个双重身份是交叉编译最容易概念混淆的地方理解透了你排错就胸有成竹了。3.4 产物检查清单编译安装完成后不要急着推设备先在开发机上做两件快速检查。第一件是用file命令确认产物里没有混入宿主机代码第二件是看看安装目录结构是否完整。file install/lib/libcares.a find install -type f | sortlibcares.a是 ar 归档格式里面的成员是若干.o目标文件。想更彻底地确认成员架构可以用 llvm-ar 列出成员挑一个出来再跑file。实际上最可靠的验证方式还是后面推送到鸿蒙 PC 上跑一次开发机上的检查只是提前挡掉一些低级错误。安装目录里还会看到include/ares.h、include/ares_version.h这些头文件。后续写代码时编译器的头文件搜索路径必须指向这个 install/include否则会碰到“找不到 ares.h”的问题。4. 在鸿蒙 PC 上验证从最小 Demo 到命令行工具4.1 最小异步 DNS 解析 Demo编译通过只是第一步真正让人放心的是在鸿蒙 PC 上跑通一个实际的 DNS 解析。下面这个 demo 展示了 c-ares 最核心的异步流程初始化库、创建 channel、提交解析请求、进入事件循环、在回调里拿到 IP。#include stdio.h #include stdlib.h #include string.h #include netdb.h #include netinet/in.h #include arpa/inet.h #include sys/select.h #include ares.h static void on_result(void *arg, int status, int timeouts, struct hostent *he) { (void)arg; (void)timeouts; if (status ! ARES_SUCCESS) { printf(resolve failed: %s\n, ares_strerror(status)); return; } printf(official name: %s\n, he-h_name); for (char **p he-h_addr_list; *p ! NULL; p) { char ip[INET6_ADDRSTRLEN] {0}; inet_ntop(he-h_addrtype, *p, ip, sizeof(ip)); printf( - %s\n, ip); } } int main(void) { ares_channel channel; fd_set rfds, wfds; struct timeval tv, *tvp; int nfds; if (ares_library_init(ARES_LIB_INIT_ALL) ! ARES_SUCCESS) { fprintf(stderr, ares_library_init failed\n); return 1; } if (ares_init(channel) ! ARES_SUCCESS) { fprintf(stderr, ares_init failed\n); return 1; } const char *dns getenv(DNS_SERVER); if (dns *dns) { if (ares_set_servers_csv(channel, dns) ! ARES_SUCCESS) { fprintf(stderr, set dns server %s failed\n, dns); } } printf(resolving example.com ...\n); ares_gethostbyname(channel, example.com, AF_INET, on_result, NULL); for (;;) { FD_ZERO(rfds); FD_ZERO(wfds); nfds ares_fds(channel, rfds, wfds); if (nfds 0) break; tvp ares_timeout(channel, NULL, tv); if (select(nfds, rfds, wfds, NULL, tvp) 0) break; ares_process(channel, rfds, wfds); } ares_destroy(channel); ares_library_cleanup(); return 0; }几个细节可能决定成功与失败。ares_fds返回的是事件循环需要的最大文件描述符加一可以直接送给 select 作为第一个参数。每次循环开始都要FD_ZERO因为 select 会改写 fd_set。ares_timeout给出下一次事件的最长等待时间不设的话遇到 UDP 丢包可能会一直卡着。设置 DNS 服务器的代码很有必要。鸿蒙 PC 上的/etc/resolv.conf经常不存在或者内容为空c-ares 自己读不到服务器列表查询就会直接失败。demo 里通过环境变量DNS_SERVER传入服务器地址比如192.168.1.1这比硬编码灵法得多。4.2 交叉编译 Demo 并推送到设备写好的 demo 用一条 clang 命令就能完成交叉编译。注意源文件在前、静态库在后这是链接器处理静态库的顺序要求。头文件路径要指到 install 目录不能指到宿主机系统路径。$OHOS_SDK/native/llvm/bin/clang \ --targetx86_64-linux-ohos \ --sysroot$OHOS_SDK/native/sysroot \ -I./install/include \ dns_demo.c ./install/lib/libcares.a \ -o dns_demo这里没有加-lpthread因为 c-ares 在鸿蒙的 musl libc 下通常不需要显式链接 pthread。如果你自己动手时遇到undefined reference to pthread_*再加也不迟加了不会有什么副作用。推送到鸿蒙 PC 上运行hdc file send dns_demo /data/local/tmp/ hdc shell chmod 755 /data/local/tmp/dns_demo hdc shell DNS_SERVER192.168.1.1 /data/local/tmp/dns_demo把192.168.1.1换成你设备当前网络环境里实际可用的 DNS 服务器。正常输出类似resolving example.com ... official name: example.com. - 93.184.216.34说明静态库在鸿蒙 PC 上完全可用。如果看到的是 “resolve failed”去参照第 5 部分的排查表多数是服务器列表没设置对或者设备网络本身就不通。4.3 顺带验证自带工具 adig编译 c-ares 时如果开启了CARES_BUILD_TOOLSONinstall/bin 目录下会多出 adig、ahost、acountry 三个命令行工具。这些工具可以直接作为实用程序使用不需要再写任何代码。hdc file send install/bin/adig /data/local/tmp/ hdc shell chmod 755 /data/local/tmp/adig hdc shell /data/local/tmp/adig example.com需要注意adig 会沿用 c-ares 默认配置也就是读取系统的/etc/resolv.conf。鸿蒙 PC 上如果该文件缺失或内容为空adig 会报 DNS 服务器故障。这不代表库有问题只是没给它喂服务器。想知道设备上到底有没有 resolv.conf可以先执行hdc shell cat /etc/resolv.conf没有的话最干净的做法是在自己的工具里显式调用ares_set_servers_csv。这也再次印证了为什么在代码里显式管理 DNS 服务器列表是鸿蒙移植里的关键经验而不是锦上添花。5. 高频踩坑实录与排障速查5.1 踩坑速查表折腾 c-ares 交叉编译的这段时间我整理了一张高频率问题的速查表按症状、根因、对策三段排好。遇到问题时先查表比重新搜遍网页高效得多。症状根因对策编译时找不到 system headers比如stdlib.h not foundsysroot 没传递到编译器在 CFLAGS 里加--sysroot$OHOS_SDK/native/sysroot链接时找不到-lc编译器拿不到鸿蒙的 C 库确认 sysroot 路径正确或改用官方 toolchain 文件生成的可执行文件在鸿蒙 PC 上报Exec format errortarget 未设为 x86_64-linux-ohos编出了宿主机 ELF检查 clang 命令里的--target或 CMake 里的 OHOS_ARCHCMake 缓存里架构串掉同一个 build 目录换过 OHOS_ARCH删除 build 目录重新配置不要复用缓存adig 报 DNS 服务器故障设备没有 /etc/resolv.confc-ares 服务器列表为空代码里调ares_set_servers_csv或构造可用的 resolv.conf动态链接时报找不到 libcares.so只推了可执行文件没推 .so或路径没设置推送 .so 到应用目录并设置 LD_LIBRARY_PATH或改成静态链接链接时undefined reference to pthread_*部分环境要求显式链接线程库编译命令追加-lpthread解析结果总是超时或 SERVFAILUDP 53 端口不通DNS 地址不可达先用 ping 或 curl 验证设备网络再换可用的 DNS 地址表中的前四行都是工具链层面的问题占了实际操作中百分之七十的出错比例。尤其要注意最后一行DNS 解析失败不一定是 c-ares 库出了问题先确认设备本身能不能访问外网否则很容易白调半天。5.2 三个必须记住的实操经验第一能静态就不要动态。命令行工具在鸿蒙 PC 上部署最怕动态库依赖一次推送一个二进制最省心。真需要动态库时一定要把 .so 一起推过去并且明确知道运行时搜索路径是什么别指望系统帮你找到。第二每次换 SDK 版本或换架构时把 build 目录彻底删掉。CMake 的交叉编译缓存非常记仇残留的旧配置会让新架构编译产生各种行为异常。重新建目录配置虽然多花两分钟但比排查诡异问题划算得多。第三要把“交叉编译环境”和“运行环境”彻底分开理解。configure 里探测用的临时程序是给开发机跑的生成的库是给鸿蒙 PC 跑的两者不能混。排错时只要分清“当前这个报错发生在编译阶段还是运行阶段”问题的定位范围立刻缩小一大半。6. 后续扩展让 c-ares 真正发挥价值6.1 给 curl 接上 c-ares如果你在鸿蒙 PC 上准备编译 curl可以把 c-ares 作为解析后端。传统同步解析在网络异常时会让 curl 整体阻塞换成 c-ares 后域名解析变成了事件驱动的一部分多请求场景下的体验会更好。编译 curl 时在 configure 参数里加上--enable-ares就行。如果 c-ares 不是装在全系统路径用--with-ares/path/to/install指定安装前缀。这样 curl 会去指定目录下找头文件和库最终生成的 curl 就带上了异步解析能力。但要注意这条路线需要你的 c-ares 安装位置对 curl 的构建系统可见头文件和库路径都要对得上。6.2 基于 ares_query 做定制 DNS 工具c-ares 不只是能做 A 记录解析它还提供ares_query、ares_search这类底层接口可以自己拼装 DNS 问题、解析任意类型记录。利用这些接口可以在鸿蒙 PC 上做一个完全可控的 DNS 查询工具比如批量解析一批域名、统计每个域名的解析耗时、输出 TTL 信息等等这些是系统自带工具给不了的。实现套路是提交查询后用第 4 章那套事件循环驱动在回调里拿到完整 DNS 响应缓冲区再手工解析答案段。数据包结构不复杂先解析头部拿到 ANCOUNT 后按记录类型依次跳转。这个做法对想深入研究 DNS 协议的人来说是很合适的练手项目。6.3 接入鸿蒙应用框架的路径嵌入式网络组件最终要服务于上层应用。c-ares 本身是 C 库鸿蒙应用层通常通过 NAPI 把 C/C 能力暴露给 JavaScript。这里核心的问题是事件循环怎么设计c-ares 需要事件驱动不能直接在 JS 主线程里死循环 select。常见做法是把它放进一个独立的 native 线程或 worker 线程解析完成后通过回调通知 JS 层。另外一个方案是把 c-ares 产生的 fd 注册到应用自己的事件循环里比如鸿蒙侧类似的 epoll 循环这样不会额外开线程资源开销更小。代价是代码复杂度高一些需要处理 fd 生命周期、channel 销毁时机等细节。不管哪种方式channel 本身的线程安全问题都要注意多线程共享同一个 channel 时必须加锁或者给每个线程创建独立 channel。6.4 一点个人体会这次 c-ares 移植让我最感慨的不是命令本身而是“宿主与目标边界”的环境认知。交叉编译最麻烦的不是记忆参数而是分清什么东西跑在开发机上什么东西跑在鸿蒙 PC 上。configure 探测程序跑在开发机、生成的库跑在鸿蒙、resolv.conf 是鸿蒙在读取、编译缓存是开发机在维护——这几个概念理顺以后很多报错一眼就能猜出原因。如果准备在鸿蒙 PC 上做大量本地库移植我建议先拿 c-ares 这种依赖少、逻辑清晰的纯 C 库试手。先把工具链、交叉编译、设备验证这套流程完整走通一遍后面再碰那些依赖 OpenSSL、zlib 的复杂库至少不会被基础环节绊倒。移植库本质上是在跟“环境一致性”作对而交叉编译只是把这个问题提前摆到桌面上而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑