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

101 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

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

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

函数逻辑分析---vfprintf

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

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

  • 入参:

    • s:vfprintf的入参stream信息
    • format:vfprintf的入参format信息

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

  1. 先调用CHECK_FILE进行参数检查,主要是保证stream指针不为NULL,而且_flags在有效范围内,不满足条件则返回-1(EOF);
  1. 通过_IO_NO_WRITES位检查stream是否可写,如果不可写,那说明本次操作是非法的,将_IO_ERR_SEEN置位,并将错误码置为EBADF(Bad file descriptor)返回-1(EOF);
  1. 判断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)))

  1. 否则调用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;
  1. 其他情况统一置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,说明是个异常值,直接返回,不做任何处理;
  1. 调用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 int
done_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报错;

  1. 调用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;

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