Calendar

十月 2014
« 十一月   十一月 »
 12345
6789101112
13141516171819
20212223242526
2728293031  

Categories

在 FreeBSD 9.x 的系統中啟用 C++11 及 libc++

看標題應該就知道是用 clang 了,至於目的,當然就是要在 FreeBSD 裡使用最完全最先進的 C++11 來寫程式,並且拋棄 GCC 及 libstdc++。
如果系統中有任何函式庫混入了 libstdc++,導致最終執行檔同時連結了 libc++ 及 libstdc++ (使用 ldd 檢查就能看見),那麼程式在啟動時就會 crash。
因此要達到這個目標,就必須自行編譯整個系統及所有套件,並確實讓使用到 C++ 的程式及函式庫都只連結到 libc++。

最新的 FreeBSD 9.x 已內建 clang,不過內建的版本並不是在這模式下編譯的,因此魯莽地在 make buildworld 時在 /etc/make.conf 的 CXXFLAGS 放 -std=gnu++11 -stdlib=libc++ 是會吃 error 的。
雖然到 FreeBSD 10.x 才會正式拿掉系統的 GCC,不過就像去年在在 FreeBSD 9.1 裡完全使用 clang 代替系統的 gcc這篇中所說的,只要在 /etc/make.conf 放一行 WITHOUT_GCC=yes 就可以在下次 installworld 後使用 make delete-old 和系統的舊版 GCC 徹底說再見。
除此之外,還必須搭上 WITH_CLANG_IS_CC=yes 將系統的 cc 更換成 clang,當然 c++ 及 cpp 也會一併被 clang++ 及 clang-cpp 替換。
至於 /etc/src.conf 能放什麼,只要 man src.conf 就能看到。

首先要重新編譯整個系統,方法在 /usr/src/Makefile 的註解裡有,應該 FreeBSD 的使用者都很熟悉。
我習慣是用的是下面共 11 步的方式來做更新,第二次 mergemaster 會下 -iU 這兩個參數:

至於 src.conf 我放的是這樣:

然後 /etc/make.conf 裡和編譯有關的變數放這樣:

幾個月前,如果不用 -march=native,根據 CPU 指令集特性的不同,可能必須使用 core-i7、core-i7-avx、core-avx-i、core-avx2 這類參數,而且 core-avx2 還有地雷不能用,否則編出來的東西會噴 illegal instruction。
而目前最新的 GCC 4.9.x 及 clang 3.4.1,-march 吃的參數已經改用 microarchitecture 的名稱了,這個有裝 lang/gcc49 的話可以 man gcc49 看到:

如果好奇下 -march=native 會偵測到什麼,我們都知道在 GCC 下 gcc -march=native -Q --help=target 然後看 -march 那欄抓到什麼來得知。
至於 clang 的話,可以下 : | clang -v -E -march=native - 然後觀察 -target-cpu 後面的參數來得知。
總之如果偵測錯誤的話,就不要使用 -march=native,是 haswell 的 CPU 就直接下 -march=haswell,自動的東西本來可信度就有限。

接下來就是照上述 11 個步驟重新編譯 kernel 及 world,當然如果 /usr/src 和你目前系統版本相符,也可以只做 world 的部分。
一旦全部做完之後,再修改 /etc/make.conf 的 CXXFLAGS:

然後重複上述 11 個步驟,或者執行和 world 相關的部分。

理論上只要 /etc/make.conf 這樣放了,後續編譯 ports 裡和 C++ 有關的程式碼,也都會是開啟 -std=gnu++11 -stdlib=libc++ 在編。
不過事與願違,其實有部份 source code 的 Makefile 是寫死 -lstdc++,這個沒辦法,只能靠 sed 處理或手動修改,譬如 libvpx 就會遇到這樣的問題:

編譯 graphviz 的時候也會遇到相同的災難,可以在 make patch 後下 find . -type f | xargs grep 'stdc++' | awk 'BEGIN { FS = ":" } {print $1}' | xargs sed -i .orig 's/lstdc++/lc++/g' 暴力搞定。
find 的用法看人看狀況,這邊我不用 -exec 參數而使用 xargs,這沒什麼強制規定,選用得順的方法就行。
至於 boost,可以在 /usr/ports/devel/boost-libs/Makefile 裡的適當處補一行這個:

當然你會問怎麼不請 ports maintainer 加成 option 就好?原因是我懶,給有空的人去提建議或 send-pr 吧,只是到時可能會有 -std=c++11 和 -std=gnu++11 之爭。

如果你有安裝 databases/mysql++3 的需求,問題看起來很像,但是其實並不相同。
你會發現 -stdlib=libc++ 明明都下了,但是 linker 卻去 link libstdc++,就好像 -stdlib=libc++ 從來沒被下過一樣。
這個現象是 -Wl,-soname,libmysqlpp.so.3 造成的,只要有它在,-stdlib=libc++ 的效果就像是被取消一樣,你必須自己設法手動解決。
至於為什麼會發生這種事?我也沒詳細去追原因,上班族的自由時間是很少的,有空再查吧。

除此之外,你可能還會遇到一些很夭壽的狀況,而這狀況是 ports maintainer 們搞出來的。
比方說,你在編譯 devel/yasm 的時候,就會在 configure 階段死掉:

去開 config.log 來看,就會看到這種詭異的東西:

你一定會覺得納悶,明明叫起來的是 clang,為什麼你寫在 CXXFLAGS 裡的東西也會跑出來?
在你打開 /usr/ports/devel/yasm/Makefile 後,你就會得到答案:

我不清楚這有什麼神奇的理由,總之把這智障無比的 ${CXXFLAGS} 拔了,這玩意就可以正常編譯和安裝。
會幹這種事的只有 yasm 的 ports maintainer 嗎?當然不是,我們來看看 textproc/sphinxsearch/Makefile 裡這精美的註解和內容:

他說 don't ask!看到了沒?他說 don't ask!
WTF 是問句不能用,看來只能用非問句直接罵 fuck you。

當然打開了 C++11 模式,也會引進一些更嚴格的型別檢查機制,譬如編譯 textproc/aspell 的時候就會遇上這兩個錯誤:

這問題沒什麼了不起,看一下上下文就能知道該怎麼改了。
如果你懶得看上下文,最保險的方法也有先檢查 pointer 是否有效,再照該行原語意幫他補 dereference operator。
同樣的問題在 database/mysql56-server 也會發生,需要進行修正:

編譯 MySQL 5.6 還會遇到另一個問題,不過這裡 clang 已經很好心地提供了解決方法,照著把 int 換成 size_t 就能解決:

這種 code 在 C++11 模式編譯下堪稱第二大死因,只能說寫 code 的人習慣不好,請他們回去重新唸好書再來寫 code 吧。
因為 mariadb 和 percona 也是 mysql 的衍生物,所以編譯它們同樣可能踩到相同的問題,不過我記得 mariadb 只會遇到上述其中一種問題。
這篇不是 C++11 教學文,而且錯誤訊息已經明確說出原因,我就不多說了。
除了 sql_trigger.cc 以外,sql_view.cc 也會踩到一模一樣的問題,照著 clang 的錯誤訊息修改好就行。

上面講到第二大死因,那麼第一大死因是什麼東西?有沒有第三大死因?我們請 multimedia/mp4v2 進場。
一開始編譯你的眉頭可能就會皺一下,因為看到了 warning message:

不過這玩意只是 deprecated,等後續版本突然變成 error 了再處理吧,當然你不爽的話可以直接改了然後 send-pr。
再來就是所謂的第三大死因煞氣進場了:

這個現象會發生在 switch statement 裡,基本上也是不會寫程式的典型案例之一。
要想過這一關,你必須從源頭將傳入 switch statement 的變數型別改寬,譬如換成 long long,否則程式的行為就不會是原作者所想要的那樣。

經過一番努力解決第三大死因後,你應該還會在 rtphint.cpp 遇到前面講過的小狀況要處理掉:

不過當這些 source code 的小問題因為 C++11 mode 而一個接一個浮上檯面後,你應該或多或少會打幾個寒顫,特別是這些東西你平常有在用的話...
除此之外,你還會看到另一個小小問題:

只要你打開 source code 看一下這函式回傳什麼型別,就會看到造成這問題的原因一切都是腦殘手賤

再來我們編譯 audio/faac 見識見識第一大死因,來看看 clang 精美的錯誤訊息吧:

是的,正如錯誤訊息所說,C++11 會要求你補空格,否則他會視為 string literal 的 suffix。
我知道你情緒很激動,你可能非常想要殺人,可能你手邊有不知道多少專案也是這樣黏著寫。
但是請接受這樣的事實,然後加油改吧...
改完以後,devel/doxygen 裡面還有類似的東西要你去面對:

除了這個第一大死因外,audio/faac 裡有一個 bundle 在一起的 mp4v2,不過和前面遇到的版本不太一樣,所以問題也不同。

這也是和前面遇到的問題相同,只要依照 clang 親切的提示做修正就行了。

再來,雖然現在 squid 3.3 已經不是最新版了,所以或許你不會想去編譯它。
但是如果你就是手賤,或者為了某些堅持非用 3.3 版不可,那麼在幾個月前你可能會踩到這個該死的問題:

這問題的肇因也是在於作者對 STL 不熟悉,沒有好好瞭解自訂 iterator 的要件是什麼所犯的低級錯誤
現在 FreeBSD 的 ports 已經有處理這問題的 patch,但還是略嫌囉唆:

如果有讀過那本藍白封面的 Generic Programming and the STL,應該就會知道這個只要讓 VectorIteratorBase 去 public 繼承 std::iterator<>,然後將適當的 template arguments 填入即可。

還有一個已經被原作者拋棄的孤兒,但是大家還是很希望能使用它,這孤兒的名字叫 suphp。
它帶來的錯誤訊息,有幫很多人擦過屁股經驗的,應該一眼就能看出是原作者對 STL 不熟悉,甚至有所誤解:

原因就是因為他寫了 std::multimap<const std::string, const std::string> 這種鬼東西,const 自己加得不亦樂乎,又一個不看書就亂寫程式的例子。

最後,讓我們來見識一下 C++11 新引進的特性,有請 graphics/silgraphite 進場。
等等...silgraphite 這東西什麼時候用到?難道不能不編嗎?
答案是編譯 print/texlive-full 的時候會跟它相依,所以你還是要編,除非你和我一樣已經是不用寫論文的社會人士;所以編吧。
當然不可免俗地要來一個華麗的爆炸,不過似乎相當單調:

如果去查 C++11 對 NULL 這 macro 怎麼定義,就會知道這東西是 implementation-defined,所以我們來看看 implementation 是怎麼定義的:

在 C++11 的模式下,這玩意會被定義成 nullptr。
對 C++11 新特性略有耳聞的人應該就會知道 nullptr 不需要也不可以使用 reinterpret_cast 去轉成其它 pointer。
這種情況下有兩種方式可以選擇,一種是利用它先天的隱式型別轉換,剝掉 reinterpret_cast 即可,或者明確寫成 static_cast。
你一定會很好奇 nullptr 到底是什麼東西?不能用 reinterpret_cast,難道它不是 pointer 嗎?它是不是 keyword?它是一個 object 嗎?
歡迎來到 C++11 的世界,我不在這裡告訴你,現在已經 2014 年了,人總是需要更新一些知識才能跟上時代腳步的。

做為這篇發在我自己個板的廢文收尾的錯誤訊息,還是要來點新鮮的東西。
來看看 graphics/silgraphite 裡華麗的爆炸吧:

很令人意外嗎?
明明 std::swap() 照經驗原本該是一個修計算機概論時就學過的實作,傳兩個 reference 進去,設個暫存變數來幫忙交換,然後就收工了。
以前 C++ 的 std::swap() 用起來似乎也是這樣,怎麼一進 C++11 就變天了?顯然你需要看書。
如果你的個性是喜歡直接看 code,但是卻沒摸過 C++ Template Metaprogramming,不認識 enable_if<>,那麼建議你還是乖乖看書,別直接追 code;我警告過你。

總而言之,書會告訴你,在 C++11 的 std::swap() 已經從 <algorithm> 搬家到 <utility> 去了,這是第一件要注意的事。
再來,在 C++98 / C++03 裡,傳入的引數型別必須同時滿足 copy-constructible 及 assignable 兩項概念的要求。
而在 C++11 裡,變成了必須滿足 move-constructible 及 move-assignable 這兩項概念的要求。
相信很多人對 C++11 新引進的 move semantic 早就不陌生,也知道 C++11 引進了 move constructor 這項特性。
這東西可說是 C++ 程式效能的救星,也能一吐長年被某些程式語言打趴在地上的怨氣。想知道這是什麼嗎?來學 C++11 吧。

滿足 move-assignable 沒什麼難度,就算你沒有自己定義 move assignment operator,只要有個正常的 copy assignment operator 就能滿足了。
copy assignment operator 這東西要存在的難度極低,因為 C++ compiler 會自動幫你產生,所以你不寫也會有,除非你設法去禁止它被使用或被產生。
打開 engine/include/graphite/Segment.h 就能看到 Segment 這個 class 的定義,完全找不到任何 overloaded operators,所以可以放心。
那麼問題必然出在不滿足 move-constructible,所以來看看究竟發生了什麼事。

滿足 move-contructible 其實也沒有難度,就算你沒有提供 move constructor,只要你有個正常的 copy constructor 就能滿足了。
這程式顯然是 legacy code,不會有 C++11 才有的東西,所以問題就出在這個正常的 copy constructor 是否存在。
來簡單掃一下 Segment 定義式的前半段:

嗯...註解有個 Basic copy constructor,看似是有個 constructor 沒錯,但是它正常嗎?
這程式的作者真的認為所有 Segment 的 instances 丟過來,這個 copy constructor 都有辦法接嗎?
我不知道這作者是不是真的這麼認為,但相信有點 C++ 程式設計經驗的應該都知道有一種東西它接不了。那是什麼?答案是 temporary object。
一個 temporary object 先天上具備 const 性質,所以把 temporary object 丟進這種 constructor 裡會 discard cv-qualifier,這件事在 C++ 是死罪。
換言之這個 basic copy constructor 一點都不 basic,請作者正名為 ridiculous copy constructor,寫這什麼鳥蛋。
所以解決方法就是補 const 就收工了,當然還得去定義這 copy constructor 的檔案裡也補一次,光補在 header file 是不夠的。

其實本來不是很想寫這篇,因為每次發文都好花時間,上班以後沒什麼動力寫。
但是因為好久沒發廢文了,想想也應該來發個一篇。
這幾年來不乏各種 C++11 相關的文章,裡面羅列了各種令人感到新奇的新特性,我想那類文章應該也已經夠多了,沒必要特別寫。
不過既然剛好有機會用不良程式碼來介紹 C++11 的新特性,所以就把最近做的事和遇到的狀況收集了一下貼上來。

  • Irwin Shen

    真不愧是LPC之神。