Autobook中文版(七)—9.一个小的GNU Autotools项目

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建立共享库时它提供的方便。
相关文章
相关标签/搜索