ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

IFCPlusPlus 库的 CMake 迁移与 Clang 编译实战:BIM 开发避坑指南

IFCPlusPlus 库的 CMake 迁移与 Clang 编译实战:BIM 开发避坑指南 简介一份IFCPlusPlus开源库的Fork版本源代码包面向C与BIM开发人员解决原始库在2014年重置后难以用CMakeClang构建的问题。该版本从源码层面做了编译适配支持以Carve和Boost作为外部依赖进行构建并提供了OS X 10.9环境下的CMake配置参考便于在macOS或类Unix平台上快速搭建IFC文件解析与处理环境。资源压缩包约7.94MB内容以C源文件、头文件及CMake构建脚本为主体积紧凑适合直接集成到现有工程中。已有159人学习下载对于需要在IFC格式数据读取、BIM模型转换或几何处理方向进行二次开发的开发者这份经过编译调整的代码库可有效缩短环境配置与排错时间帮助更快聚焦业务逻辑。 IFCPlusPlus 这个库接触过 BIM 相关底层开发的应该都不陌生。在 C 生态里能正经解析 IFCIndustry Foundation Classes工业基础类文件的开源库本来就屈指可数IFCPlusPlus 算是老牌选择之一。但老牌不代表好用它的构建系统长期绑定在 Windows 工具链上工程文件结构也是多年没动过的老式布局想在非 Windows 环境里用 Clang 把它编起来几乎等于做一次代码考古。我 Fork 它并做了 CMake 改造就是为了解决这件事。这篇文章把我踩过的坑、改过的构建脚本、遇到过的编译器报错都摊开讲给想在这个库上做二次开发的读者一条能直接走通的路。1. IFCPlusPlus 的现状与这次 Fork 的来龙去脉1.1 什么是 IFCPlusPlusBIM 数据世界里的老牌开源库IFC 是建筑信息模型BIM领域最通用的开放数据格式用来描述建筑物里的构件、空间、材料、成本等信息。如果你要写一个工具去读取或转换 IFC 文件IFCPlusPlus 几乎是 C 方向绕不开的起点。它提供了从 IFC 文件解析到内存对象模型、再到几何表达的一系列基础能力甚至还能做部分几何操作。但它的“起点”属性也是问题所在。这个库的活跃开发大概集中在 2010 年前后作者后续维护力度不够代码长期停留在老式 C03/早期 C11 的书写风格。更重要的是它的工程文件主要围绕 Visual Studio 组织带有一整套 .vcxproj 和 .lib 目录结构对非 Windows 用户非常不友好。你想在 macOS 或 Linux 上用 Clang 编译它基本只能靠手动造 Makefile而且造完之后大概率会撞上一堆和编译器标准相关的错误。这也就解释了为什么我见到“IFCPlusPlusArchiv1”这个仓库时会觉得亲切。它做了我一直想做但没系统做的事把这个老库拉到现代工具链下让它至少能顺利产出库文件。1.2 2014 年 6 月 1 日的“存储库神秘重置”与“第二波”标题里提到“存储库神秘重置”时间点是 2014 年 6 月 1 日。这件事现在去看跟开源项目本身没有直接关系它更像一次 GitHub 上的仓库维护事故原仓库的树被某次强制提交或管理员操作清空历史提交大量消失导致当时不少依赖它的下游 Fork 全部失效。如果你不熟悉 Git可以把 Fork 理解成“复制一份仓库到自己的账号下”。原仓库重置之后所有基于旧历史的 Fork 就会失去维护基线。你没办法再安全地拉取上游更新因为 git 会告诉你 histories 不相关。“第二波”这个词就是指在重启后的新历史之上有人重新提交了相关内容。这大概也是“Archiv1”这个仓库名里“Archiv”Archive归档的含义它更像是一个档案性的快照把老版本的代码和后续修复固定下来供后人直接拿去用。那次事件后来在圈子里被反复提及原因倒不是事件本身多严重而是它提醒了所有做开源维护的人仓库历史在任何时候都可能被意外破坏本地必须保留原始代码的备份和补丁集。这个仓库就是这种备份思路的产物之一。1.3 为什么不换一个库非要自己 Fork有人可能会问都这么老旧了换一个更现代的开源 IFC 库不行吗现实原因是直到现在能稳定解析 IFC4 和 IFC2x3 并用 C API 暴露出来的开源库可选范围非常窄。有的库侧重几何可视化并没有完全覆盖数据模型有的库研究性质偏重API 设计很不稳定不适合作为底层依赖还有的库技术栈绑定太深引入成本比修复老代码更大。IFCPlusPlus 的价值在于它的语义层覆盖相对完整。它的类体系基本对应 IFC 规范里的实体定义拿到一个 IFC 文件你可以很直观地把 ifcWall、ifcWindow、ifcDoor 这类实体映射成内存对象。对做数据清洗、格式转换、工程量统计的人来说这种语义映射非常省事。所以我决定 Fork 这个方向并不纠结问题只剩下一个把它顺利编起来。2. 构建系统选型为什么是 CMake为什么搭配 Clang2.1 原有构建方式为什么让人头疼IFCPlusPlus 的原始代码里带着 Visual Studio 解决方案文件.sln和一堆工程文件.vcxproj同时也保留了一个老式的 Makefile 体系但那个 Makefile 只覆盖了很有限的源文件子集完全达不到一键编出全部模块的程度。它在 Windows 上可以直接打开解决方案编译问题是依赖项处理非常“裸”没有统一的第三方库管理机制Boost、ZLIB 这类库需要你手工下载、设置环境变量、再手动拖进工程。到了 Linux 或 macOS 上这套基本就废了。你得先手动列出所有需要参与编译的 .cpp 文件把它们拼到 Makefile 里还得保证每个源文件包含的头文件路径都正确。更麻烦的是这个库的目录结构不是现代 C 项目常见的扁平结构而是多层级的文件夹每个模块下有各自的 source 和 include 目录。把几十个源文件一条条写进构建脚本工作量极大而且一旦后续要对源码增删维护成本立刻失控。2.2 CMake 加 Clang 的适配逻辑选 CMake 的原因很直接它是目前跨平台 C 项目的事实标准可以生成 Visual Studio 工程、Xcode 工程、Unix Makefile、Ninja 等不同构建产物。这意味着同一份构建脚本在 Windows 上能生成 .sln在 macOS 上能生成 Xcode 工程在 Linux 上能生成 Makefile 或 Ninja。假如未来某个团队想在 Windows 上重新组织构建流程我只需要在 CMake 里增加一个 generator 参数而不需要重写整套脚本。Clang 则是和 GCC 并列的主流的现代化编译器。选它不是因为它比 GCC 优秀多少而是因为它的错误信息可读性更好而且在处理老代码时对未定义行为和一些 C 标准差异的告警更明确。老库对编译器标准常常有一种隐性的依赖只有换一个不同的编译器前端才能暴露出这些隐性问题。和 Visual Studio 的 MSVC 相比Clang 还会更严格地拒绝那些非标准代码这让整个移植过程更像一次代码健康检查。另外CMake 天然支持设置编译器前缀我可以在 CMakeLists.txt 里写一句话让构建时选择合适的编译器也可以在命令行中传入-DCMAKE_CXX_COMPILERclang来临时切换这比手动改 Makefile 的 CC、CXX 变量优雅得多。3. 迁移到 CMake 的落地过程3.1 顶层构建文件设计整个迁移的第一步是确定模块边界。IFCPlusPlus 的源码大致分成几个板块核心数据结构、IFC 解析器、几何处理模块、示例程序。我没有在顶层一个 CMakeLists.txt 里把所有源文件堆在一起而是采用了一种相对常见的多级结构源码根目录放一个顶层 CMakeLists.txt用于统一声明项目信息、编译选项和全局 include 路径然后各模块用自己的 CMakeLists.txt 管理各自的源文件。顶层文件的关键部分是这样写的cmake_minimum_required(VERSION 3.10) project(IFCPlusPlus LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) option(IFCPLUSPLUS_BUILD_SHARED Build shared libraries ON) option(IFCPLUSPLUS_BUILD_EXAMPLES Build example programs ON) if(IFCPLUSPLUS_BUILD_SHARED) set(IFCPLUSPLUS_LIBRARY_TYPE SHARED) else() set(IFCPLUSPLUS_LIBRARY_TYPE STATIC) endif() add_subdirectory(ifcparse) add_subdirectory(ifcgeom) add_subdirectory(ifcplusplus) add_subdirectory(examples)这里有个值得注意的细节CMAKE_CXX_STANDARD 11。我把标准级别限定在 C11而不是无脑拔高到 C17 或 C20。因为老代码是带着 C03 时代的习惯写的一次性跳到最新标准会引发大量 API 变更的错误比如std::auto_ptr被移除、std::bind1st被废弃等这些修起来很费劲也容易在无意中改动原有语义。C11 是一个相对温和的中间点它既让代码在现代编译器上能通过也不至于破坏旧代码的根基。3.2 目标划分与依赖处理接下来看每个子模块的 CMakeLists.txt。以 ifcparse 目录为例file(GLOB IFC_PARSE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp) file(GLOB IFC_PARSE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h) add_library(ifcparse ${IFCPLUSPLUS_LIBRARY_TYPE} ${IFC_PARSE_SOURCES} ${IFC_PARSE_HEADERS}) target_include_directories(ifcparse PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/source )这里用file(GLOB ...)来收集源文件严格来说不是最佳实践——CMake 官方建议显式列出每个源文件因为 GLOB 在新增文件后不会自动触发重新配置。不过对于 IFCPlusPlus 这种源码相对固定的老项目GLOB 能省掉一大截维护成本而且我们在 CI 里会强制使用CONFIGURE_DEPENDS标志来缓解这个问题也算是在效率和规范之间找了个平衡。依赖处理上这个库主要依赖 Boost 和 ZLIB。我用find_package让使用者自己准备系统级别的库而不是把第三方库源码直接拖进仓库。比如 ZLIB 可以这样声明find_package(ZLIB REQUIRED) if(NOT ZLIB_FOUND) message(FATAL_ERROR ZLIB is required to build ifcparse) endif() target_link_libraries(ifcparse PUBLIC ZLIB::ZLIB)Boost 则比较特殊这个库老版本用的头文件目录结构和现代 Boost 差异很大所以我没有盲目依赖最新版而是指定了较宽泛的范围在 README 里注明推荐使用 Boost 1.58 左右的版本同时在代码里对 Boos 的容器头部做了兼容处理。PUBLIC和PRIVATE的区别也要说清楚如果 ifcgeom 模块的头文件里带有对 ifcparse 头文件的使用那么 ifcgeom 必须把 ifcparse 标记为 PUBLIC 链接关系否则创建 ifcgeom 目标的 CMakeLists 里编译器会找不到 ifcparse 的头文件。我在第一次迁移时就是忽略了这个细节导致报了一堆 include 路径错误。3.3 几个容易忽略的 CMake 配置细节第一个是动态库和静态库的导出宏。老代码里可能依赖__declspec(dllexport)或__attribute__((visibility(default)))来导出符号但在 CMake 里如果不定义相应的宏动态库构建出来可能丢符号导致运行时报Undefined symbol。IFCPlusPlus 的导出宏分散在多个头文件里和编译器平台密切相关。我用一个ifcplusplus_export.h头文件把它们统一封装再在 CMake 里通过target_compile_definitions给所有模块加上统一的宏target_compile_definitions(ifcparse PRIVATE IFCPARSE_EXPORTS)第二个是设置运行时库。在非 Windows 平台上我们可以不做额外处理但如果你未来在 Visual Studio 里跑这个 CMake 工程MSVC 的调试版和发布版默认使用不同的运行时库/MDd 和 /MD如果第三方库是用另一种方式编译的链接阶段会报错。我建议在顶层 CMakeLists.txt 里加上if(MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$$CONFIG:Debug:Debug) endif()这一行能避免一大类“LNK2005 / LNK2038”链接错误。第三个是路径中的空格。老项目里有人把工程放在带空格的目录下运行构建时CMake 生成的 Makefile 偶尔会把路径拆错。这个问题在 CMake 3.0 之后基本解决了但如果你在自定义命令里拼接路径最好还是用${CMAKE_COMMAND} -E这种方式而不是直接手动拼字符串。4. 用 Clang 编译时的实际报错与排查记录CMake 脚本写好之后真正的战斗才开始。用 Clang 编译老库报错基本上是必然的。下面是我实际处理过程里最有代表性的几类。4.1 错误一C 标准版本不一致第一次编译时的报错类型很典型error: auto_ptr is deprecated in C11 error: use of undeclared identifier strdupstrdup这个函数在 C11 标准里不是全局函数而是 POSIX 扩展。老代码直接调用它在 GCC 的某些版本上可能因为默认开了 GNU 扩展而通过但在 Clang 的严格模式下立刻就暴露了。修复方式不涉及大改我在源文件里加了一层兼容封装如果是非 Windows 平台就手动声明extern char *strdup(const char *);或者直接用strndup替代并加上内存释放处理。strdup和strndup的区别在于后者可以指定最大复制长度对字符串溢出的防护更好。第二个错误是auto_ptr。碰到的是典型的老代码习惯std::auto_ptrSomeClass ptr(new SomeClass());C11 里auto_ptr已经被unique_ptr取代但直接替换会引发所有权语义变化auto_ptr允许隐式转移所有权而unique_ptr必须用std::move显式转移。我把代码里所有auto_ptr替换为unique_ptr同时检查了所有作为函数参数传递的地方为每个调用点补上std::move。这里还出现了一个典型次生问题本来auto_ptr可以作为函数参数按值传递但unique_ptr不能。如果你的函数原签名是这样void update(std::auto_ptrObject obj);改成unique_ptr之后必须把签名也调整为void update(std::unique_ptrObject obj);并在调用处用std::move。这个改动量不小但它是正确性所必需的因为auto_ptr的隐式所有权转移本身已经被标准废弃继续保留只会埋隐患。4.2 错误二旧式隐式类型转换这类错误在 Clang 下最显眼error: cannot initialize return object of type std::string with an rvalue of type char *老代码常有用return getenv(HOME);这种直接把 C 字符串转成std::string的习惯。这在很多编译器上能过因为存在std::string的隐式构造函数但如果getenv返回的指针在某些分支上为空就会在运行时触发未定义行为。Clang 在某些代码路径上会把它识别成可能为空的指针然后直接报错。我的处理方案是给所有这类地方都加上显式判断std::string homeDir; if (const char* home std::getenv(HOME)) { homeDir home; } else { homeDir .; }这类修改看起来琐碎但它其实是在做一件非常重要的事把“碰巧能在编译器上跑”的代码变成“逻辑上一定安全”的代码。Clang 的严格检查相当于一次免费的静态审查。4.3 错误三链接器和 pthread 的问题在 Linux 上编译完所有目标文件之后链接阶段报了一个很经典的错误undefined reference to pthread_create原因很简单Clang 和 GCC 默认在链接时不会自动把libpthread加进来。虽然 glibc 2.34 以上的版本已经将 pthread 符号合并进 libc但老版本系统仍然需要显式链接。在 CMake 里我并没有用target_link_libraries(... pthread)这种硬编码方式因为那样在 macOS 上会报错。更稳妥的做法是用 CMake 自带的线程模块find_package(Threads REQUIRED) target_link_libraries(ifcparse PUBLIC Threads::Threads)Threads::Threads是 CMake 提供的跨平台线程库封装它会在 Linux 上自动链接 pthread在 macOS 上链接正确的系统库。这种写法看起来很不起眼但确实能避免平台相关的链接错误。4.4 处理警告、启用 Werror 的策略Clang 编译时会吐出一大堆 warning其中很多是“unused parameter”、“deprecated declaration”。如果你直接开启-Werror会让整个构建寸步难行因为老代码里的无用参数和废弃 API 调用实在太多清完一遍需要大量修改。我的策略是分两步走第一步先把编译告警当作信息流看待不打断构建只把它们全部记录到日志文件里。这样可以快速筛选出真正会导致问题的告警比如“可能空指针解引用”、“越界访问”这种而不是被几百条“unused parameter”淹没。第二步对愿意接受的告警类型逐类关闭或调整级别。重点是把-Werror控制在“新代码必须严格”的原则上而不强制老代码一次性全部达标。一个实际可参考的参数是-Wno-unused-parameter -Wno-deprecated-declarations -Wno-sign-compare这几个参数是我在调整 IFCPlusPlus 时最常用的。-Wno-unused-parameter是因为这个库大量使用了接口继承很多虚函数就是有参数但用不到-Wno-deprecated-declarations是因为它内部互相调用了自己标记为废弃的函数-Wno-sign-compare是因为很多地方拿int和无符号整数比较这个在现代代码里也算不太安全的习惯但要一次性改完所有比较逻辑工作量太大。5. 编译通过后的验证与维护经验5.1 用一个小工具读取真实 IFC 文件构建通过并不等于整个过程成功必须用一个真实的 IFC 文件跑一遍。写一个最小的读取器其实很简单核心代码只有这些#include ifcparse/IfcFile.h int main(int argc, char* argv[]) { if (argc 2) { std::cerr Usage: ifcread file.ifc std::endl; return 1; } IfcFile file; if (!file.Init(argv[1])) { std::cerr Failed to parse IFC file: argv[1] std::endl; return 1; } std::cout IFC schema: file.GetFileSchema() std::endl; std::cout Entity count: file.GetNumEntities() std::endl; return 0; }然后我在系统里找了一个 IFC 2x3 的样本文件跑起来之后程序正确输出了 entity 数量说明整个解析流程的核心链路没问题。如果这一步能通过剩下的就是进一步验证几何模块。IFC 文件里的几何信息比较多样如果几何模块没有编译成功很多模型查看器就无法加载。我用官方 IFC 测试套件里的几个典型模型做了一遍 smoke test确认 ifcgeom 模块生成的几何实体个数和预期的已知值一致。这里要强调一下对于这类解析库光看编译产物存在是不够的必须用它真实解析一次数据才能证明你的构建配置没有在链接阶段把不该丢的符号丢掉。5.2 Fork 与上游的同步策略Fork 之后不要和上游脱节。这也是我在这次实践中的一个重要体会。因为仓库已经有过一次重置历史上游的发展可能并不活跃但在某个时间点如果再次涌现新的提交同步策略就显得格外重要。我采用的同步方式是本地维护一个upstreamremote 指向原仓库的地址定期执行git fetch upstream。如果上游有更新先把我的 CMake 改动单独剥离出来放在一个名为feature/cmake-build的分支上然后在 main 分支上直接 merge 上游最后再把 CMake 改动用 cherry-pick 的方式重新应用到新主线上。这个流程能保证两点一是构建系统的改动始终独立于上游代码的更新两者不会互相污染二是一旦上游有冲突可以精准定位到具体是哪几行代码不至于把整个构建脚本也卷进去。5.3 后续想再往哪个方向改进CMake 和 Clang 的适配只是第一步这个库要真正现代起来还有很多可以做的事。我想到的第一个方向是引入 Conan 或 vcpkg 来做第三方库管理。现在 Boost 和 ZLIB 都是依赖使用者自己配置虽然对老手来说不是问题但对初次接触这个库的开发者来说门槛确实高了一些。如果用 vcpkg 封装使用者只需要在 CMake 里写一句find_package其余依赖项不用自己准备。第二个方向是把 C 标准再往上推。C11 只是让编译通过要让代码更安全、更高效可以考虑升级到 C17引入std::optional、结构化绑定、if constexpr等特性来简化数据解析逻辑。第三个方向是建立完整的自动化测试体系。目前编译验证只覆盖了最简单的场景就是“能编过、能跑起来”。如果后续有人修改了解析逻辑想要快速判断是否破坏了现有功能手工验证是不够的还是需要一套自动化的单元测试和集成测试。以上就是我 Fork IFCPlusPlusArchiv1 并把它迁移到 CMake 到 Clang 工具链上的完整过程。如果你想基于这个库做二次开发希望这篇记录能帮你少走些弯路。你也可以直接去看仓库里的 CMakeLists 文件把一个库的重新构建当作一次理解这个项目内部结构的起点。本文还有配套的精品资源点击获取
返回列表