代化實(shí)踐)
簡介IFCPlusPlusArchiv1 是 IFCPlusPlus 開源庫的派生分支來自 2014 年存儲(chǔ)庫重置后的“第二波”提交整理核心改動(dòng)在于改用 CMake 構(gòu)建系統(tǒng)并適配 Clang 編譯器適合需要在 IFC 與 BIM 相關(guān)項(xiàng)目中處理建筑信息數(shù)據(jù)模型的 C 開發(fā)者。資源以 zip 壓縮包交付大小約 7.94MB可快速下載解壓便于直接查看工程文件與構(gòu)建配置。當(dāng)前已有 159 人學(xué)習(xí)說明它對(duì)從事建筑數(shù)據(jù)解析、模型轉(zhuǎn)換的工程師有一定參考價(jià)值尤其適合正在做歷史代碼構(gòu)建遷移或需要復(fù)現(xiàn)舊版編譯環(huán)境的開發(fā)者。壓縮包內(nèi)含調(diào)整后的源碼與完整 CMake 工程配置重點(diǎn)展現(xiàn)了如何將 Carve 和 Boost 等外部依賴引入構(gòu)建流程并針對(duì) macOS 等環(huán)境設(shè)置了 Clang 編譯參數(shù)對(duì)于希望避開原存儲(chǔ)庫歷史問題、快速搭建可編譯 IFC 解析環(huán)境的開發(fā)者這是一份可直接使用的工程模板也能幫助理解源碼外構(gòu)建的目錄組織與依賴項(xiàng)路徑設(shè)置思路。1. 一次倉庫重置之后我為什么還留著這個(gè) IFCPlusPlus 的 Fork提到 IFCPlusPlusArchiv1很多接觸過 IFC 模型解析的人不會(huì)陌生。它是 IFCPlusPlus 的一個(gè) Fork在 2014 年 6 月 1 日倉庫被一次神秘重置之后后續(xù)提交被稱為“第二波”我當(dāng)時(shí)關(guān)注它是因?yàn)檫@個(gè)分支沒有停留在“能編譯就行”而是把整個(gè)工程向 CMake 構(gòu)建系統(tǒng)遷移并保證 Clang 能順利編過。對(duì)要拿 IFC 做數(shù)據(jù)交換、模型審查或二次開發(fā)的工程師來說這套改動(dòng)解決一個(gè)實(shí)際問題老 C 代碼在今天的工具鏈上怎么活下來。下面我會(huì)按“改了什么、怎么跑、坑在哪”的順序把這個(gè) Fork 值得投入的地方說清楚。2. 把 IFCPlusPlus 從 Autotools 搬到 CMake這場遷移改的是構(gòu)建骨架2.1 老工程為什么非換不可Autotools 的邊界十年前 IFCPlusPlus 的構(gòu)建系統(tǒng)還帶著那個(gè)年代的典型味道一堆 configure.ac、Makefile.am再加上手工維護(hù)的 Visual Studio 工程。我在 Ubuntu 上跑 “./configure make” 很順但換一臺(tái)機(jī)器、換一個(gè) IDE、換一套交叉編譯工具鏈問題就全出來了。Autotools 并不是不好而是它的“輸出”是 Makefile和編輯器、調(diào)試器的配合非常間接你沒法像現(xiàn)在這樣讓 CMake 一鍵生成 Ninja、Visual Studio、Xcode 三套工程更別提把編譯參數(shù)、宏定義、頭文件路徑用一份腳本統(tǒng)一管理。這個(gè) Fork 的“主要更改”恰好落在 CMake 上。它把原來散落在各個(gè) Makefile.am 里的源文件列表、include 路徑、鏈接庫定義收斂到 CMakeLists.txt然后用add_library和target_include_directories重新表達(dá)。這樣做的實(shí)際收益很直接我可以在一個(gè)目錄下同時(shí)維護(hù) Debug 和 Release 構(gòu)建也可以在 CMake GUI 里勾選項(xiàng)而不是去手工改 Makefile 里的CXXFLAGS。對(duì) IFCPlusPlus 這種既有 I/O 解析又有幾何計(jì)算的工程構(gòu)建系統(tǒng)一旦統(tǒng)一后面換編譯器、加模塊、跑測試都會(huì)省很多事。2.2 頂層 CMakeLists 的最小骨架我一般會(huì)在拿到這個(gè) Fork 后先建一個(gè)干凈的頂層 CMakeLists.txt不急著把原有源文件全部照搬。最小骨架長這樣cmake_minimum_required(VERSION 3.16) project(IFCPlusPlusArchiv1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_library(ifcplusplus STATIC src/IfcPlusPlusStatic/IfcPlusPlusManager.cpp src/IfcPlusPlusStatic/IfcPlusPlusModel.cpp src/IfcPlusPlusStatic/IfcPlusPlusGuid.cpp # 后面按 git ls-files 繼續(xù)補(bǔ) ) target_include_directories(ifcplusplus PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src $INSTALL_INTERFACE:include ) target_compile_definitions(ifcplusplus PRIVATE IFCPLUSPLUS_BUILD_STATIC _USE_MATH_DEFINES )這里的門道不在這幾行本身而在你為什么要這么寫。CXX_STANDARD 11是在給老代碼一個(gè)緩沖IFCPlusPlus 的原始源碼里有大量隱式轉(zhuǎn)換和舊式寫法直接按 C17 編會(huì)爆出幾百個(gè)警告改成 C11 先能過后面再逐步收緊。IFCPLUSPLUS_BUILD_STATIC是我加的私有宏因?yàn)槔洗a里經(jīng)常有一組條件編譯用來區(qū)分動(dòng)態(tài)庫導(dǎo)出和靜態(tài)庫構(gòu)建你不定義它導(dǎo)出符號(hào)相關(guān)的__declspec(dllexport)邏輯就會(huì)走錯(cuò)分支Windows 上尤其明顯。2.3 用 file(GLOB) 還是逐個(gè)寫源文件源文件列表是這個(gè)工程里最容易翻車的地方。老 Autotools 維護(hù)者喜歡把新代碼寫進(jìn)Makefile.am而你在 CMake 里如果漏掉一個(gè).cpp最后看到的錯(cuò)誤往往是“鏈接時(shí)找不到某個(gè)符號(hào)”——這種黑匣子式的問題最耗時(shí)間。我的做法是用file(GLOB_RECURSE)先把所有源文件收進(jìn)來再用list(FILTER)排除測試和構(gòu)建目錄file(GLOB_RECURSE IFC_PLUS_PLUS_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ) list(FILTER IFC_PLUS_PLUS_SOURCES EXCLUDE REGEX .*/(tests?|test|build|third_party)/.* ) add_library(ifcplusplus STATIC ${IFC_PLUS_PLUS_SOURCES})CONFIGURE_DEPENDS是我重點(diǎn)要說的參數(shù)。不加它CMake 在重新生成構(gòu)建系統(tǒng)時(shí)不會(huì)自動(dòng)感知新增的.cpp你往src里放進(jìn)一個(gè)新文件再去cmake --build可能什么都不發(fā)生因?yàn)闃?gòu)建系統(tǒng)里的源文件列表還是舊的。加了它CMake 每次構(gòu)建都會(huì)掃描目錄變化代價(jià)是多了點(diǎn)文件系統(tǒng)開銷。對(duì)這個(gè) Archive 性質(zhì)、源文件數(shù)量幾百個(gè)的工程來說這點(diǎn)開銷值得。不過也別把所有依賴都?jí)涸?GLOB 上。我在實(shí)際維護(hù)中會(huì)把 GLOB 當(dāng)成“初篩”跑通之后再用git ls-files src/*.cpp去核對(duì)防止把不該編進(jìn)去的第三方樣例代碼一起編進(jìn)來。3. 用 Clang 編譯老 C 代碼先改標(biāo)準(zhǔn)再清警告3.1 Clang 與 GCC 的差別為什么在 IFC 這類代碼上被放大GCC 和 Clang 對(duì)老代碼的容忍度不太一樣。IFCPlusPlus 源碼里有大量std::vector的 size 轉(zhuǎn) int、浮點(diǎn)轉(zhuǎn)整數(shù)的隱式窄化GCC 在很多情況下只給一個(gè) warningClang 卻經(jīng)常以 error 級(jí)別停下來尤其是你開了-Werror之后。很多人第一次切 Clang 會(huì)覺得這是“Clang 太嚴(yán)格”實(shí)際上不是是代碼本身就站在懸邊上GetChildren().size()返回size_t你接到int count里在 32 位平臺(tái)可能沒事在 64 位平臺(tái)只要實(shí)體數(shù)量超過 2^31 就溢出。Clang 的好處是會(huì)把這類問題描述得更清洗配合-Wconversion能定位到具體是哪一行隱式轉(zhuǎn)換。這個(gè) Fork 的“第二波”提交里很大一部分改動(dòng)就是在處理這種告警把返回值類型改成size_t或者顯式static_cast。如果你要接盤別急著把警告都關(guān)掉那等于把問題留到數(shù)據(jù)量最大的那條 IFC 文件里。3.2 CMake 里切到 Clang 的最小配置切 Clang 不需要魔法。在 CMake 里只要在生成構(gòu)建系統(tǒng)時(shí)指定編譯器cmake -S . -B build-clang \ -DCMAKE_C_COMPILERclang \ -DCMAKE_CXX_COMPILERclang \ -DCMAKE_BUILD_TYPERelWithDebInfo如果你在 Windows 上且沒有裝 LLVM 的 Unix 風(fēng)格命令行可以用clang-clCMake 的 Visual Studio 生成器也能識(shí)別。關(guān)鍵的一點(diǎn)是先清掉舊的build目錄。我踩過好幾次之前用 GCC 配置生成的CMakeCache.txt里還留著CMAKE_CXX_COMPILER/usr/bin/g你即使指定-DCMAKE_CXX_COMPILERclangCMake 也可能因?yàn)榫彺娌桓露^續(xù)用舊編譯器。所以切編譯器之前先rm -rf build-clang或者用一個(gè)新的構(gòu)建目錄最保險(xiǎn)。在 CMakeLists 里給編譯參數(shù)盡量放到target_compile_options上不要用全局add_compile_optionstarget_compile_options(ifcplusplus PRIVATE -Wall -Wextra -Wpedantic -Wno-unused-parameter )-Wno-unused-parameter是我故意留的。IFCPlusPlus 里不少函數(shù)是給接口占位用的參數(shù)傳進(jìn)來了但不使用Clang 會(huì)把每個(gè)函數(shù)都報(bào)一遍幾百個(gè)警告刷下來真正重要的轉(zhuǎn)換告警反而被淹沒。先讓構(gòu)建通過再把 unused parameter 這個(gè)噪音屏蔽掉是接盤老代碼時(shí)的血淚經(jīng)驗(yàn)。3.3 C03 時(shí)代的隱式轉(zhuǎn)換Clang 報(bào)錯(cuò)比 GCC 慢半拍但更準(zhǔn)我遇到過一段典型代碼幾乎可以代表這個(gè) Fork 里常見的 Clang 告警來源int count ifc_object-GetChildren().size(); double length GetLengthFromValue(ifc_representation);第一行把size_t隱式轉(zhuǎn)成int第二行把 double 轉(zhuǎn)成更窄的類型這在當(dāng)時(shí)的 VS 編譯器里都是 warning 甚至不提示Clang 在 C11 模式下會(huì)直接告訴你可能是你寫錯(cuò)了。正確改法是size_t count ifc_object-GetChildren().size(); double length GetLengthFromValue(ifc_representation);size_t和auto在這里不是風(fēng)格問題是數(shù)據(jù)完整性問題。IFC 文件里一個(gè)建筑可能有幾十萬個(gè)構(gòu)件節(jié)點(diǎn)數(shù)超過 2^31 雖然少見但結(jié)構(gòu)再深一點(diǎn)、子節(jié)點(diǎn)再分一點(diǎn)用 int 裝 size_t 就變成懸空的炸彈。保留原本的窄化轉(zhuǎn)換短期能編譯通過長期就是生產(chǎn)環(huán)境里數(shù)據(jù)量一上來就崩的隱患。4. 把 IFCPlusPlusArchiv1 在本地跑起來從 CMake 下載到生成構(gòu)建系統(tǒng)4.1 環(huán)境準(zhǔn)備CMake 下載與編譯器三件套在一臺(tái)干凈機(jī)器上跑這個(gè) Fork我習(xí)慣先確認(rèn)三樣?xùn)|西CMake、Clang、Ninja。Linux 上可以直接用包管理器裝macOS 上用 Homebrew 也行Windows 上則去官網(wǎng)做 CMake 下載裝的時(shí)候記得勾選加入 PATH。裝完先跑一次cmake --version clang --version ninja --version三個(gè)命令都能輸出版本號(hào)再繼續(xù)。不要省這一步很多“編譯到一半找不到編譯器”的破事都是因?yàn)?CMake 找到了系統(tǒng)里某個(gè)舊 GCC而 Clang 沒被正確加入 PATH。Ninja 不是必須的但生成速度比 Makefile 快很多對(duì)這個(gè)源文件數(shù)量不小的工程N(yùn)inja 的增量構(gòu)建尤其順手。如果你更想用圖形界面確認(rèn)每個(gè)選項(xiàng)用 CMake GUI 指定源碼目錄和構(gòu)建目錄后點(diǎn) Generate 之前還能檢查所有緩存變量后面我會(huì)說具體看哪幾個(gè)。4.2 三條命令生成構(gòu)建系統(tǒng)拿到 Fork 的代碼并完成準(zhǔn)備后核心就這三條命令cd ifcplusplus-archiv1 cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPERelease cmake --build build-S . -B build是 CMake 三要素語法里的標(biāo)準(zhǔn)姿勢源碼目錄是當(dāng)前目錄構(gòu)建目錄是build所有生成物都隔離在那里。-G Ninja指定生成器如果你機(jī)器上沒裝 Ninja把-G Ninja去掉就會(huì)退化到 Unix Makefiles我不建議在沒有特殊原因時(shí)這么干Makefile 的并行度控制比 Ninja 更費(fèi)手。最后一條命令會(huì)先執(zhí)行cmake --build觸發(fā)重新生成再調(diào)用實(shí)際編譯器干活。如果構(gòu)建目錄里因?yàn)榍袚Q CMake 版本或編譯器留下了臟緩存刪掉build重來并不丟人。這個(gè) Fork 的歸檔性質(zhì)就該享受這種“后悔藥”退一步清構(gòu)建目錄比在舊緩存里找問題快得多。4.3 CMake GUI 里核對(duì)四個(gè)開關(guān)用 CMake GUI 打開源碼目錄后你會(huì)看到所有緩存變量。我一般重點(diǎn)看這四個(gè)變量我選的值理由CMAKE_BUILD_TYPERelease或RelWithDebInfo生產(chǎn)解析用 Release排查用 RelWithDebInfoCMAKE_CXX_COMPILERclang這是 Fork 的技術(shù)主題別讓 CMake 背著你選 GCCCMAKE_CXX_STANDARD11老代碼跑 C11 最穩(wěn)不建議直接上 17CMAKE_EXPORT_COMPILE_COMMANDSON后面跑 clang-tidy 依賴compile_commands.json前兩個(gè)是環(huán)境層面的后兩個(gè)是你后續(xù)調(diào)試的關(guān)鍵。CMAKE_EXPORT_COMPILE_COMMANDS很多老壓縮包里默認(rèn)不開但它生成的文件是 clang-tidy、clangd 這些工具能準(zhǔn)確理解你工程結(jié)構(gòu)的前提。你在 CMake GUI 里勾上它并重新 Generate之后構(gòu)建目錄里就會(huì)出現(xiàn)compile_commands.json沒有這個(gè)文件后面每一步靜態(tài)檢查都要手工拼參數(shù)費(fèi)時(shí)費(fèi)力。5. 避坑接盤 IFCPlusPlus 這個(gè) Fork 的五個(gè)常見問題5.1 找不到ifcplusplus庫目標(biāo)名大小寫和命名空間現(xiàn)象代碼 include 的是ifcpp/IFC4/IfcWall.hCMake 里target_link_libraries(myapp ifcplusplus)也寫了鏈接時(shí)還是說找不到libifcplusplus。原因老代碼里工程名和 CMake 目標(biāo)名不是一個(gè)東西。CMake 的add_library(ifcplusplus static ...)生成的是libifcplusplus.a但頭文件路徑前綴是ifcppinclude 目錄根本沒指到/src/ifcpp。解決在target_include_directories里同時(shí)給出src和src/ifcpp兩個(gè)路徑別只放一級(jí)。鏈接名用 CMake 目標(biāo)名ifcplusplus不要自己去拼-lifcplusplus因?yàn)?CMake 會(huì)自動(dòng)負(fù)責(zé)平臺(tái)前后綴。你要是寫了target_link_libraries(... /usr/lib/libifcplusplus.a)換平臺(tái)就全得重寫等于白用 CMake。5.2 Clang 下_USE_MATH_DEFINES缺失導(dǎo)致 M_PI 未定義現(xiàn)象編譯進(jìn)入幾何計(jì)算相關(guān)的.cpp時(shí)報(bào)M_PI未定義或者asinf等數(shù)學(xué)函數(shù)溢出。原因老代碼可能默認(rèn) MSVC 的寫法MSVC 里M_PI需要_USE_MATH_DEFINES開宏。在 Clang 的 libc 或 Linux glibc 下這個(gè)宏不是必須的但代碼里已經(jīng)用了一堆#ifdef _WIN32導(dǎo)致分支判定到了 Windows 路徑然后找不到 MSVC 才有的宏。解決在 CMake 里加target_compile_definitions(ifcplusplus PRIVATE _USE_MATH_DEFINES)確保即使跑到_WIN32分支也不會(huì)因?yàn)槿焙甓袛?。同時(shí)把涉及數(shù)學(xué)常量的位置改成constexpr double kPi M_PI;這種顯式局部變量比依賴全局宏更可控。5.3 使用file(GLOB_RECURSE)后源文件重復(fù)編譯現(xiàn)象你會(huì)發(fā)現(xiàn)同一個(gè).cpp被編譯了兩遍鏈接時(shí)一堆重復(fù)符號(hào)。原因GLOB 把src下的源碼和build目錄里 CMake 自動(dòng)復(fù)制出來的源文件一起收進(jìn)來了。這種情況在源碼樹里原本就帶著一個(gè) CMake 輸出子目錄時(shí)尤其常見GLOB 不區(qū)分。解決把build、cmake-build-*、Debug、Release這類目錄都加進(jìn)list(FILTER)的排除正則里并且在頂層用if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR})做個(gè)防呆提醒 CMake 的構(gòu)建目錄不要放在源碼樹內(nèi)。5.4 切換 Clang 后仍然在編譯 GCC 的-fopenmp選項(xiàng)現(xiàn)象構(gòu)建日志顯示 clang 在跑但命令行里還帶著-fopenmp隨后報(bào)錯(cuò)找不到omp.h。原因CMAKE_CXX_FLAGS在CMakeCache.txt里殘留了 GCC 的編譯選項(xiàng)你只改了編譯器路徑?jīng)]清掉附加參數(shù)。CMake 的緩存比你想的更“忠厚”舊值不會(huì)被自動(dòng)覆蓋。解決徹底刪掉構(gòu)建目錄重新用干凈參數(shù)配置。如果你以后要在同一棵樹里切編譯器建議用 CMake toolchain 文件區(qū)分而不是在命令行里反復(fù)覆蓋同一個(gè)緩存變量toolchain 文件會(huì)讓編譯器路徑和 flags 一起切換再也不用擔(dān)心殘留。5.5 鏈接期undefined reference to boost::filesystem::...現(xiàn)象編譯都過了鏈接時(shí)報(bào) boost filesystem 符號(hào)找不到換到 Release 又好了。原因IFCPlusPlus 老代碼經(jīng)常用了 boost舊的 Autotools 配置會(huì)自動(dòng)鏈接 boost 庫但你的 CMake 里沒find_package(Boost REQUIRED COMPONENTS filesystem system)于是庫目錄沒進(jìn)來。Release 下偶然能過是因?yàn)榫幾g器用了內(nèi)聯(lián)優(yōu)化把部分符號(hào)優(yōu)化沒了但這不是解決方案。解決在 CMakeLists 里顯式找包find_package(Boost REQUIRED COMPONENTS filesystem system) target_link_libraries(ifcplusplus PUBLIC Boost::filesystem Boost::system)如果用的是 Boost 1.70 以上的版本Boost::system可能已經(jīng)變成 header-only保留也沒關(guān)系但Boost::filesystem必須鏈。6. 用 clang-tidy 和 ASan 驗(yàn)證這個(gè) Fork 能不能進(jìn)生產(chǎn)驗(yàn)證一個(gè) Archive 性質(zhì)的 Fork 能不能進(jìn)生產(chǎn)我不會(huì)只看編譯通過還要跑兩輪自動(dòng)化檢查。第一輪是 clang-tidy。你已經(jīng)在 CMake 里開了CMAKE_EXPORT_COMPILE_COMMANDS現(xiàn)在只需要在構(gòu)建目錄里執(zhí)行clang-tidy src/IfcPlusPlusStatic/IfcPlusPlusManager.cpp \ -p build \ -checksclang-diagnostic-*,clang-converter-*,-clang-analyzer-alpha*-p build讓 clang-tidy 自動(dòng)讀取compile_commands.json。這一輪的目標(biāo)不是把所有警告清零而是盯住clang-converter那幾個(gè)檢查它們專門抓 C11 之后加入的類型轉(zhuǎn)換問題。老 IFC 代碼里有不少“把double塞進(jìn)float字段”的轉(zhuǎn)換這些在運(yùn)行期會(huì)直接造成幾何精度損失clang-tidy 能直接定位到賦值行比人工 review 可靠得多。第二輪是 ASan。給 CMake 加一組全局 flag 重新配置cmake -S . -B build-asan -G Ninja \ -DCMAKE_CXX_FLAGS-fsanitizeaddress -fno-omit-frame-pointer \ -DCMAKE_EXE_LINKER_FLAGS-fsanitizeaddress cmake --build build-asanASan 的價(jià)值在于找出“越界但不崩”的隱蔽訪問。IFCPlusPlus 的解析器把 STEP 文件讀進(jìn)內(nèi)存時(shí)如果某個(gè)實(shí)體實(shí)例的字段索引越界常見的現(xiàn)象是解析結(jié)果里坐標(biāo)變成 0 或 NaNASan 會(huì)在你第一次越界寫入時(shí)就喊停。我記得在跑某個(gè)商業(yè) IFC 文件時(shí)一個(gè)vector索引越界導(dǎo)致后面幾十個(gè)構(gòu)件的 transform 全錯(cuò)當(dāng)時(shí)只有 ASan 指出了第一現(xiàn)場而普通 Debug 構(gòu)建根本沒反應(yīng)。對(duì)這個(gè) Fork 來說編譯通過只是起點(diǎn)。真正能讓人放心投入的是把手里的 IFC 樣本文件放進(jìn)一個(gè)循環(huán)解析、讀屬性、寫回再和原始文件做差異比對(duì)。我現(xiàn)在的習(xí)慣是每次改完 CMake 配置都跑一個(gè) 20MB 的 IFC 文件做回歸在 ASan 下能跑完我心里才默認(rèn)這個(gè)版本是能帶的。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取