9.一個小的GNU Autotools項目 原文:http://www.sourceware.org/autobook/ 本章介紹一個真實的小例子,演示一些GNU Autotools具備的特性,指明一些GNU Autotools使用上的陷阱。全部的源碼能被下載從本書的主頁上。這篇文章是我多年使用GNU Autotools的經驗,你應該可以很容易地在你的項目裏應用這些。首先,我將講述在項目早期階段遇到的一些問題。而後舉例說明所涉及的問題,繼續給你展現一個個人項目所使用的技術的基本結構,接着是可移植命令行shell庫的實現細節。而後用一個小的shell庫的實例程序結束本章。 後面,在12. A Large GNU Autotools Project and 20. A Complex GNU Autotools Project,這個例子將被逐漸地擴展,介紹一下新的GNU Autotools特性。 9.1 GNU Autotools 實戰 這章詳細講述在這個項目開始的時候我遇到的一些具體的問題,是一些典型的,你可能想要在本身的項目中使用的技術,要不是那個正確的解決方案不可能當即明白的。若是你偶然遇到了相似的狀況,你老是可以回來參考這章。我將介紹一下項目的架構,以便你作出更好的權衡,你可能有一個相對於我這裏的項目,更有意義的特別項目。 9.1.1 項目目錄結構 開始寫項目代碼之前,你須要決定項目代碼的目錄結構。我喜歡把項目的每一個組件放在一個子目錄裏,與其它源碼分開配置。一些比較成熟的gnu項目,我都使用這種方法,你能夠採用項目開發者們熟悉的方式組織代碼結構。 在根目錄裏有不少配置文件,例如configure和`aclocal.m4',還有一些其它的各類各樣的文件,例如項目的`README和license文件。 一些重要的類庫有一個獨立的子目錄,包含全部類庫相關的源文件和頭文件以及一個makefile.am文件,還有一些僅對於類庫有用的文件。類庫是放在一個單一目錄裏的可插入的應用模塊的組合。 項目主要應用的源文件和頭文件也是被單獨放在一個叫src的目錄裏的。還有一些其它的慣用的目錄:doc目錄存放項目文檔和test目錄存放項目的測試文件。 儘量地保持根目錄的整潔,我喜歡利用Autoconf的AC_CONFIG_AUX_DIR建立一些其它的目錄,例如config,它存放許多GNU Autotools的中間文件,例如install-sh等。我老是把項目全部的Autoconf M4宏存放到同一個目錄。 所以,我將以如下的形式開始: $ pwd ~/mypackage $ ls -F Makefile.am config/ configure.in lib/ test/ README configure* doc/ src/ 9.1.2 C頭文件 有少許的模板代碼是應該被加到全部的頭文件,特別是防止頭文件的內容被屢次檢查的一些代碼。經過把整個文件放在條件預處理程序裏,預處理器第一次處理後就再也不作處理。通常狀況下,宏都是大寫的,以安裝路徑去掉前綴的剩餘部分命名。假設一個頭文件被安裝在`/usr/local/include/sys/foo.h',預處理程序的代碼以下: #ifndef SYS_FOO_H #define SYS_FOO_H 1 ... #endif /* !SYS_FOO_H */ 除註釋之外,整個頭文件的剩餘部分必須在ifndef和endif之間。值得注意的是在ifndef以前,宏SYS_FOO_H必須定義在任何被#include包含的其它文件以前。直到文件結尾之前不定義宏是一個一般的錯誤,若是看守宏是在#include以前被定義,可是相互依賴的週期僅僅是被拖延。 若是頭文件是被設計安裝的,它必須包含其它當前目錄使用尖括號被安裝的項目頭文件。像這樣作有一些含義: .你必須注意在源碼目錄裏頭文件夾的名稱與在安裝目錄裏的文件夾名稱要相應地匹配。例如,我計劃安裝上面提到的,使用`#include<project/foo.h>命令包含的foo.h到`/usr/local/include/project/foo.h',爲了一樣的代碼安裝後能正常工做,我必須保持項目原有文件及文件夾之間的對應關係。 . 當你開發項目的下一個版本的時候,你必須注意頭文件的正確性。Automake使用選項 '-I'能夠強制編譯器先在當前的文件夾裏尋找,而後再去系統目錄搜索同名的被安裝的頭文件。 . 你沒必要安裝全部的頭文件到`/usr/include'裏,你可使用子文件夾。在安裝時徹底不須要重寫頭文件。 9.1.3 C++編譯器 爲了在c++程序中使用一個用c編譯器編譯的類庫,必須在`extern "C" {' 和 `}'的括號以內聲明來自c類庫的變量符號。這是很重要的,由於C++編譯器會破壞全部變量和函數的名字,C編譯器不會那樣。另外一方面,C編譯器將不識別extern所在行的內容,所以你必須注意在C編譯器裏隱匿它們。 有時你將看到這個方法被使用,在每一個被安裝的頭文件裏像這樣寫出: #ifdef __cplusplus extern "C" { #endif ... #ifdef __cplusplus } #endif 若是在你的項目裏有不少頭文件,這樣寫是很是沒有必要的。而且有些編輯器也很難識別這裏大括號,例如基於大括號作源碼自動縮進的emacs。 比較好的方法是在一個通用的頭文件裏聲明它們爲宏,而後在你的頭文件裏使用那些宏: #ifdef __cplusplus # define BEGIN_C_DECLS extern "C" { # define END_C_DECLS } #else /* !__cplusplus */ # define BEGIN_C_DECLS # define END_C_DECLS #endif /* __cplusplus */ 我已經看到幾個使用如下劃線開頭的`_BEGIN_C_DECLS'宏的項目。任何如下劃線開頭的變量符號都是爲編譯器預留的,所以你不該該用這種方式命名任何本身的變量符號。有這樣的一個實例,我最近移植一個Small語言編譯器到unix,幾乎全部的工做就是寫一個perl腳本重命名在編譯器預留命名空間裏的大量變量符號,以便GCC能夠更好地解析。Small本來是在window上開發的,做者已經使用了大量的如下劃線開頭的變量。雖然他的變量名稱和他本身的編譯器不衝突,在某些狀況下這些變量名稱一樣被GCC使用。 9.1.4 函數定義 做爲一種約定慣例,全部函數的返回值類型應該是在單獨的一列。這樣命名的緣由是容易經過括號前面的函數名找到在文件裏函數: $ egrep '^[_a-zA-Z][_a-zA-Z0-9]*[ \t]*\(' error.c set_program_name (const char *path) error (int exit_status, const char *mode, const char *message) sic_warning (const char *message) sic_error (const char *message) sic_fatal (const char *message) emacs lisp的函數和各類代碼分析工具,例如 ansi2knr就依賴這種慣例。即便你本身不使用這些工具,你開發同伴可能喜歡使用,所以這是一個號的慣例。 9.1.5 實現後退功能 因爲有大量的Unix變種在被廣泛使用,在你比較信賴的首選開發平臺上,極有可能缺乏編譯你的代碼所須要的許多C函數庫。基本上有兩種方法處理這種狀況: . 僅使用任何平臺均可用的類庫。實際上這是不可能的,由於最通用的來自BSD Unix的(bcopy,rindex)和SYSV Unix(memcpy,strrchr)兩個類庫都有相互衝突的API.處理這個問題的惟一方法是根據使用這個預處理程序的具體狀況定義一個API.新的POSIX標準反對不少源自BSD的命令(有些例外,例如BSD socket API).甚至在非POSIX平臺上,有不少交叉影響,例如一個特定的命令一般會有兩種實現,不管如何你應該使用POSIX批准的命令,一些平臺若是沒有實現,那就根據平臺本身定義它們。 這種方法須要很是熟悉各類系統類庫和標準,能經過擴展預處理器處理APIS之間的不一樣。你也須要用configure.in作大量的檢查,弄明白哪一些命令是可用的。例如,容許你其他的代碼能正常地使用strcpy,你須要在configure.in里加如下的代碼: AC_CHECK_FUNCS(strcpy bcopy) 下列的預處理器代碼與每一個源文件分離,在一個頭文件裏: #if !HAVE_STRCPY # if HAVE_BCOPY # define strcpy(dest, src) bcopy (src, dest, 1 + strlen (src)) # else /* !HAVE_BCOPY */ error no strcpy or bcopy # endif /* HAVE_BCOPY */ #endif /* HAVE_STRCPY */ .另外一種方式,你能提供本身的函數後退功能的實如今沒有實現它的一些平臺上。實際上在使用這種方式的時候你不須要很是理解有疑問的函數。你能夠關注GNU libiberty 或者 Fran?ois Pinard's libit 項目,看看函數的其餘GNU開發者有須要實現後退代碼。在這方面libit項目是很是有用的,由於它包含後退功能的權威版本和集合了遍及整個項目的Autoconf宏。我不會給出一個使用這種方法設置你的項目的例子,由於我已經選擇組織一個項目在這一章裏介紹。 與實現最少功能的系統庫相比,在多數案例裏我比較提倡後一個方法。正如全部的事情須要採起實用的方法;不擔心中間立場,在具體問題具體分析的基礎上作出選擇。 9.1.6 K&R編譯器 K&R C 如今特指由Brian Kernighan 和 Dennis Ritchie開發的原始的C語言。我尚未看到不支持K&R風格的C編譯器,它已經廢棄,被新的ANSI C標準取代。相對於ANSI C 它已經不多使用,我曾經使用過得全部技術架構對於GCC項目都是可用的。 兩種C語言標準有4個不一樣之處: 1.在函數原型裏,ANSI C 要求有完整的類型說明,所以在頭文件裏你應該這樣寫: extern int functionname (const char *parameter1, size_t parameter 2); 這相似於K&R風格C裏的使用函數以前的提早聲明,它相似的定義: extern int functionname (); 正如你想到的K&R的類型安全不好,不進行類型檢查,只有正確的函數參數被使用。 2.函數定義的頭是不一樣的,在ANSI C裏你可能看到下面的寫法: int functionname (const char *parameter1, size_t parameter2) { ... } K&R要求參數類型分行聲明,像這樣: int functionname (parameter1, parameter2) const char *parameter1; size_t parameter2; { ... } 3.在K&R C裏沒有隱式類型的概念。在ANSI代碼裏你可能看到'void *'指針,你必須重載’char *‘爲K&R編譯器。 4.在K&R C裏varargs.h的Variadic函數是用一個不一樣的api實現的。K&R的variadic函數的實現看起來像這樣: int functionname (va_alist) va_dcl { va_list ap; char *arg; va_start (ap); ... arg = va_arg (ap, char *); ... va_end (ap); return arg ? strlen (arg) : 0; } ANSI C在stdarg.h提供了一個相似的api,雖然它不像上面的variadic函數那樣參數沒有名稱。實際上,這不是一個問題,由於你老是須要至少一個參數,或者以某種方式指定參數的總數,或者標記參數列表的結 束。一個ANSI variadic函數的實現以下: int functionname (char *format, ...) { va_list ap; char *arg; va_start (ap, format); ... arg = va_arg (ap, char *); ... va_end (ap); return format ? strlen (format) : 0; } 除非你實現一個很是底層的項目(例如 GCC),你可能不須要太多的擔憂K&R編譯器。雖然,兼容K&R C語法是很是容易的,而且你也很願意那樣作,可使用Automake裏的ansi2knr程序處理,或者經過預處理器 處理。 在這個項目裏使用的ansi2knr在Automake手冊裏的`Automatic de-ANSI-fication'章節裏作了詳細的介紹,可是可歸結爲如下: .在你的configure.in文件里加這個宏:AM_C_PROTOTYPES .用下面的方式重寫`LIBOBJS' and/or `LTLIBOBJS'的內容: # This is necessary so that .o files in LIBOBJS are also built via # the ANSI2KNR-filtering rules. Xsed='sed -e "s/^X//"' LIBOBJS=`echo X"$LIBOBJS"|\ [$Xsed -e 's/\.[^.]* /.\$U& /g;s/\.[^.]*$/.\$U&/']` 就我的而言,我不喜歡這個方法,由於在編譯時每一個源文件被過濾,用ANSI函數原型重寫,聲明被轉換成K&R風格,這些會增長額外的系統開銷。這是合理的和足夠的抽象概念,容許你徹底地忘記關於K&R,可是ansi2knr是一個簡單的程序,它不處理任何上面說起的編譯器之間的不一樣,它不能處理定義的定義的函數原型裏的宏。若是你決定在本身的項目裏使用ansi2knr,你必須在寫任何代碼以前作出決定,並意識到它對於你開發工做的限制。 對於我本身的不少項目,我更喜歡使用一組預處理程序宏以及一些文字規範,以便K&R和ANSI編譯器之間的差別被真正地處理,不必使用ANSI編譯器和因某些緣由不能使用GCC的開發者不須要耗費ansi2knr帶來的額外開銷。 在這章開頭列舉的4個風格差別是用如下方式處理的,列舉以下: 1.在PARAMS宏的裏面聲明函數原型的參數列表,以便K&R編譯器可以編譯源碼樹。PARAMS移除函數原型的ANSI參數列表爲K&R編譯器。但嚴格來講,以_(尤爲是__)開始的宏是爲編譯器和系統頭文件預留的,所以像下面這樣使用「PARAMS是比較安全的: #if __STDC__ # ifndef NOPROTOS # define PARAMS(args) args # endif #endif #ifndef PARAMS # define PARAMS(args) () #endif 這個宏而後像下面這樣用在全部的函數聲明裏: extern int functionname PARAMS((const char *parameter)); 2.在全部的函數聲明裏使用PARAMS宏,ANSI編譯器提供在完整編譯時類型檢查所須要的所有類型信息。函數必須徹底以K&R風格聲明,以便K&R編譯器不阻止ANSI語法。以這種方式寫程序會有少許的額外開銷,若是它是第一次碰到一個ANSI函數原型,無論怎樣ANSI編譯時類型檢查僅能與K&R函數定義一塊兒工做。這要求你在開發項目時有一個好的原型聲明習慣。即便是靜態函數。 3.解決viod * 指針缺點的最容易的方式是定義一個條件性地爲ANSI編譯器設置爲void *和爲K&R編譯器設置爲char *的新類型。你應該在一個通用的頭文件里加下面的代碼: #if __STDC__ typedef void *void_ptr; #else /* !__STDC__ */ typedef char *void_ptr; #endif /* __STDC__ */ 4.兩種函數API變體之間差別致使了一個難解決的問題,解決方案是難看的。可是它能夠工做。首先你必須檢查在configure.in裏的頭文件: AC_CHECK_HEADERS(stdarg.h varargs.h, break) 接着,加下面的代碼到一個項目通用的頭文件裏: #if HAVE_STDARG_H # include <stdarg.h> # define VA_START(a, f) va_start(a, f) #else # if HAVE_VARARGS_H # include <varargs.h> # define VA_START(a, f) va_start(a) # endif #endif #ifndef VA_START error no variadic api #endif 如今你必須爲每一個函數都提供K&R和ANSI兩種版本,以下: int #if HAVE_STDARG_H functionname (const char *format, ...) #else functionname (format, va_alist) const char *format; va_dcl #endif { va_alist ap; char *arg; VA_START (ap, format); ... arg = va_arg (ap, char *); ... va_end (ap); return arg : strlen (arg) ? 0; } 9.2 一個簡單的Shell建立庫 許多開發者考驗他們本身的技術的一個應用是一個UNIX shell。傳統的命令行shell一般有不少功能,當我遇到並克服第一個困難時我想我將要推進一個可移植庫的發展。在詳細介紹之前我須要先命名這個項目。我叫它sic,它來自拉丁語,所以像全部的好名字同樣,它是有點作做的。它有助於循環首字母縮寫詞累積。 這本書不介紹駭人聽聞的源碼細節,出於對需求的考慮,如下是一些影響設計的目的: .Sic必須很是小,除了用做一個完整的緩解shell,它能夠被引入一個應用,作一些不重要的任務,例如讀取啓動配置文件。 .它不比受限於特別的語法和預留關鍵字。若是你使用它讀取你的啓動配置文件,我不想強制你使用個人語法和命令。 .類庫(libsic)和應用程序之間的分界線必須好好地定義。sic將拿特殊的字符串做爲輸入,按照已記錄的命令和語法本質地解析和計算它們,返回結果或者適當的診斷。 .它必須是很是可移植的--最終我將在這裏嘗試闡明它。 9.2.1 可移植性基礎結構 正如我在「9.1.1項目目錄結構」裏講解的那樣,我首先建立項目的目錄結構,在類庫源碼裏建立1個頂級目錄和1個子目錄。我想要安裝類庫頭文件到`/usr/local/include/sic',所以必須適當地命名類庫子目錄。詳見:9.1.2 C Header Files. $ mkdir sic $ mkdir sic/sic $ cd sic/sic 除項目特殊的源碼之外我將更詳細地介紹在本章增長的文件,由於對於個人GNU Autotools項目而言,它們造成了一個相對穩定的基礎結構。你能夠保留一份這些文件的備份,每次使用它們做爲你的新項目的起始點。 9.2.1.1 錯誤管理 開始任何項目設計的一個好出發點是錯誤管理功能。在sic裏我將使用一個簡單顯示錯誤信息的函數單羣。在這裏它是sic/error.h: #ifndef SIC_ERROR_H #define SIC_ERROR_H 1 #include <sic/common.h> BEGIN_C_DECLS extern const char *program_name; extern void set_program_name (const char *argv0); extern void sic_warning (const char *message); extern void sic_error (const char *message); extern void sic_fatal (const char *message); END_C_DECLS #endif /* !SIC_ERROR_H */ 這個頭文件遵循在9.1.2 C 頭文件裏介紹的原理。 我在使用它的類庫裏保存program_name變量,因此我能夠肯定類庫不容許未定義的符號在類庫裏。 在一個單獨的文件裏,定義這些被設計用於不斷地提升代碼可移植性的宏是一個保持代碼可讀性的好方式。對於這個項目我將在‘common.h'裏定義宏: #ifndef SIC_COMMON_H #define SIC_COMMON_H 1 #if HAVE_CONFIG_H # include <config.h> #endif #include <stdio.h> #include <sys/types.h> #if STDC_HEADERS # include <stdlib.h> # include <string.h> #elif HAVE_STRINGS_H # include <strings.h> #endif /*STDC_HEADERS*/ #if HAVE_UNISTD_H # include <unistd.h> #endif #if HAVE_ERRNO_H # include <errno.h> #endif /*HAVE_ERRNO_H*/ #ifndef errno /* Some systems #define this! */ extern int errno; #endif #endif /* !SIC_COMMON_H */ 這裏你可使用一些Autoconf手冊裏的代碼片斷--尤爲是將立刻生成的項目包含文件'config.h'。注意,我已經當心地有條件地包含不是在每一個系統結構裏都存在的頭文件。雖然我從沒有看到那個機器上沒有’sys/types.h',單憑經驗來看僅‘stdio.h是無處不在的.在GUN Autoconf手冊的`Existing Tests'(存在測試)章節裏你能夠發現更詳細的相關介紹。 這裏是一些來自’common.h'的代碼: #ifndef EXIT_SUCCESS # define EXIT_SUCCESS 0 # define EXIT_FAILURE 1 #endif 錯誤處理函數的實現開始於‘error.c'和是很是簡明的: #if HAVE_CONFIG_H # include <config.h> #endif #include "common.h" #include "error.h" static void error (int exit_status, const char *mode, const char *message); static void error (int exit_status, const char *mode, const char *message) { fprintf (stderr, "%s: %s: %s.\n", program_name, mode, message); if (exit_status >= 0) exit (exit_status); } void sic_warning (const char *message) { error (-1, "warning", message); } void sic_error (const char *message) { error (-1, "ERROR", message); } void sic_fatal (const char *message) { error (EXIT_FAILURE, "FATAL", message); } 我也須要一個program_name的定義;set_program_name複製路徑的文件名部分到輸出數據program_name。xstrdup函數僅調用strup,但若是沒有足夠的內存進行復制調用會終止: const char *program_name = NULL; void set_program_name (const char *path) { if (!program_name) program_name = xstrdup (basename (path)); } 9.2.1.2 內存管理 對於許多GNU項目有用的慣用語法是局部化內存溢出的處理,用前綴’x'命名它們,把這些封裝成內存管理函數.經過這樣作,項目的其他部分就不用記着檢查各類內存函數的返回值是否爲NULL。這些函數使用錯誤處理API報告內存耗盡和終止問題程序。我把實現代碼放在xmalloc.c文件裏: #if HAVE_CONFIG_H # include <config.h> #endif #include "common.h" #include "error.h" void * xmalloc (size_t num) { void *new = malloc (num); if (!new) sic_fatal ("Memory exhausted"); return new; } void * xrealloc (void *p, size_t num) { void *new; if (!p) return xmalloc (num); new = realloc (p, num); if (!new) sic_fatal ("Memory exhausted"); return new; } void * xcalloc (size_t num, size_t size) { void *new = xmalloc (num * size); bzero (new, num * size); return new; } 注意上面的代碼,xcalloc是用xmalloc實現的,之前在一些老的C類庫裏calloc是不可用的。另外,在現代C類庫裏bzero函數是被反對的,支持memset--我將在後面的9.2.3 Beginnings of a `configure.in'章節裏講解怎樣重視這個。 與其建立一個單獨的,幾乎到處被包含的‘xmlloc.h’文件,不如在‘common.h'裏聲明這些函數,所以封裝將被從代碼裏的任何地方調用: #ifdef __cplusplus # define BEGIN_C_DECLS extern "C" { # define END_C_DECLS } #else # define BEGIN_C_DECLS # define END_C_DECLS #endif #define XCALLOC(type, num) \ ((type *) xcalloc ((num), sizeof(type))) #define XMALLOC(type, num) \ ((type *) xmalloc ((num) * sizeof(type))) #define XREALLOC(type, p, num) \ ((type *) xrealloc ((p), (num) * sizeof(type))) #define XFREE(stale) do { \ if (stale) { free (stale); stale = 0; } \ } while (0) BEGIN_C_DECLS extern void *xcalloc (size_t num, size_t size); extern void *xmalloc (size_t num); extern void *xrealloc (void *p, size_t num); extern char *xstrdup (const char *string); extern char *xstrerror (int errnum); END_C_DECLS 經過使用在這裏定義的宏,簡化了堆內存的分配和釋放: char **argv = (char **) xmalloc (sizeof (char *) * 3); do_stuff (argv); if (argv) free (argv); 簡單版,更易讀: char **argv = XMALLOC (char *, 3); do_stuff (argv); XFREE (argv); 以一樣的方式,我從GNU的libiberty借來‘xstrdup.c'和’xstrerror.c'。詳見9.1.5 Fallback Function Implementations(回調函數的實現)。 9.2.1.3 泛型列表數據類型 在不少C語言程序裏你將看到列表和堆棧的各類各樣的實現和從新實現,各自侷限於特殊的項目。寫一個垃圾箱的實現是很是簡單的,例如我這裏實現的list.h裏的一個廣泛的列表操做API: #ifndef SIC_LIST_H #define SIC_LIST_H 1 #include <sic/common.h> BEGIN_C_DECLS typedef struct list { struct list *next; /* chain forward pointer*/ void *userdata; /* incase you want to use raw Lists */ } List; extern List *list_new (void *userdata); extern List *list_cons (List *head, List *tail); extern List *list_tail (List *head); extern size_t list_length (List *head); END_C_DECLS #endif /* !SIC_LIST_H */ 這種技巧能確保你想要連接在一塊兒的結構體在其第一個變量了保存着正向指針。這樣作,上述的普通函數聲明經過把鏈表轉化成列表指針的形式能夠操做任何鏈表,在必要的時候能夠轉化成鏈表。 例如: struct foo { struct foo *next; char *bar; struct baz *qux; ... }; ... struct foo *foo_list = NULL; foo_list = (struct foo *) list_cons ((List *) new_foo (), (List *) foo_list); ... 列表操做函數的實現是在‘list.c'裏的: #include "list.h" List * list_new (void *userdata) { List *new = XMALLOC (List, 1); new->next = NULL; new->userdata = userdata; return new; } List * list_cons (List *head, List *tail) { head->next = tail; return head; } List * list_tail (List *head) { return head->next; } size_t list_length (List *head) { size_t n; for (n = 0; head; ++n) head = head->next; return n; } 9.2.2 類庫的實現 爲了給擴展上面的例子的後面章節作好準備,我將在這章介紹組合實現shell類庫的目的。在這裏我將不詳細剖析代碼--你能夠從本書的主頁(http://sources.redhat.com/autobook/)下載源碼。 超出前面已描述的支持文件的範圍的類庫剩餘代碼被分紅4對文件: 9.2.2.1 `sic.c' & `sic.h' 這裏是建立和管理sic的分析程序函數。 #ifndef SIC_SIC_H #define SIC_SIC_H 1 #include <sic/common.h> #include <sic/error.h> #include <sic/list.h> #include <sic/syntax.h> typedef struct sic { char *result; /* result string */ size_t len; /* bytes used by result field */ size_t lim; /* bytes allocated to result field */ struct builtintab *builtins; /* tables of builtin functions */ SyntaxTable **syntax; /* dispatch table for syntax of input */ List *syntax_init; /* stack of syntax state initialisers */ List *syntax_finish; /* stack of syntax state finalizers */ SicState *state; /* state data from syntax extensions */ } Sic; #endif /* !SIC_SIC_H */ 這個結構體有存儲內置命令和語法分析程序的字段,以及能夠用於在各類處理器之間共享信息的其餘狀態信息字段(state),以及一些存儲結果集或者錯誤信息的result字段。 9.2.2.2 `builtin.c' & `builtin.h' 如下是用Sic結構體管理內置命令table的函數: typedef int (*builtin_handler) (Sic *sic, int argc, char *const argv[]); typedef struct { const char *name; builtin_handler func; int min, max; } Builtin; typedef struct builtintab BuiltinTab; extern Builtin *builtin_find (Sic *sic, const char *name); extern int builtin_install (Sic *sic, Builtin *table); extern int builtin_remove (Sic *sic, Builtin *table); 9.2.2.3 `eval.c' & `eval.h' 有建立一個sic解析器,它使用了一些內置處理器,這個庫的用戶必須標記化,並且驗證它的輸入流。這些文件定義了一個結構體,用於存儲標記化字符串,以及用於轉化結構體類型到char * string的函數: #ifndef SIC_EVAL_H #define SIC_EVAL_H 1 #include <sic/common.h> #include <sic/sic.h> BEGIN_C_DECLS typedef struct { int argc; /* number of elements in ARGV */ char **argv; /* array of pointers to elements */ size_t lim; /* number of bytes allocated */ } Tokens; extern int eval (Sic *sic, Tokens *tokens); extern int untokenize (Sic *sic, char **pcommand, Tokens *tokens); extern int tokenize (Sic *sic, Tokens **ptokens, char **pcommand); END_C_DECLS #endif /* !SIC_EVAL_H */ 9.2.2.4 `syntax.c' & `syntax.h' 當標記化把一個char * 字符串分爲幾部分時,默認是以空格爲分隔符把字符串分紅幾個單詞。這些文件定義改變如下默認行爲的接口,當解析器在輸入流裏遇到一個使人感興趣的符號時,解析器運行已經註冊的 回調函數。如下是來自syntax.h的聲明: BEGIN_C_DECLS typedef int SyntaxHandler (struct sic *sic, BufferIn *in, BufferOut *out); typedef struct syntax { SyntaxHandler *handler; char *ch; } Syntax; extern int syntax_install (struct sic *sic, Syntax *table); extern SyntaxHandler *syntax_handler (struct sic *sic, int ch); END_C_DECLS SyntaxHandler是一個函數,當tokenize用它的輸入建立一個tokens結構體時調用它;這兩個函數關聯一個帶有特定的Sic解析器的處理程序表,在Sic解析器裏發現關於指定字符的特殊處理程序。 9.2.3 開始‘configure.in 因爲我有一些代碼,我能夠運行autscan生成一個初級的configure.in。autoscan將檢查在當前目錄下的全部源碼尋找常見的不可移植的點,添加一些適合探測發現問題的宏。autoscan生成以下的configure.scan: # Process this file with autoconf to produce a configure script. AC_INIT(sic/eval.h) # Checks for programs. # Checks for libraries. # Checks for header files. AC_HEADER_STDC AC_CHECK_HEADERS(strings.h unistd.h) # Checks for typedefs, structures, and compiler characteristics. AC_C_CONST AC_TYPE_SIZE_T # Checks for library functions. AC_FUNC_VPRINTF AC_CHECK_FUNCS(strerror) AC_OUTPUT() 因爲生成的configure.scan不覆寫你項目的configure.in,甚至在已創建的項目源碼裏週期性地運行autoscan和比較這兩個文件是一個好主意。有時autoscan將發現一些被你忽略或者沒有意識到的可移植性問題。 翻閱關於在configure.scan裏的宏的文檔,AC_C_CONST 和 AC_TYPE_SIZE_T將管理他們本身(假如我確保config.h是被包含進每一個源文件),以及AC_HEADER_STDC 和 AC_CHECK_HEADERS(unistd.h)是在common.h裏。 autoscan不是銀色子彈(萬靈藥)!甚至在這裏這個簡單的例子裏,我須要手動地添加檢查‘errno.h'存在的宏: AC_CHECK_HEADERS(errno.h strings.h unistd.h) 爲了生成’config.h'我也必須手動地添加Autoconf宏;支持初始化automake的宏;檢查ranlib是否存在的宏。這些應該放在接近configure.in文件開始的地方: ... AC_CONFIG_HEADER(config.h) AM_INIT_AUTOMAKE(sic, 0.5) AC_PROG_CC AC_PROG_RANLIB ... 回顧在9.2.1.2 內存管理的bzero的用法不是徹底地可移植的。訣竅是提供一個具備相似行爲的bzero,依賴下列添加在configure.in結尾處autoconf檢測函數: ... AC_CHECK_FUNCS(bzero memset, break) ... 外加下面的代碼片斷到common.h裏,即便連接了一個沒有實現bzero的C類庫我也可使用bzero: #if !HAVE_BZERO && HAVE_MEMSET # define bzero(buf, bytes) ((void) memset (buf, 0, bytes)) #endif 一個autoscan建議的使人關注的宏是AC_CHECK_FUNCS(strerror)。AC_CHECK_FUNCS(strerror)會告訴我,我須要爲那些沒有實現strerror的系統類庫提供一個strerror實現的替代者。經過建立一個有命名函數的回退實現的文件,而且在這個文件裏以及configure發現缺乏系統類庫的主機上建立一個庫來解決這個問題。 你將回想起configure,configure是最終用戶在他們機器上測試程序包須要哪些特性的shell腳本。被建立的類庫容許用項目必須的函數,除了在安裝程序系統類庫中缺乏的部分寫項目的剩餘部分,雖然如此類庫是可用的。GUN ‘libiberty’再來營救--它已經有一個我可以作點修改的‘strerror.c’的實現。 能提供一個簡單的strerror的實現,像‘libiberty"的’strerror.c‘裏的實現同樣,依賴有一個好的變量sys_errlist。若是目標主機沒有strerror的實現,這是一個合理的觀點,然而,系統sys_errlist將是被損壞了或者缺失的。我須要寫一個configure宏檢測系統是否認義了sys_errlist,而且據此裁剪’strerror.c‘裏的代碼。 爲了不根目錄的混亂,我儘量地把許多配置文件放在它們本身的子目錄裏。首先,我將在根目錄下建立一個叫「config」的新文件夾,而且放‘sys_errlist.m4’在裏面。 # Process this file with autoconf to produce a configure script. AC_INIT(sic/eval.h) # Checks for programs. # Checks for libraries. # Checks for header files. AC_HEADER_STDC AC_CHECK_HEADERS(strings.h unistd.h) # Checks for typedefs, structures, and compiler characteristics. AC_C_CONST AC_TYPE_SIZE_T # Checks for library functions. AC_FUNC_VPRINTF AC_CHECK_FUNCS(strerror) AC_OUTPUT() 而後依據configure.scan裏的註釋在typedefs、structures和庫函數之間,我必須在configure.in文件裏的正確位置調用這個新宏: SIC_VAR_SYS_ERRLIST GNU Autotools也能夠設置在子文件夾裏存放它們的文件,經過在configure.in的頭部調用AC_CONFIG_AUX_DIR宏,更好的位置在AC_INIT後面: AC_INIT(sic/eval.c) AC_CONFIG_AUX_DIR(config) AM_CONFIG_HEADER(config.h) ... 有了這種變化,許多經過運行autoconf和automake --add-missing添加的文件將被放進aux_dir。源碼目錄如今看起來像這樣: sic/ +-- configure.scan +-- config/ | +-- sys_errlist.m4 +-- replace/ | +-- strerror.c +-- sic/ +-- builtin.c +-- builtin.h +-- common.h +-- error.c +-- error.h +-- eval.c +-- eval.h +-- list.c +-- list.h +-- sic.c +-- sic.h +-- syntax.c +-- syntax.h +-- xmalloc.c +-- xstrdup.c +-- xstrerror.c 爲了正確地利用回退實現,AC_CHECK_FUNCS(strerror)須要移除,strerror被加入AC_REPLACE_FUNCS: # Checks for library functions. AC_REPLACE_FUNCS(strerror) 若是你看Makefile.am中的關於replace子文件夾,這將是更清楚的: ## Makefile.am -- Process this file with automake to produce Makefile.in INCLUDES = -I$(top_builddir) -I$(top_srcdir) noinst_LIBRARIES = libreplace.a libreplace_a_SOURCES = dummy.c libreplace_a_LIBADD = @LIBOBJS@ 源碼告訴automake,我想在源碼目錄裏的建立一個類庫(不安裝),默認沒有源碼文件。這裏聰明的部分是當有人安裝Sic的時候它們將運行檢測strerror的configure,若是目標主機環境缺乏strerror的實現configure添加strerror.o到LIBOBJS。configure建立‘replace/Makefile'(像我請求它用AC_OUTPUT),`@LIBOBJS@'將被目標機器上必須的對象列表替換。 在configure運行的時候作這一切,當個人用戶運行make的時候,替換在他們的目標機器上缺乏的函數的必須文件將被添加進’libreplace.a'。 不幸地,這不是足夠的開始構建一個項目。首先我須要添加一個頂級的Makefile.am,最終會生成一個頂級的Makefile,它將分散到項目的各個子文件夾裏: ## Makefile.am -- Process this file with automake to produce Makefile.in SUBDIRS = replace sic configure.in必須放在它能夠找到Makefile.in文件的地方: AC_OUTPUT(Makefile replace/Makefile sic/Makefile) 我已經爲Sic寫了一個bootstrap腳本,更多詳細內容參照 8. Bootstrapping: #! /bin/sh autoreconf -fvi automake的‘--foregin'選項爲應該出如今GNU發佈裏面的各類文件放寬GNU標準。使用這個選項使我免於建立像咱們在第5章(5. A Minimal GNU Autotools Project)裏建立的空文件。 好,咱們來建立類庫!首先,我將運行bootstrap: $ ./bootstrap + aclocal -I config + autoheader + automake --foreign --add-missing --copy automake: configure.in: installing config/install-sh automake: configure.in: installing config/mkinstalldirs automake: configure.in: installing config/missing + autoconf 項目如今是和最終用戶將要看到的同樣的,正在解壓一個發佈壓縮包。下面多是一個最終用戶在從壓縮包建立時指望看到的: $ ./configure creating cache ./config.cache checking for a BSD compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking whether make sets ${MAKE}... yes checking for working aclocal... found checking for working autoconf... found checking for working automake... found checking for working autoheader... found checking for working makeinfo... found checking for gcc... gcc checking whether the C compiler (gcc ) works... yes checking whether the C compiler (gcc ) is a cross-compiler... no checking whether we are using GNU C... yes checking whether gcc accepts -g... yes checking for ranlib... ranlib checking how to run the C preprocessor... gcc -E checking for ANSI C header files... yes checking for unistd.h... yes checking for errno.h... yes checking for string.h... yes checking for working const... yes checking for size_t... yes checking for strerror... yes updating cache ./config.cache creating ./config.status creating Makefile creating replace/Makefile creating sic/Makefile creating config.h 比較這些configure.in的輸出內容,注意每一個宏最終是怎樣負責一個或多個連續測試的(經過congfigure裏的shell腳本生成的)。如今Makefile文件已經成功建立,調用make執行實際的編譯是安全的: $ make make all-recursive make[1]: Entering directory `/tmp/sic' Making all in replace make[2]: Entering directory `/tmp/sic/replace' rm -f libreplace.a ar cru libreplace.a ranlib libreplace.a make[2]: Leaving directory `/tmp/sic/replace' Making all in sic make[2]: Entering directory `/tmp/sic/sic' gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c builtin.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c error.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c eval.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c list.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c sic.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c syntax.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c xmalloc.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c xstrdup.c gcc -DHAVE_CONFIG_H -I. -I. -I.. -I.. -g -O2 -c xstrerror.c rm -f libsic.a ar cru libsic.a builtin.o error.o eval.o list.o sic.o syntax.o xmalloc.o xstrdup.o xstrerror.o ranlib libsic.a make[2]: Leaving directory `/tmp/sic/sic' make[1]: Leaving directory `/tmp/sic' 在這個機器上,如上面你能看到的configure的輸出,我不須要strerror的回退實現,所以libreplace.a是空的。在其它的機器上可能不是這樣的。無論怎樣,我如今有一個編譯好的libsic.a---到目前爲止,很好。 9.3 一個小的shell應用程序 如今我須要什麼,是一個使用libsic.a的程序,只要你給我一個它能夠工做的信心。在這節,我將寫一個使用這個庫的簡單的shell。但首先,我將先建立一個放它的目錄: $ mkdir src $ ls -F COPYING Makefile.am aclocal.m4 configure* config/ sic/ INSTALL Makefile.in bootstrap* configure.in replace/ src/ $ cd src 爲了把這個shell放在一塊兒,對於整合‘libsic.a',咱們須要準備一些事情。 9.3.1 ’sic_repl.c' 在sic_repl.c裏有一個讀取用戶輸入的循環計算並打印其結果。GNU readline理論上適合這個,但它不老是可用的,有時人們不能夠簡單地但願使用它。 關於GNU Autotools 的幫助,它是很是容易適合有和沒有GNU reading 的建立。sic_repl.c使用這個函數讀取用戶的輸入行: static char * getline (FILE *in, const char *prompt) { static char *buf = NULL; /* Always allocated and freed from inside this function. */ XFREE (buf); buf = (char *) readline ((char *) prompt); #ifdef HAVE_ADD_HISTORY if (buf && *buf) add_history (buf); #endif return buf; } 爲了作這個操做,我必須寫一個加在configure的選項裏的Autoconf宏,以便包安裝的時候,經過`--with-readline'選項使用readline庫: AC_DEFUN([SIC_WITH_READLINE], [AC_ARG_WITH(readline, [ --with-readline compile with the system readline library], [if test x"${withval-no}" != xno; then sic_save_LIBS=$LIBS AC_CHECK_LIB(readline, readline) if test x"${ac_cv_lib_readline_readline}" = xno; then AC_MSG_ERROR(libreadline not found) fi LIBS=$sic_save_LIBS fi]) AM_CONDITIONAL(WITH_READLINE, test x"${with_readline-no}" != xno) ]) 把這個宏放在`config/readline.m4'文件裏,我也必須調用來自configure.in的新宏(SIC_WITH_READLINE)。 9.3.2 sic_syntax.c 我正在寫的shell命令的語法定義在一組語法處理程序裏,在啓動的時候加載進libsic。我能用c預處理器作爲我作許多重複的代碼,僅填充函數體: #if HAVE_CONFIG_H # include <config.h> #endif #include "sic.h" /* List of builtin syntax. */ #define syntax_functions \ SYNTAX(escape, "\\") \ SYNTAX(space, " \f\n\r\t\v") \ SYNTAX(comment, "#") \ SYNTAX(string, "\"") \ SYNTAX(endcmd, ";") \ SYNTAX(endstr, "") /* Prototype Generator. */ #define SIC_SYNTAX(name) \ int name (Sic *sic, BufferIn *in, BufferOut *out) #define SYNTAX(name, string) \ extern SIC_SYNTAX (CONC (syntax_, name)); syntax_functions #undef SYNTAX /* Syntax handler mappings. */ Syntax syntax_table[] = { #define SYNTAX(name, string) \ { CONC (syntax_, name), string }, syntax_functions #undef SYNTAX { NULL, NULL } }; 這個代碼爲語法處理函數定義屬性,建立一個可能出如今輸入流裏的一個或多個彼此相關的字符的表。這種方式寫代碼的優勢是之後我想加一個新的語法處理器的時候,它是一個簡單的事情,向syntax_function宏添加一個新行,把函數的名稱寫進去。 9.3.3 sic_builtin.c 除了我剛纔添加到Sic Shell的語法處理程序,這個shell的語言經過它提供的內置的命令定義。這個文件的基礎結構是由一個提供給各類各樣的C預處理器的宏函數表組合成的,正如我建立的語法處理器。 這個內置的處理器函數有特殊的狀態,builtin_unknown。若是sic庫不能找到合適的內置函數處理當前輸入的命令,builtin_unknown是內置地被調用的。首先這不像特別重要--可是它是任何shell實現的關鍵。當沒有關於命令的內置處理程序時,shell將搜索用戶的命令路徑,'$PATH',找到一個合適的可執行的。這是builtin_unknown的工做: int builtin_unknown (Sic *sic, int argc, char *const argv[]) { char *path = path_find (argv[0]); int status = SIC_ERROR; if (!path) { sic_result_append (sic, "command \""); sic_result_append (sic, argv[0]); sic_result_append (sic, "\" not found"); } else if (path_execute (sic, path, argv) != SIC_OKAY) { sic_result_append (sic, "command \""); sic_result_append (sic, argv[0]); sic_result_append (sic, "\" failed: "); sic_result_append (sic, strerror (errno)); } else status = SIC_OKAY; return status; } static char * path_find (const char *command) { char *path = xstrdup (command); if (*command == '/') { if (access (command, X_OK) < 0) goto notfound; } else { char *PATH = getenv ("PATH"); char *pbeg, *pend; size_t len; for (pbeg = PATH; *pbeg != '\0'; pbeg = pend) { pbeg += strspn (pbeg, ":"); len = strcspn (pbeg, ":"); pend = pbeg + len; path = XREALLOC (char, path, 2 + len + strlen(command)); *path = '\0'; strncat (path, pbeg, len); if (path[len -1] != '/') strcat (path, "/"); strcat (path, command); if (access (path, X_OK) == 0) break; } if (*pbeg == '\0') goto notfound; } return path; notfound: XFREE (path); return NULL; } 就此再一次運行autoscan添加AC_CHECK_FUNCS(strcspn strspn)到configure.scan。這告訴我這些函數不是真正可移植的。像以前我爲缺失這些函數的主機提供的這些函數的fallback實現同樣--如它的結果,它們是容易寫的: /* strcspn.c -- implement strcspn() for architectures without it */ #if HAVE_CONFIG_H # include <config.h> #endif #include <sys/types.h> #if STDC_HEADERS # include <string.h> #elif HAVE_STRINGS_H # include <strings.h> #endif #if !HAVE_STRCHR # ifndef strchr # define strchr index # endif #endif size_t strcspn (const char *string, const char *reject) { size_t count = 0; while (strchr (reject, *string) == 0) ++count, ++string; return count; } 不須要加任何代碼到Makefile.am,由於configure腳本將自動加缺失函數源碼的名稱到`@LIBOBJS@'。 這個實現使用autoconf生成的config.h得到頭文件和類型定義的可用性。autoscan報告用在strcspn和strspn的fallback實現裏的strchr和strrchr是不可移植的。幸好,autoconf手冊準確地告訴我怎樣經過在個人common.h里加一些代碼處理這個問題(手冊裏代碼的意思): #if !STDC_HEADERS # if !HAVE_STRCHR # define strchr index # define strrchr rindex # endif #endif 在configure.in裏的另外一個宏: AC_CHECK_FUNCS(strchr strrchr) 9.3.4 `sic.c' & `sic.h' 由於二進制的應用沒有安裝頭文件,有少許的爲每一個源碼維護一個適當的頭文件,全部的結構經過這些文件共享,在這些文件裏的非靜態函數被定義在sic.h: #ifndef SIC_H #define SIC_H 1 #include <sic/common.h> #include <sic/sic.h> #include <sic/builtin.h> BEGIN_C_DECLS extern Syntax syntax_table[]; extern Builtin builtin_table[]; extern Syntax syntax_table[]; extern int evalstream (Sic *sic, FILE *stream); extern int evalline (Sic *sic, char **pline); extern int source (Sic *sic, const char *path); extern int syntax_init (Sic *sic); extern int syntax_finish (Sic *sic, BufferIn *in, BufferOut *out); END_C_DECLS #endif /* !SIC_H */ 把全部的迄今你已經看到的東西放到一塊兒,main函數建立一個Sic解析器,在處理輸入流執行完時最終將退出的evalstream以前,經過添加以前定義在2個表裏systax處理函數和內置函數初始化它。 int main (int argc, char * const argv[]) { int result = EXIT_SUCCESS; Sic *sic = sic_new (); /* initialise the system */ if (sic_init (sic) != SIC_OKAY) sic_fatal ("sic initialisation failed"); signal (SIGINT, SIG_IGN); setbuf (stdout, NULL); /* initial symbols */ sicstate_set (sic, "PS1", "] ", NULL); sicstate_set (sic, "PS2", "- ", NULL); /* evaluate the input stream */ evalstream (sic, stdin); exit (result); } 如今,這個shell能被建立和使用: $ bootstrap ... $ ./configure --with-readline ... $ make ... make[2]: Entering directory `/tmp/sic/src' gcc -DHAVE_CONFIG_H -I. -I.. -I../sic -I.. -I../sic -g -c sic.c gcc -DHAVE_CONFIG_H -I. -I.. -I../sic -I.. -I../sic -g -c sic_builtin.c gcc -DHAVE_CONFIG_H -I. -I.. -I../sic -I.. -I../sic -g -c sic_repl.c gcc -DHAVE_CONFIG_H -I. -I.. -I../sic -I.. -I../sic -g -c sic_syntax.c gcc -g -O2 -o sic sic.o sic_builtin.o sic_repl.o sic_syntax.o \ ../sic/libsic.a ../replace/libreplace.a -lreadline make[2]: Leaving directory `/tmp/sic/src' ... $ ./src/sic ] pwd /tmp/sic ] ls -F Makefile aclocal.m4 config.cache configure* sic/ Makefile.am bootstrap* config.log configure.in src/ Makefile.in config/ config.status* replace/ ] exit $ 這章已經開發了一個可靠的在後面講解Libtool時我將在第12章引用到的基礎代碼。這章提早介紹Libtool的用途,怎樣使用它以及怎樣整合它進入你本身的項目,和只用automake建立共享庫時它提供的方便。