写点什么

cstdio 的源码学习分析 10- 格式化输入输出函数 fprintf--- 宏定义 / 辅助函数分析 04

作者:桑榆
  • 2022-10-16
    广东
  • 本文字数:5862 字

    阅读完需:约 1 分钟

cstdio 中的格式化输入输出函数

fprintf 函数的实现 vfprintf 中包含了相当多的宏定义和辅助函数,接下来我们一起来分析一下它们对应的源码实现。

函数逻辑分析---vfprintf

2.宏定义/辅助函数分析

(14)__printf_fp_spec---打印一个特定类型的变量

  • 入参

    FILE *fp:文件流对象

    const struct printf_info *info:本次识别的 format 占位符对应解析出的相关信息

    const void *const *args:本次占位符对应的参数列表

这个函数是其中比较核心的函数,在每一次处理 format string 时,提取出一个占位符,并填充相关识别信息到 printf_info 结构体中,传入其对应的参数指针,对当前的特定类型变量进行打印。

由于这个函数承载了大量的细节处理,所以还是通过函数调用的方式进行实现的,根据对应的类型,如果是'a'或'A'即 16 进制打印,那需要调用__printf_fphex 完成相关的打印工作,否则统一使用__printf_fp 完成相关的打印工作,这里我们将__printf_fphex/__printf_fp 的核心实现留到后文解析,目前就当成这个函数能够完成一个特定类型数据的打印即可。

        struct printf_info info =          {        .prec = prec,        .width = width,        .spec = spec,        .is_long_double = is_long_double,        .is_short = is_short,        .is_long = is_long,        .alt = alt,        .space = space,        .left = left,        .showsign = showsign,        .group = group,        .pad = pad,        .extra = 0,        .i18n = use_outdigits,        .wide = sizeof (CHAR_T) != 1,        .is_binary128 = 0          };
PARSE_FLOAT_VA_ARG_EXTENDED (info); const void *ptr = &the_arg;
int function_done = __printf_fp_spec (s, &info, &ptr); /* Calls __printf_fp or __printf_fphex based on the value of the format specifier INFO->spec. */static inline int__printf_fp_spec (FILE *fp, const struct printf_info *info, const void *const *args){ if (info->spec == 'a' || info->spec == 'A') return __printf_fphex (fp, info, args); else return __printf_fp (fp, info, args);}
复制代码

当然,在这里,我们有必要看一下 printf_info 结构体的内容,如下:

参照前文所说一个 format 占位符的格式

%[flags][width][.precision][length]specifier

可以看到,在 printg_info 结构体中,实际上是按顺序包含我们在打印过程中所需要的所有控制信息,vfprintf 函数的核心逻辑就是识别字符串并构造这样一个结构体,然后传给__printf_fp_spec 进行打印处理,不断循环,直到处理结束。

// glibc/stdio-common/printf.hstruct printf_info{  int prec;         /* Precision.  */  int width;            /* Width.  */  wchar_t spec;         /* Format letter.  */  unsigned int is_long_double:1;/* L flag.  */  unsigned int is_short:1;  /* h flag.  */  unsigned int is_long:1;   /* l flag.  */  unsigned int alt:1;       /* # flag.  */  unsigned int space:1;     /* Space flag.  */  unsigned int left:1;      /* - flag.  */  unsigned int showsign:1;  /* + flag.  */  unsigned int group:1;     /* ' flag.  */  unsigned int extra:1;     /* For special use.  */  unsigned int is_char:1;   /* hh flag.  */  unsigned int wide:1;      /* Nonzero for wide character streams.  */  unsigned int i18n:1;      /* I flag.  */  unsigned int is_binary128:1;  /* Floating-point argument is ABI-compatible                   with IEC 60559 binary128.  */  unsigned int __pad:3;     /* Unused so far.  */  unsigned short int user;  /* Bits for user-installed modifiers.  */  wchar_t pad;          /* Padding character.  */};
复制代码

(15).is_longlong 与 is_long_num---针对 32 位和 64 位的兼容机制

如果 LONG_MAX == LONG_LONG_MAX,说明是 32 位机器,需要将 is_longlong 置为 0,否则与 is_long_double 保持一致;

如果 INT_MAX == LONG_MAX,说明是 32 位机器,需要将 is_long_num 置为 0,否则与 is_long 保持一致。

/* For handling long_double and longlong we use the same flag.  If   `long' and `long long' are effectively the same type define it to   zero.  */#if LONG_MAX == LONG_LONG_MAX# define is_longlong 0#else# define is_longlong is_long_double#endif
/* If `long' and `int' is effectively the same type we don't have to handle `long separately. */#if INT_MAX == LONG_MAX# define is_long_num 0#else# define is_long_num is_long#endif
复制代码

(16)跳转表机制---用于处理 format 占位符的识别

回到上面提到的 vfprintf 的核心任务,一个是识别 format 占位符构造 printf_info 结构体,一个是打印该种特定类型的变量,这个跳转表机制就是为了识别 format 占位符而创建的。

%[flags][width][.precision][length]specifier对于这样的一个字符串类型识别,我们通常会采用自动机的方式进行识别,设定多个状态,每个状态代表一个识别位,状态之间的跳转需要条件,直到最后识别到specifier类型,这时,我们就能够完全解析出所有字段,上面构想的方法其实就是有限状态自动机的方法。

但是,因为 C 标准中规定了上面五个字段中可以有很多种组合,这个状态数量的叠加将是非常巨大的,我们能否寻找到一种比较简单的方式实现这个逻辑呢?

答案是有的,在 C 语言中有一种语法深为人所诟病,那就 goto 语句,可以进行无条件跳转到设定好的标号处,当然大部分情况下我们是不建议使用的,但是它在这种场景下就很契合,试想一下,我们的有限状态自动机中每一个状态是否就是一个标号呢?在这个标号中处理当前符号的识别,处理完成之后,移动到下一个字符,并通过我们设定好的跳转表跳转到下一个标号处进行处理,这个过程中,我们需要维护好的就是这个关键的跳转表,它记录了当前符号的下一个处理标号的位置,跳转表充当了有限状态自动机中触发状态变化函数的作用。

我们来看一看具体代码中的例子:

这里我们以打印一个 %c 为例

首先,在 vfprintf 中,我们首先找到第一个 %号;

  /* Find the first format specifier.  */  f = lead_str_end = __find_specmb ((const UCHAR_T *) format);
复制代码

然后输出第一个 %号之前的所有字符串,

这些字符串是不需要解析的

  /* Write the literal text before the first format.  */  outstring ((const UCHAR_T *) format,         lead_str_end - (const UCHAR_T *) format);
复制代码

定义跳转表

  /* Process whole format string.  */  do    {      STEP0_3_TABLE;      STEP4_TABLE;      ...  }
复制代码

处理 %后面的第一个字符

      /* Get current character in format string.  */      JUMP (*++f, step0_jumps);
复制代码

JUMP 宏解析

接下来我们来看看这个 JUMP 宏是如何进行解析的,注意:按照我们例子,现在传入的是字符 c

这里核心是找到 ptr(即当前字符 c 对应的 label 地址),然后 goto *ptr 跳转。

# 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)
复制代码

首先是判断输入字符是否在跳转范围内,跳转表是从 L_(' ')到 L_('z')的所有字符(看到这里不由得对 ASCII 码设计者和 C 语言中 print format 关键字设置的逻辑感到赞叹,都是相互联系的,否则这里的代码就更加混乱不能理解了)。

  • 如果不在这其中,那我们默认返回 REF (form_unknown),即 do_form_unknown 与 do_form_unknown 的地址差,为 0;

  • 如果在其中,那么我们查表可知字符’c‘对应的值为 20,所以我们需要访问表 table[20],注意现在的表是传入的 step0_jumps

/* 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  };
#define NOT_IN_JUMP_RANGE(Ch) ((Ch) < L_(' ') || (Ch) > L_('z'))#define CHAR_CLASS(Ch) (jump_table[(INT_T) (Ch) - L_(' ')])
# define JUMP_TABLE_TYPE const int# define JUMP_TABLE_BASE_LABEL do_form_unknown# define REF(Name) &&do_##Name - &&JUMP_TABLE_BASE_LABEL
复制代码

查表 step0_jumps[20],可以看到 index20 的位置是 REF (form_character),根据 REF 的解析格式,应该是

&&do_form_character - &&do_form_unknown,接下来我们需要找到 do_form_character 的标号位置进行处理

    /* 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' */                     \...}
复制代码

LABEL(form_character)的处理逻辑

注意到有专门的宏 LABEL 定义标号,所以我们要找 LABEL(form_character)

这个标号下的处理逻辑就比较明晰了:

  1. 先处理宽字符情况,跳转到 LABEL (form_wcharacter);

  2. 因为标准字符只占用一个字节,所以对齐宽度先减一;

  3. 如果指定非左对齐,那么则需要在左侧增加空格进行对齐;

  4. 然后调用 outchar 输出当前字符,当前字符的获取通过 process_arg_int 得到(实际上就是通过 va_arg 获取 int 型数据)

  5. 最后进行左对齐的处理,在右侧增加空格进行对齐。

#define LABEL(Name) do_##Name
// glibc/stdio-common/vfprintf-process-arg.cLABEL (form_character): /* Character. */ if (is_long) goto LABEL (form_wcharacter); --width; /* Account for the character itself. */ if (!left) PAD (L_(' '));#ifdef COMPILE_WPRINTF outchar (__btowc ((unsigned char) process_arg_int ())); /* Promoted. */#else outchar ((unsigned char) process_arg_int ()); /* Promoted. */#endif if (left) PAD (L_(' ')); break; #define process_arg_int() va_arg (ap, int)
复制代码

因为跳转表的机制过于繁琐,上面我们只是以 %c 这样一个小例子作为解析方式,后续会专门写文章说明跳转表的机制,因为跳转表不止一张,其中的跳转逻辑也不尽相同,读者主要要理解这种有限状态自动机转换的思想。

发布于: 刚刚阅读数: 4
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020-02-29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
cstdio的源码学习分析10-格式化输入输出函数fprintf---宏定义/辅助函数分析04_源码刨析_桑榆_InfoQ写作社区