gcc研究笔记(三)libcpp中的宏扩展机制

 
1、libcpp中和宏相关的基础知识
libcpp是gcc的C/C++语言预处理器,gcc将C/C++语言预处理器cpp以库的形式独立出来,故取名曰libcpp。libcpp的输出为预处理标记cpp_token序列,为了实现回退机制、预处理指令处理和宏扩展,libcpp分三层输出预处理标记cpp_token序列:
a)_cpp_lex_direct:这是预处理标记序列输出的第一层,它直接对程序文件进行词法分析,不处理任何预处理指令,也不提供回退功能,它反映的是真实的程序文本;
b)cpp_lex_token:第二层在第一层的基础上实现了回退和预处理指令处理功能,调用这一层的用户看不到符合C/C++标准的预处理指令行;
c)cpp_get_token:这是最外一层,调用这个函数看到的将是C/C++标准中的翻译单元。这一层相对上一层主要是实现了宏扩展和##操作符处理功能。
2、预处理指令的处理
预处理指令的处理流程起始于cpp_lex_token函数,不考虑回退机制(回退机制将在后续文章中单独讨论),cpp_lex_token调用第一层分析函数_cpp_lex_direct,如果得到标记#,且其为当前行的第一个标记,转入_cpp_handle_directive。_cpp_handle_directive继续调用_cpp_lex_direct读取下一个标记,如果其为标识符(dname->type == CPP_NAME),并且为有效的预处理指令名字,转入相应的预处理指令处理函数。
为了提高效率,libcpp使用了两个小技巧。首先,为了快速判别出一个标识符是否为预处理指令名字,libcpp在标识符的存储结构cpp_hashnode中提供了一个is_directive标记,用于标识该标识符是否为预处理指令名字。另外,为了快速定位相应预处理指令的处理函数,libcpp将所有的预处理指令处理函数按顺序保存在一个数组当中,并且在cpp_hashnode中额外提供了一个directive_index成员,用于对处理函数数组索引。
处理函数数组的定义代码如下:
#define DIRECTIVE_TABLE                                                                 /
D(define,     T_DEFINE = 0, KANDR,     IN_I)      /* 270554 */ /
D(include,   T_INCLUDE,     KANDR,     INCL | EXPAND) /* 52262 */ /
D(endif,       T_ENDIF, KANDR,     COND)           /* 45855 */ /
D(ifdef,       T_IFDEF, KANDR,     COND | IF_COND) /* 22000 */ /
D(if,            T_IF,                   KANDR, COND | IF_COND | EXPAND) /* 18162 */ /
D(else,                  T_ELSE,             KANDR,     COND)           /*   9863 */ /
D(ifndef,     T_IFNDEF,        KANDR,     COND | IF_COND) /*   9675 */ /
D(undef,      T_UNDEF,         KANDR,     IN_I)      /*   4837 */ /
D(line,                  T_LINE,             KANDR,     EXPAND)      /*   2465 */ /
D(elif,                   T_ELIF,              STDC89,    COND | EXPAND) /*    610 */ /
D(error,       T_ERROR,         STDC89,    0)              /*    475 */ /
D(pragma,   T_PRAGMA,    STDC89,    IN_I)        /*    195 */ /
D(warning, T_WARNING,   EXTENSION, 0)                      /*     22 */ /
D(include_next,    T_INCLUDE_NEXT, EXTENSION, INCL | EXPAND) /*     19 */ /
D(ident,       T_IDENT, EXTENSION, IN_I)       /*     11 */ /
D(import,    T_IMPORT,       EXTENSION, INCL | EXPAND) /* 0 ObjC */      /
D(assert,     T_ASSERT,        EXTENSION, 0)                      /* 0 SVR4 */ /
D(unassert, T_UNASSERT, EXTENSION, 0)                      /* 0 SVR4 */ /
D(sccs,                 T_SCCS,             EXTENSION, IN_I)       /* 0 SVR4? */
 
#define D(name, t, origin, flags) /
{ do_##name, (const uchar *) #name, /
 sizeof #name - 1, origin, flags },
static const directive dtable[] =
{
DIRECTIVE_TABLE
};
#undef D
 
上述代码定义了一个名字为dtable,类型为directive的数组,directive结构的第一个成员便为处理函数的指针。可以看出#define指令的处理函数为do_define。_cpp_init_directives函数完成预处理指令名字的初始化工作。
3、宏定义
如前所述,#define指令的处理函数为do_define。do_define首先调用lex_macro_node获取宏名(lex_macro_node除了做一些宏名的有效性检查外,等价于_cpp_lex_direct),接下来调用_cpp_create_definition创建宏定义cpp_macro(cpp_macro主要由两部分组成:参数列表和替换列表)。_cpp_create_definition的工作主要有两步(都是在create_iso_definition子函数中完成的):
a)如果下一个标记为(,创建函数式宏,调用parse_params解析宏参列表;
b)解析宏替换列表。
_cpp_create_definition的一个额外的工作是调用warn_of_redefinition进行宏的重定义检测。
       宏参列表是一个cpp_hashnode *数组。为了快速检测出重名的宏参,并且在解析替换列表的时候快速判断出一个标识符是否为宏参,parse_params对每一个参数设置一个NODE_MACRO_ARG标记(调用子函数_cpp_save_parameter完成)。在宏定义完成后,_cpp_create_definition马上将每一个宏参的NODE_MACRO_ARG标记清除,也就是说,NODE_MACRO_ARG标记只在宏定义过程中有效。
宏替换列表为一个cpp_token结构数组(而不是cpp_token*数组),create_iso_definition复制每一个_cpp_lex_direct返回的结果,并谨慎的处理#和##操作符(原理不赘述),调整#和##操作符对应操作数的标记。如果cpp_lex_direct返回的结果是宏参,复制该标记,将其类型从CPP_NAME改为CPP_MACRO_ARG,并保存宏参索引(即对应于宏参列表中的第几个参数)。
4、宏扩展
4.1cpp_context结构
libcpp采用递归机制处理宏扩展,cpp_context则是递归机制实现的关键结构,其定义如下:
struct cpp_context
{
 cpp_context *next, *prev;
 
        struct
      {
        union utoken first;
        union utoken last;
      } iso;
 
 _cpp_buff *buff;
 cpp_hashnode *macro;
 bool direct_p;
};
cpp_context说到底可以看成是一个cpp_token或cpp_token*数组,union utoken的定义如下:
union utoken
{
 const cpp_token *token;
 const cpp_token **ptoken;
};
direct_p成员决定是cpp_token *还是cpp_token **有效;buff是一个局部内存分配器;macro成员是产生该cpp_context结构的宏,可以为空;next和prev用于内存管理和生成cpp_context列表。
4.2cpp_get_token函数
不考虑##操作符的处理,cpp_get_token函数的简化流程如下:
while(true)
{
      if(!context->prev)
               result = _cpp_lex_token ();
      else if (FIRST (context).token != LAST (context).token)
      {
               if (context->direct_p)
                         result = FIRST (context).token++;
              else
                        result = *FIRST (context).ptoken++;
}
else
{
      _cpp_pop_context ();
      coninue;
}
 
if(result->IsMacro() && !result->Disabled())
{
      enter_macro_context(result->node);
      return cpp_get_token();
}
 
break;
}
从上述伪代码可以看出,cpp_get_token函数优先从context列表中取标记,当context为空时才调用_cpp_lex_token获取下一个标记,这是形成递归的关键算法。如果获取的标记为没有被屏蔽的宏,调用enter_macro_context进行宏展开。
4.3enter_macro_context
       对于对象式宏或参数数目为零的函数式宏,直接调用_cpp_push_token_context函数将替换序列压栈,便能实现函数的替换了,下面主要讨论需要进行参量替换的宏调用。
对于函数式宏展开,第一步需要做的工作是收集宏调用的参量,enter_macro_context调用funlike_invocation_p子函数完成此项工作。宏参量使用macro_arg结构表示:
struct macro_arg
{
 const cpp_token **first;         /* First token in unexpanded argument. */
 const cpp_token **expanded; /* Macro-expanded argument. */
 const cpp_token *stringified; /* Stringified argument. */
 unsigned int count;                  /* # of tokens in argument. */
 unsigned int expanded_count; /* # of tokens in expanded argument. */
};
单个宏参量可能由多个标记组成,所以使用cpp_token *数组first表示宏参量的组成。stringified表示该宏参量的字符化形式,expanded则是该参量的最终扩展结果。由于参量可能在替换列表中出现多次,所以保存它的stringified结果,特别是expanded结果是有必要的(稍候讨论stringified和expanded的生成时机)。
完成宏参量的收集工作后,enter_macro_context调用replace_args函数进行参量替换,即把替换列表中类型为CPP_MACRO_ARG的标记替换为相应宏参量的expanded结果(如果expanded为空,则调用expand_arg生成expanded结果),或把#操作符和其操作数替换为相应宏参量的stringified结果(如果stringified为空,则调用stringify_arg生成stringified结果)。最后,replace_args调用push_ptoken_context将替换结果压栈,完成函数式宏的展开。
宏参量的展开机制是函数式宏展开的关键所在,expand_arg的基本流程如下:
1、调用push_ptoken_context函数将组成宏参量的first压栈;
2、调用cpp_get_token函数获取下一个标记,并保存结果;
3、调用_cpp_pop_context弹出压栈的first标记序列。
代码虽然很简单,但设计却很巧妙,用短短的几行代码便完成了宏展开的重检测,当然,这些都归功于递归机制的使用。
 
posted @ 2007-04-14 18:14  Goncely  阅读(447)  评论(0编辑  收藏  举报