持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情
背景
在stdio.h的printf类型的函数中,要完成变量打印,主要有两个核心任务:一个是识别format占位符构造printf_info结构体,一个是打印该种特定类型的变量,这个跳转表机制就是为了识别format占位符而创建的。
%[flags][width][.precision][length]specifier对于这样的一个字符串类型识别,我们通常会采用自动机的方式进行识别,设定多个状态,每个状态代表一个识别位,状态之间的跳转需要条件,直到最后识别到specifier类型,这时,我们就能够完全解析出所有字段,上面构想的方法其实就是有限状态自动机的方法。
但是,因为C标准中规定了上面五个字段中可以有很多种组合,这个状态数量的叠加将是非常巨大的,我们能否寻找到一种比较简单的方式实现这个逻辑呢?
答案是有的,在C语言中有一种语法深为人所诟病,那就goto语句,可以进行无条件跳转到设定好的标号处,当然大部分情况下我们是不建议使用的,但是它在这种场景下就很契合,试想一下,我们的有限状态自动机中每一个状态是否就是一个标号呢?在这个标号中处理当前符号的识别,处理完成之后,移动到下一个字符,并通过我们设定好的跳转表跳转到下一个标号处进行处理,这个过程中,我们需要维护好的就是这个关键的跳转表,它记录了当前符号的下一个处理标号的位置,跳转表充当了有限状态自动机中触发状态变化函数的作用。
跳转表相关的宏定义/变量
1.jump_table---字符对应的跳转位置,字符在L_(' ')和L_('z')之间
其实这些字符的设定规律也比较明显:
- 跳转值为0代表在format字符串解析过程中没有特殊含义的字符:如大写的LMNOPQ等;
- 跳转值为1代表' ',2代表'+',3代表'-',4代表'#',5代表'0',6代表'',7代表'*',9代表'.';
- 跳转值8代表数字1到9;
- 跳转值10代表'h'(类型前缀,表示长度扩展信息,如hd表示short int);
- 跳转值11代表'l'(类型前缀,表示长度扩展信息,如ld表示long int);
- 跳转值12代表'L'和'q''(类型前缀,表示长度扩展信息,如Lf表示long long double)
- 跳转值13代表'Z'和'z',size_t类型
- 跳转值14代表'%'
- 跳转值15代表'd'和'i',有符号整型数据
- 跳转值16代表'u',无符号整型数据
- 跳转值17代表'o',无符号八进制整型数据
- 跳转值18代表'X'和'x',无符号16进制整型数据
- 跳转值19代表'E', 'e', 'F', 'f', 'G', 'g',浮点数
- 跳转值20代表'c',字符类型
- 跳转值21代表's', 'S',字符串类型
- 跳转值22代表'p',指针类型
- 跳转值23代表'n',数字类型
- 跳转值24代表'm',标准错误类型
- 跳转值25代表'C',宽字符类型
- 跳转值26代表'A', 'a',浮点数16进制类型
- 跳转值27代表't',ptrdiff_t类型
- 跳转值28代表'j',intmax_t类型
- 跳转值29代表'I',标识是否使用额外的字符解释,如在标准字符中使用到了宽字符
- 跳转值30代表'B', 'b',二进制整型数据
/* This table maps a character into a number representing a class. In
each step there is a destination label for each class. */
static const uint8_t jump_table[] =
{
/* ' ' */ 1, 0, 0, /* '#' */ 4,
0, /* '%' */ 14, 0, /* '''*/ 6,
0, 0, /* '*' */ 7, /* '+' */ 2,
0, /* '-' */ 3, /* '.' */ 9, 0,
/* '0' */ 5, /* '1' */ 8, /* '2' */ 8, /* '3' */ 8,
/* '4' */ 8, /* '5' */ 8, /* '6' */ 8, /* '7' */ 8,
/* '8' */ 8, /* '9' */ 8, 0, 0,
0, 0, 0, 0,
0, /* 'A' */ 26, /* 'B' */ 30, /* 'C' */ 25,
0, /* 'E' */ 19, /* F */ 19, /* 'G' */ 19,
0, /* 'I' */ 29, 0, 0,
/* 'L' */ 12, 0, 0, 0,
0, 0, 0, /* 'S' */ 21,
0, 0, 0, 0,
/* 'X' */ 18, 0, /* 'Z' */ 13, 0,
0, 0, 0, 0,
0, /* 'a' */ 26, /* 'b' */ 30, /* 'c' */ 20,
/* 'd' */ 15, /* 'e' */ 19, /* 'f' */ 19, /* 'g' */ 19,
/* 'h' */ 10, /* 'i' */ 15, /* 'j' */ 28, 0,
/* 'l' */ 11, /* 'm' */ 24, /* 'n' */ 23, /* 'o' */ 17,
/* 'p' */ 22, /* 'q' */ 12, 0, /* 's' */ 21,
/* 't' */ 27, /* 'u' */ 16, 0, 0,
/* 'x' */ 18, 0, /* 'z' */ 13
};
2.NOT_IN_JUMP_RANGE(Ch)---判断字符是否在跳转范围内
因为我们规定可以识别跳转的字符在L_(' ')和L_('z')之间,其他字符均视为未知字符
#define NOT_IN_JUMP_RANGE(Ch) ((Ch) < L_(' ') || (Ch) > L_('z'))
3.CHAR_CLASS(Ch)---获取对应的跳转值
因为jump_table是按照L_(' ')和L_('z')之间编写的,所以将对应字符值减去L_(' ')可以得到索引值,进而取出对应的跳转值
#define CHAR_CLASS(Ch) (jump_table[(INT_T) (Ch) - L_(' ')])
4.LABEL(Name)---label宏
定义label的格式,增加do_前缀
如:LABEL (flag_space):表示do_flag_space:label
#define LABEL(Name) do_##Name
5.JUMP_TABLE_TYPE---跳转表中存储的数据类型
# define JUMP_TABLE_TYPE const int
6.JUMP_TABLE_BASE_LABEL---基础label
即do_form_unknown,未知格式跳转
# define JUMP_TABLE_BASE_LABEL do_form_unknown
7.REF(Name)---计算与基础label的地址差
C语言中可以使用&&符号获取标签名的地址
# define REF(Name) &&do_##Name - &&JUMP_TABLE_BASE_LABEL
8.JUMP(ChExpr, table)---根据表与输入字符进行跳转
- 首先计算当前输入字符的偏移:如果是有效字符,则返回表的对应跳转位置,否则返回REF (form_unknown),即0;
- 取得要跳转的标号的地址base+offset;
- 进行跳转 goto *ptr。
这里其实可以把标号当做函数指针,goto语句类似函数调用。
# define JUMP(ChExpr, table) \
do \
{ \
int offset; \
void *ptr; \
spec = (ChExpr); \
offset = NOT_IN_JUMP_RANGE (spec) ? REF (form_unknown) \
: table[CHAR_CLASS (spec)]; \
ptr = &&JUMP_TABLE_BASE_LABEL + offset; \
goto *ptr; \
} \
while (0)
核心跳转表定义
以下跳转表都定义在STEP0_3_TABLE宏中
1.step0_jumps[31]---开始进行format占位符识别或识别到flags时使用
仔细看,我们可以看到,这里的顺序与jump_table的标号是保持一致的,比如我们前面的例子中%c,在此处进行跳转时,会直接跳转到REF (form_character),针对这样的简单format,一般在这里就跳转成功了,进行相应的打印。
/* Step 0: at the beginning. */ \
static JUMP_TABLE_TYPE step0_jumps[31] = \
{ \
REF (form_unknown), \
REF (flag_space), /* for ' ' */ \
REF (flag_plus), /* for '+' */ \
REF (flag_minus), /* for '-' */ \
REF (flag_hash), /* for '<hash>' */ \
REF (flag_zero), /* for '0' */ \
REF (flag_quote), /* for ''' */ \
REF (width_asterics), /* for '*' */ \
REF (width), /* for '1'...'9' */ \
REF (precision), /* for '.' */ \
REF (mod_half), /* for 'h' */ \
REF (mod_long), /* for 'l' */ \
REF (mod_longlong), /* for 'L', 'q' */ \
REF (mod_size_t), /* for 'z', 'Z' */ \
REF (form_percent), /* for '%' */ \
REF (form_integer), /* for 'd', 'i' */ \
REF (form_unsigned), /* for 'u' */ \
REF (form_octal), /* for 'o' */ \
REF (form_hexa), /* for 'X', 'x' */ \
REF (form_float), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \
REF (form_character), /* for 'c' */ \
REF (form_string), /* for 's', 'S' */ \
REF (form_pointer), /* for 'p' */ \
REF (form_number), /* for 'n' */ \
REF (form_strerror), /* for 'm' */ \
REF (form_wcharacter), /* for 'C' */ \
REF (form_floathex), /* for 'A', 'a' */ \
REF (mod_ptrdiff_t), /* for 't' */ \
REF (mod_intmax_t), /* for 'j' */ \
REF (flag_i18n), /* for 'I' */ \
REF (form_binary), /* for 'B', 'b' */ \
};
那么在哪些情况下会使用到这张表呢?通过代码中的分析,可以看到如下几种情况:
因为针对合法字符格式,在这些字符后跟着的应该是step0_jumps这样对应的格式,此时识别的就是flags相关的字符
format占位符的一般形式:%[flags][width][.precision][length]specifier
(1).识别了%符号后的第一个字符
/* Get current character in format string. */
JUMP (*++f, step0_jumps);
(2).识别空格符之后
/* ' ' flag. */
LABEL (flag_space):
space = 1;
JUMP (*++f, step0_jumps);
(3).识别'+'之后
/* '+' flag. */
LABEL (flag_plus):
showsign = 1;
JUMP (*++f, step0_jumps);
(4).识别'-'之后
/* The '-' flag. */
LABEL (flag_minus):
left = 1;
pad = L_(' ');
JUMP (*++f, step0_jumps);
(5).识别'#'之后
/* The '#' flag. */
LABEL (flag_hash):
alt = 1;
JUMP (*++f, step0_jumps);
(6).识别‘0’之后
/* The '0' flag. */
LABEL (flag_zero):
if (!left)
pad = L_('0');
JUMP (*++f, step0_jumps);
(7).识别'''之后
/* The ''' flag. */
LABEL (flag_quote):
group = 1;
...
JUMP (*++f, step0_jumps);
(8).识别'I'之后
LABEL (flag_i18n):
use_outdigits = 1;
JUMP (*++f, step0_jumps);
2.step1_jumps[31]---识别到width相关的字符时使用
注意这个表与step0_jumps的对比,我们可以发现,如下字符对应的位置,都被置为了REF (form_unknown):
- ' ','+','-','#','0',''','I'---因为在flags识别中出现了,后续不可能再出现了,如果出现,则认为是异常模式;
- '*','1'...'9'---是width的识别关键字符,识别出现后,将会把相关字符取出,后续如果继续出现,那说明是异常模式
/* Step 1: after processing width. */ \
static JUMP_TABLE_TYPE step1_jumps[31] = \
{ \
REF (form_unknown), \
REF (form_unknown), /* for ' ' */ \
REF (form_unknown), /* for '+' */ \
REF (form_unknown), /* for '-' */ \
REF (form_unknown), /* for '<hash>' */ \
REF (form_unknown), /* for '0' */ \
REF (form_unknown), /* for ''' */ \
REF (form_unknown), /* for '*' */ \
REF (form_unknown), /* for '1'...'9' */ \
REF (precision), /* for '.' */ \
REF (mod_half), /* for 'h' */ \
REF (mod_long), /* for 'l' */ \
REF (mod_longlong), /* for 'L', 'q' */ \
REF (mod_size_t), /* for 'z', 'Z' */ \
REF (form_percent), /* for '%' */ \
REF (form_integer), /* for 'd', 'i' */ \
REF (form_unsigned), /* for 'u' */ \
REF (form_octal), /* for 'o' */ \
REF (form_hexa), /* for 'X', 'x' */ \
REF (form_float), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \
REF (form_character), /* for 'c' */ \
REF (form_string), /* for 's', 'S' */ \
REF (form_pointer), /* for 'p' */ \
REF (form_number), /* for 'n' */ \
REF (form_strerror), /* for 'm' */ \
REF (form_wcharacter), /* for 'C' */ \
REF (form_floathex), /* for 'A', 'a' */ \
REF (mod_ptrdiff_t), /* for 't' */ \
REF (mod_intmax_t), /* for 'j' */ \
REF (form_unknown), /* for 'I' */ \
REF (form_binary), /* for 'B', 'b' */ \
};
使用表的情况:
(1).识别到'*'之后
/* Get width from argument. */
LABEL (width_asterics):
...
JUMP (*f, step1_jumps);
(2).识别到'1'...'9'之后
/* Given width in format string. */
LABEL (width):
...
JUMP (*f, step1_jumps);
3.step2_jumps[31]---识别到precision相关字符时使用
对比step1_jumps可以发现,'.' 位置被置为REF (form_unknown)
/* Step 2: after processing precision. */ \
static JUMP_TABLE_TYPE step2_jumps[31] = \
{ \
REF (form_unknown), \
REF (form_unknown), /* for ' ' */ \
REF (form_unknown), /* for '+' */ \
REF (form_unknown), /* for '-' */ \
REF (form_unknown), /* for '<hash>' */ \
REF (form_unknown), /* for '0' */ \
REF (form_unknown), /* for ''' */ \
REF (form_unknown), /* for '*' */ \
REF (form_unknown), /* for '1'...'9' */ \
REF (form_unknown), /* for '.' */ \
REF (mod_half), /* for 'h' */ \
REF (mod_long), /* for 'l' */ \
REF (mod_longlong), /* for 'L', 'q' */ \
REF (mod_size_t), /* for 'z', 'Z' */ \
REF (form_percent), /* for '%' */ \
REF (form_integer), /* for 'd', 'i' */ \
REF (form_unsigned), /* for 'u' */ \
REF (form_octal), /* for 'o' */ \
REF (form_hexa), /* for 'X', 'x' */ \
REF (form_float), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \
REF (form_character), /* for 'c' */ \
REF (form_string), /* for 's', 'S' */ \
REF (form_pointer), /* for 'p' */ \
REF (form_number), /* for 'n' */ \
REF (form_strerror), /* for 'm' */ \
REF (form_wcharacter), /* for 'C' */ \
REF (form_floathex), /* for 'A', 'a' */ \
REF (mod_ptrdiff_t), /* for 't' */ \
REF (mod_intmax_t), /* for 'j' */ \
REF (form_unknown), /* for 'I' */ \
REF (form_binary), /* for 'B', 'b' */ \
};
(1)识别到'*'后
LABEL (precision):
...
JUMP (*f, step2_jumps);
4.step3a_jumps[31]---识别到第一个'h'修饰符之后使用
对比step2_jumps之后发现:
- 原来'h'的位置从REF (mod_half)变为REF (mod_halfhalf),为了识别'hh'的情况
- 'l','L','q','z','Z','E','e','F','f','G','g','c','s','S','p','m','C',’a’,'A','t','j',都被置为REF (form_unknown),因为这些都是非法组合
/* Step 3a: after processing first 'h' modifier. */ \
static JUMP_TABLE_TYPE step3a_jumps[31] = \
{ \
REF (form_unknown), \
REF (form_unknown), /* for ' ' */ \
REF (form_unknown), /* for '+' */ \
REF (form_unknown), /* for '-' */ \
REF (form_unknown), /* for '<hash>' */ \
REF (form_unknown), /* for '0' */ \
REF (form_unknown), /* for ''' */ \
REF (form_unknown), /* for '*' */ \
REF (form_unknown), /* for '1'...'9' */ \
REF (form_unknown), /* for '.' */ \
REF (mod_halfhalf), /* for 'h' */ \
REF (form_unknown), /* for 'l' */ \
REF (form_unknown), /* for 'L', 'q' */ \
REF (form_unknown), /* for 'z', 'Z' */ \
REF (form_percent), /* for '%' */ \
REF (form_integer), /* for 'd', 'i' */ \
REF (form_unsigned), /* for 'u' */ \
REF (form_octal), /* for 'o' */ \
REF (form_hexa), /* for 'X', 'x' */ \
REF (form_unknown), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \
REF (form_unknown), /* for 'c' */ \
REF (form_unknown), /* for 's', 'S' */ \
REF (form_unknown), /* for 'p' */ \
REF (form_number), /* for 'n' */ \
REF (form_unknown), /* for 'm' */ \
REF (form_unknown), /* for 'C' */ \
REF (form_unknown), /* for 'A', 'a' */ \
REF (form_unknown), /* for 't' */ \
REF (form_unknown), /* for 'j' */ \
REF (form_unknown), /* for 'I' */ \
REF (form_binary), /* for 'B', 'b' */ \
};
(1).识别到'h'后
/* Process 'h' modifier. There might another 'h' following. */
LABEL (mod_half):
is_short = 1;
JUMP (*++f, step3a_jumps);
5.step3b_jumps[31]---识别到第一个'l'修饰符之后使用
对比step2_jumps之后发现:
- 原来'l'的位置从REF (mod_long)变为REF (mod_longlong),为了识别'll'的情况
- 'h','L','q','z','Z','t','j'被置为REF (form_unknown),因为这些都是非法组合
/* Step 3b: after processing first 'l' modifier. */ \
static JUMP_TABLE_TYPE step3b_jumps[31] = \
{ \
REF (form_unknown), \
REF (form_unknown), /* for ' ' */ \
REF (form_unknown), /* for '+' */ \
REF (form_unknown), /* for '-' */ \
REF (form_unknown), /* for '<hash>' */ \
REF (form_unknown), /* for '0' */ \
REF (form_unknown), /* for ''' */ \
REF (form_unknown), /* for '*' */ \
REF (form_unknown), /* for '1'...'9' */ \
REF (form_unknown), /* for '.' */ \
REF (form_unknown), /* for 'h' */ \
REF (mod_longlong), /* for 'l' */ \
REF (form_unknown), /* for 'L', 'q' */ \
REF (form_unknown), /* for 'z', 'Z' */ \
REF (form_percent), /* for '%' */ \
REF (form_integer), /* for 'd', 'i' */ \
REF (form_unsigned), /* for 'u' */ \
REF (form_octal), /* for 'o' */ \
REF (form_hexa), /* for 'X', 'x' */ \
REF (form_float), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \
REF (form_character), /* for 'c' */ \
REF (form_string), /* for 's', 'S' */ \
REF (form_pointer), /* for 'p' */ \
REF (form_number), /* for 'n' */ \
REF (form_strerror), /* for 'm' */ \
REF (form_wcharacter), /* for 'C' */ \
REF (form_floathex), /* for 'A', 'a' */ \
REF (form_unknown), /* for 't' */ \
REF (form_unknown), /* for 'j' */ \
REF (form_unknown), /* for 'I' */ \
REF (form_binary), /* for 'B', 'b' */ \
}
(1),识别到'l'之后
/* Process 'l' modifier. There might another 'l' following. */
LABEL (mod_long):
is_long = 1;
JUMP (*++f, step3b_jumps);
以下跳转表都定义在STEP4_TABLE宏中
6.step4_jumps[31]---识别具体类型时使用
这里同step3a_jumps对比:
- 'h'位置从REF (mod_halfhalf)变为REF (form_unknown),表明'hh'识别完成
- 还原了之前不能和'h'组合的类型
同step3b_jumps对比:
- 'l'位置从REF (mod_longlong)变为REF (form_unknown),表明'll'识别完成
- 还原了之前不能和'l'组合的类型
总的来说,这里就是识别所有可能类型的
/* Step 4: processing format specifier. */ \
static JUMP_TABLE_TYPE step4_jumps[31] = \
{ \
REF (form_unknown), \
REF (form_unknown), /* for ' ' */ \
REF (form_unknown), /* for '+' */ \
REF (form_unknown), /* for '-' */ \
REF (form_unknown), /* for '<hash>' */ \
REF (form_unknown), /* for '0' */ \
REF (form_unknown), /* for ''' */ \
REF (form_unknown), /* for '*' */ \
REF (form_unknown), /* for '1'...'9' */ \
REF (form_unknown), /* for '.' */ \
REF (form_unknown), /* for 'h' */ \
REF (form_unknown), /* for 'l' */ \
REF (form_unknown), /* for 'L', 'q' */ \
REF (form_unknown), /* for 'z', 'Z' */ \
REF (form_percent), /* for '%' */ \
REF (form_integer), /* for 'd', 'i' */ \
REF (form_unsigned), /* for 'u' */ \
REF (form_octal), /* for 'o' */ \
REF (form_hexa), /* for 'X', 'x' */ \
REF (form_float), /* for 'E', 'e', 'F', 'f', 'G', 'g' */ \
REF (form_character), /* for 'c' */ \
REF (form_string), /* for 's', 'S' */ \
REF (form_pointer), /* for 'p' */ \
REF (form_number), /* for 'n' */ \
REF (form_strerror), /* for 'm' */ \
REF (form_wcharacter), /* for 'C' */ \
REF (form_floathex), /* for 'A', 'a' */ \
REF (form_unknown), /* for 't' */ \
REF (form_unknown), /* for 'j' */ \
REF (form_unknown), /* for 'I' */ \
REF (form_binary), /* for 'B', 'b' */ \
}
(1).识别到'hh'之后
/* Process 'hh' modifier. */
LABEL (mod_halfhalf):
is_short = 0;
is_char = 1;
JUMP (*++f, step4_jumps);
(2).识别到'L'、‘q’、'll'之后
/* Process 'L', 'q', or 'll' modifier. No other modifier is
allowed to follow. */
LABEL (mod_longlong):
is_long_double = 1;
is_long = 1;
JUMP (*++f, step4_jumps);
(3).识别到'z'之后
LABEL (mod_size_t):
is_long_double = sizeof (size_t) > sizeof (unsigned long int);
is_long = sizeof (size_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);
(4).识别到't'之后
LABEL (mod_ptrdiff_t):
is_long_double = sizeof (ptrdiff_t) > sizeof (unsigned long int);
is_long = sizeof (ptrdiff_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);
(5).识别到'j'之后
LABEL (mod_intmax_t):
is_long_double = sizeof (intmax_t) > sizeof (unsigned long int);
is_long = sizeof (intmax_t) > sizeof (unsigned int);
JUMP (*++f, step4_jumps);
(6).识别specs中的每一个特定类型时使用
/* Now walk through all format specifiers and process them. */
for (; (size_t) nspecs_done < nspecs; ++nspecs_done)
{
STEP4_TABLE;
...
CHAR_T spec = specs[nspecs_done].info.spec;
/* Process format specifiers. */
while (1)
{
...
JUMP (spec, step4_jumps);
}
}
总结
跳转表的核心任务是解析
format占位符的一般形式:%[flags][width][.precision][length]specifier
通过使用五张跳转表(3a与3b处理两种情况,看做一张),对应识别的五个字段,后一个字段相比前一个字段,可跳转范围减少,对应限制只有合法的组合才能出现,一旦出现异常模式则直接跳转到REF (form_unknown)。
通过使用跳转表机制,将识别过程进行了拆分,使得逻辑更加清晰,每个阶段每张表任务不同,也不会造成干扰,值得我们学习借鉴。