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

9.一个小的GNU Autotools项目

本章介绍一个真实的小例子,演示一些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创建共享库时它提供的方便。

原文链接: https://www.cnblogs.com/med-dandelion/p/4532324.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/97381

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月10日 上午4:12
下一篇 2023年2月10日 上午4:13

相关推荐