您的位置:首页 > 编程语言 > C语言/C++

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

2007-04-14 18:14 288 查看
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.1、cpp_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.2、cpp_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.3、enter_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标记序列。
代码虽然很简单,但设计却很巧妙,用短短的几行代码便完成了宏展开的重检测,当然,这些都归功于递归机制的使用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: