写点什么

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

作者:桑榆
  • 2022-10-13
    上海
  • 本文字数:5346 字

    阅读完需:约 18 分钟

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

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

函数逻辑分析---vfprintf

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

(1).ARGCHECK---参数检查

  • 入参:

    s:vfprintf 的入参 stream 信息

    format:vfprintf 的入参 format 信息

按顺序做如下的参数检查:

  1. 先调用 CHECK_FILE 进行参数检查,主要是保证 stream 指针不为 NULL,而且_flags 在有效范围内,不满足条件则返回-1(EOF);

  2. 通过_IO_NO_WRITES 位检查 stream 是否可写,如果不可写,那说明本次操作是非法的,将_IO_ERR_SEEN 置位,并将错误码置为 EBADF(Bad file descriptor)返回-1(EOF);

  3. 判断 format 指针是否为空,如果为空,则将错误码置为 EINVAL(Invalid argument),返回-1(EOF).

  /* Sanity check of arguments.  */  ARGCHECK (s, format);    #define ARGCHECK(S, Format) \  do                                          \    {                                         \      /* Check file argument for consistence.  */                 \      CHECK_FILE (S, -1);                             \      if (S->_flags & _IO_NO_WRITES)                          \    {                                     \      S->_flags |= _IO_ERR_SEEN;                          \      __set_errno (EBADF);                            \      return -1;                                  \    }                                     \      if (Format == NULL)                             \    {                                     \      __set_errno (EINVAL);                           \      return -1;                                  \    }                                     \    } while (0)
#ifdef IO_DEBUG# define CHECK_FILE(FILE, RET) do { \ if ((FILE) == NULL \ || ((FILE)->_flags & _IO_MAGIC_MASK) != _IO_MAGIC) \ { \ __set_errno (EINVAL); \ return RET; \ } \ } while (0)#else# define CHECK_FILE(FILE, RET) do { } while (0)#endif
复制代码

(2).UNBUFFERED_P---判断 stream 是否是 unbufferd 状态,即不使用缓存 buffer

  • 入参:

    s:vfprintf 的入参 stream 信息

判断逻辑也比较简单,检查 FILE 对象的_flags 信息就好,即_IO_UNBUFFERED 是否置位

  if (UNBUFFERED_P (s))                                                                                                                                      /* Use a helper function which will allocate a local temporary buffer       for the stream and then call us again.  */    return buffered_vfprintf (s, format, ap, mode_flags);        #define UNBUFFERED_P(S) ((S)->_flags & _IO_UNBUFFERED) 
复制代码

(3).PARSE_FLOAT_VA_ARG---解析浮点数变参变量

  • 入参:

    struct printf_info 结构体

定义在 glibc/stdio-common/printf.h 文件中,通过定义我们可以看到,里面主要是我们前文我们提到的

%[flags][width][.precision][length]specifier表达式中各个特殊字段的信息。

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

与该宏一同定义的还有一个 PARSE_FLOAT_VA_ARG_EXTENDED,根据是否有 long double 类型数据做了一层封装,主要的解析工作如下:

  1. 从宏展开的上下文来看,根据 is_long_double 和 mode_flags 的置位情况(是否置位 PRINTF_LDBL_USES_FLOAT128---0x0008)决定当前是否要解析为 long double 类型

a. 如果是 long double 类型,那么首先将 is_binary128 置位 1(注意初始化时是 0)

b. 调用 va_arg 宏,将下一个可变参数解释为_Float128,并将值赋给 the_arg.pa_float128

va_arg 的一种实现方式,实际上就是将当前地址按照对应参数 type 解释,并将 ap 指针移到下一个位置

#define va_arg(ap,t) ((t)((ap+=sizeof(t))-sizeof(t)))

2.否则调用 PARSE_FLOAT_VA_ARG 进行实现,即非 128 位 long double 的情况

a. 这时 is_binary128 要保持置 0 状态

b. 根据是否是 long double 类型,决定参数是按照 long double 解析赋值给 the_arg.pa_long_double,还是按照 double 解析赋值给 the_arg.pa_double

        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);
#if __HAVE_FLOAT128_UNLIKE_LDBL# define PARSE_FLOAT_VA_ARG_EXTENDED(INFO) \ do \ { \ if (is_long_double \ && (mode_flags & PRINTF_LDBL_USES_FLOAT128) != 0) \ { \ INFO.is_binary128 = 1; \ the_arg.pa_float128 = va_arg (ap, _Float128); \ } \ else \ { \ PARSE_FLOAT_VA_ARG (INFO); \ } \ } \ while (0)#else# define PARSE_FLOAT_VA_ARG_EXTENDED(INFO) \ PARSE_FLOAT_VA_ARG (INFO);#endif
#define PARSE_FLOAT_VA_ARG(INFO) \ do \ { \ INFO.is_binary128 = 0; \ if (is_long_double) \ the_arg.pa_long_double = va_arg (ap, long double); \ else \ the_arg.pa_double = va_arg (ap, double); \ } \ while (0)
复制代码

(4).SETUP_FLOAT128_INFO---设置 is_binary128 flag 信息

  • 入参:

    struct printf_info 结构体

主要的逻辑判断如下:

  1. 通过 mode_flags 的置位情况(是否置位 PRINTF_LDBL_USES_FLOAT128---0x0008)决定当前是否将 is_binary128 置为 is_long_double;

  2. 其他情况统一置 0

        SETUP_FLOAT128_INFO (specs[nspecs_done].info);        #if __HAVE_FLOAT128_UNLIKE_LDBL# define SETUP_FLOAT128_INFO(INFO)                        \  do                                          \    {                                         \      if ((mode_flags & PRINTF_LDBL_USES_FLOAT128) != 0)              \    INFO.is_binary128 = is_long_double;                   \      else                                    \    INFO.is_binary128 = 0;                            \    }                                         \  while (0)#else# define SETUP_FLOAT128_INFO(INFO)                        \  do                                          \    {                                         \      INFO.is_binary128 = 0;                              \    }                                         \  while (0)#endif
复制代码

(5).done_add_func---给最后输出的累加字符数做加法

  • 入参

    length:本次打印输出的字符长度

    done:已有的输出字符长度

  • 出参:累加之后的结果

从这个函数可以很明显地看出来 Glibc 函数的处理规范(做好参数判断和处理):

  1. 首先判断 done 是否小于 0,如果小于 0,说明是个异常值,直接返回,不做任何处理;

  2. 调用 INT_ADD_WRAPV 完成加法操作(注意如果溢出返回 1),如果溢出,那么将错误码置为 EOVERFLOW,返回-1,如果加法操作正常,则正常返回计算结果 ret

  /* Place to accumulate the result.  */  int done;    return done_add_func (length, done);    /* Add LENGTH to DONE.  Return the new value of DONE, or -1 on   overflow (and set errno accordingly).  */static inline intdone_add_func (size_t length, int done)                                                                                                                  {  if (done < 0)    return done;  int ret;  if (INT_ADD_WRAPV (done, length, &ret))    {      __set_errno (EOVERFLOW);      return -1;    }  return ret;}
// glibc/include/intprops.h/* Store the low-order bits of A + B, A - B, A * B, respectively, into *R. Return 1 if the result overflows. See above for restrictions. */#if _GL_HAS_BUILTIN_ADD_OVERFLOW# define INT_ADD_WRAPV(a, b, r) __builtin_add_overflow (a, b, r)# define INT_SUBTRACT_WRAPV(a, b, r) __builtin_sub_overflow (a, b, r)#else# define INT_ADD_WRAPV(a, b, r) \ _GL_INT_OP_WRAPV (a, b, r, +, _GL_INT_ADD_RANGE_OVERFLOW)# define INT_SUBTRACT_WRAPV(a, b, r) \ _GL_INT_OP_WRAPV (a, b, r, -, _GL_INT_SUBTRACT_RANGE_OVERFLOW)#endif
复制代码

(6).done_add---给最后输出的累加字符数做加法

  • 入参:

    val:一个 int 类型的数据,与上一个中的 length 变量类似

这个宏展开实际上是调用了 done_add_func 完成的,但是在这个函数里面也做了重要的事情:

  1. 参数 val 的类型检测:

    a. 首先检测 val 的 size 大小是否和 int 类型一致,不一致则 assert 报错;

    b. 其次调用__typeof__获取 val 的类型,并使用该类型强转-1(补码表示为 0xFFFFFFFF),如果是无符号类型,则没有符号位,0xFFFFFFFF 则是表示最大的正整数,不可能大于 0,所以这里要求 val 必须是有符号数,否则 assert 报错;

  2. 调用 done_add_func 进行加法运算,检查返回值 done,上面说如果 done 本来是负数,或者出现溢出,done_add_func 都会返回负数,那么这里就会直接走到 all_done 标号处,解锁,释放资源,结束函数

       int function_done = printf_unknown (s, &specs[nspecs_done].info);       ...       done_add (function_done);
#define done_add(val) \ do \ { \ /* Ensure that VAL has a type similar to int. */ \ _Static_assert (sizeof (val) == sizeof (int), "value int size"); \ _Static_assert ((__typeof__ (val)) -1 < 0, "value signed"); \ done = done_add_func ((val), done); \ if (done < 0) \ goto all_done; \ } \ while (0) all_done: /* Unlock the stream. */ _IO_funlockfile (s); _IO_cleanup_region_end (0);
return done;
复制代码

后续函数作用分析见后文。

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

桑榆

关注

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

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

评论

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