SQLite 的内置 printf()

、概述

SQLite 包含它自己的字符串格式化例程“printf()”的实现,可通过以下接口访问:

SQLite 内部也使用相同的核心字符串格式化程序。

1.1. 优点

为什么 SQLite 有自己私有的内置 printf() 实现?为什么不使用标准 C 库中的 printf() 实现?几个原因:

  1. 通过使用自己的内置实现,SQLite 保证输出在所有平台和所有 LOCALE 中都是相同的。这对于一致性和测试很重要。如果一台机器给出“5.25e+08”的答案而另一台机器给出“5.250e+008”的答案,那就有问题了。两个答案都是正确的,但最好是 SQLite 总是给出相同的答案。

  2. 我们知道没有办法使用标准库 printf() C 接口来实现 SQLite 的format() SQL 函数特性。然而,内置的 printf() 实现可以很容易地适应该任务。

  3. SQLite 中的 printf() 支持新的非标准替换类型(%q%Q%w%z),以及增强的替换行为( %s 和%z),它们对 SQLite 内部和使用SQLite。标准库 printf() 通常不能以这种方式扩展。

  4. 通过sqlite3_mprintf()sqlite3_vmprintf()接口,内置的 printf() 实现支持将任意长度的字符串渲染到从sqlite3_malloc64()获得的内存缓冲区中的能力。与尝试预先计算结果字符串的大小上限、分配适当大小的缓冲区然后调用 snprintf() 相比,这更安全且更不容易出错。

  5. SQLite 特定的 printf() 支持一个名为“alternate-form-2”标志的新标志 (!)。alternate-form-2 标志以微妙的方式改变了浮点转换的处理,因此输出始终是浮点数的 SQL 兼容文本表示 - 这是标准库 printf( ). 对于字符串替换,alternate-form-2 标志导致宽度和精度以字符而不是字节为单位进行测量,这简化了包含多字节 UTF8 字符的字符串的处理。

  6. 内置的 SQLite 具有诸如 SQLITE_PRINTF_PRECISION_LIMIT 之类的编译时选项,可以为将 printf() 功能暴露给不受信任的用户的应用程序提供针对拒绝服务攻击的防御。

  7. 使用内置的 printf() 实现意味着 SQLite 对主机环境的依赖性更少,使其更具可移植性。

1.2. 缺点

公平地说,内置的 printf() 实现也有一些缺点。以机智:

  1. 内置的 printf() 实现使用额外的代码空间(在带有 -Os 的 GCC 5.4 上大约 7800 字节)。

  2. 内置 printf() 的浮点到文本转换子函数的精度限制为 16 位有效数字或 26 位有效数字(如果“!”)使用 alternate-form-2 标志。每个 IEEE-754 双精度数都可以精确地表示为十进制浮点值,但有些双精度数需要超过 16 或 26 位有效数字。

  3. 内置 snprintf() 实现中缓冲区指针和缓冲区大小参数的顺序与标准库实现中使用的顺序相反。

  4. 内置的 printf() 实现不处理 posix 位置引用修饰符,这些修饰符允许 printf() 的参数顺序不同于 %-替换的顺序。在内置的 printf() 中,参数的顺序必须与 % 替换的顺序完全匹配。

尽管有这些缺点,开发人员认为在 SQLite 中内置 printf() 实现是一个净积极因素。

2.格式化细节

printf() 的格式字符串是生成字符串的模板。只要格式字符串中出现“%”字符,就会进行替换。“%”后跟一个或多个描述替换的附加字符。每个替换都具有以下格式:

% [标志][宽度][ . 精度][长度]类型

所有替换都以单个“%”开头并以单个类型字符结束。替换的其他元素是可选的。

要在输出中包含单个“%”字符,请在模板中放置两个连续的“%”字符。

2.1. 替代类型

下图显示了 SQLite 支持的替换类型:

Substitution TypeMeaning
% Two "%" characters in a row are translated into a single "%" in the output, without substituting any values.
d, i The argument is a signed integer which is displayed in decimal.
u The argument is an unsigned integer which is displayed in decimal.
f The argument is a double which is displayed in decimal.
e, E The argument is a double which is displayed in exponential notation. The exponent character is 'e' or 'E' depending on the type.
g, G The argument is a double which is displayed in either normal decimal notation or if the exponent is not close to zero, in exponential notation.
x, X The argument is an integer which is displayed in hexadecimal. Lower-case hexadecimal is used for %x and upper-case is used for %X
o The argument is an integer which is displayed in octal.
s, z The argument is either a zero-terminated string that is displayed, or a null pointer which is treated as an empty string. For the %z type in C-language interfaces, sqlite3_free() is invoked on the string after it has been copied into the output. The %s and %z substitutions are identical for the SQL printf() function, with a NULL argument treated as an empty string.

The %s substitution is universal among printf functions, but the %z substitution and safe treatment of null pointers are SQLite enhancements, not found in other printf() implementations.
c For the C-language interfaces, the argument is an integer which is interpreted as a character. For the format() SQL function the argument is a string from which the first character is extracted and displayed.
p The argument is a pointer which is displayed as a hexadecimal address. Since the SQL language has no concept of a pointer, the %p substitution for the format() SQL function works like %x.
n The argument is a pointer to an integer. Nothing is displayed for this substitution type. Instead, the integer to which the argument points is overwritten with the number of characters in the generated string that result from all format symbols to the left of the %n.
q, Q The argument is a zero-terminated string. The string is printed with all single quote (') characters doubled so that the string can safely appear inside an SQL string literal. The %Q substitution type also puts single-quotes on both ends of the substituted string.

If the argument to %Q is a null pointer then the output is an unquoted "NULL". In other words, a null pointer generates an SQL NULL, and a non-null pointer generates a valid SQL string literal. If the argument to %q is a null pointer then no output is generated. Thus a null-pointer to %q is the same as an empty string.

For these substitutions, the precision is the number of bytes or characters taken from the argument, not the number of bytes or characters that are written into the output.

The %q and %Q substitutions are SQLite enhancements, not found in most other printf() implementations.
w This substitution works like %q except that it doubles all double-quote characters (") instead of single-quotes, making the result suitable for using with a double-quoted identifier name in an SQL statement.

The %w substitution is an SQLite enhancements, not found in most other printf() implementations.

2.2. 可选长度字段

参数值的长度可以由一个或多个出现在替换类型字母之前的字母指定。在 SQLite 中,长度只对整数类型有影响。对于始终使用 64 位值的format() SQL 函数,忽略长度 。下表显示了 SQLite 允许的长度说明符:

Length Specifier Meaning
(default) An "int" or "unsigned int". 32-bits on all modern systems.
l A "long int" or "long unsigned int". Also 32-bits on all modern systems.
ll A "long long int" or "long long unsigned" or an "sqlite3_int64" or "sqlite3_uint64" value. These are 64-bit integers on all modern systems.

只有“ll”长度修饰符对 SQLite 有影响。只有在使用 C 语言接口时才会有所不同。

2.3. 可选宽度字段

宽度字段指定输出中替换值的最小宽度。如果写入输出的字符串或数字比宽度短,则会填充该值。默认情况下,填充位于左侧(值右对齐)。如果使用“-”标志,则填充在右边并且值是左对齐的。

默认情况下,宽度以字节为单位。但是,如果“!” 标志存在则宽度以字符为单位。这只会对多字节 utf-8 字符产生影响,并且这些字符只会出现在字符串替换中。

如果宽度是单个“*”字符而不是数字,则实际宽度值将从参数列表中读取为整数。如果读取的值为负数,则绝对值用于宽度,并且该值左对齐,就好像存在“-”标志一样。

如果被替换的值大于宽度,则将完整值添加到输出。换句话说,宽度是输出中呈现的值的最小宽度。

2.4. 可选精度字段

精度字段(如果存在)必须跟在由单个“.”分隔的宽度之后。特点。如果没有宽度,则为“.” 引入精度的紧跟在标志(如果有的话)或初始“%”之后。

对于字符串替换(%s、%z、%q、%Q 或 %w),精度是参数中使用的字节数或字符数。默认情况下,数字是字节,但如果是“!”,则为字符。旗帜存在。如果没有精度,则替换整个字符串。示例:“%.3s”替换参数字符串的前 3 个字节。"%!.3s" 替换参数字符串的前三个字符。

对于整数替换(%d、%i、%x、%X、%o 和 %p),精度指定要显示的最小位数。如有必要,将添加前导零,以将输出扩展到最小位数。

对于浮点替换(%e、%E、%f、%g、%G),精度指定要显示在小数点右侧的位数。

对于字符替换 (%c),精度 N 大于 1 会导致字符重复 N 次。这是仅在 SQLite 中发现的非标准扩展。

如果精度是单个“*”字符而不是数字,则实际精度值将从参数列表中读取为整数。

2.5. 选项标志字段

标志由零个或多个字符组成,这些字符紧跟在引入替换的“%”之后。各种标志及其含义如下:

Flag Meaning
- Left-justify the value in the output. The default is to right-justify. If the width is zero or is otherwise less than the length of the value being substituted, then there is no padding and the "-" flag is a no-op.
+ For signed numeric substitutions, include a "+" sign before positive numbers. A "-" sign always appears before negative numbers regardless of flag settings.
(space) For signed numeric substitutions, prepend a single space before positive numbers.
0 Prepend as many "0" characters to numeric substitutions as necessary to expand the value out to the specified width. If the width field is omitted, then this flag is a no-op.
# This is the "alternate-form-1" flag. For %g and %G substitutions, this causes trailing zeros to be removed. This flag forces a decimal point to appear for all floating-point substitutions. For %o, %x, and %X substitutions, the alternate-form-1 flag cause the value to be prepended with "0", "0x", or "0X", respectively.
, This flag causes comma-separators to be added to the output of %d and %i substitutions, between every 3 digits from the left. This can help humans to more easily discern the magnitude of large integer values. For example, the value 2147483647 would be rendered as "2147483647" using "%d" but would appear as "2,147,483,647" with "%,d". This flag is a non-standard extension.
! This is the "alternate-form-2 flag. For string substitutions, this flag causes the width and precision to be understand in terms of characters rather than bytes. For floating point substitutions, the alternate-form-2 flag increases the maximum number of significant digits displayed from 16 to 26, forces the display of the decimal point and causes at least one digit to appear after the decimal point.

The alternate-form-2 flag is a non-standard extension that appears in no other printf() implementations, as far as we know.

3.实施和历史

核心字符串格式化例程是 printf.c源文件中的 sqlite3VXPrintf() 函数。所有不同的接口都会调用(有时是间接调用)这一核心功能。sqlite3VXPrintf() 函数最初是由 SQLite 的第一作者 ( Hipp ) 在 1980 年代后期在杜克大学读研究生时编写的代码。在 2000 年开始使用 SQLite 之前,Hipp 一直将此 printf() 实现保留在他的个人工具箱中。该代码于 2000 年 10 月 8 日合并到 SQLite 1.0.9 版的 SQLite 源代码树

Fossil 版本控制系统使用自己的 printf() 实现,该实现派生自早期版本的 SQLite printf() 实现,但是这两个实现后来发生了分歧

sqlite3_snprintf()函数 的缓冲区指针和缓冲区大小参数与标准 C 库 snprintf() 例程中的相反。这是因为当 Hipp 第一次实现他的版本时,标准 C 库中没有 snprintf() 例程,并且他选择了与标准 C 库的设计者不同的顺序。