ICU 54.1 源码包构建指南:老项目国际化迁移的避坑手册 简介ICU4C 54.1版本的完整源码压缩包面向Windows平台下需要借助Visual C编译并集成国际化能力的C/C开发者。ICU作为全球广泛采用的Unicode与全球化库能解决字符集转换、文本布局、日期与数字格式化、排序规则和本地化资源管理等跨语言场景的核心问题。资源共4519个文件压缩后25.7MB主要包括C/C源文件.h/.cpp/.c、构建配置脚本.in/.ac/.sh/.pl、Visual Studio工程文件.sln/.vcxproj/.filters以及测试与性能工具.bat/.ucm/.bin目录组织清晰已有539人学习下载。资源自带完整构建系统可大幅节省环境配置与库编译时间。开发者可借助配置脚本与VS工程文件快速产出libicudata、libicui18n、libicuuc等核心库并通过内置的perf测试工具验证字符集转换、格式化与文本边界等功能的运行效果为多语言应用提供可靠底层支撑。1. 翻出这份 icu4c-54_1-src.zip 的人多半是被老项目卡住了前阵子帮某开发者把一套老 C 服务从旧环境搬到新容器编译到一半冒出一堆跟auto_ptr、库顺序有关的报错追了半天才发现代码里到处是ucnv_open、ucol_open——这套东西从多年前就一直锁在 icu4c-54_1-src.zip 这个源码包上。当时我第一反应是换成系统自带的新版 ICU结果转码行为变了、排序结果也对不上最后还是老老实实把这份老源码包重新构建了一遍。这份 zip 是 ICU 的 C/C 实现源码解决的是编码转换、排序规则、日期格式化这些国际化底座问题。如果你手里有老项目要复现、要迁移或者想在可控版本上做嵌入式多语言处理这份源码包就是最稳的基准。新版库功能多但 54.1 的行为已经被大量老代码当成了默认预期绕过它会踩出更多意料之外的坑。2. 源码包解压与构建configure 参数、数据打包与安装路径解压这一步看起来简单但这里有一个很常见的操作失误ICU 的源码包解压后不能在解压出来的根目录直接敲 configure。它把所有构建相关文件放在source子目录里configure、Makefile 的入口都在那里很多人第一步就敲错目录然后对着 configure: No such file or directory 发愣。2.1 解压后的目录结构source 目录才是真正的构建入口mkdir -p ~/icu cd ~/icu unzip icu4c-54_1-src.zip ls icu/ # 常见输出LICENSE readme.html source cd icu/source ls -F能看到一个configure脚本和若干子目录。重点记住几个common/对应 libicuuci18n/对应 libicui18nio/对应 libicuiotools/下面是 pkgdata、genrb、gencnval 这些构建期工具test/是 intltest 和 cintltest 两套测试。如果你打算裁剪数据或者调整内部宏最先接触到的就是common/unicode下的头文件。这个目录布局从 54.1 一直延续到后来的版本所以熟悉它之后再看新版源码也不会迷路。2.2 最常用的构建参数静态库、数据打包与安装前缀./configure --prefix$HOME/icu54 \ --enable-static \ --disable-shared \ --with-data-packagingarchive--enable-static与--disable-shared的组合在 GNU/Linux 上几乎是万能解产物是三个.a文件部署时把静态库拷走就行如果做插件系统或者想多个模块共享同一份 ICU才考虑反过来用 shared。--prefix建议装到用户目录而不是/usr/local这样一台机器上可以同时维护 54.1 和后来版本两套运行时只改环境变量就能切换。--with-data-packaging的取值直接影响程序发布形态后面单独说明。提示configure 必须在 icu/source 目录下运行否则找不到脚本。make -j$(nproc) make check make installmake check会跑完整测试集首次构建建议留出时间跑一遍。如果只想快速验证核心可以只跑test/intltest。个别精简系统因为缺少 locale 数据测试会有零星失败但主流程通过就能继续往下走。构建失败的排错路径也有规律先看config.log末尾绝大多数缺依赖问题都写在最后几十行里最常见的两种情况是机器上没有 g 导致 configure 直接退出或者缺 libtool 相关组件。ICU 54 的 configure 对 C 编译器版本敏感老 GCC 和新 GCC 会报不同的错这一点在第 5 章展开。2.3 安装产物清单与 pkg-config 文件生成make install 之后安装目录会有四块内容include/unicode/下的头文件、lib/下的静态库和 pkgconfig 文件、bin/下的工具uconv、genrb、pkgdata 等、share/icu/54.1/下的数据文件。数据文件是否出现取决于打包方式--with-data-packaging取值数据去向运行时行为适用场景archive默认单独的icudt54l.dat按 ICU_DATA 环境变量找文件常规部署static编译进 libicudata.a无需外部数据文件单文件、嵌入式plain散文件放在 share/icu 下按目录加载需要手动裁剪调试archive 是默认也最常见的static 适合做成单文件程序plain 适合调试内部数据。我一般给嵌入式设备编译时选 static给桌面或服务端程序保持 archive。注意--enable-static是编译静态库--with-data-packagingstatic是把数据打进静态库两者是不同维度别混在一起。安装完成后检查 pkg-config 文件是否生成。正常情况下$prefix/lib/pkgconfig下会有icu-uc.pc、icu-i18n.pc、icu-io.pc三个文件。如果 configure 时用了非标准 prefixpkg-config 找不到这些.pc文件是常事设置PKG_CONFIG_PATH指向该目录即可后面集成项目会用到。3. 把 54.1 接进你的 C/C 项目CMake、Makefile 与链接顺序库构建好了接下来就是把它接进项目。这一章的命令按静态库场景写如果用的是动态库链接顺序和参数基本一致只是-ldl的依赖可能不再需要。ICU 和普通库有个显著区别它的导出符号带着版本后缀而且静态库链接顺序非常敏感这两点处理不好会浪费大量时间。3.1 手写编译命令cflags、libs 与静态库链接顺序export ICU_PREFIX$HOME/icu54 g demo.cpp -o demo \ -I$ICU_PREFIX/include \ -L$ICU_PREFIX/lib \ -licui18n -licuuc -licudata \ -lpthread -ldl-I让编译器找到 unicode/utypes.h-L指定库查找路径-licui18n -licuuc -licudata是链接顺序i18n 依赖 ucuc 依赖 data顺序不能反过来。后面两个-lpthread -ldl在 54.1 时代是必须的因为数据加载和线程初始化依赖 POSIX 线程与动态链接器。如果只做转码只链-licuuc -licudata就够了用了 Collator 或日期格式化就必须带-licui18n。更省心的做法是交给 pkg-configexport PKG_CONFIG_PATH$HOME/icu54/lib/pkgconfig g demo.cpp -o demo $(pkg-config --cflags --libs icu-uc icu-i18n)pkg-config 会把库顺序、附加的-pthread、-ldl全部处理好。注意这里传了icu-uc和icu-i18n两个 pc 文件如果你只用转码icu-uc就够了用排序就必须追加icu-i18n。3.2 CMake 集成两套方案find_package 与 pkg-config 回退方案 A依赖系统有 pkg-config 工具CMake 3.6 以上支持IMPORTED_TARGETfind_package(PkgConfig REQUIRED) pkg_check_modules(ICU REQUIRED IMPORTED_TARGET icu-uc icu-i18n) add_executable(demo demo.cpp) target_link_libraries(demo PRIVATE PkgConfig::ICU)方案 B不依赖 pkg-config显式指定路径交叉编译时更直接set(ICU_ROOT $ENV{HOME}/icu54) find_path(ICU_INCLUDE_DIR NAMES unicode/utypes.h HINTS ${ICU_ROOT}/include) find_library(ICU_I18N_LIBRARY NAMES icui18n HINTS ${ICU_ROOT}/lib) find_library(ICU_UC_LIBRARY NAMES icuuc HINTS ${ICU_ROOT}/lib) find_library(ICU_DATA_LIBRARY NAMES icudata HINTS ${ICU_ROOT}/lib) add_executable(demo demo.cpp) target_include_directories(demo PRIVATE ${ICU_INCLUDE_DIR}) target_link_libraries(demo PRIVATE ${ICU_I18N_LIBRARY} ${ICU_UC_LIBRARY} ${ICU_DATA_LIBRARY})方案 A 在容器环境里如果没装 pkg-config 会直接失败方案 B 只调 find_path 和 find_library我把 ICU_ROOT 显式写出来避免 CMake 去系统目录碰运气。实际项目里我优先用 B因为构建机环境不可控A 少装一个工具都跑不起来。3.3 链接期符号规则为什么所有函数都带着 _54ICU 的导出符号默认带版本后缀这是它区别于普通库的重要机制nm $HOME/icu54/lib/libicuuc.a | grep T ucnv_open | head # 输出形如0000000000123ab0 T ucnv_open_54库里导出的是ucnv_open_54头文件通过urename.h里的一组宏把ucnv_open映射到ucnv_open_54。这个机制允许同一进程内加载多个 ICU 版本而不冲突。如果客户端编译时定义了U_DISABLE_RENAMING宏映射失效头文件声明的是不带后缀的名字库里却只有带后缀的符号链接阶段就会报 undefined reference。第 5 章会专门讲这个坑在这里只要记住客户端不要额外定义U_DISABLE_RENAMING头文件与库必须来自同一套 54.1 安装目录。4. 高频功能实战转码与排序规则的正确用法ICU 54.1 用得最多的两块是字符编码转换和排序规则。转码处理的是“字节序列 ↔ UTF-16 代码单元”排序解决的是“字符串在某种语言习惯下谁先谁后”。这两个功能代码量不大但参数细节多一旦出错不是编译期能发现的必须运行期验证。4.1 ucnv 转码回路从 UTF-8 到 UTF-16 再到 UTF-8#include stdio.h #include unicode/utypes.h #include unicode/uerror.h #include unicode/ucnv.h int main(void) { UErrorCode status U_ZERO_ERROR; UConverter *conv ucnv_open(UTF-8, status); if (U_FAILURE(status)) { fprintf(stderr, ucnv_open: %s\n, u_errorName(status)); return 1; } const char *utf8_in ICU 54 转码验证; UChar u16[64]; int32_t len ucnv_toUChars(conv, u16, 64, utf8_in, -1, status); if (U_FAILURE(status)) { fprintf(stderr, toUChars: %s\n, u_errorName(status)); ucnv_close(conv); return 1; } printf(code unit 数: %d\n, len); for (int32_t i 0; i len; i) { printf(U%04X , u16[i]); } printf(\n); ucnv_close(conv); return 0; }编译时如果只做转码不需要链接 i18ngcc ucnv_demo.c -o ucnv_demo -I$HOME/icu54/include \ -L$HOME/icu54/lib -licuuc -licudata注意ucnv_toUChars的第五个参数传-1表示输入以 NUL 结尾、让函数自己数长度如果数据不一定以 NUL 结尾传实际字节数更安全。destCapacity是 64如果输出超过上限函数会先返回U_BUFFER_OVERFLOW_ERROR同时把需要的长度写进len。常见做法是先传 NULL 调一次拿长度再分配缓冲最后再真正转码。4.2 Collator 排序规则强度、地区与比较结果#include iostream #include unicode/coll.h #include unicode/utypes.h int main() { UErrorCode status U_ZERO_ERROR; icu::Collator* coll icu::Collator::createInstance(zh_CN, status); if (U_FAILURE(status)) { std::cerr createInstance: u_errorName(status) std::endl; return 1; } coll-setStrength(icu::Collator::TERTIARY); icu::UnicodeString s1(app), s2(apple); UCollationResult r coll-compare(s1, s2); if (r UCOL_LESS) std::cout app 在 apple 前面 std::endl; else if (r UCOL_EQUAL) std::cout 相等 std::endl; else std::cout app 在 apple 后面 std::endl; delete coll; return 0; }编译时要用 g 并链接 i18ng collator_demo.cpp -o collator_demo \ $(pkg-config --cflags --libs icu-i18n)比较强度从小到大是 PRIMARY、SECONDARY、TERTIARY、QUATERNARY、IDENTICAL。PRIMARY 只看基本字符a和A视为相等TERTIARY 会区分大小写。字符串排序场景里我一般用 TERTIARY 兼顾可读性和准确性如果要字典序且不区分大小写把强度设为 SECONDARY 即可。createInstance的第一个参数是 locale 字符串。zh_CN会按中文默认排序多数环境是拼音想用笔画排序可以写成zhcollationstroke这种关键字写法。locale 名写错不会直接崩溃ICU 会 fallback 到 root但排序行为就会变成英文习惯这是很多项目排序结果“看起来不对”的隐藏原因。4.3 资源释放与线程隔离每个线程维护自己的转换器UConverter 不是线程安全的同一实例不能同时在两个线程里调用ucnv_toUChars常见做法是按线程创建或用线程局部存储缓存。Collator 的只读compare在共享场景下问题不大但setStrength之后再共享就要小心我一般每个线程 new 一个。另外ICU 54 用的是裸指针Collator::createInstance返回的指针必须deleteUConverter必须ucnv_close没有智能指针包装忘了就泄露。不要在信号处理函数里调用 ICU——它的数据加载可能触发文件 I/O在信号上下文里会出无法预估的问题。5. 避坑记录ICU 54.1 在不同工具链上的五个翻车点54.1 是多年前的版本没赶上后来 GCC 和 MSVC 的改动。下面五个坑我基本都亲手踩过按现象、原因、解决写清楚每条都能省下至少半天排查时间。5.1 C17 编译错误auto_ptr 被移除现象用较新的 GCC 编译时报一堆std::auto_ptr相关错误提示这个类型已不存在。原因ICU 54.1 的部分头文件和 i18n 源码里还用了 C98 时代的std::auto_ptrC17 标准已经把它从标准库移除新 GCC 默认按 C17 编译于是直接找不到这个类型。解决configure 时强制指定 C11其他参数不变CXXFLAGS-stdc11 ./configure --prefix$HOME/icu54 \ --enable-static --disable-shared如果编译时还有别的兼容告警可以在 CXXFLAGS 里追加-Wno-deprecated和-Wno-deprecated-declarations但不建议无脑关告警先看清楚是哪一类问题。5.2 链接失败版本符号 _54 与 U_DISABLE_RENAMING 的博弈现象编译客户端代码没问题链接时报一堆undefined reference to ucnv_open_54或者反过来报ucnv_open is undefined。原因ICU 的导出符号默认带_54后缀这是版本隔离机制。头文件通过urename.h把ucnv_open宏映射到ucnv_open_54。如果客户端定义了U_DISABLE_RENAMING宏映射失效头文件声明的是不带后缀的名字库里却没有这个符号。解决客户端代码不要定义U_DISABLE_RENAMING如果是从老项目里拷来的 CPPFlags检查有没有这个宏。拿不准时用nm libicuuc.a | grep ucnv_open看实际导出符号确认是否带_54后缀。5.3 运行时报错找不到 icudt54l.dat现象程序编译链接都通过运行时提示file not found: icudt54l.dat或者干脆显示内存不足、直接退出。原因archive 打包模式下数据文件与库分离。程序运行时 ICU 会按编译期路径找数据文件换机器或换目录后路径失效。解决部署时带上share/icu/54.1/icudt54l.dat并设置环境变量ICU_DATA指向它。想彻底消除这个外部依赖就在 configure 时把--with-data-packaging改为static重新编译库数据会直接编进 libicudata.a。5.4 交叉编译宿主机不能执行目标二进制现象交叉编译到 ARM 平台make 进行到一半执行某个工具时报cannot execute binary file构建中断。原因ICU 构建过程中需要运行 genrb、pkgdata 等工具。交叉编译出的这些工具是 ARM 二进制无法在 x86 宿主机上执行。解决交叉编译的 configure 命令要加--with-cross-build宿主机上已经编译好的同一版本 ICU 的 source 目录。这个参数让构建系统继续使用宿主机上能执行的工具版本交叉编译链只负责生成目标库。注意指向的是源码目录不是安装目录。5.5 Windows 老工程VS 升级引发的构建连锁反应现象Windows 上打开源码里 allinone 的 VS 工程较新 VS 升级工程后编译报大量 C4996或者链接阶段找不到符号。原因54.1 在 Windows 上只有 MSVC 工程路径。老工程文件被新版 IDE 升级时默认开启了更严格的安全检查和警告级别把fopen这类 CRT 调用直接当成错误暴露出来。解决在工程属性里关闭 SDL 检查、把警告设置为不视作错误再选择 Release 配置重新编译。如果不想折腾 IDE 工程也可以改用 MinGW 环境跑 configure 流程Linux 上的脚本路径在 Windows 下同样能用我实际走这条路的成功率更高。6. 收尾验证写一个最小程序确认库与数据都完好无论前面构建多顺利最后都要跑一个最小验证程序确认库、头文件、数据文件三者版本一致。下面这段代码只依赖 icu-uc不涉及排序适合作为冒烟测试#include stdio.h #include unicode/utypes.h #include unicode/uerror.h #include unicode/ucnv.h int main(void) { UErrorCode status U_ZERO_ERROR; UConverter *conv ucnv_open(UTF-8, status); if (U_FAILURE(status)) { fprintf(stderr, open failed: %s\n, u_errorName(status)); return 1; } const char *msg icu54 check; UChar buf[64]; int32_t n ucnv_toUChars(conv, buf, 64, msg, -1, status); if (U_SUCCESS(status)) { printf(ICU %s, units%d\n, U_ICU_VERSION, n); } ucnv_close(conv); return 0; }编译运行export PKG_CONFIG_PATH$HOME/icu54/lib/pkgconfig gcc verify.c -o verify $(pkg-config --cflags --libs icu-uc) ./verify预期输出ICU 54.1, units11其中 units 表示转换后的 UTF-16 code unit 数量。如果输出版本不对或 units 对不上优先检查头文件路径是不是指向了别的 ICU 版本。再顺手验证一下转换工具和数据文件是否正常echo hello | $HOME/icu54/bin/uconv -f UTF-8 -t UTF-8正常会原样输出 hello这一步能确认 icudt54l.dat 被正确加载。如果想连排序一起验证就把第 4 章的 Collator 例子编出来跑一遍两个输出都对这套 54.1 基本就可以放心投入使用了。从那以后我每次接到老 C 项目第一件事就是问清楚项目锁的是哪一版 ICU再决定构建参数。这份 54.1 源码包也因此一直留在工具箱里当基准参照。希望这份笔记帮到你。本文还有配套的精品资源点击获取