事實上,這個世界並無幾份 GNU m4 教程。前端
這個文檔系列是我第一次認真學習 GNU m4 並進行了一些實踐以後的一些總結。因爲我在撰寫此文的過程當中充滿着像 m4 展開一個又一個宏通常的耐心,所以這篇文章會比較長。在這個信息碎片化的時代,彷佛沒有不少人願意去看很長的文章,你們更喜歡乾貨。爲了節省你們的時間,必須聲明,這個文檔系列沒有乾貨,它是寫給我本身或者那些像我本身的人看的。編程
書名是『宏』,它被做者展開爲這本書的所有內容。藥瓶上的標籤是『宏』,將藥片從瓶中傾倒出來,就是這個宏的展開結果。被用的最多的『宏』,應該是 Internet 的超級連接。每當你點擊一個超級連接,就至關於將這個宏展開爲網頁中的內容。生活中,相似的例子還有不少,只要你給某種具體的事物貼上了一個標籤,那麼這個標籤就至關於宏。segmentfault
人類很是喜歡給事物貼標籤,儘管不管他們貼與不貼,那些事物自己依然是存在的。在編程中,若是你想給一段代碼貼標籤,最簡單最直接的辦法就是使用宏。那些還在用匯編語言編程的人,他們是離不開宏的,由於彙編語言自己就是將一大堆標籤貼在了更大的一堆機器代碼上。若是所用的編程語言不提供宏功能,能夠用這種編程語言爲一段代碼製做一個標籤——函數,不過這種標籤就不是宏了,並且要付出一些性能上的代價,由於標籤的展開過程被推遲到程序的運行過程。緩存
C 語言自誕生後,只用了 5 年就讓彙編語言歸隱山林了,這可能要歸功於 Unix 的成功以及 Dennis Ritchie 的忽悠。Steve Johnson——yacc, lint, spell 以及 PCC(Portable C Compiler)的做者說:『Dennis Ritchie 告訴全部人,C 函數的調用開銷真的很小很小。因而人人都開始編寫小函數,搞模塊化。然而幾年後,咱們發如今 PDF-11 中函數的調用開銷依然很是大,而 VAX 機器上的代碼每每在 CALL 指令上花費掉 50% 的運行時間。Dennis 對咱們撒了謊!但爲時已晚,咱們已經欲罷不能……』安全
現代的編程語言,幾乎都贊同用函數來取代宏。擁護者們每每會給出一些堂而皇之的理由是,諸如沒必要額外實現一個宏處理器,函數比宏更安全而且更容易調試。事實上,他們的理由僅僅是迎合現實而已。若是將這些人扔進時空裂縫讓他們穿越到 Ken Thompson 編寫 Unix 系統的時代,讓他們也在一臺廢棄的 PDP-7 型號的計算機上寫程序。在這種內存只有 8KB 的計算機上,那些堂而皇之的理由近乎與科幻小說等價。函數之因此可以取代宏,僅僅是由於 CPU 的計算速度比過去更快了,內存比之前更大了,犧牲一些程序性能,讓編程工做更容易一些,這樣比較合算而已。編程語言的性能與機器的性能彷佛老是成反比的。編程語言
宏被不少人主觀的棄用了,得益於現代編程語言的表達能力,他們彷佛幾乎不須要用宏,因而他們做出結論:宏過期了。事實上,宏會永遠居於衆編程語言之上的,由於前者老是可以生成後者。編程專家老是會告訴咱們,要慎用宏。膽子小的程序猿看到宏就躲得遠遠的,以致於他們總以爲那些使用宏的代碼是糟糕的,是不安全的。事實上,在編程中,若能恰如其分的使用宏,可讓代碼更加簡潔易讀,特別是對 C 語言這種表現力不足的語言。模塊化
例以下面 C 代碼中的宏:函數
#define DEF_PAIR_OF(dtype) \ typedef struct pair_of_##dtype { \ dtype first; \ dtype second; \ } pair_of_##dtype##_t DEF_PAIR_OF(int); DEF_PAIR_OF(double); DEF_PAIR_OF(MyStruct);
是否是有點 C++ 模板的意味?像 C 標準庫提供的 qsort
函數所接受的回調函數,也能夠用相似的方法半自動生成。有關 C 語言宏的基本規則與技巧,可參考『宏定義的黑魔法 - 宏菜鳥起飛手冊』。即便是表達能力很強的現代編程語言,在處理複雜問題上,也沒法避免代碼自身的頻繁重複,妥善的使用宏老是能夠消除這種重複,甚至能夠創造一些 DSL(領域專用語言)。性能
在代碼中適當的運用宏,創造優雅易讀的代碼,這樣或許更能體現編程是一種藝術。雖然有些編程語言未提供宏功能,可是咱們老是會有 GNU m4 這種通用的宏處理器可用。學習
m4 是一種宏處理器,它掃描用戶輸入的文本並將其輸出,期間若是遇到宏就將其展開後輸出。宏有兩種,一種是內建的,另外一種是用戶定義的,它們能接受任意數量的參數。除了作展開宏的工做以外,m4 內建的宏可以加載文件,執行 Shell 命令,作整數運算,操縱文本,造成遞歸等等。m4 可用做編譯器的前端,或者單純做爲宏處理器來用。
全部的 Unix 系統都會提供 m4 宏處理器,由於它是 POSIX 標準的一部分。一般只有不多一部分人知道它的存在,這些發現了 m4 的人每每會在某個方面成爲專家。這不是我說的,這是 m4 手冊說的。
其實,手冊裏的原話翻譯過來應該是:一般只有不多一部分人知道它的存在,發現它的人每每會成爲它的忠實用戶。(這裏要多謝 @mohu3g 的正本清源之舉)
有些人對 m4 很是着迷,他們先是用 m4 解決一些簡單的問題,而後解決了一個比一個更大的問題,直至掌握如何編寫一個複雜的 m4 宏集。若癡迷於此,每每會對一些簡單的問題寫出複雜的 m4 腳本,而後耗費不少時間去調試,反而不如直接手動解決問題更有效。因此,對於程序猿中的強迫症患者,要對 m4 有所警戒,它可能會危及你的健康。這也不是我說的,是 m4 手冊說的。
上文提到『m4 是一種宏處理器,它掃描用戶輸入的文本並將其輸出,期間若是遇到宏就將其展開後輸出』,其實更正式的說,應該是:m4 從文本輸入流中獲取文本並將其發送到文本輸出流,期間若是遇到宏就將其展開後發送到文本輸出流。
在 Brian Kernighan 與 Dennis Ritchie 合著的《C Programming Language》中將流(Stream)定義爲『與磁盤或其它外圍設備關聯的數據的源或目的地』。基於這個定義,m4 的輸入流就是與磁盤或其它外圍設備關聯的數據的源,其輸出流就是與磁盤或其它外圍設備關聯的數據的源或目的地,只不過 m4 但願它的輸入流與輸出流的內容是文本。若是你不那麼較真,能夠將流理解爲文件,對於 m4 而言,就是文本文件,可是下文會堅持使用流的概念。
m4 使用流的概念並不是巧合,若是說巧合,也只是由於它的做者剛好也是 Dennis Ritchie。
m4 是如何從輸入流中獲取文本並將其發送到輸出流的?確定不是簡單的讀取文本就了事,由於 m4 有一個任務是『遇到宏就將其展開』。這意味着 m4 在從輸入流中讀取文本的過程當中至少須要檢測所讀取的某段文本是否是宏。也就是說,從 m4 的角度來看,它首先要將輸入流所提供的文本分爲兩類:宏與非宏。若是 m4 讀取的是一段文本是非宏,它基本上會將它們直接發送到輸出流。之因此說是『基本上』,是由於非宏的文本會被進一步分類處理,其中細節後文會講。若是 m4 讀取的文本片斷是宏,m4 就會將它展開,而後將展開結果發送到輸出流。
m4 的工做過程具備必定程度的即時性,它不須要將輸入流中所有信息都讀取出來,而後再進行處理,而是扮演了一種過濾器的角色。從用戶的角度來看,文本流入 m4,而後又流出。
從圖靈的角度來看 m4,輸入流與輸出流能夠銜接起來構成一條無限延伸的紙帶,m4 是這條紙帶的讀寫頭,因此 m4 是一種圖靈機。事實上,m4 的確是一種圖靈機。所以 m4 的計算能力與任何一種編程語言等同,區別只體如今編程效率以及所編寫的程序的運行效率方面。感受基於 m4 來說解計算機原理仍是挺不錯的。
m4 既然是圖靈機,它至少須要有一個『狀態寄存器』,不然它沒法判斷當前從輸入流中讀取的文本是宏仍是非宏。爲了提升文本處理效率,還應該有一個緩存空間,使得 m4 在這一空間中高效工做。現代的 CPU,沒有緩存的應該很罕見。
m4 緩存的容量爲 512KB。當它滿了的時候,m4 會將自動將其中的內容妥善的保存到一份臨時文件中備用。因此,只要你的磁盤或其它外圍設備的容量足夠,就不要擔憂 m4 沒法處理大文件。
注意,m4 緩存,這個概念是我瞎杜撰的。GNU m4 官方文檔沒這個概念,官方的概念是轉移(Diversion)。
相似 CPU 的多級緩存,m4 的緩存空間也是劃分了級別的。符合 POSIX 標準的 m4,可將緩存空間劃分爲 10 種級別,編號依次爲 0, 1, 2, ..., 9
。GNU m4 對緩存空間的級別數量不做限制。
m4 默認在 0 號緩存中工做,它在這個緩存對文本進行處理,而後將其發送到輸出流。使用 m4 內建的宏 divert
,能夠從當前緩存切換到其餘緩存。例如:
divert(3)
就從當前的緩存切換到 3 號緩存了,而後 m4 就在 3 號緩存中對輸入流中的文本進行處理。若是不繼續使用 divert
進行緩存切換,m4 會一直在 3 號緩存中工做,直到輸入流終結。最後,m4 會將各個緩存中的文本彙總到 0 號緩存中。
緩存的彙總過程是按照緩存級別進行的。m4 會根據緩存級別的編號的增序進行彙總。例如,它老是先將 1 號緩存的內容彙總到 0 號緩存中,而後將 2 號緩存的內容彙總到 0 號緩存中,以此類推,最後將 0 號緩存中的內容依序發送到輸出流中。
劃分了級別的緩存,像是一道一道分水嶺,使得文本流像河流同樣擁有支流,不一樣的支流最終又聚集到一塊兒,奔流到海……是否是有些氣勢恢宏的感受,然而你也應該考慮到這樣的現實:百川東到海,什麼時候復西歸?也就是說,文本流經 m4 的過程也像河流入海同樣的不可逆。這是宏最大的弱點。在程序中濫用宏,形同過分開採水資源。
軟件領域有一門學科,叫逆向工程,研究如何藉助反彙編技術重現某個程序的原有邏輯。具體技術我不是很瞭解,可是幸虧有這門學科,不然個人顯卡很難在新版本的 Linux 內核上工做。由於 Nvidia 官方的 Linux 驅動自某個版本以後就宣佈再也不支持我這種型號的顯卡了,而 Nvidia 官方驅動已經被大神實施逆向工程產生了 Nouveau 驅動,然後者又被集成到了 Linux 內核中。
彷佛跑題了,我想表達的是,逆向工程當然可以在必定程度上覆原某個程序的源碼,但它卻永遠沒法基於宏的展開結果重現宏的定義,只有宏的做者才知道當初究竟發生了什麼。
這時,你應該有一個問題。若是你真的想學習 m4,那就必需要有這個問題——m4 爲何要對緩存劃分級別?回顧一下上文,各個緩存的彙總過程是遵循特定次序的。有了這種分級的緩存彙總機制,你就有能力藉助緩存來控制文本的支流,決定哪條支流先匯入 0 號緩存。你能夠說這樣你有機會扮演大禹,可是我以爲這更像鐵路調度員所作的事。對於鐵路調度員而言,文本流是他要調度的一組列車。
更有趣的是,m4 也提供了暗黑緩存,它的編號是 -1
。GNU m4 對暗黑緩存也不限制數量,只要它們的編號是負數就能夠。
暗黑緩存,彷佛有點恐怖,實際上你能夠將它們理解爲地下河。也就是流過暗黑緩存的文本,m4 會將它們彙總到 0 號緩存,彙總過程按照暗黑緩存編號的遞減次序進行的,可是 m4 不會將暗黑緩存彙總的內容發送到輸出流。這沒什麼很差理解的,現實中沒有什麼東西是負數的。
在 m4 的應用中,暗黑緩存的主要做用就是做爲宏定義的空間。若是在 0 號緩存定義一個宏,例如:
divert(0) define(say_hello_world, Hello World!)
定義了一個名爲 say_hello_world
的 m4 宏。宏定義語句『展開』爲一個長度爲 0 的字符串,而後發送到輸出流。長度爲 0 的字符串,就是空文本,即便它被髮送到輸出流,對輸出流不會產生任何影響,可是 say_hello_world
宏以前,也就是 divert(0)
以後存在一個換行符,m4 會將這個換行符發送到輸出流。除非你本來就但願輸出流中須要這個換行符,不然你就在輸出流中引入了一個額外的換行符,一般狀況下,它不是你想要的結果。爲了更好的說明這一點,能夠看下面的示例:
divert(0) define(say_hello_world, Hello World!) say_hello_world
這個示例就是在上述代碼中又增長了一行文本,它表示調用了上一行所定義的 say_hello_world
宏。假設示例代碼保存在 hello.m4 文件中,而後執行如下命令:
$ m4 hello.m4
此時,hello.m4 就是 m4 的輸入流。m4 從輸入流中讀取文本,處理文本,而後將處理結果發送到輸出流。此時,輸出流是系統的標準輸出設備(stdout),也就是當前的終端屏幕。
執行上述命令後,咱們指望的結果一般是:
$ m4 hello.m4 say_hello_world
然而,m4 輸出的倒是:
$ m4 hello.m4 Hello World!
Hello World!
前面出現了兩處空行,一處是 divert
語句後面的換行符致使的,另處是 say_hello_world
宏定義語句後面的換行符致使的。
若是將 say_hello_world
宏定義語句放在暗黑緩存中,能夠解決一半問題。例如:
divert(-1) define(say_hello_world, Hello World!) divert(0) say_hello_world
再次執行 m4 命令,可得:
$ m4 hello.m4 Hello World!
如今 Hello World!
前面只有 1 處空行了,它是 divert(0)
後面的換行符致使的。要消除它,有兩種方法。第一種方法就是 divert(0)
後面不換行,例如:
divert(-1) define(say_hello_world, Hello World!) divert(0)say_hello_world
另外一種方法是使用 m4 內建的 dnl
宏,它會從將它被調用的位置到後面的第一個換行符之間的文本(包括換行符自己)一併刪除,例如:
divert(-1) define(say_hello_world, Hello World!) divert(0)dnl say_hello_world
這兩種方法輸出的結果是相同的。爲了讓文本具備更好的可讀性,一般用 dnl
來作這樣的事。
(1) 對於如下 m4 代碼
divert(-1) define(say, ) define(hello, HELLO) define(world, WORLD!) divert(0)dnl say hello world
推測一下 m4 的處理結果,而後執行 m4 命令檢驗所作的推測是否正確。
(2) 對於如下 m4 代碼
divert(2) define(say, ) define(hello, HELLO) divert(1) define(world, WORLD!) divert(0)dnl say hello world
推測一下 m4 的處理結果,而後執行 m4 命令檢驗所作的推測是否正確。