PHP7扩展开发教程[11] – 如何引入php文件?
确保你已经阅读《PHP7扩展开发教程[10] – 如何使用资源类型?》
这应该是本系列教程的最后一章,整个系列一共11章节,它们涉及了PHP7扩展开发的主要内容,我相信其意义于我与你都是一样的。
当然,在今后的工作中可能会遇到新的问题、新的知识点,这些潜在的新内容将会以扩展章节的方式补充上来,与本教程的11个章节区分开来。
在本章中,我将演示如何在PHP扩展中引入另外一个PHP文件,就像你在PHP中执行include或者include_once一样。
正式开始
本章代码:https://github.com/owenliang/php7-extension-explore/tree/master/course11-how-to-include-php-file。
我增加了一个函数:
1 |
void zif_myext_test_include(zend_execute_data *execute_data, zval *return_value) |
并在test.php中这样测试:
1 2 3 4 5 6 7 |
test_resource(); $file = __DIR__ . "/other.php"; var_dump(test_include("other.php")); // array var_dump(test_include($file)); // false echo SOME_DEF . PHP_EOL; |
test_include方法模拟了include_once的实现,也就是只包含一次该文件,下面看一下实现过程。
1 2 3 4 5 6 7 8 9 10 |
int num_args = ZEND_CALL_NUM_ARGS(execute_data); zval *args = ZEND_CALL_ARG(execute_data, 1); assert(num_args == 1); // relative path to absolute full path char realpath[MAXPATHLEN]; if (!virtual_realpath(Z_STRVAL(args[0]), realpath)) { ZVAL_BOOL(return_value, 0); return; } |
test_include方法接收1个参数,是要引入的文件路径,可以是相对的或者绝对的文件路径。通过Zend的virtual_realpath方法,可以按照PHP的文件搜索规则得到绝对路径,如果路径不合法返回false。
1 2 3 4 5 6 7 8 9 |
zend_string *filename = zend_string_init(realpath, strlen(realpath), 0); // already loaded before zval *existed = zend_hash_find(&EG(included_files), filename); if (existed) { zend_string_release(filename); ZVAL_BOOL(return_value, 0); return; } |
接下来生成一个zend_string类型的文件路径,并在全局哈希表EG(included_files)中查找是否已经引入了该文件,如果引入我们就立即返回false。
1 2 3 4 5 6 7 8 9 10 11 12 |
// not opened file handle zend_file_handle file_handle = { filename: filename->val, free_filename: 0, type: ZEND_HANDLE_FILENAME, opened_path: NULL, handle: {fp: NULL}, }; // compile file into opcode zend_op_array *op_array = zend_compile_file(&file_handle, ZEND_INCLUDE); assert(op_array); |
接下来,我们创建一个zend_file_handle文件句柄,它的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
typedef struct _zend_stream { void *handle; int isatty; zend_mmap mmap; zend_stream_reader_t reader; zend_stream_fsizer_t fsizer; zend_stream_closer_t closer; } zend_stream; typedef struct _zend_file_handle { union { int fd; FILE *fp; zend_stream stream; } handle; const char *filename; zend_string *opened_path; zend_stream_type type; zend_bool free_filename; } zend_file_handle; |
- filename:const char *类型,文件名字符串。
- free_filename:表示filename的内存是否需要帮助我们释放,这里不需要,因为我们直接引用了zend_string*里的内存,后面会主动释放它。
- type:表示这个zend_file_handle仅仅填充了文件名,尚未真实打开文件。
- opened_path:传NULL即可,是API内部使用的。
- handle:保存底层文件指针的一个union,Zend内其实最终会用zend_stream stream,而zend_stream的第一个字段void *handle恰好与handle.fp共享内存。
准备好这个file handle后,传给zend_compile_file来编译这个文件,这个API内部会为zend_file_handle打开真实的文件,并编译PHP代码为opcode。第二个参数有几个可选值:
1 2 3 4 |
#define ZEND_INCLUDE (1<<1) #define ZEND_INCLUDE_ONCE (1<<2) #define ZEND_REQUIRE (1<<3) #define ZEND_REQUIRE_ONCE (1<<4) |
貌似ONCE并没有什么用,通常传ZEND_INCLUDE即可。
现在可以关闭文件句柄了:
1 2 |
// close file handle zend_destroy_file_handle(&file_handle); |
接下来,我们在EG(included_files)中记录该文件已经加载:
1 2 3 4 5 |
// mark file is included zval empty_zv; ZVAL_NULL(&empty_zv); assert(zend_hash_add_new(&EG(included_files), filename, &empty_zv)); // 1 ref added inner zend_string_release(filename); |
这里value并不重要,只要文件路径作为key存在即可,因为zend_hash_add_xxx会给key增加1个引用计数,并且此后也不再使用filename,我们主动释放掉原先zend_string的1个引用计数。
现在,我们准备一个zval接收include的返回值,然后将之前zend_compile_file输出的opcode传入zend_execute执行代码:
1 2 3 4 |
// execute opcode zval result; ZVAL_UNDEF(&result); zend_execute(op_array, &result); |
opcode执行完成,可以释放其内部以及自身的内存:
1 2 3 |
// free opcode destroy_op_array(op_array); efree(op_array); |
最后,我们判定返回值是一个数组(test.php里return了1个array),并把这个返回值作为test_include的返回值:
1 2 3 |
// array included from php file assert(Z_TYPE(result) == IS_ARRAY); *return_value = result; |
注意,这里include_once的语义是我们自己实现的,我们完全可以不去查验EG(include_files),对同一个PHP文件进行多次的解释执行。
结语
通过本章,我们应该掌握:
- 编译与执行php文件。
- 确保php文件只引入一次。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

源码中的备注错了:
var_dump(test_include(“other.php”)); // array
var_dump(test_include($file)); // false
应该是:
var_dump(test_include(“other.php”)); // false
var_dump(test_include($file)); // array