以前的学习记录,复习整理了一下。

Zvals

php中的任意值的数据类型都是由zvals表示的,因此它是一个非常重要的数据结构。

php5(>5.3)中的zvals的数据结构是下面这样的。

struct _zval_struct {
    union {
        long lval;
        double dval;
        struct {
            char *val;
            int len;
        } str;
        HashTable *ht;
        zend_object_value obj;
        zend_ast *ast;
    } value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
};

按照鸟哥的文章,尽管这个结构存在大量的问题,但是这个简单的模型可以更好地理解zvals。

我们知道zvals结构体可以代表任何值,所以它包含了一个类型type。为了存储不同的值它包含了一个联合体的value,当type为IS_NULL, IS_LONG, IS_STRING, IS_ARRAY, IS_OBJECT等等时,value值就选择对应的方式读取。refcount__gc字段表示这个zval的引用数目用域垃圾回收。is_ref__gc字段表示是否引用。

下面是php7中的zval的数据结构。

    struct _zval_struct {
         union {
              zend_long         lval;             /* long value */
              double            dval;             /* double value */
              zend_refcounted  *counted;
              zend_string      *str;
              zend_array       *arr;
              zend_object      *obj;
              zend_resource    *res;
              zend_reference   *ref;
              zend_ast_ref     *ast;
              zval             *zv;
              void             *ptr;
              zend_class_entry *ce;
              zend_function    *func;
              struct {
                   uint32_t w1;
                   uint32_t w2;
              } ww;
         } value;
        union {
            struct {
                ZEND_ENDIAN_LOHI_4(
                    zend_uchar    type,         /* active type */
                    zend_uchar    type_flags,
                    zend_uchar    const_flags,
                    zend_uchar    reserved)     /* call info for EX(This) */
            } v;
            uint32_t type_info;
        } u1;
        union {
            uint32_t     var_flags;
            uint32_t     next;                 /* hash collision chain */
            uint32_t     cache_slot;           /* literal cache slot */
            uint32_t     lineno;               /* line number (for ast nodes) */
            uint32_t     num_args;             /* arguments number for EX(This) */
            uint32_t     fe_pos;               /* foreach position */
            uint32_t     fe_iter_idx;          /* foreach iterator index */
        } u2;
    };

其中value字段除了long和double全用指针替代(拷贝时直接复制,不进行引用计数),所以这个字段只用占8字节。其余的u1、u2是扩充字段,u1主要是type info,u2是各种辅助字段。

image-20210904113126255

看下面的zend_string结构,可以看到把引用计数等放到了指针中。

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

https://www.laruence.com/2018/04/08/3170.html

访问宏

使用Z_TYPE_P 获取zval*的类型, 使用Z_XXXX读取值

zval zv;
zval *zv_ptr;

Z_TYPE(zv);       // Same as Z_TYPE_P(&zv).
Z_TYPE_P(zv_ptr); // Same as Z_TYPE(*zv_ptr).

if (Z_TYPE_P(zv_ptr) == IS_LONG) {

}

读取值的函数名以及对应类型如下(读取指针在后面加_P

Macro Returned type Required zval type Description
Z_TYPE unsigned char Type of the zval. One of the IS_* constants.
Z_LVAL zend_long IS_LONG Integer value.
Z_DVAL double IS_DOUBLE Floating-point value.
Z_STR zend_string * IS_STRING Pointer to full zend_string structure.
Z_STRVAL char * IS_STRING String contents of the zend_string struct.
Z_STRLEN size_t IS_STRING String length of the zend_string struct.
Z_ARR HashTable * IS_ARRAY Pointer to HashTable structure.
Z_ARRVAL HashTable * IS_ARRAY Alias of Z_ARR.
Z_OBJ zend_object * IS_OBJECT Pointer to zend_object structure.
Z_OBJCE zend_class_entry * IS_OBJECT Class entry of the object.
Z_RES zend_resource * IS_RESOURCE Pointer to zend_resource structure.
Z_REF zend_reference * IS_REFERENCE Pointer to zend_reference structure.
Z_REFVAL zval * IS_REFERENCE Pointer to the zval the reference wraps.

内存管理

引用计数

在php5中,引用计数依赖于zvals结构体的refcount__gc字段,而在php7中,可以看到除了long/double类型的变量以值得形式存储在zvals结构体中、以及IS_NULL/IS_FALSE/IS_TRUE只有类型没有值的类型,其他的类型都是以指针的形式存放在里面的。

每种复杂的类型都有一种结构体,比如下面的字符串。每种结构体都会有一个zend_refcounted_h结构的字段用于引用计数。

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

这个结构里除了引用计数字段外,还有个type_info字段,主要是便于GC回收的操作。

typedef struct _zend_refcounted_h {
    uint32_t refcount;
    union {
        uint32_t type_info;
    } u;
} zend_refcounted_h;

还有就是在zvals结构体中的u1字段结构体包含的type_flags标志位,用于标志变量的非类型的其他属性。

作用于zval的有:

IS_TYPE_CONSTANT            //是常量类型
IS_TYPE_IMMUTABLE           //不可变的类型, 比如存在共享内存的数组
IS_TYPE_REFCOUNTED          //需要引用计数的类型
IS_TYPE_COLLECTABLE         //可能包含循环引用的类型(IS_ARRAY, IS_OBJECT)
IS_TYPE_COPYABLE            //可被复制的类型, 还记得我之前讲的对象和资源的例外么? 对象和资源就不是
IS_TYPE_SYMBOLTABLE         //zval保存的是全局符号表, 这个在我之前做了一个调整以后没用了, 但还保留着兼容,                            //下个版本会去掉

作用于字符串的有:

IS_STR_PERSISTENT             //是malloc分配内存的字符串,同GC_PERSISTENT 
IS_STR_INTERNED             //INTERNED 
STRINGIS_STR_PERMANENT            //不可变的字符串, 用作哨兵作用
IS_STR_CONSTANT             //代表常量的字符串
IS_STR_CONSTANT_UNQUALIFIED //带有可能命名空间的常量字符串

作用于数组的有:

#define IS_ARRAY_IMMUTABLE  //同IS_TYPE_IMMUTABLE

作用于对象的有:

IS_OBJ_APPLY_COUNT          //递归保护
IS_OBJ_DESTRUCTOR_CALLED    //析构函数已经调用
IS_OBJ_FREE_CALLED          //清理函数已经调用
IS_OBJ_USE_GUARDS           //魔术方法递归保护
IS_OBJ_HAS_GUARDS           //是否有魔术方法递归保护标志

下面是一些宏来获取zvals的属性值。

Macro Description
GC_TYPE Get type of the structure (IS_* constant). 获得zvals的类型
GC_FLAGS Get flags. 获得zvals的type_flags
GC_REFCOUNT Get reference count.
GC_ADDREF Increment refcount. Structure must be mutable.
GC_DELREF Decrement refcount. Structure must be mutable. Does not release structure if refcount reaches zero.
GC_TRY_ADDREF Increment refcount if mutable, otherwise do nothing.

不可变的值

php中的一些字符串和数组是不变的(Immutable values),包括它的值和引用计数,而且在请求结束之前也不会被销毁。比如从php.ini配置文件中读取的字符串、尝试修改以const定义的空数组会导致崩溃等等地方都是需要用到不可变的值。

在使用GC_ADDREFGC_DELREF增减zvals的引用计数的时候,需要首先判断是否为不可变的值,否则会出现错误。

zend_string *str = /* ... */;

if (!(GC_FLAGS(str) & GC_IMMUTABLE)) {
    GC_ADDREF(str);
}

// Same as:
GC_TRY_ADDREF(str);

// 当然高阶api已经自动考虑这个问题了
zend_string_addref(str);

持久的结构

php存在两种内存分配器,单请求分配器和持久分配器。单请求分配器在一次请求完成后会释放所有内存,而持久分配器会跨请求保存内存。

对于持久的变量,它的zvals有GC_PERSISTENT标志位来表示。比如zend_string_init初始化字符串的函数就会接收一个布尔参数指示其初始的字符串是持久的与否。

我们知道持久的结构的变量是跨请求保存的,这主要是用于php的内存优化。而php是无法在多请求中保持一致性的,所以要求持久的结构的变量是不可变的或者将其变为线程本地(thread-local)的。

zval的内存管理

php7的zval通常是分配在栈上的,所以为了实现php引用传值,需要参数以指针传入

// retval is an output parameter.
void init_zval(zval *retval) {
    ZVAL_STRING(retval, "foo");
}

void some_other_function() {
    zval val;
    init_zval(&val);
    // ... Do something with val.
    zval_ptr_dtor(&val);
}

复制zvals

ZVAL_COPY_VALUE() 复制zvals,不会增加引用计数,且不会复制u2扩展部分

void init_zval_indirect(zval *retval) {
    zval val;
    init_zval(&val);
    ZVAL_COPY_VALUE(retval, &val);
}

ZVAL_COPY 复制zvals,是ZVAL_COPY_VALUE and Z_TRY_ADDREF的结合,复制并且尝试增加引用计数。

ZVAL_DUP复制数组。ZVAL_COPY_OR_DUP能够尝试复制持久的结构数据。

初始和销毁zvals

zval_ptr_dtor函数尝试减少引用计数,如果减为0就将其销毁,不为0还需要进一步判断是否为循环引用的垃圾。 zval_ptr_dtor_nogc/zval_dtor就是不判断是否为循环引用的垃圾。

初始化zvals有很多宏, 下面是简单类型初始化。

zval val;
ZVAL_UNDEF(&val);

zval val;
ZVAL_NULL(&val);

zval val;
ZVAL_FALSE(&val);

zval val;
ZVAL_TRUE(&val);

zval val;
ZVAL_BOOL(&val, zero_or_one);

zval val;
ZVAL_LONG(&val, 42);

zval val;
ZVAL_DOUBLE(&val, 3.141);

对于字符串

zval val;
ZVAL_STR(&val, zend_string_init("test", sizeof("test")-1, 0));

zval val;
ZVAL_STRINGL(&val, "test", sizeof("test")-1);

zval val;
ZVAL_STRING(&val, "test"); // Uses strlen() for length.

对于数组,两种都是初始化空数组,第二种是基于不可变数组,所以需要修改使用第一种

zval val;
ZVAL_ARR(&val, zend_new_array(/* size_hint */ 0));

zval val;
ZVAL_EMPTY_ARRAY(&val);

php引用

php中的引用并不是指针,如下图的例子,$a$b指向的是同一个zval。

$a = 0;
$b =& $a;
$a++;
$b++;
var_dump($a); // int(2)
var_dump($b); // int(2)

_zend_reference的结构如下,除了引用计数和,还有一个sources字段,用于php7.4以后的有类型的类属性的引用记录其引用类型。

struct _zend_reference {
    zend_refcounted_h              gc;
    zval                           val;
    zend_property_info_source_list sources;
};

一些宏

  • Z_ISREF(zv) – checks if the value is a PHP reference (has type IS_REFERENCE).
  • Z_REF(zv) – returns dependent zend_reference structure (type must be IS_REFERENCE).
  • Z_REFVAL(zv) – returns a pointer to the referenced value (zval).

There are also few macros for constructing references and de-referencing:

  • ZVAL_REF(zv, ref) – initializes zval by IS_REFERENCE type and give zend_reference pointer.
  • ZVAL_NEW_EMPTY_REF(zv) – initializes zval by IS_REFERENCE type and a new zend_reference structure. Z_REFVAL_P(zv) needs to be initialized after this call.
  • ZVAL_NEW_REF(zv, value) – initializes zval by IS_REFERENCE type and a new zend_reference structure with a given value.
  • ZVAL_MAKE_REF_EX(zv, refcount) – converts “zv” to PHP reference with the given reference-counter.
  • ZVAL_DEREF(zv) – if “zv” is a reference, it’s de-referenced (a pointer to the referenced value is assigned to “zv”).

php7虚拟机与Opcode

http://yangxikun.github.io/php/2016/11/04/php-7-engine.html

https://www.npopov.com/2017/04/14/PHP-7-Virtual-machine.html

php源码调试

gdb调试

github下载源码

# 编译php
./buildconf
./configure --disable-all --enable-debug --prefix=xxxx/php   # 关闭所有扩展,开启debug类似gcc-g,然后设置php的目录以免影响工作目录。
make -j8
make install

# 编译扩展
/home/jrxnm/share/static_analysis/php-src/php/bin/phpize
./configure --with-php-config=/home/jrxnm/share/static_analysis/php-src/php/bin/php-config --enable-debug
make

# 调试php
gdb  phppath
> run  php_path
> b zif_xxxx

b src/main.cpp:127
b *address

# 数据断点,该位置上的数据改变
b *0x400522
b &变量名
# 删除断点
clear
delete
# 调用栈
backtrace
# 打开源码窗口
layout src

# 调试扩展
gdb --args ../php/bin/php -d "extension=ext/xmark/modules/xmark.so" ext/xmark/tests/003.php

vscode 远程 gdb调试

首先用vscode的remote插件脸上远程主机

然后是配置launch.json,配置好要调试的php(已debug编译),还有gdb位置以及gdb运行参数等。

{
    // Use IntelliSense to learn about related properties. 
    // Hover to see a description of an existing property.
    // For more information, please visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gcc - Recompile and debug php-cli",
            "type": "cppdbg",
            "request": "launch",
            "program": "/home/jrxnm/share/static_analysis/php-src/php/bin/php",
            "args": [
                "-d","extension=ext/xmark/modules/xmark.so",
                "-d","xmark.enable=1",
                "-d","xmark.enable_rename=1",
                "-d","auto_prepend_file=ext/xmark/tests/base.php",
                "${workspaceFolder}/ext/xmark/tests/003.php"
            ],
            "stopAtEntry": true,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "by gdb Enable neat printing",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "build",
            "miDebuggerPath": "/usr/bin/gdb"
        }
    ]
}
image-20210917140743880

https://www.fatalerrors.org/a/debugging-php-source-code-with-vscode.html

PHP生命周期

我们知道,php可以单独以CLI或FPM形式运行,也可以作为web服务器(如 apache)的模块运行。此时我们称为模块启动步骤(MINIT)

此后,php等待处理请求。CLI形式的php仅处理一个php脚本,而FPM或web服务器模块启动的php就要等待处理多个请求。在php处理每个请求时,php都会运行请求启动步骤(RINIT)

然后请求被php处理,解析php并生成内容,此时需要关闭这个请求以便准备处理下一个。这个阶段被称为请求关闭步骤(RSHUTDOWN)

php进程运行一段时间,最后的退出阶段被称为模块关闭步骤(MSHUTDOWN)

../../_images/php_classic_lifetime.png

并行

如果是CLI形式的话,php每次只处理一个请求,并不需要考虑并行的问题。但是通常php是运行在Web环境下的,必须要以某种形式实现并行。

php实现了以下两种并行策略。

  • 基于进程的模型
  • 基于线程的模型

基于进程的并行模型在处理每个请求时fock新的进程,这样的话在系统层面就把各请求的资源隔离开来。CLI、FPM、CGI通常会使用这种模型运行在UNIX系统下。

基于线程的并行模型只能将各个请求隔离在不同线程中,它要求php和扩展都是编译在线程安全ZTS模式下。一般windows系统才会用这种模式。

下面是基于进程的

img

下面是基于线程的

../../_images/php_lifetime_thread.png

生命周期钩子

php对上面每个生命周期都提供了一个生命周期钩子以供扩展开发者使用。

每个模块在构建时都要填充一个下面的这种结构体_zend_module_entry。其中包括这些生命周期钩子函数的指针。

struct _zend_module_entry {
    unsigned short size;                                /*
    unsigned int zend_api;                               * STANDARD_MODULE_HEADER
    unsigned char zend_debug;                            *
    unsigned char zts;                                   */

    const struct _zend_ini_entry *ini_entry;            /* 没用过 */
    const struct _zend_module_dep *deps;                /* 模块依赖 */
    const char *name;                                   /* 模块名称 */
    const struct _zend_function_entry *functions;       /* 模块发布函数 */

    int (*module_startup_func)(INIT_FUNC_ARGS);         /* MINIT() */
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);    /* MSHUTDOWN() */
    int (*request_startup_func)(INIT_FUNC_ARGS);        /* RINIT()          生命周期函数(钩子) */
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);   /* RSHUTDOWN()*/
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);      /* PHPINFO() */

    const char *version;                                /* 模块版本 */
                                                        //-------下面是 STANDARD_MODULE_PROPERTIES宏可填充的部分
    size_t globals_size;                                /*
#ifdef ZTS                                               *
    ts_rsrc_id* globals_id_ptr;                          *
#else                                                    * Globals management
    void* globals_ptr;                                   *
#endif                                                   *
    void (*globals_ctor)(void *global);                  * GINIT()
    void (*globals_dtor)(void *global);                  * GSHUTDOWN()/

    int (*post_deactivate_func)(void);                   /* PRSHUTDOWN() 很少使用的生命周期钩子 */
    int module_started;                                  /* 是否已启动模块(内部使用) */
    unsigned char type;                                  /* 模块类型(内部使用) */
    void *handle;                                        /* dlopen() 返回句柄 */
    int module_number;                                   /* 模块号 */
    const char *build_id;                                /* 构建编号, STANDARD_MODULE_PROPERTIES_EX 的一部分*/
};

可以看到除了上面说的MINIT()/RINIT()/RSHUTDOWN()/MSHUTDOWN()还多了几个钩子函数。其中PRSHUTDOWN()很少用到,在RSHUTDOWN()后运行,意思是POST请求关闭时的行为。

GINIT()MINIT()前运行,每为请求新建一个线程就会调用一次,所以如果是基于进程的并行模型就只会运行一次。通常可以在这里初始化全局变量。然后在RINIT()中重置全局变量。

GSHUTDOWN()是在每次退出线程时调用。在这里可以释放GINIT()中打开的资源。

MINFO() 在调用phpinfo或者其他方式获取扩展信息时会调用它。

../../_images/php_extensions_lifecycle.png

php扩展的编写

自动生成扩展结构

在php源码的ext目录存在名为ext_skel的可执行文件,执行可以自动生成以扩展名为名字的目录,里面包含了扩展的基本结构。

./ext_skel --extname=my_ext_name

声明注册函数

声明函数

我们这个扩展有啥函数,再写之前先用PHP_FUNCTION声明一下,找到某个头文件中,它可能也自带了一些测试的函数声明,括号中的参数为函数名

image-20210508163443854

声明函数可用PHP_FUNCTION也可以用ZEND_FUNCTION,它俩是一样的,下面是定义。

#define PHP_FUNCTION ZEND_FUNCTION
#define ZEND_FUNCTION(name)     ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS)
#define ZEND_FN(name) zif_##name

注册函数

找到一个zend_function_entry结构体中,使用PHP_PE注册刚刚声明的扩展函数

image-20210508164023357

zend_function_entry 的结构如下

typedef struct _zend_function_entry {
    const char *fname;
    zif_handler handler;
    const struct _zend_internal_arg_info *arg_info;
    uint32_t num_args;
    uint32_t flags;
} zend_function_entry;

ZEND_FE 也就是PHP_FE 就是简单构造zend_function_entry 结构的宏

#define ZEND_FE(name, arg_info)                     ZEND_FENTRY(name, ZEND_FN(name), arg_info, 0)
#define ZEND_FENTRY(zend_name, name, arg_info, flags)   { #zend_name, name, arg_info, (uint32_t) (sizeof(arg_info)/sizeof(struct _zend_internal_arg_info)-1), flags },
#define ZEND_FN(name) zif_##name

这样,使用ZEND_FE我们只需要定义函数名name,和arg_info参数。

声明参数

arg_info的结构体_zend_internal_arg_info为如下

typedef struct _zend_internal_arg_info {
    const char *name;
    zend_type type;
    zend_uchar pass_by_reference;
    zend_bool is_variadic;
} zend_internal_arg_info;

顾名思义,定义了参数名、类型、是否引用传参、是否为可变参数

我们知道上面ZEND_FE的参数arg_info是一个zend_internal_arg_info结构体数组或指针,php源码同样定义了很多宏让我们简便的声明参数。

可以看到ZEND_BEGIN_ARG_INFO_EX函数定义了一个zend_internal_arg_info数组,其参数允许你声明你的函数能接收多少个必要参数。ZEND_END_ARG_INFO实际就是},可以看到ZEND_BEGIN_ARG_INFO_EX是少一个}

#define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args)  \
    static const zend_internal_arg_info name[] = { \
        { (const char*)(zend_uintptr_t)(required_num_args), 0, return_reference, 0 },

#define ZEND_END_ARG_INFO()     };

放在ZEND_BEGIN_ARG_INFO_EXZEND_END_ARG_INFO之间的参数定义就是如下,每个参数都需要 ZEND_ARG_***_INFO() 之一。使用它你可以判断参数是否为 &$passed_by_ref 以及是否需要类型提示。

#define ZEND_ARG_INFO(pass_by_ref, name)
#define ZEND_ARG_OBJ_INFO(pass_by_ref, name, classname, allow_null)
#define ZEND_ARG_ARRAY_INFO(pass_by_ref, name, allow_null)
#define ZEND_ARG_CALLABLE_INFO(pass_by_ref, name, allow_null)
#define ZEND_ARG_TYPE_INFO(pass_by_ref, name, type_hint, allow_null)
#define ZEND_ARG_VARIADIC_INFO(pass_by_ref, name)

举例使用, 下面就是定义了3个参数,第一个无类型限制,第二个为array,第三个为可变参数,全都不是引用传参。

ZEND_BEGIN_ARG_INFO_EX(arginfo_jrxnm_argtest, 0, 0, 3)
    ZEND_ARG_INFO(0, arg1)
    ZEND_ARG_ARRAY_INFO(0, array_arg2)
    ZEND_ARG_VARIADIC_INFO(0, variadic_arg3)
ZEND_END_ARG_INFO()

还有个ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX宏,与ZEND_BEGIN_ARG_INFO_EX区别在于还限制了返回值类型。

#define ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(name, return_reference, required_num_args, type, allow_null) \
    static const zend_internal_arg_info name[] = { \
        { (const char*)(zend_uintptr_t)(required_num_args), ZEND_TYPE_ENCODE(type, allow_null), return_reference, 0 },

配置入口

就是在一个zend_module_entry结构体中填写扩展模块的入口信息,这个入口就是刚刚的zend_function_entry结构体内容,在上图中名字就是ext_functions

image-20210508164219465

整体的结构如下, 在这个入口中配置好了模块的基本信息比如,模块依赖、名称、包含的函数、还有生命周期的钩子函数。

struct _zend_module_entry {
    unsigned short size;                                /*
    unsigned int zend_api;                               * STANDARD_MODULE_HEADER
    unsigned char zend_debug;                            *
    unsigned char zts;                                   */

    const struct _zend_ini_entry *ini_entry;            /* 没用过 */
    const struct _zend_module_dep *deps;                /* 模块依赖 */
    const char *name;                                   /* 模块名称 */
    const struct _zend_function_entry *functions;       /* 模块发布函数 */

    int (*module_startup_func)(INIT_FUNC_ARGS);         /* MINIT() */
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);    /* MSHUTDOWN() */
    int (*request_startup_func)(INIT_FUNC_ARGS);        /* RINIT()          生命周期函数(钩子) */
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);   /* RSHUTDOWN()*/
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);      /* PHPINFO() */

    const char *version;                                /* 模块版本 */
                                                        //-------下面是 STANDARD_MODULE_PROPERTIES宏可填充的部分
    size_t globals_size;                                /*
#ifdef ZTS                                               *
    ts_rsrc_id* globals_id_ptr;                          *
#else                                                    * Globals management
    void* globals_ptr;                                   *
#endif                                                   *
    void (*globals_ctor)(void *global);                  *
    void (*globals_dtor)(void *global);                  */

    int (*post_deactivate_func)(void);                   /* PRSHUTDOWN() 很少使用的生命周期钩子 */
    int module_started;                                  /* 是否已启动模块(内部使用) */
    unsigned char type;                                  /* 模块类型(内部使用) */
    void *handle;                                        /* dlopen() 返回句柄 */
    int module_number;                                   /* 模块号 */
    const char *build_id;                                /* 构建编号, STANDARD_MODULE_PROPERTIES_EX 的一部分*/
};

看上面图片中的例子,我们可以利用STANDARD_MODULE_HEADERSTANDARD_MODULE_PROPERTIES宏来填充头尾中我们通常不需要配置的数据。

编写函数

在扩展名.c文件中编写扩展函数。

image-20210508164458090

上面的简单例子使用PHP_FUNCTION创建名为hello_jrxnm的扩展函数。我们将PHP_FUNCTION按宏展开

void zif_hello_jrxnm(zend_execute_data *execute_data, zval *return_value)
{

}

虽然这个函数是void的,但是接受了return_value参数,这样我们也能传递返回值。

关于接收的hello_jrxnm函数的参数在哪,查到的资料是这么解释的

image-20210831223209908

简单理解我们能从execute_data中获取。

解析参数

php源码提供了一个zend_parse_parameters函数,如下定义。第一个参数为解析几个参数,第二个为参数类型,后面的参数为按引用依次填进的变量。

ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...)

ZEND_NUM_ARGS是一个能返回参数个数的宏,我们直接用就行。"d"每一个参数都要写一个字母表示它的类型,这里的d就表示第一个参数为double/float(如果有两个参数且都为double/float那就可以写"dd")。后面的参数就是按引用解析参数到变量中。

这个函数成功则返回SUCCESS,失败返回FAILURE

PHP_FUNCTION(hello_jrxnm)
{
    double f;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "d", &f) == FAILURE) {
        return;
    }
}

这里这里我们可以看到有关字母与参数类型的对应关系,下面简单贴出一些。注意有些一个字母是代表多个解析的参数的,比如s代表字符串,但是解析两个参数一个是字符串本身一个是字符串长度。

 a  - array (zval*)
 A  - array or object (zval*)
 b  - boolean (zend_bool)
 C  - class (zend_class_entry*)
 d  - double (double)
 f  - function or array containing php method call info (returned as 
      zend_fcall_info and zend_fcall_info_cache)
 h  - array (returned as HashTable*)
 H  - array or HASH_OF(object) (returned as HashTable*)
 l  - long (zend_long)
 L  - long, limits out-of-range numbers to LONG_MAX/LONG_MIN (zend_long, ZEND_LONG_MAX/ZEND_LONG_MIN)
 o  - object of any type (zval*)
 O  - object of specific type given by class entry (zval*, zend_class_entry)
 p  - valid path (string without null bytes in the middle) and its length (char*, size_t)
 P  - valid path (string without null bytes in the middle) as zend_string (zend_string*)
 r  - resource (zval*)
 s  - string (with possible null bytes) and its length (char*, size_t)
 S  - string (with possible null bytes) as zend_string (zend_string*)
 z  - the actual zval (zval*)
 *  - variable arguments list (0 or more)
 +  - variable arguments list (1 or more)

除了直接使用zend_parse_parameters函数,php同样也提供了一些宏来简便写法。

ZEND_PARSE_PARAMETERS_START接收两个参数为最少参数个数和最大参数个数。

ZEND_PARSE_PARAMETERS_START(min_argument_count, max_argument_count)
ZEND_PARSE_PARAMETERS_END();

可用的参数宏可以列出如下(还有一些更复杂的宏可以在Zend/zend_API.h中查看),

Z_PARAM_ARRAY()                /* old "a" */
Z_PARAM_ARRAY_OR_OBJECT()      /* old "A" */
Z_PARAM_BOOL()                 /* old "b" */
Z_PARAM_CLASS()                /* old "C" */
Z_PARAM_DOUBLE()               /* old "d" */
Z_PARAM_FUNC()                 /* old "f" */
Z_PARAM_ARRAY_HT()             /* old "h" */
Z_PARAM_ARRAY_OR_OBJECT_HT()   /* old "H" */
Z_PARAM_LONG()                 /* old "l" */
Z_PARAM_STRICT_LONG()          /* old "L" */
Z_PARAM_OBJECT()               /* old "o" */
Z_PARAM_OBJECT_OF_CLASS()      /* old "O" */
Z_PARAM_PATH()                 /* old "p" */
Z_PARAM_PATH_STR()             /* old "P" */
Z_PARAM_RESOURCE()             /* old "r" */
Z_PARAM_STRING()               /* old "s" */
Z_PARAM_STR()                  /* old "S" */
Z_PARAM_ZVAL()                 /* old "z" */
Z_PARAM_VARIADIC()             /* old "+" and "*" */

//可选参数标志,表示下面的都是可选的参数
Z_PARAM_OPTIONAL              /* old "|" */

所以上面的解析参数也能写作下面方式

PHP_FUNCTION(hello_jrxnm)
{
    double f;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_DOUBLE(f);
    ZEND_PARSE_PARAMETERS_END();
}

函数返回

上面我们知道用C写的php函数都是void的,返回值都是通过设置zval *return_value值实现的。

php也给了处理函数返回操作的宏,RETURN_XXXX就是用来填充return_value然后返回的。

#define RETURN_BOOL(b)                  { RETVAL_BOOL(b); return; }
#define RETURN_NULL()                   { RETVAL_NULL(); return;}
#define RETURN_LONG(l)                  { RETVAL_LONG(l); return; }
#define RETURN_DOUBLE(d)                { RETVAL_DOUBLE(d); return; }
#define RETURN_STR(s)                   { RETVAL_STR(s); return; }
#define RETURN_INTERNED_STR(s)          { RETVAL_INTERNED_STR(s); return; }
#define RETURN_NEW_STR(s)               { RETVAL_NEW_STR(s); return; }
#define RETURN_STR_COPY(s)              { RETVAL_STR_COPY(s); return; }
#define RETURN_STRING(s)                { RETVAL_STRING(s); return; }
#define RETURN_STRINGL(s, l)            { RETVAL_STRINGL(s, l); return; }
#define RETURN_EMPTY_STRING()           { RETVAL_EMPTY_STRING(); return; }
#define RETURN_RES(r)                   { RETVAL_RES(r); return; }
#define RETURN_ARR(r)                   { RETVAL_ARR(r); return; }
#define RETURN_EMPTY_ARRAY()            { RETVAL_EMPTY_ARRAY(); return; }
#define RETURN_OBJ(r)                   { RETVAL_OBJ(r); return; }
#define RETURN_ZVAL(zv, copy, dtor)     { RETVAL_ZVAL(zv, copy, dtor); return; }
#define RETURN_FALSE                    { RETVAL_FALSE; return; }
#define RETURN_TRUE                     { RETVAL_TRUE; return; }

// RETVAL_XXXX的定义 举个例子
#define RETVAL_DOUBLE(d)                ZVAL_DOUBLE(return_value, d)

可以看到,RETURN_XXXXRETVAL_XXXX多一个return;

使用常量

常量有时需要是持久性的跨请求存在,所以最好是在MINIT 阶段向引擎注册。 常量的数据结构也很简单,常量名name、值value、还有一些标志位比如是否大小写敏感等。

typedef struct _zend_constant {
    zval value;
    zend_string *name;
    int flags;
    int module_number;
} zend_constant;

php给出了很多REGISTER_XXXX_CONSTANT的宏来注册各种类型的常量,加了ns的表示常量的命名空间。

image-20210904224633953

这些函数第一个参数为常量名,第二个为常量值,第三个为这个常量的标志位(CONST_CS表示大小写敏感,CONST_PERSISTENT表示持久的)。

#define TEMP_CONVERTER_TO_FAHRENHEIT 2
#define TEMP_CONVERTER_TO_CELSIUS 1

PHP_MINIT_FUNCTION(pib)
{
    REGISTER_LONG_CONSTANT("TEMP_CONVERTER_TO_CELSIUS", TEMP_CONVERTER_TO_CELSIUS, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("TEMP_CONVERTER_TO_FAHRENHEIT", TEMP_CONVERTER_TO_FAHRENHEIT, CONST_CS|CONST_PERSISTENT);

    return SUCCESS;
}

生命周期钩子函数

在生命周期部分介绍了生命周期,在这里介绍一下在扩展中的使用方法。

php提供了两种宏PHP_XXXXX_FUNCTIONPHP_XXXXX,其中的XXXXX为生命周期缩写如MINITRINIT

前者是钩子函数定义时使用的宏。

PHP_RINIT_FUNCTION(jrxnm){

}

后者在注册zend_module_entry的时候使用。

zend_module_entry jrxnm_module_entry = {
    STANDARD_MODULE_HEADER,
    "jrxnm",                    /* Extension name */
    ext_functions,                  /* zend_function_entry */
    NULL,                           /* PHP_MINIT - Module initialization */
    NULL,                           /* PHP_MSHUTDOWN - Module shutdown */
    PHP_RINIT(jrxnm),           /* PHP_RINIT - Request initialization */
    NULL,                           /* PHP_RSHUTDOWN - Request shutdown */
    PHP_MINFO(jrxnm),           /* PHP_MINFO - Module info */
    PHP_JRXNM_VERSION,      /* Version */
    STANDARD_MODULE_PROPERTIES
};

全局变量

php将全局变量分为请求绑定的全局变量(request-bound globals)和真全局变量(true globals)。

请求绑定的全局变量就是平时我们在php中使用的全局变量,它的值仅会在一次请求中被php函数方法共享。而真全局变量会在不同的请求中保留信息,但它必须是只读的,否则用户得自己实现在多线程/进程的处理模型下的全局变量的一致性问题。

下面我们分别分析一下php中这两种全局变量如何实现的。

request-bound globals

这有个internalsbook中的例子。

在扩展中定义了个全局变量rnd记录随机数,注册一个函数pib_guess,猜随机数正确返回true。

/* true C global */
static zend_long rnd = 0;

static void pib_rnd_init(void)
{
    /* Pick a number to guess between 0 and 100 */
    php_random_int(0, 100, &rnd, 0);
}

PHP_RINIT_FUNCTION(pib)
{
    pib_rnd_init();

    return SUCCESS;
}

PHP_FUNCTION(pib_guess)
{
    zend_long r;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &r) == FAILURE) {
        return;
    }

    if (r == rnd) {
        /* Reset the number to guess */
        pib_rnd_init();
        RETURN_TRUE;
    }

    if (r < rnd) {
        RETURN_STRING("more");
    }

    RETURN_STRING("less");
}

PHP_FUNCTION(pib_reset)
{
    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    pib_rnd_init();
}

很明显,如果在基于线程的并行模型下,多个线程访问到的随机数都是同一个,然而我们需要将各个请求隔离开来。

TSRM(Thread Safe Resource Manager)

线程安全资源管理器TSRM可以保证我们的全局变量在各个线程中隔离开来。TSRM中定义了一些宏让我们更方便的使用它。

首先使用ZEND_BEGIN_MODULE_GLOBALSZEND_END_MODULE_GLOBALS宏定义全局变量的结构体

ZEND_BEGIN_MODULE_GLOBALS(pib)
    zend_long rnd;
ZEND_END_MODULE_GLOBALS(pib)

/* Resolved as :
*
* typedef struct _zend_pib_globals {
*    zend_long rnd;
* } zend_pib_globals;
*/

然后用这个结构体初始化一个这个类型的变量。名字就为参数_globals

ZEND_DECLARE_MODULE_GLOBALS(pib)

/* Resolved as zend_pib_globals pib_globals; */

现在,我们需要创建一个统一的宏(在线程模式或进程模式)用PIB_G(v)来访问我们上面定义的全局变量。

// 如果是ZTS模式通过ZEND_MODULE_GLOBALS_ACCESSOR读取全局变量,否则直接读取
#ifdef ZTS
#define PIB_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(pib, v)
#else
#define PIB_G(v) (pib_globals.v)
#endif

这样我们就可以利用TSRM来正确的访问我们的全局变量。

下面也是phpinternalsbook中的例子。

// 定义全局变量结构体,包含三个全局变量,随机数rnd,当前分数cur_score,分数score
ZEND_BEGIN_MODULE_GLOBALS(pib)
    zend_long rnd;
    zend_ulong cur_score;
    zval scores;
ZEND_END_MODULE_GLOBALS(pib)

// 初始化上面的全局变量
ZEND_DECLARE_MODULE_GLOBALS(pib)

// 重置当前分数为0,重置随机数
static void pib_rnd_init(void)
{
    /* reset current score as well */
    PIB_G(cur_score) = 0;
    php_random_int(0, 100, &PIB_G(rnd), 0);
}

// 在GINIT阶段初始化所有全局变量为0
PHP_GINIT_FUNCTION(pib)
{
    /* ZEND_SECURE_ZERO is a memset(0). Could resolve to bzero() as well */
    ZEND_SECURE_ZERO(pib_globals, sizeof(*pib_globals));
}

// 定义参数
ZEND_BEGIN_ARG_INFO_EX(arginfo_guess, 0, 0, 1)
    ZEND_ARG_INFO(0, num)
ZEND_END_ARG_INFO()

// 在RINIT阶段初始化重置三个全局变量
PHP_RINIT_FUNCTION(pib)
{
    array_init(&PIB_G(scores));
    pib_rnd_init();

    return SUCCESS;
}

// RSHUTDOWN阶段释放资源
PHP_RSHUTDOWN_FUNCTION(pib)
{
    zval_dtor(&PIB_G(scores));

    return SUCCESS;
}

// 猜数函数
PHP_FUNCTION(pib_guess)
{
    zend_long r;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &r) == FAILURE) {
        return;
    }

    if (r == PIB_G(rnd)) {
        add_next_index_long(&PIB_G(scores), PIB_G(cur_score));
        pib_rnd_init();
        RETURN_TRUE;
    }

    PIB_G(cur_score)++;

    if (r < PIB_G(rnd)) {
        RETURN_STRING("more");
    }

    RETURN_STRING("less");
}

// 分数读取函数
PHP_FUNCTION(pib_get_scores)
{
    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    RETVAL_ZVAL(&PIB_G(scores), 1, 0);
}

// 重置函数
PHP_FUNCTION(pib_reset)
{
    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    pib_rnd_init();
}

// 扩展开放的三个函数
static const zend_function_entry func[] = {
    PHP_FE(pib_reset, NULL)
    PHP_FE(pib_get_scores, NULL)
    PHP_FE(pib_guess, arginfo_guess)
    PHP_FE_END
};

// 入口
zend_module_entry pib_module_entry = {
    STANDARD_MODULE_HEADER,
    "pib",
    func, /* Function entries */
    NULL, /* Module init */
    NULL, /* Module shutdown */
    PHP_RINIT(pib), /* Request init */
    PHP_RSHUTDOWN(pib), /* Request shutdown */
    NULL, /* Module information */
    "0.1", /* Replace with version number for your extension */
    PHP_MODULE_GLOBALS(pib),
    PHP_GINIT(pib),
    NULL,
    NULL,
    STANDARD_MODULE_PROPERTIES_EX
};

仍然是一个猜数的扩展,猜对了就将总猜测次数放入score全局变量中。

true globals

真全局变量是线程无保护的真C语言中的全局变量,所以要求我们在每个请求中对它是只读的。

因此php建议在 MINIT() or MSHUTDOWN()生命周期中对它进行修改。

编译加载

注:编译时要保证php, phpize, php-config同时为目标版本,可以使用update-alternatives切换。

phpize
./configure
make
make install

然后再php.ini中添加

extension="jrxnm.so"

reference

https://www.phpinternalsbook.com/

https://www.laruence.com/2018/04/08/3170.html

https://breeze2.github.io/blog/php-a-simple-extension