[跟吉姆一起讀LevelDB]1.Makefile與項目結構

這是我邊看邊寫的文章, 按RPG的套路, 現在我和諸位應該都在新手村, 說得自然會比較細. 覺得無聊的人大可跳過, 我總會寫到複雜的. 我也不認為LevelDB會有我不能理解的部分.

言歸正傳, 我以前只用過CLion捆綁的CMake. 自己寫的Makefile也僅限於標準三段式. 為了讀懂LevelDB的Makefile, 我大致上看完了GNU官方150多頁的PDF文檔. 我都這麼努力了就別黑我了. Makefile核心功能很簡單, 但附加的各種語法(糖/毒藥)非常多, 看得我眼花. Google的二位工程師也沒有用到多少. 工程上的事, 越簡單越好.

下面是我認為best practice的代碼片段:

Makefile 9-21行,

# (A) Production use (optimized mode)OPT ?= -O2 -DNDEBUG# (B) Debug mode, w/ full line-level debugging symbols# OPT ?= -g2# (C) Profiling mode: opt, but w/debugging symbols# OPT ?= -O2 -g2 -DNDEBUG#-----------------------------------------------# detect what platform were building on$(shell CC="$(CC)" CXX="$(CXX)" TARGET_OS="$(TARGET_OS)" ./build_detect_platform build_config.mk ./)# this file is generated by the previous line to set build flags and sourcesinclude build_config.mk

Production/Debug模式用flag"OPT"控制, 注釋/反注釋即可切換. 這永遠是最優的. 我見到所有合理的項目都是這樣.

"?="在Makefile表示, 如果之前沒有定義這個變數, 則賦值. 這讓程序員可以在make時覆蓋掉原有定義. 比如, make OPT=-O3, 由於OPT在Makefile被讀取之前就定義了, 所以賦值不會生效, 全部文件將以O3優化編譯.

$(shell SOME_CMD)命令是make設計的點睛之筆, 表示將用bash執行之後的所有命令. 這裡先用預自定義的$(CC)等賦值了環境變數, 然後調用腳本"build_detect_platform".

這個腳本完成了兩件事情,

1. find所有需要編譯的.cc文件(這個巨實用)

2. 各種跨平台設置

結果將會寫入到"build_config.mk", 然後被include進來, 繼續執行.

如果明白了Makefile的運作機理加上略懂一點bash或者別的腳本語言, Makefile完全可以做到跟CMake或者Scons一樣的事情, 甚至更方便. 我今後如果要寫C++項目, 估計Makefile風格也會跟Google的一樣, 建議認真閱讀.

接下來直到第398行, 都是很平淡無奇的經典三段式/變數定義.

有幾個很詭異的語法, 這裡提一下,

A := B 和 A = B, 一般情況下沒有什麼差別. 但如果有, 肯定會困惑你一陣子. A = B, 你可以理解為引用. A獲得了B的引用, B中途修改了, A也跟著變. A := B, 就是獲得了值, 官方文縐縐的叫法是"immediate expansion". 但你就按現代編程語言的理解, 我是沒遇到過問題. 還有個二次展開, Google都沒用上, 那就算了吧.

第87行的$(SOURCES:.cc=.o), 這個表示把".cc"替換成".o", 相當於編程語言中的replace函數.

158行的$(STATIC_OUTDIR)/db: | $(STATIC_OUTDIR), 這個豎杠是什麼意思呢? 想像一下, make時在不斷往目錄A裡面寫文件a, b, c... a, b和c自然是依賴於目錄A的, 因為要先建立目錄才能寫文件. 但問題在於不斷添加新文件的時候, A目錄的last-modified time在不斷變更, 然後又導致依賴於A的a, b, c反覆編譯.

依舊用現代編程語言來比喻, 那就是對象相互引用, 導致計數器無法歸零, 然後內存泄漏了. "|"就相當於weak_ptr. $(STATIC_OUTDIR)/db依賴於$(STATIC_OUTDIR)的完成, 但完成之後的刷新操作不會繼續波及到$(STATIC_OUTDIR)/db. 這個學名叫"order-only-prerequisites".

零零碎碎的點, "$@"表示object的名字, "$^"是所有依賴的名字, "$<"代表第一個依賴.

接著看, Makefile 399-421行,

$(SIMULATOR_OUTDIR)/%.o: %.cc xcrun -sdk iphonesimulator $(CXX) $(CXXFLAGS) $(SIMULATOR_CFLAGS) -c $< -o $@$(DEVICE_OUTDIR)/%.o: %.cc xcrun -sdk iphoneos $(CXX) $(CXXFLAGS) $(DEVICE_CFLAGS) -c $< -o $@$(SIMULATOR_OUTDIR)/%.o: %.c xcrun -sdk iphonesimulator $(CC) $(CFLAGS) $(SIMULATOR_CFLAGS) -c $< -o $@$(DEVICE_OUTDIR)/%.o: %.c xcrun -sdk iphoneos $(CC) $(CFLAGS) $(DEVICE_CFLAGS) -c $< -o $@$(STATIC_OUTDIR)/%.o: %.cc $(CXX) $(CXXFLAGS) -c $< -o $@$(STATIC_OUTDIR)/%.o: %.c $(CC) $(CFLAGS) -c $< -o $@$(SHARED_OUTDIR)/%.o: %.cc $(CXX) $(CXXFLAGS) $(PLATFORM_SHARED_CFLAGS) -c $< -o $@$(SHARED_OUTDIR)/%.o: %.c $(CC) $(CFLAGS) $(PLATFORM_SHARED_CFLAGS) -c $< -o $@

我以前如果有10個cc文件, 我肯定會寫10條規則. 其實可以用通配符"%", make會自動把所有匹配到的結果展開. 比如, %.cc匹配到1.cc和2.cc, 就會自動展開為針對1.cc和2.cc寫的兩條一模一樣的規則. 太爽了!

LevelDB的Makefile只用了make允許的語法的很少一部分特性, 但看得真的很流暢很舒服. 代碼少部分時間是機器讀的, 大部分是人讀的. 還有一個做法很有趣, 但我認為有待商榷. 其Makefile文件沒有對.h頭文件做制約. 就是說如果.h文件修改了, 你就只能clean了再make一遍, 不然就可能得到你以為修改了, 其實什麼都沒變化的程序.

------

項目結構

LevelDB的項目結構是相當扁平化的, 目錄沒超過3級的, 好像C/C++程序員都喜歡都這樣. Redis也是源碼一大票文件都在src下. 我個人是偏Java那種, 要分好多級的. 這個也可以學習下, 代碼之間本來就是高度復用的, 強行劃分成各種"優美"的小塊, 反而降低了質量.

  • db/, 資料庫邏輯
  • doc/, MD文檔
  • helpers/, LevelDB內存版, 通過namespace覆蓋
  • port/, 平台相關代碼
  • table/, LSM有關的

文檔, 編譯, 源代碼三合一, 乾淨利落. 我曾經去過2家小公司, 每次的第一天, 我都在折騰開發環境, 要麼這個裝不上, 要麼那個裝不上.

一個項目能不能一鍵編譯測試部署, 應該是跟項目質量正相關的. LevelDB就是良好的典範.

------

接來下就玩硬派的, 跟我單步跟蹤看看LevelDB具體是怎麼work的吧. 我會先看看Big-Table的論文. 不要再說我無聊啦! (╯﹏╰)


推薦閱讀:

在 corfu 模型上的幾點擴展
資料庫建表時一定要設置外鍵約束關係嗎?
剛開始學習資料庫對資料庫概念一竅不通。?
雲計算架構下 Cloud TiDB的技術奧秘「上」
大家如何看待類似於達夢資料庫這樣的號稱具有自主知識產權的國產軟體?

TAG:Makefile | 数据库 | LevelDB |