Strings

PHP 不使用普通的 char * 指针,而是使用自定义的 zend_string 类型来表示字符串。本章讨论如何使用此结构以及各种与字符串相关的实用程序。

zend_string API

C 中的字符串通常表示为以空字符结尾的 char * 指针。由于 PHP 支持包含null 字节的字符串,因此 PHP 需要明确存储字符串的长度。此外,PHP 需要将字符串纳入其引用计数结构的通用框架中。这就是 zend_string 类型的用途。

结构

zend_string 具有以下结构:

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong        h;
    size_t            len;
    char              val[1];
};

与 PHP 中的许多其他结构一样,它嵌入了一个 zend_refcounted_h 标头,用于存储 引用计数 以及一些标志。

字符串的实际字符内容使用所谓的 "struct hack" 存储:字符串内容附加到结构的末尾。虽然它被声明为 char[1],但实际大小是动态确定的。这意味着 zend_string 标头和字符串内容被组合成一个内存分配,这比使用两个单独的内存分配更有效。您会发现 PHP 在很多地方都使用了 struct hack,其中固定大小的标头与动态数量的数据相结合。

字符串的长度明确存储在 len 成员中。这对于支持包含空字节的字符串是必要的,并且也有利于提高性能,因为不需要不断重新计算字符串长度。应该注意的是,虽然 len 存储的长度没有尾随空字节,但 val 中的实际字符串内容必须始终包含尾随空字节。原因是有相当多的 C API 接受以空字符结尾的字符串,我们希望能够使用这些 API,而无需创建单独的以空字符结尾的字符串副本。举个例子,PHP 字符串 "foo\0bar" 将以 len = 7 存储,但 val = "foo\0bar\0"

最后,字符串存储哈希值 h 的缓存,当使用字符串作为哈希表键时会使用该缓存。它以值 0 开头,表示哈希尚未计算,而真正的哈希是在第一次使用时计算的。

字符串访问器

就像 zvals 一样,你不需要手动操作 zend_string 字段,而是使用一些访问宏:

zend_string *str = zend_string_init("foo", strlen("foo"), 0);
php_printf("This is my string: %s\n", ZSTR_VAL(str));
php_printf("It is %zd char long\n", ZSTR_LEN(str)); // %zd is the printf format for size_t
zend_string_release(str);

其中最重要的两个是 ZSTR_VAL(),它将字符串内容返回为 char *,以及 ZSTR_LEN(),它将字符串长度返回为 size_t

这些宏的命名有点不幸,因为 ZSTR_VAL/ZSTR_LEN 以及 Z_STRVAL/Z_STRLEN 都存在,并且两者仅在下划线的位置上有所不同。请记住,ZSTR_* 宏适用于 zend_string,而 Z_ 宏适用于 zval

zval val;
ZVAL_STRING(&val, "foo");

// Z_STRLEN, Z_STRVAL work on zval.
php_printf("string(%zd) \"%s\"\n", Z_STRLEN(val), Z_STRVAL(val));

// ZSTR_LEN, ZSTR_VAL work on zend_string.
zend_string *str = Z_STR(val);
php_printf("string(%zd) \"%s\"\n", ZSTR_LEN(str), ZSTR_VAL(str));

zval_ptr_dtor(&val);

可以使用 ZSTR_H() 访问字符串的哈希值缓存。但是,这会访问原始缓存,如果尚未计算哈希,则该缓存将为零。相反,应该使用 ZSTR_HASH()zend_string_hash_val() 来获取预缓存的哈希,或计算它。在极少数情况下,字符串在初始构造后被修改,可以使用 zend_string_forget_hash_val() 丢弃缓存值。

内存管理

虽然我们已经知道如何初始化字符串 zval,但迄今为止引入的唯一直接字符串创建 API 是 zend_string_init(),它用于根据现有字符串和长度创建 zend_string

所有其他字符串创建函数都基于最基本的字符串创建函数,即 zend_string_alloc()

size_t len = 40;
zend_string *str = zend_string_alloc(len, /* persistent */ 0);
for (size_t i = 0; i < len; i++) {
    ZSTR_VAL(str)[i] = 'a';
}
// Don't forget to null-terminate!
ZSTR_VAL(str)[len] = '\0';

此函数分配一个特定长度的字符串(与往常一样,长度不包括尾随的空字节),并将其初始化留给您。与所有字符串分配函数一样,它接受一个参数,该参数确定是使用每个请求分配器还是持久分配器。

zend_string_safe_alloc(n, m, l, persistent) 函数分配一个长度为 n * m + l 的字符串。此函数通常用于编码更改。例如,我们可以这样对字符串进行十六进制编码:

zend_string *convert_to_hex(zend_string *orig_str) {
    zend_string *hex_str = zend_string_safe_alloc(2, ZSTR_LEN(orig_str), 0, /* persistent */ 0);
    char *p = ZSTR_VAL(hex_str);
    for (size_t i = 0; i < ZSTR_LEN(orig_str), i++) {
        const char *to_hex = "0123456789abcdef";
        unsigned char c = ZSTR_VAL(orig_str)[i];
        *p++ = to_hex[c >> 4];
        *p++ = to_hex[c & 0xf];
    }
    *p = '\0';
    return hex_str;
}

为什么我们不能简单地使用 zend_string_alloc(2 * ZSTR_LEN(orig_str), 0) 呢?原因是 zend_string_safe_alloc() 函数将确保 n * m + l 计算不会溢出。例如,如果您使用的是 32 位系统,并且字符串正好是 2GB,那么将长度乘以二将溢出并导致长度为零。以下代码将超出分配的界限并损坏不相关的内存。zend_string_safe_alloc() API 会检测到这种情况并在这种情况下抛出致命错误。

还可以使用 zend_string_realloc() 及其变体更改字符串的大小:

zend_string *zend_string_realloc(zend_string *s, size_t len, bool persistent);
// Requires new length larger old length.
zend_string *zend_string_extend(zend_string *s, size_t len, bool persistent);
// Requires new length smaller new length.
zend_string *zend_string_truncate(zend_string *s, size_t len, bool persistent)
// n * m + l safe variant of zend_string_realloc.
zend_string *zend_string_safe_realloc(zend_string *s, size_t n, size_t m, size_t l, bool persistent);

由于字符串是引用计数结构,因此 realloc 函数也会考虑引用计数。虽然这不是这些函数的实现方式,但它们的语义相当于执行如下操作:

zend_string *new_str = zend_string_init(ZSTR_VAL(s), ZSTR_LEN(s), persistent);
zend_string_release(s);
return new_str;

也就是说,这些函数会释放传递给它们的字符串,但将它们与共享(或不可变)字符串一起使用是安全的。如果字符串是共享的,则引用计数会减少,但字符串不会被销毁。

这也引出了下一个主题:引用计数管理。zend_string API 包含两个帮助程序来增加引用计数,而不是使用原始 GC_* 宏:

zend_string_addref(str);
return str;

// More compact:
return zend_string_copy(str);

GC_ADDREF() 不同,zend_string_addref() 函数将正确处理不可变字符串。但是,迄今为止最常用的函数是 zend_string_copy()。此函数不仅会增加引用计数,还会返回原始字符串。这在实践中使代码更具可读性。

虽然也存在执行字符串实际复制(而不仅仅是引用计数增加)的 zend_string_dup() 函数,但这种行为通常被认为令人困惑,因为它只复制非不可变字符串。如果您想强制复制字符串,最好使用 zend_string_init() 创建一个新字符串。

如果复制是为了修改已经存在的字符串,则可以改用 zend_string_separate()

zend_string *modify_char(zend_string *orig_str) {
    zend_string *str = zend_string_separate(orig_str, /* persistent */ 0);
    ZEND_ASSERT(ZSTR_LEN(str) > 0);
    ZSTR_VAL(str)[0] = 'A';
    return str;
}

就像一般的 zval 分离概念一样,如果字符串的引用计数为 1,则返回原始字符串(带有丢弃的哈希缓存),因此该字符串是唯一拥有的,否则将创建一个副本。

最后,字符串在不再使用时需要释放。您已经熟悉 zend_string_release() API,它将减少引用计数,并在引用计数降至零时释放字符串。仅使用此函数即可为您提供良好的服务。

但是,您还可能会遇到许多优化变体。最常见的是 zend_string_release_ex(),它允许您指定传递的字符串是持久的还是非持久的:

zend_string_release_ex(str, /* persistent */ 0);

通常,这将根据字符串标志来确定。这样可以避免运行时检查,并生成更少的代码。最后,还有两个函数仅适用于引用计数为 1 的字符串:

// Requires refcount 1 or immutable.
zend_string_free(str);
// Requires refcount 1 and not immutable.
zend_string_efree(str);

您应该避免使用这些函数,因为当某些 API 从返回新字符串更改为重用现有字符串时,很容易引入严重的错误。

其他操作

zend_string API 支持一些附加操作。最常见的是比较字符串:

zend_string *foo = zend_string_init("foo", sizeof("foo")-1, 0);
zend_string *FOO = zend_string_init("FOO", sizeof("FOO")-1, 0);

// Case-sensitive comparison between zend_strings.
bool result = zend_string_equals(foo, FOO); // false
// Case-insensitive comparison between zend_strings.
bool result = zend_string_equals_ci(foo, FOO); // true

// Case-sensitive comparison with a string literal.
bool result = zend_string_equals_literal(foo, "FOO"); // false
// Case-insensitive comparison with a string literal.
bool result = zend_string_equals_literal_ci(foo, "FOO"); // false

zend_string_release(foo);
zend_string_release(FOO);

还有一些辅助函数可以连接两个或三个字符串。如果需要连接更多字符串,则应使用下一章中讨论的 smart_str API。

zend_string *foo = zend_string_init("foo", sizeof("foo")-1, 0);
zend_string *bar = zend_string_init("bar", sizeof("bar")-1, 0);

// Creates "foobar"
zend_string *foobar = zend_string_concat2(
    ZSTR_VAL(foo), ZSTR_LEN(foo),
    ZSTR_VAL(bar), ZSTR_LEN(bar));
// Creates "foo::bar"
zend_string *foo_bar = zend_string_concat3(
    ZSTR_VAL(foo), ZSTR_LEN(foo),
    "::", sizeof("::")-1,
    ZSTR_VAL(bar), ZSTR_LEN(bar));

zend_string_release(foo);
zend_string_release(bar);
zend_string_release(foobar);
zend_string_release(foo_bar);

如您所见,这些 API 接受 char * 和长度的对,而不是 zend_string 结构。这允许使用字符串文字提供连接的各个部分,而无需为它们分配 zend_string

最后,zend_string_tolower() API 可用于将字符串转换为小写:

zend_string *FOO = zend_string_init("FOO", sizeof("FOO")-1, 0);
zend_string *foo = zend_string_tolower(FOO);
zend_string_release(foo);
zend_string_release(FOO);

驻留字符串

在 PHP 的 Zend 引擎中,Interned Strings(驻留字符串) 是一种优化技术,用于减少内存使用和提高字符串操作的性能。驻留字符串的基本思想是,字符串在内存中只存储一次,并在整个程序中共享该存储。

Interned Strings 的作用

  1. 减少内存使用:

    • 当相同的字符串在程序中多次出现时,只存储一个实例。这意味着相同内容的多个字符串将共享同一段内存,而不是为每个实例分配独立的内存。

    • 例如,如果字符串 "example" 在程序中出现了 100 次,使用驻留字符串技术,这个字符串只会在内存中存储一次,从而大大减少内存消耗。

  2. 提高字符串比较性能:

    • 字符串比较操作可以通过比较内存地址来完成,而不需要逐字符比较内容。这是因为驻留字符串保证了相同内容的字符串在内存中只有一个实例,所以比较两个驻留字符串的内容实际上是比较它们的指针。

  3. 提高字符串操作的效率:

    • 由于驻留字符串在内存中是唯一的,因此可以更快地进行查找和操作。比如在哈希表中查找字符串键时,可以通过比较内存地址来快速确定字符串是否相同。

这里简单说一下 驻留字符串。在扩展开发中,您可能需要这样的概念。驻留字符串也与 opcache 扩展交互。

驻留字符串(Interned strings)是去重的字符串。当与 opcache 一起使用时,它们在每次请求中也会被重复使用。

假设您想创建字符串 “foo”。您倾向于做的只是创建一个新的字符串 “foo”:

zend_string *foo;
foo = zend_string_init("foo", strlen("foo"), 0);

/* ... */

但问题来了:那段字符串在你需要它之前是否已经被创建了?当你需要一个字符串时,你的代码会在 PHP 的某个生命周期内被执行,这意味着在你的代码之前发生的一些代码可能已经需要了完全相同的那段字符串(以 “foo” 为例)。

驻留字符串是指要求引擎探测驻留字符串存储,如果能找到你的字符串,则重用已分配的指针。如果找不到,则创建一个新字符串并 “驻留” 它,即使其可供 PHP 源代码的其他部分(其他扩展、引擎本身等)使用。

以下是示例:

zend_string *foo;
foo = zend_string_init("foo", strlen("foo"), 0);

foo = zend_new_interned_string(foo);

php_printf("This string is interned : %s", ZSTR_VAL(foo));

zend_string_release(foo);

在上面的代码中,我们所做的是创建一个非常经典的新 zend_string。然后,我们将创建的 zend_string 传递给 zend_new_interned_string()。此函数在引擎驻留字符串缓冲区中查找相同的字符串片段(此处为 “foo”)。如果找到它(意味着有人已经创建了这样的字符串),它会释放您的字符串(可能释放它)并将其替换为驻留字符串缓冲区中的字符串。如果找不到它:它会将其添加到驻留字符串缓冲区,以便将来使用或 PHP 的其他部分。

您必须注意内存分配。驻留字符串的引用计数始终设置为 1,因为它们不需要被引用计数,因为它们将与驻留字符串缓冲区共享,因此它们不能被销毁。

示例:

zend_string *foo, *foo2;

foo  = zend_string_init("foo", strlen("foo"), 0);
foo2 = zend_string_copy(foo); /* increments refcount of foo */

 /* foo points to the interned string buffer, and refcount
  * in original zend_string falls back to 1 */
foo = zend_new_interned_string(foo);

/* This doesn't do anything, as foo is interned */
zend_string_release(foo);

/* The original buffer referenced by foo2 is released */
zend_string_release(foo2);

/* At the end of the process, PHP will purge its interned
  string buffer, and thus free() our "foo" string itself */

这全是关于垃圾收集的。

当字符串被驻留时,无论它们使用什么内存分配类(永久或基于请求),其 GC 标志都会更改为添加 IS_STR_INTERNED 标志。当您想要复制或释放字符串时,会探测此标志。如果字符串被驻留,则引擎不会在您复制字符串时增加其引用计数。但是,如果您释放字符串,它不会减少它或释放它。它暗中什么也不做。在进程生命周期结束时,它将销毁其驻留字符串缓冲区,并释放您的驻留字符串。

这个过程实际上比这稍微复杂一点。如果您在 请求处理 之外使用驻留字符串,该字符串肯定会被驻留。但是,如果您在 PHP 处理请求时使用驻留字符串,那么这个字符串只会在当前请求中被驻留,之后将被清除。在不使用 opcache 扩展时,所有这些都是有效的。

使用 opcache 扩展时,如果您在 请求处理 之外使用了一个驻留字符串,那么该字符串肯定会被驻留,并且还会共享给由并行层生成的每个 PHP 进程或线程。此外,如果您在 PHP 处理请求时使用了一个驻留字符串,那么这个字符串也会被 opcache 本身驻留,并共享给由并行层生成的每个 PHP 进程或线程。

opcache 扩展启动时,驻留字符串机制会发生变化。Opcache 不仅允许驻留来自请求的字符串,还允许将它们共享给同一池中的每个 PHP 进程。这是使用共享内存完成的。保存驻留字符串时,opcache 还会将 IS_STR_PERMANENT 标志添加到其 GC 信息中。该标志表示用于结构(此处为 zend_string)的内存分配是永久的,它可以是共享的只读内存段。

驻留字符串节省内存,因为同一个字符串永远不会在内存中存储多次。但它可能会浪费一些 CPU 时间,因为它经常需要查找驻留字符串存储,即使该过程已经得到很好的优化。作为扩展设计者,以下是全局规则:

  • 如果使用了 opcache(应该使用),并且您需要创建只读字符串:请使用驻留字符串。

  • 如果您需要一个您确定 PHP 会驻留的字符串(众所周知的 PHP 字符串,例如 “php” 或 “str_replace”),请使用驻留字符串。

  • 如果字符串不是只读的,并且在创建后可以/应该被更改,请不要使用驻留字符串。

  • 如果该字符串将来不太可能被重用,请不要使用驻留字符串。

永远不要尝试修改(写入)一个驻留的字符串,否则你很可能会崩溃。

驻留字符串在 Zend/zend_string.c 中有详细说明。

smart_str API

这可能看起来很奇怪,但 C 语言几乎没有提供任何处理字符串的功能(构建、连接、收缩、扩展、转换等)。C 是一种低级通用语言,可用于构建 API 来处理更具体的任务,例如字符串构造。

显然大家都知道我们讨论的是 ASCII 字符串,也就是字节。其中没有 Unicode

PHP 的 smart_str 是一种 API,可帮助您构建字符串,尤其是将字节块连接成字符串。此 API 位于 PHP 的特殊 printf() API 和 zend_string 旁边,可帮助进行字符串管理。

smart_str VS smart_string

以下是两个结构:

typedef struct {
    char *c;
    size_t len;
    size_t a;
} smart_string;

typedef struct {
    zend_string *s;
    size_t a;
} smart_str;

如您所见,一个将使用传统的 C 字符串(如 char*/size_t),另一个将使用 PHP 特定的 zend_string 结构。

我们将详细介绍后者:smart_str,它与 zend_strings 一起使用。这两个 API 完全相同,只需注意一个(我们将在这里详细介绍的那个)以 smart_str_**() 开头,另一个以 smart_string_***() 开头。不要混淆!

smart_str API 详细介绍在 Zend/zend_smart_str.h(也在 .c 文件)中。

不要将 smart_strsmart_string 混淆。

基础 API 使用

到目前为止一切顺利,该 API 确实易于管理。您基本上是堆栈分配一个 smart_str,并将其指针传递给为您管理嵌入的 zend_stringsmart_str_***() API 函数。您构建字符串,使用它,然后释放它。这里面没有什么特别强的,对吧?

嵌入的 zend_string 将被 永久分配或请求绑定,这取决于您将使用的最后一个扩展 API 参数:

smart_str my_str = {0};

smart_str_appends(&my_str, "Hello, you are using PHP version ");
smart_str_appends(&my_str, PHP_VERSION);

smart_str_appendc(&my_str, '\n');

smart_str_appends(&my_str, "You are using ");
smart_str_append_unsigned(&my_str, zend_hash_num_elements(CG(function_table)));
smart_str_appends(&my_str, " PHP functions");

smart_str_0(&my_str);

/* Use my_str now */
PHPWRITE(ZSTR_VAL(my_str.s), ZSTR_LEN(my_str.s));

/* Don't forget to release/free it */
smart_str_free(&my_str);

我们还可以独立于 smart_str 使用嵌入的 zend_string

smart_str my_str = {0};

smart_str_appends(&my_str, "Hello, you are using PHP version ");
smart_str_appends(&my_str, PHP_VERSION);

zend_string *str = smart_str_extract(my_str);
RETURN_STR(str);

/* We must not free my_str in this case */

如果 smart_str.sNULLsmart_str_extract() 将返回预分配的空字符串。否则,它会添加尾随的 NUL 字节并将分配的内存修剪为字符串大小。

我们在这里使用了简单的 API,扩展的 API 以 _ex() 结尾,并允许您判断是否需要对底层 zend_string 进行持久分配或请求绑定分配。示例:

smart_str my_str = {0};

smart_str_appends_ex(&my_str, "Hello world", 1); /* 1 means persistent allocation */

然后,根据您想要附加的内容,您将使用正确的 API 调用。如果您附加一个经典的 C 字符串,则可以使用 smart_str_appends(smart_str *dst, const char *src)。如果您使用二进制字符串,并且知道其长度,则使用 smart_str_appendl(smart_str *dst, const char *src, size_t len)

不太具体的 smart_str_append(smart_str *dest, const zend_string *src) 只是将 zend_string 附加到您的 smart_str 字符串。如果您要使用其他 smart_str,请使用 smart_str_append_smart_str(smart_str *dst, const smart_str *src) 将它们组合在一起。

智能STR特定技巧

  • 永远不要忘记通过调用 smart_str_0() 来完成字符串。这会在嵌入字符串的末尾放置一个 NUL 字符,并使其与 libc 字符串函数兼容。

  • 永远不要忘记在完成字符串后使用 smart_str_free() 释放字符串。

  • 完成字符串构建后,使用 smart_str_extract() 获取独立的 zend_string。这将负责调用 smart_str_0() 和优化分配。在这种情况下,无需调用 smart_str_free()

  • 稍后您可以在其他地方分享独立的 zend_string,并使用它引用计数器。请访问 zend_string 专用章节以了解更多信息。

  • 您可以使用 smart_str 分配。查看 smart_str_alloc() 和相关函数。

  • smart_str 在 PHP 的核心中被广泛使用。例如,PHP 的特定 printf() 函数在内部使用 smart_str 缓冲区。

  • smart_str 绝对是您需要掌握的简单结构。

PHP 自定义 printf 函数

大家都知道 libcprintf() 及其家族。本章将详细介绍 PHP 声明和使用的众多克隆、它们的目标是什么、为什么使用它们以及何时使用它们。

Libc 关于 printf() 及其相关函数的 文档 位于此处

您知道这些函数很有用,但有时提供的功能不够。此外,您知道向 printf() 系列添加格式字符串并非易事,不可移植且存在安全风险。

PHP 添加了自己的类似 printf 的函数来替换 libc 函数并供内部开发人员使用。它们主要会添加新格式,使用 zend_string 而不是 char * 等……让我们一起来看看。

您必须掌握 libc 默认的 printf() 格式。请在此处阅读其 文档

这些函数被添加是为了替换 libc 的函数,这意味着如果你使用例如 sprintf(),它不会调用 libcsprintf(),而是调用 PHP 的替代函数。除了传统的 printf(),其他的都被替换了。

传统用途

首先,您不应该使用 sprintf(),因为该函数不执行任何检查,并且会导致许多缓冲区溢出错误。请尽量避免使用它。

请尽量避免使用 sprintf()

然后,您可以做出一些选择。

你知道结果缓冲区的大小

如果您知道缓冲区的大小,snprintf()slprintf() 将为您完成这项工作。这些函数返回的内容有所不同,但功能相同。

它们都根据传递的格式进行打印,并且无论发生什么情况,它们都以 NUL 字节“\0” 终止缓冲区。但是,snprintf() 返回可以使用的字符数,而 slprintf() 返回有效使用的字符数,从而能够检测太小的缓冲区和字符串截断。这还不包括最后的 “\0”。

以下是一个示例,以便您完全理解:

char foo[8]; /* 8-char large buffer */
const char str[] = "Hello world"; /* 12 chars including \0 in count */
int r;

r = snprintf(foo, sizeof(foo), "%s", str);
/* r = 11 here even if only 7 printable chars were written in foo */

/* foo value is now 'H' 'e' 'l' 'l' 'o' ' ' 'w' '\0' */

snprintf() 不是一个好用的函数,因为它不允许检测最终的字符串截断。从上面的例子中可以看出,“Hello world\0” 不能完全写入八字节缓冲区,这是显而易见的,但 snprintf() 仍然返回 11,即 strlen("Hello world\0")。您无法检测到字符串是否被截断。

下面是 slprintf()

char foo[8]; /* 8-char large buffer */
const char str[] = "Hello world"; /* 12 chars including \0 in count */
int r;

r = slprintf(foo, sizeof(foo), "%s", str);
/* r = 7 here , because 7 printable chars were written in foo */

/* foo value is now 'H' 'e' 'l' 'l' 'o' ' ' 'w' '\0' */

使用 slprintf(),结果缓冲区 foo 包含完全相同的字符串,但返回值现在为 77 小于 “Hello world” 字符串中的 11 个字符,因此您可以检测到它被截断了:

if (slprintf(foo, sizeof(foo), "%s", str) < strlen(str)) {
    /* A string truncation occurred */
}

请记住:

  • 这两个函数总是以 NUL 终止字符串,无论是否截断。结果字符串是安全的 C 字符串。

  • 只有 slprintf() 允许检测字符串截断。

这两个函数在 main/snprintf.c 中定义。

你不知道结果缓冲区的大小

现在,如果您不知道结果缓冲区大小,则需要动态分配一个缓冲区,然后使用 spprintf()。请记住,你必须自己释放缓冲区!

以下是示例:

#include <time.h>

char *result;
int r;

time_t timestamp = time(NULL);

r = spprintf(&result, 0, "Here is the date: %s", asctime(localtime(&timestamp)));

/* now use result that contains something like "Here is the date: Thu Jun 15 19:12:51 2017\n" */

efree(result);

spprintf() 返回已打印到结果缓冲区的字符数,不计算最后的 “\0”,因此您知道为您分配的字节数(减一)。

请注意,分配是使用 ZendMM(请求分配)完成的,因此应将其用作请求的一部分,并使用 efree() 而不是 free() 释放。

有关 Zend 内存管理器 (ZendMM) 的章节详细介绍了如何通过 PHP 分配动态内存。

如果要限制缓冲区大小,可以将该限制作为第二个参数传递,如果传递 0,则表示无限制:

#include <time.h>

char *result;
int r;

time_t timestamp = time(NULL);

/* Do not print more than 10 bytes || allocate more than 11 bytes */
r = spprintf(&result, 10, "Here is the date: %s", asctime(localtime(&timestamp)));

/* r == 10 here, and 11 bytes were allocated into result */

efree(result);

尽可能不要使用动态内存分配。这会影响性能。如果可以选择,请选择静态堆栈分配缓冲区。

spprintf() 写在 main/spprintf.c 中。

那么 printf() 怎么样?

如果您需要 printf(),也就是将格式化的内容打印到输出流,请使用 php_printf()。该函数在内部使用 spprintf(),因此执行动态分配,在将其发送到 SAPI 输出(即 CLI 中的 stdout)或其他 SAPI 的输出缓冲区(例如 CGI 缓冲区)后,它会自行释放。

特殊的 PHP printf 格式

请记住,PHP 用自己设计的函数替换了大多数 libcprintf() 函数。您可以查看参数解析 API,通过阅读 源代码 很容易理解。

这意味着参数解析算法已被完全重写,可能与您在 libc 中习惯的不同。例如,在大多数情况下,libc 语言环境不受关注。

可以使用特殊格式,例如 “%I64” 明确打印为 int64,或 “%I32”。您还可以使用 “%Z” 使 zval 可打印(根据 PHP 转换为字符串的规则),这是一个很好的补充。

格式化程序还将识别无限数字并打印 “INF”,或 “NAN” 表示非数字。

如果您犯了一个错误,并要求格式化程序打印一个 NULL 指针,libc 肯定会崩溃,PHP 将返回 “(null)” 作为结果字符串。

如果在 printf 中看到出现神奇的 “(null)”,则意味着您将 NULL 指针传递给了 PHP printf 系列函数之一。

将 Printf() 传入 zend_strings

由于 zend_string 是 PHP 源代码中非常常见的结构,您可能需要将 printf() 转换为 zend_string,而不是传统的 C char *。为此,请使用 strpprintf()

API 是 zend_string *strpprintf(size_t max_len, const char *format, …​),这意味着将返回 zend_string,而不是您期望的打印字符数。但是,您可以使用第一个参数限制该数字(传递 0 表示无限);您必须记住,zend_string 将使用 Zend 内存管理器进行分配,因此与当前请求绑定。

显然,格式 API 与上面看到的 API 共享。

这是一个简单示例:

zend_string *result;

result = strpprintf(0, "You are using PHP %s", PHP_VERSION);

/* Do something with result */

zend_string_release(result);

关于 zend_ API 的说明

您可能会遇到 zend_spprintf()zend_strpprintf() 函数。它们与上面看到的完全相同。

它们只是 Zend Engine 和 PHP Core 之间分离的一部分,这个细节对我们来说并不重要,因为在源代码中,所有内容都混在一起了。