
PHP 内部 Streams 抽象从 php_stream API 到自研流实现的完整指南【免费下载链接】php-srcThe PHP Interpreter项目地址: https://gitcode.com/GitHub_Trending/ph/php-srcPHP 扩展开发中最常见的痛点之一是代码里到处乱飞的issock参数——它强迫你在“普通文件”和“套接字”之间做大量特判。PHP 内核为此引入了 streams流抽象用统一的php_stream*指针替代FILE*提供一套与 ANSI stdio 风格一致的 API并可扩展到文件之外的数据源套接字、内存、数据库结果集、PHP 注册的自定义 wrapper 等。本文基于 PHP 仓库中的官方流抽象文档 docs-old/streams.md结合 main/php_streams.h 与 main/streams/ 下的真实实现完整覆盖流的读写、打开、数据拷贝、seekable 转换、类型转换casting以及自行编写流实现的全部细节读完之后你可以直接在扩展代码中正确使用内核流 API并能为自己的数据源编写一个完整的php_stream_ops实现。需要说明原文档开头自带一条警告——“some prototypes in this file are out of date”部分原型已过时。因此下文在完整保留文档骨架与示例的同时会对每一处签名漂移给出当前仓库中的实际原型并以源码为准标注差异。一、为什么要用 Streams按文档的说法issock参数“又丑又累赘”迫使每一个需要处理“用户层 PHP 文件指针”的扩展代码都对 socket 和 file 做特殊处理。Streams 的解决思路是用php_stream*参数取代FILE*参数语义与 ANSI stdiofread等一一对应API 形态更接近 ANSI stdio更易用抽象层之下可以挂接任意“非文件型”数据源URL wrapper、套接字、内存流、数据库等。从源码结构看这套抽象的“虚方法表”就是 main/php_streams.h 中的php_stream_ops而“插件”机制则是php_stream_wrapper_opsmain/php_streams.h——后者的stream_opener/stream_closer/stream_stat/dir_opener等回调正是 PHP 层面fopen()支持http://、php://memory等协议的底层实现。二、流的核心 I/O 函数文档列出的主要函数及其 ANSI stdio 对照关系如下。对照当前 main/php_streams.h 中的实际原型函数ANSI stdio 对应当前仓库实际原型main/php_streams.hphp_stream_readfreadPHPAPI ssize_t php_stream_read(php_stream *stream, char *buf, size_t count);php_stream_writefwritePHPAPI ssize_t php_stream_write(php_stream *stream, const char *buf, size_t count);另有便捷宏php_stream_write_stringphp_stream_printffprintfPHPAPI ssize_t php_stream_printf(php_stream *stream, const char *fmt, ...) PHP_ATTRIBUTE_FORMAT(printf, 2, 3);php_stream_eoffeofPHPAPI bool php_stream_eof(php_stream *stream);php_stream_getcfgetcPHPAPI int php_stream_getc(php_stream *stream);php_stream_getsfgets现为宏php_stream_gets(stream, buf, maxlen)→php_stream_get_line(stream, buf, maxlen, NULL)php_stream_closefclose现为宏php_stream_close(stream)→php_stream_free(stream, PHP_STREAM_FREE_CLOSE)另有php_stream_pclose用于持久流php_stream_flushfflushPHPAPI int php_stream_flush(php_stream *stream);另有php_stream_sync(stream, data_only)php_stream_seekfseekPHPAPI int php_stream_seek(php_stream *stream, zend_off_t offset, int whence);另有php_stream_rewind宏php_stream_tellftellPHPAPI zend_off_t php_stream_tell(const php_stream *stream);php_stream_lockflock见“流工具”小节这些函数“应当”文档原文用词为 should表现出与同名 stdio 函数一致的行为。注意两个与文档签名的主要差异read/write/printf的返回类型从size_t变为ssize_t便于返回 -1 表示错误seek/tell的偏移量从off_t变为zend_off_t内核统一的 64 位偏移类型。三、打开流3.1 通用入口php_stream_open_wrapper大多数场景应使用文档推荐的统一入口。当前实现是一个宏展开到带 stream context 的_ex版本main/php_streams.h// 文档中的原型 PHPAPI php_stream *php_stream_open_wrapper(const char *path, const char *mode, int options, char **opened_path); // 当前仓库main/php_streams.h PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC); #define php_stream_open_wrapper(path, mode, options, opened) \ _php_stream_open_wrapper_ex((path), (mode), (options), (opened), NULL STREAMS_CC) #define php_stream_open_wrapper_ex(path, mode, options, opened, context) \ _php_stream_open_wrapper_ex((path), (mode), (options), (opened), (context) STREAMS_CC)参数说明继承文档并补充源码中的实际取值path要打开的文件或资源modestdio 兼容的模式如rb、wboptions以下值的组合实际数值定义在 main/php_streams.hIGNORE_PATH默认不使用 include path 搜索文件USE_PATH0x00000001使用 include path 搜索IGNORE_URL0x00000002不使用插件 wrapper即不走http://等协议解析REPORT_ERRORS0x00000008出错时以标准格式输出错误STREAM_MUST_SEEK0x00000010确实需要 seek 能力且不要求写回原文件/URL 时用它让内核在必要时把内容复制到一个可seek()的流里。opened_path返回实际打开的路径若使用了STREAM_MUST_SEEK返回值可能无效。当前实现中类型为zend_string **不再是文档里的char **。若使用了该参数你负责释放opened_path当前为zend_string_release该参数可以为且通常就是NULL。3.2 定向打开与 FILE*/socket 的转换当需要打开特定类型的流或把标准资源转换为流时php_streams.h提供了一系列函数。文档列出的三个最常用的文件类函数与当前仓库原型一致PHPAPI php_stream *php_stream_fopen_from_file(FILE *file, const char *mode); /* 把 FILE * 转换为流。 */ PHPAPI php_stream *php_stream_fopen_tmpfile(void); /* 用 tmpfile() 打开一个 FILE * 并转换为流。 */ PHPAPI php_stream *php_stream_fopen_temporary_file(const char *dir, const char *pfx, char **opened_path); /* 生成临时文件名并打开它。 */网络相关的对应函数定义在 main/php_network.hPHPAPI php_stream *php_stream_sock_open_from_socket(int socket, int persistent); /* 把 socket 转换为流。 */ PHPAPI php_stream *php_stream_sock_open_host(const char *host, unsigned short port, int socktype, int timeout, int persistent); /* 打开到主机的连接并返回流。 */ PHPAPI php_stream *php_stream_sock_open_unix(const char *path, int persistent, struct timeval *timeout); /* 打开 UNIX 域套接字。 */3.3 一个值得注意的工程细节STREAMS_CC 宏当前实现中php_stream_open_wrapper等宏都带着STREAMS_CC后缀参数。这不是噪音STREAMS_DC/STREAMS_CC/STREAMS_REL_CC宏main/php_streams.h在 debug 构建下会把调用点的文件名/行号__zend_orig_filename/__zend_orig_lineno一路透传下去使错误信息能定位到“最外层祖先”调用者在 release 构建下它们展开为空。文档在“编写自己的流”一节也明确要求函数定义使用这些宏原因在此。四、流工具函数4.1 流到流的拷贝php_stream_copy_to_streamPHPAPI size_t php_stream_copy_to_stream(php_stream *src, php_stream *dest, size_t maxlen);传入PHP_STREAM_COPY_ALL定义在 main/php_streams.h值为(size_t)-1表示拷贝src中剩余的全部数据否则maxlen表示要拷贝的字节数。该函数会尽量使用 mmap 提升拷贝效率——对应的实现支撑就在 main/streams/mmap.c 与 main/streams/cast.c 中的 copy 路径里。4.2 流到内存的拷贝php_stream_copy_to_memPHPAPI size_t php_stream_copy_to_mem(php_stream *src, char **buf, size_t maxlen, int persistent);buf被设置为函数内部分配缓冲区的地址长度等于maxlen或当maxlen为PHP_STREAM_COPY_ALL时等于流中剩余数据的完整长度缓冲区通过pemalloc()分配使用完毕必须用pefree()释放与copy_to_stream一样它也会在可用时尝试使用 mmap。4.3 让不可 seek 的流变为可 seekphp_stream_make_seekablePHPAPI int php_stream_make_seekable(php_stream *origstream, php_stream **newstream);当前实现增加了flags参数并以枚举返回main/php_streams.h但枚举值与文档完全一致PHP_STREAM_UNCHANGED 0 /* orig stream was seekable anyway */ PHP_STREAM_RELEASED 1 /* newstream should be used; origstream is no longer valid */ PHP_STREAM_FAILED 2 /* an error occurred while attempting conversion */ PHP_STREAM_CRITICAL 3 /* an error occurred; origstream is in an unknown state; you should close origstream */文档给出的五条使用注意全部应当遵循make_seekable成功时总是把newstream设置为有效的那个流用完后记得关闭如果只需要向前 seek不必调用该函数——php_stream_seek在whence为SEEK_CUR时能模拟前向 seek对新流的写入未必会写回原始数据源因此该函数只适合只读用途若origstream基于网络此函数会阻塞直到全部内容下载完毕切勿对作为资源resource被引用的origstream调用该函数——它成功时会关闭origstream资源后续被使用/释放时可能直接崩溃若打开流时就需要 seekable直接在php_stream_open_wrapper()中使用STREAM_MUST_SEEK选项即 3.1 节的推荐做法。4.4 加锁支持php_stream_supports_lockPHPAPI int php_stream_supports_lock(php_stream * stream);返回 1支持或 0不支持表示能否对该流设置锁。通常只有 stdio 流支持加锁。五、流的类型转换Casting5.1 把流“转换”为 FILE*当扩展必须访问用户层文件指针背后的FILE*时用php_stream_castFILE * fp; php_stream * stream; /* already opened */ if (php_stream_cast(stream, PHP_STREAM_AS_STDIO, (void*)fp, REPORT_ERRORS) FAILURE) { RETURN_FALSE; }原型main/php_streams.hPHPAPI int php_stream_cast(php_stream * stream, int castas, void ** ret, int show_err);show_err非零时转换失败会以E_WARNING级别输出相应错误信息castas可取以下值当前仓库定义在 main/php_streams.h后两个是文档之后新增的PHP_STREAM_AS_STDIO 0 /* 一个 stdio FILE* */ PHP_STREAM_AS_FD 1 /* 一个通用的文件描述符 */ PHP_STREAM_AS_SOCKETD 2 /* 一个套接字描述符 */ PHP_STREAM_AS_FD_FOR_SELECT 3 PHP_STREAM_AS_FD_FOR_COPY 45.2 转换的副作用与 FILE* 合成对一个 socket 流请求FILE*时抽象层会用fdopen为你创建它。警告在FILE*上混用 ANSI stdio 调用、同时在流上调用 php stream 函数可能丢失缓冲区中的数据。如果系统提供fopencookiephp streams 可以在任意流之上合成一个FILE*——这对 SSL 套接字、基于内存的流、数据库流等场景非常有用。若不想要这种合成行为应先查询流是否天然支持FILE *if (php_stream_is(stream, PHP_STREAM_IS_STDIO)) { /* can safely cast to FILE* with no adverse side effects */ }其中PHP_STREAM_IS_STDIO就是php_stream_stdio_ops这个 ops 结构体指针main/php_streams.h即通过比较 ops 表来判断流类型。用php_stream_can_cast可以在真正执行转换之前预检PHPAPI int php_stream_can_cast(php_stream * stream, int castas) if (php_stream_can_cast(stream, PHP_STREAM_AS_SOCKETD) SUCCESS) { /* it can be a socket */ }文档特别强调php_stream_is与php_stream_can_cast的区别前者只回答“这个流是不是某种类型”不改变任何状态后者回答“这个流能否被强制转换为你要求的形态”并且可能改变流内部状态例如真的去调用fdopen/fopencookie。从 main/streams/cast.c 的实现看这个区别是实打实的castas PHP_STREAM_AS_STDIO时会走fopencookie/fdopen路径并可能触发内部flush。六、流内部结构php_stream 与 php_stream_ops每个流由两部分组成php_stream本体持有状态信息、可能还有缓冲区和一个php_stream_ops结构体底层实现的“虚方法表”。当前仓库中的 ops 定义main/php_streams.htypedef struct _php_stream_ops { /* stdio like functions - these are mandatory! */ ssize_t (*write)(php_stream *stream, const char *buf, size_t count); ssize_t (*read)(php_stream *stream, char *buf, size_t count); int (*close)(php_stream *stream, int close_handle); int (*flush)(php_stream *stream); const char *label; /* label for this ops structure */ /* these are optional */ int (*seek)(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset); int (*cast)(php_stream *stream, int castas, void **ret); int (*stat)(php_stream *stream, php_stream_statbuf *ssb); int (*set_option)(php_stream *stream, int option, int value, void *ptrparam); } php_stream_ops;与文档的对应关系write、read、close、flush 四个是必须实现的label是实现名标签打印错误信息时使用——stdio 实现的标签就是STDIO文档说 ops 包含 read、write、close、flush、seek、gets、cast 七个方法其中“gets 方法供底层能高效模拟 fgets 的流使用”。当前结构体中没有独立的gets回调读整行的能力由内核缓冲层配合read实现文档描述的gets属于过时原型之一。实现方的思路是定义一个php_stream_ops结构体并用php_stream_alloc把它与一个php_stream关联起来。文档举的php_stream_fopen()例子其当前真实实现位于 main/streams/plain_wrapper.cphp_stream_fopen→php_stream_fopen_from_fd→php_stream_alloc_rel(php_stream_stdio_ops, self, persistent_id, mode)见 main/streams/plain_wrapper.c结构与文档示例一致。php_stream_stdio_ops是处理FILE*基础流的 ops 表其声明在 main/streams/php_stream_plain_wrapper.h。当前php_stream_alloc的原型main/php_streams.hPHPAPI php_stream *_php_stream_alloc(const php_stream_ops *ops, void *abstract, const char *persistent_id, const char *mode STREAMS_DC); #define php_stream_alloc(ops, thisptr, persistent_id, mode) \ _php_stream_alloc((ops), (thisptr), (persistent_id), (mode) STREAMS_CC)参数语义文档解释仍然有效ops指向你的实现ops 表abstract本流实例的实现相关数据文档中的bufsize0 表示禁用流层缓冲推荐用于自带缓冲的底层如FILE*。注意当前签名中缓冲区策略不再通过该参数显式传递而是体现在 ops 与流标志如PHP_STREAM_FLAG_NO_BUFFERmain/php_streams.h中persistent_id控制内存分配方式——持久跨请求存活或非持久请求结束时用 pemalloc 语义释放modestdio 风格的工作模式。php streams 本身对 mode 不赋予太多语义只是在尝试写入时检查字符串里是否有w文档注明这点未来可能变化。由于 mode 会在流被 cast 成FILE*时透传给fdopen/fopencookie它必须与fopen()的 mode 兼容。七、编写你自己的流实现规则一务必用 --enable-debug 配置 PHP文档第一条规则编写自己的流时确保 PHP 是用--enable-debug配置的。内核为流接口挂了 Zend 内存管理器的追踪钩子有助于定位分配问题同时它能帮你发现STREAMS_DC、STREAMS_CC以及半私有的STREAMS_REL_CC宏在函数定义中的错误用法。规则二以 stdio 流实现为参考文档第二条规则请参照 stdio 流实现来理解各操作的语义——它永远比这些文档更新。对应源码即 main/streams/plain_wrapper.c。第一步确定要挂到 php_stream 上的数据例如内存流需要指向某段内存的指针如果是从 MySQL 这类 RDBMS 读数据的流你可能想存放连接和结果集句柄。流有个叫abstract的字段供你存放这些数据需要多字段时定义一个结构体、用pemalloc按 persistent 标志设置分配并让abstract指向它。文档给出的示例struct my_state { MYSQL conn; MYSQL_RES * result; }; struct my_state * state pemalloc(sizeof(struct my_state), persistent); /* initialize the connection, and run a query, using the fields in state to * hold the results */ state-result mysql_use_result(state-conn); /* now allocate the stream itself */ stream php_stream_alloc(my_ops, state, 0, persistent, r); /* now stream-abstract state */注意文档此例的php_stream_alloc调用是旧版 5 参数签名其中含bufsize当前实现为 4 参数版本见第六节。第二步实现 read 回调以文档中这个“奇怪的 MySQL 流”为例读操作长这样static size_t php_mysqlop_read(php_stream * stream, char * buf, size_t count) { struct my_state * state (struct my_state*)stream-abstract; if (buf NULL count 0) { /* in this special case, php_streams is asking if we have reached the * end of file */ if (... at end of file ...) return EOF; else return 0; } /* pull out some data from the stream and put it in buf */ ... mysql_fetch_row(state-result) ... /* we could do something strange, like format the data as XML here, and place that in the buf, but that brings in some complexities, such as coping with a buffer size too small to hold the data, so I wont even go in to how to do that here */ }关键点内核用read(stream, NULL, 0)这种特殊调用探测是否到达 EOF——你的实现必须正确处理这个分支。当前内核签名中read返回ssize_t可按当前类型风格适配。其余操作同理实现——记住write、read、close、flush 都是强制的其余可选。然后声明你的 ops 表php_stream_ops my_ops { php_mysqlop_write, php_mysqlop_read, php_mysqlop_close, php_mysqlop_flush, NULL, NULL, NULL, Strange MySQL example };字段顺序按文档示例排列按当前结构体定义label位于四个强制回调之后、可选回调之前实际编写时请以 main/php_streams.h 中的字段顺序为准。第三步close 里释放 abstract 持有的资源文档最后一段也是最容易漏的一步在你的 close 操作中必须释放并为abstract字段分配的所有资源。以上例来说你需要对结果集调用mysql_free_result、关闭连接然后用pefree释放你分配的结构体。可以通过读stream-persistent字段判断你的结构体是否以持久模式分配。八、小结与源码索引回到文档开头的问题streams 存在的意义就是让扩展作者不再关心“这到底是个文件还是 socket”。实际编码时的落地清单用php_stream_open_wrapper_ex()打开选项按 main/php_streams.h 中的位值组合需要 seek 时加STREAM_MUST_SEEKI/O 全部走php_stream_read/write/printf/seek/tell/flush/close一族行为对齐 ANSI stdio数据搬运用php_stream_copy_to_stream/php_stream_copy_to_memmmap 加速注意PHP_STREAM_COPY_ALL需要FILE*或 fd 时用php_stream_cast先用php_stream_is/php_stream_can_cast预检自研流时abstract存上下文、label给错误信息、四强制回调、close 中释放资源、--enable-debug调试。主要源码索引便于继续深入核心 API 头文件main/php_streams.h流实现目录main/streams/其中 stdio/普通文件实现见 main/streams/plain_wrapper.c类型转换见 main/streams/cast.cmmap 见 main/streams/mmap.c网络流 APImain/php_network.h本文依据的原始文档docs-old/streams.md【免费下载链接】php-srcThe PHP Interpreter项目地址: https://gitcode.com/GitHub_Trending/ph/php-src创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考