c/c++ 開發環境要比其他後出的語言稍為麻煩一點,但只要知道自己卡在哪就不會毫無解決的想法了。
標準的 c/c++ 其實是 portable
的,可以
write once compile anywhere
所以,常會有接到工作說,可不可以幫忙 porting
到什麼平台。「不!」請說清楚,你沒有動到組語不要跟別人說 porting
啊!那只是 compile
的動作。
但要能正常的 compile,對專案使用到的「每一個相依函式庫」都得去各別的 compile
(如果沒有直接的安裝方法),或是去理解該函式庫使用的 toolchain 或編譯的方式。
通常有各自的「特色」,再加上它不像跑在 VM 的語言,可以假裝有一個世界大同的環境,才會處理起來比較麻煩。OS Distribution 的 Package Manager 中沒提供的,就是自己編譯啦!
昨日參與討論的過程,就是搞定編譯中常見的問題,其實撇開專案各自的 toolchain 選擇不同,說到底單純是搞定 Compiler 與 Linker 的參數而已。
看大家把焦點放在 cmake,但其實解決昨天的問題跟 cmake 本身的關係沒那麼直接,幫自己的思路 recap 一下。
問題的本質:要讓一組 c/c++ 的專案能夠編譯成功,就是搞定 compiler 的參數而已。
aka 缺什麼補什麼,多的就拿掉。 <= 湊答案
CMake 幫我們做什麼?CMake 是因為人工直接手刻的 Makefile 常常不能適用在不同的平台,刻 Linux 用的,不太考慮 Windows 用的開發者,刻 Windows 用的,不太考慮 Linux 的開發者,還有常常被遺忘的 Freedbsd 開發者。
因此,就會想要有一個幫忙產生 Makefile 的工具,雖然已經有 Autotools 系列的工具了,可是它對 Windows 不那麼友善,外加學習門檻不低。我自己也一直沒學會 Makefile 內的各種東西,但至少記得 target 的 indent 只能用 TAB
字元。
cmake -> makefile -> make -> (compiler 指令) -> 生出結果
我們使用 cmake,用它來生 makefile,再用 make 指令去間接執行 compiler 指令生出結果。為什麼要弄個像 pipeline 的東西呢?通常我們會把一個任務由出發到結果重要的環節「先準備好」,出事的時候先問自己,是哪一個環節,或哪二個之間出事了。
昨天的 2 個問題主要在 (compiler 指令)
階段出錯:
- 找不到 header file
- 沒辦法編譯出可執行的 test (有 error 產生)
知道 compiler 出錯,就要在腦中抓出相關的知識。對 c/c++ 來說有哪些常見的參數需要搞定?
-I
header file 的 search path-L
(static/dynamic) library 的 search path-l
linker 參數
找不到 header file
就簡單對應到 -I
的問題,而 linker 階段出錯,就要研究 linker 的參數。
另一個問題是,得先知道 compiler 指令
具體下了什麼,我們對應回 pipeline
知,它的上一階段是 make
,而 make 是依 makefile 定義的內容來的。只要回頭閱讀 makefile 內容,就會知道它下了什麼。但通常,我們不會人工去讀 code generator 生出來的東西 (除了組語之外)。
我需要它在 runtime 時的資料,有沒有像 shell
使用的 -x
能印出 debug info 的參數呢?試著上網 Google 有沒有讓 make 時也產生它下什麼指令的方法:
make VERBOSE=1
於是,我們得到最後 compiler 的指令是什麼。這時,我們就可以先透過這個指令,在跟他一樣的目錄 (test) 下執行,先看是不是 reproducible
,只要能精準的重現問題,你的「實驗環境」就是建立成功了。
$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk -mmacosx-version-min=12.1 -Wl,-search_paths_first -Wl,-headerpad_max_install_names CMakeFiles/MachiKoroCPPTest.dir/test_main.cc.o -o /Users/qrtt1/temp/Machi-Koro-Cpp/bin/MachiKoroCPPTest /opt/drogon/lib/libdrogon.a /opt/trantor/lib/libtrantor.a -lpthread -ldl /opt/homebrew/lib/libjsoncpp.dylib /opt/homebrew/lib/postgresql@14/libpq.dylib /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/libsqlite3.tbd /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/libz.tbd
Undefined symbols for architecture arm64:
"MachiKoroGame::MachiKoroGame()", referenced from:
drtest__BasicTest::doTest_(std::__1::shared_ptr<drogon::test::Case>) in test_main.cc.o
"MachiKoroGame::~MachiKoroGame()", referenced from:
drtest__BasicTest::doTest_(std::__1::shared_ptr<drogon::test::Case>) in test_main.cc.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
指令很長,先用順手的工具把它切開方便閱讀:
(⎈ |microk8s:default)(base) ➜ test git:(main) ✗ python
Python 3.10.6 | packaged by conda-forge | (main, Aug 22 2022, 20:41:22) [Clang 13.0.1 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> for x in "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk -mmacosx-version-min=12.1 -Wl,-search_paths_first -Wl,-headerpad_max_install_names CMakeFiles/MachiKoroCPPTest.dir/test_main.cc.o -o /Users/qrtt1/temp/Machi-Koro-Cpp/bin/MachiKoroCPPTest /opt/drogon/lib/libdrogon.a /opt/trantor/lib/libtrantor.a -lpthread -ldl /opt/homebrew/lib/libjsoncpp.dylib /opt/homebrew/lib/postgresql@14/libpq.dylib /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/libsqlite3.tbd /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/libz.tbd".split(" "): print(x)
...
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-arch
arm64
-isysroot
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk
-mmacosx-version-min=12.1
-Wl,-search_paths_first
-Wl,-headerpad_max_install_names
CMakeFiles/MachiKoroCPPTest.dir/test_main.cc.o
-o
/Users/qrtt1/temp/Machi-Koro-Cpp/bin/MachiKoroCPPTest
/opt/drogon/lib/libdrogon.a
/opt/trantor/lib/libtrantor.a
-lpthread
-ldl
/opt/homebrew/lib/libjsoncpp.dylib
/opt/homebrew/lib/postgresql@14/libpq.dylib
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/libsqlite3.tbd
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/libz.tbd
>>>
實際上觀察,我們想要的 library 都有了,但在追加了 MachiKoroGame
類別後死掉的,所以它八成是少 link 了這個 obj 檔。在專案中需找一下它可能的位置:
(⎈ |microk8s:default)(base) ➜ test git:(main) ✗ find .. -name "*.o"
../test/CMakeFiles/MachiKoroCPPTest.dir/test_main.cc.o
../CMakeFiles/3.25.0/CompilerIdCXX/CMakeCXXCompilerId.o
../CMakeFiles/MachiKoroCPP.dir/models/src/player.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/main.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/landmark.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/machikoro_game.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/bank.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/card.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/architecture_market.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/hand.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/initial_building.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/building.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/important_building.cpp.o
../CMakeFiles/MachiKoroCPP.dir/models/src/general_building.cpp.o
發現了:
../CMakeFiles/MachiKoroCPP.dir/models/src/machikoro_game.cpp.o
將它補到 compiler 指令的最後一個參數,可編譯成功。最終得 root cause
是缺少了 linker 需要一起用的 obj 檔。到了這個步驟,我們才會再回頭看一下這個流程:
cmake -> makefile -> make -> (compiler 指令) -> 生出結果
需要回頭修幫我們產生 makefile 的 CMakeLists.txt
:
CMakeLists.txt -> cmake -> makefile -> make -> (compiler 指令) -> 生出結果
畢竟,我們只是透過 cmake 來組出最後 Compiler 與 Linker 需要的參數,至少怎麼在 cmake 中組出常見的 3 個參數,還有必要的 2 類輸入檔案 (source code 還有 obj file) 那就是學到的時候,做好小抄下回拿來抄就夠了。