IFCPlusPlus 库的 CMake 迁移与 Clang 编译实战:BIM 开发避坑指南
2026/9/18 19:41:04 网站建设 项目流程

简介:一份IFCPlusPlus开源库的Fork版本源代码包,面向C++与BIM开发人员,解决原始库在2014年重置后难以用CMake+Clang构建的问题。该版本从源码层面做了编译适配,支持以Carve和Boost作为外部依赖进行构建,并提供了OS X 10.9环境下的CMake配置参考,便于在macOS或类Unix平台上快速搭建IFC文件解析与处理环境。资源压缩包约7.94MB,内容以C++源文件、头文件及CMake构建脚本为主,体积紧凑,适合直接集成到现有工程中。已有159人学习下载,对于需要在IFC格式数据读取、BIM模型转换或几何处理方向进行二次开发的开发者,这份经过编译调整的代码库可有效缩短环境配置与排错时间,帮助更快聚焦业务逻辑。 IFCPlusPlus 这个库,接触过 BIM 相关底层开发的应该都不陌生。在 C++ 生态里,能正经解析 IFC(Industry Foundation Classes,工业基础类)文件的开源库本来就屈指可数,IFCPlusPlus 算是老牌选择之一。但老牌不代表好用,它的构建系统长期绑定在 Windows 工具链上,工程文件结构也是多年没动过的老式布局,想在非 Windows 环境里用 Clang 把它编起来,几乎等于做一次代码考古。我 Fork 它并做了 CMake 改造,就是为了解决这件事。这篇文章把我踩过的坑、改过的构建脚本、遇到过的编译器报错都摊开讲,给想在这个库上做二次开发的读者一条能直接走通的路。

1. IFCPlusPlus 的现状与这次 Fork 的来龙去脉

1.1 什么是 IFCPlusPlus:BIM 数据世界里的老牌开源库

IFC 是建筑信息模型(BIM)领域最通用的开放数据格式,用来描述建筑物里的构件、空间、材料、成本等信息。如果你要写一个工具去读取或转换 IFC 文件,IFCPlusPlus 几乎是 C++ 方向绕不开的起点。它提供了从 IFC 文件解析到内存对象模型、再到几何表达的一系列基础能力,甚至还能做部分几何操作。

但它的“起点”属性也是问题所在。这个库的活跃开发大概集中在 2010 年前后,作者后续维护力度不够,代码长期停留在老式 C++03/早期 C++11 的书写风格。更重要的是,它的工程文件主要围绕 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,为什么搭配 Clang

2.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_COMPILER=clang++来临时切换,这比手动改 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。我把标准级别限定在 C++11,而不是无脑拔高到 C++17 或 C++20。因为老代码是带着 C++03 时代的习惯写的,一次性跳到最新标准会引发大量 API 变更的错误,比如std::auto_ptr被移除、std::bind1st被废弃等,这些修起来很费劲,也容易在无意中改动原有语义。C++11 是一个相对温和的中间点,它既让代码在现代编译器上能通过,也不至于破坏旧代码的根基。

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 的容器头部做了兼容处理。

PUBLICPRIVATE的区别也要说清楚:如果 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 C++11 error: use of undeclared identifier 'strdup'

strdup这个函数在 C++11 标准里不是全局函数,而是 POSIX 扩展。老代码直接调用它,在 GCC 的某些版本上可能因为默认开了 GNU 扩展而通过,但在 Clang 的严格模式下立刻就暴露了。

修复方式不涉及大改,我在源文件里加了一层兼容封装:如果是非 Windows 平台,就手动声明extern char *strdup(const char *);,或者直接用strndup替代并加上内存释放处理。strdupstrndup的区别在于后者可以指定最大复制长度,对字符串溢出的防护更好。

第二个错误是auto_ptr。碰到的是典型的老代码习惯:

std::auto_ptr<SomeClass> ptr(new SomeClass());

C++11 里auto_ptr已经被unique_ptr取代,但直接替换会引发所有权语义变化;auto_ptr允许隐式转移所有权,而unique_ptr必须用std::move显式转移。我把代码里所有auto_ptr替换为unique_ptr,同时检查了所有作为函数参数传递的地方,为每个调用点补上std::move

这里还出现了一个典型次生问题:本来auto_ptr可以作为函数参数按值传递,但unique_ptr不能。如果你的函数原签名是这样:

void update(std::auto_ptr<Object> obj);

改成unique_ptr之后,必须把签名也调整为void update(std::unique_ptr<Object> 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++ 标准再往上推。C++11 只是让编译通过,要让代码更安全、更高效,可以考虑升级到 C++17,引入std::optional、结构化绑定、if constexpr等特性来简化数据解析逻辑。

第三个方向是建立完整的自动化测试体系。目前编译验证只覆盖了最简单的场景,就是“能编过、能跑起来”。如果后续有人修改了解析逻辑,想要快速判断是否破坏了现有功能,手工验证是不够的,还是需要一套自动化的单元测试和集成测试。

以上就是我 Fork IFCPlusPlusArchiv1 并把它迁移到 CMake 到 Clang 工具链上的完整过程。如果你想基于这个库做二次开发,希望这篇记录能帮你少走些弯路。你也可以直接去看仓库里的 CMakeLists 文件,把一个库的重新构建当作一次理解这个项目内部结构的起点。

本文还有配套的精品资源,点击获取

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

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

立即咨询