小巧。快速。可靠。
三者选其二。
SQLite 内置 printf()

1. 概述

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

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

1.1. 优势

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

  1. 通过使用自己的内置实现,SQLite 保证输出在所有平台和所有区域设置中都相同。这对一致性和测试非常重要。如果一台机器给出了“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 位有效数字,如果使用“!”alternate-form-2 标志,则精度限制为 26 位有效数字。每个 IEEE-754 双精度浮点数都可以精确地表示为十进制值,但对于许多双精度浮点数,精确的十进制表示需要超过 16 或 26 位有效数字。SQLite printf() 函数仅呈现前 16 或 26 位有效数字,因为这可以有效地完成,并且因为 16 位十进制数字足以区分所有可能的双精度浮点数值。对于在极少数情况下需要确切的双精度浮点数值的十进制等效值的,请使用 十进制扩展

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

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

尽管有这些缺点,但开发人员认为,在 SQLite 内部拥有内置的 printf() 实现是一个净收益。

2. 格式化细节

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

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

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

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

2.1. 替换类型

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

替换类型含义
% 格式字符串中的两个连续“%”字符会被翻译成输出中的一个“%”,无需替换任何值。
d, i参数是一个有符号整数,以十进制显示。
u参数是一个无符号整数,以十进制显示。
f参数是一个双精度浮点数,以十进制显示。
e, E参数是一个双精度浮点数,以指数表示法显示。指数字符根据类型为“e”或“E”。
g, G参数是一个双精度浮点数,以普通十进制表示法显示,如果指数不接近零,则以指数表示法显示。
x, X参数是一个整数,以十六进制显示。%x 使用小写十六进制,%X 使用大写十六进制。
o参数是一个整数,以八进制显示。
s, z 参数是一个以零结尾的字符串,该字符串将被显示,或者是一个空指针,该指针将被视为一个空字符串。对于 C 语言接口中的 %z 类型,在将字符串复制到输出后,将对字符串调用 sqlite3_free()。对于 SQL printf() 函数,%s 和 %z 替换是相同的,空指针被视为一个空字符串。

%s 替换在所有 printf 函数中都是通用的,但 %z 替换和对空指针的安全处理是 SQLite 的增强功能,在其他 printf() 实现中没有找到。
c对于 C 语言接口,参数是一个整数,该整数被解释为一个字符。对于 format() SQL 函数,参数是一个字符串,从中提取第一个字符并显示。
p参数是一个指针,以十六进制地址显示。由于 SQL 语言没有指针的概念,因此对于 format() SQL 函数,%p 替换与 %x 一样。
n参数是一个指向整数的指针。此替换类型不显示任何内容。相反,参数指向的整数将被覆盖为从所有 %-替换左侧生成的字符串中的字符数。
q, Q 参数是一个以零结尾的字符串。该字符串将打印所有单引号 (') 字符加倍,以便该字符串可以安全地出现在 SQL 字符串文字中。%Q 替换类型还在替换字符串的两端添加单引号。

如果 %Q 的参数为空指针,则输出将是未引用的“NULL”。换句话说,空指针会生成一个 SQL NULL,非空指针会生成一个有效的 SQL 字符串文字。如果 %q 的参数为空指针,则不会生成任何输出。因此,指向 %q 的空指针与空字符串相同。

对于这些替换,精度是指从参数中获取的字节或字符数,而不是写入输出的字节或字符数。

%q 和 %Q 替换是 SQLite 的增强功能,在大多数其他 printf() 实现中没有找到。
w 此替换与 %q 相似,只是它将所有双引号字符 (") 加倍而不是单引号,使其适用于在 SQL 语句中使用双引号标识符名称。

%w 替换是 SQLite 的增强功能,在大多数其他 printf() 实现中没有找到。

2.2. 可选长度字段

参数值的长度可以通过一个或多个字母指定,这些字母紧接在替换类型字母之前。在 SQLite 中,长度仅对整数类型有效。对于 format() SQL 函数,长度会被忽略,该函数始终使用 64 位值。下表显示了 SQLite 允许的长度说明符:

长度说明符含义
(默认) 一个“int”或“unsigned int”。在所有现代系统上都是 32 位。
l一个“long int”或“long unsigned int”。在所有现代系统上也是 32 位。
ll一个“long long int”或“long long unsigned”或一个“sqlite3_int64”或“sqlite3_uint64”值。在所有现代系统上都是 64 位整数。

只有“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,精度是有效数字的总数,如果指定的精度为 0,则四舍五入到 1。

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

如果精度是一个单独的 "*" 字符而不是一个数字,那么实际精度值将从参数列表中读取为整数。

2.5. 选项标志字段

标志由零个或多个紧接在引入替换的 "%" 之后的字符组成。各种标志及其含义如下

标志含义
- 在输出中将值左对齐。默认情况下为右对齐。如果宽度为零或小于被替换值的长度,则不进行填充,并且 "-" 标志将无效。
+ 对于带符号的数字替换,在正数之前包含一个 "+" 号。无论标志设置如何,负数之前始终会出现 "-" 号。
(空格) 对于带符号的数字替换,在正数之前添加一个空格。
0 (零填充选项) 在数字替换之前添加尽可能多的 "0" 字符,以将值扩展到指定的宽度。如果省略了宽度字段,则此标志将无效。无穷大和 NaN(非数字)浮点数通常分别呈现为 "Inf" 和 "NaN",但在启用零填充选项时,它们将呈现为 "9.0e+999" 和 "null"。换句话说,在启用零填充选项的情况下,浮点数无穷大和 NaN 将呈现为有效的 SQL 和 JSON 字面量。
# 这是 "alternate-form-1" 标志。对于 %g 和 %G 替换,这会导致删除尾随零。此标志强制所有浮点数替换出现小数点。对于 %o、%x 和 %X 替换,alternate-form-1 标志会导致在值之前分别添加 "0"、"0x" 或 "0X"。
, 逗号选项会导致在数字替换(%d、%f 和类似)的输出中添加逗号分隔符,在小数点左侧每 3 位数字之前添加一个逗号。小数点右侧的数字不会添加逗号。这可以帮助人们更轻松地识别大型整数的值的大小。例如,值 2147483647 将使用 "%d" 呈现为 "2147483647",但使用 "%,d" 将显示为 "2,147,483,647"。此标志是一个非标准扩展。
! 这是 "alternate-form-2" 标志。对于字符串替换,此标志会导致宽度和精度以字符而不是字节为单位理解。对于浮点数替换,alternate-form-2 标志将显示的有效数字的最大数量从 16 增加到 26,强制显示小数点,并导致小数点后至少出现一位数字。

alternate-form-2 标志是一个非标准扩展,据我们所知,它没有出现在任何其他 printf() 实现中。

3. 实现和历史

核心字符串格式化例程是 printf.c 源文件中的 sqlite3VXPrintf() 函数。所有不同的接口(有时是间接地)都调用此一个核心函数。sqlite3VXPrintf() 函数起源于 SQLite 首位作者 (Hipp) 在 20 世纪 80 年代末杜克大学读研究生时编写的代码。Hipp 将此 printf() 实现保留在他的个人工具箱中,直到 2000 年开始开发 SQLite。该代码于 2000-10-08 为 SQLite 版本 1.0.9 合并到 SQLite 源代码树中。

Fossil 版本控制系统 使用它自己的 printf() 实现,该实现源自 SQLite printf() 实现的早期版本,但这两个实现此后已经分道扬镳。

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