PHP `Extension` 开发:用 C 语言扩展 PHP 功能与性能优化

老铁们,大家好!今天咱来聊点儿刺激的——用 C 语言给 PHP 搞点儿“大保健”,啊不,是扩展它的功能,提升它的性能! 别怕,C 语言没那么可怕,咱用最接地气的方式,带你一步步玩转 PHP 扩展开发。

开场白:PHP 为啥需要 C 扩展?

PHP 够强大了吧?为啥还要用 C 扩展?原因很简单:

  • 性能瓶颈: PHP 毕竟是解释型语言,执行速度比编译型语言 C 慢。对于计算密集型任务,C 扩展能大幅提升性能。
  • 系统级操作: 有些底层系统操作,PHP 搞不定,或者搞起来很麻烦。C 扩展可以轻松搞定。
  • 复用现有 C/C++ 代码: 很多成熟的 C/C++ 库,可以直接封装成 PHP 扩展来使用,避免重复造轮子。
  • 安全考虑: 一些敏感操作,用 C 扩展实现更安全,避免 PHP 代码直接暴露敏感信息。

第一部分:环境搭建与基本框架

  1. 安装 PHP 开发环境: 这个不用多说,确保你的 PHP 版本高于 7.0,最好是 8.0+。

  2. 安装 PHP 开发包: 这是关键!不同系统安装方式不一样,但目的都是为了获得 phpizephp-config 这两个神器。

    • Debian/Ubuntu: sudo apt-get install php-dev
    • CentOS/RHEL: sudo yum install php-devel
    • macOS (Homebrew): brew install php (确保你的 PHP 版本和 Homebrew 安装的版本一致)
  3. 生成扩展框架: 打开终端,进入你想要存放扩展代码的目录,执行:

    phpize
    ./configure --with-php-config=/path/to/php-config
    make

    /path/to/php-config 是你的 php-config 文件的路径,可以用 which php-config 命令找到。

    成功执行后,会生成一堆文件,其中 config.m4 是配置文件,php_<your_extension>.c 是扩展的 C 代码文件。

    注意: 如果 phpize 提示找不到命令,说明你没安装或者没正确配置 PHP 开发包。

  4. 修改 config.m4 打开 config.m4 文件,找到下面这行:

    PHP_ARG_ENABLE(your_extension, whether to enable your_extension support,
    Make sure that the comment is aligned:
    [  --enable-your_extension  Enable your_extension support])

    your_extension 替换成你扩展的名字,比如 my_awesome_extension。然后去掉注释符号 dnl。修改后类似这样:

    PHP_ARG_ENABLE(my_awesome_extension, whether to enable my_awesome_extension support,
    Make sure that the comment is aligned:
    [  --enable-my_awesome_extension  Enable my_awesome_extension support])
  5. 编写 C 代码: 打开 php_<your_extension>.c 文件,你会看到一个基本的扩展框架。里面有很多注释,可以先忽略。

第二部分:编写你的第一个 PHP 扩展

咱来写一个简单的扩展,实现一个 my_add 函数,接受两个整数参数,返回它们的和。

  1. 修改 php_<your_extension>.c 在文件末尾,找到 PHP_FUNCTION(confirm_your_extension_compiled) 函数,把它改成我们的 my_add 函数:

    PHP_FUNCTION(my_add)
    {
        zend_long a, b;
    
        ZEND_PARSE_PARAMETERS_START(2, 2)
            Z_PARAM_LONG(a)
            Z_PARAM_LONG(b)
        ZEND_PARSE_PARAMETERS_END();
    
        RETURN_LONG(a + b);
    }
    • PHP_FUNCTION(my_add):定义一个 PHP 函数,名字是 my_add
    • zend_long a, b;:声明两个 zend_long 类型的变量,用来存储参数。zend_long 是 PHP 内部使用的整数类型。
    • ZEND_PARSE_PARAMETERS_START(2, 2)ZEND_PARSE_PARAMETERS_END():用来解析 PHP 函数的参数。
    • Z_PARAM_LONG(a)Z_PARAM_LONG(b):指定参数类型是 long,并将参数值分别赋值给变量 ab
    • RETURN_LONG(a + b):返回 a + b 的结果,类型是 long
  2. 注册函数: 找到 PHP_FE_END 前面的位置,添加一行代码,将 my_add 函数注册到 PHP 中:

    const zend_function_entry my_awesome_extension_functions[] = {
        PHP_FE(my_add,  NULL)        /* For testing, remove later. */
        PHP_FE_END    /* Must be the last line in my_awesome_extension_functions[] */
    };
    • PHP_FE(my_add, NULL):将 my_add 函数注册到 PHP 中。第一个参数是函数名,第二个参数是参数信息,这里我们先用 NULL
  3. 重新编译安装: 依次执行以下命令:

    phpize
    ./configure --with-php-config=/path/to/php-config
    make
    sudo make install

    注意: 每次修改 C 代码后,都需要重新编译安装。

  4. 修改 php.ini 找到你的 php.ini 文件,添加一行:

    extension=php_my_awesome_extension.so

    my_awesome_extension 替换成你的扩展名。

  5. 重启 Web 服务器: 重启 Apache 或 Nginx,让 PHP 加载新的扩展。
  6. 测试: 创建一个 PHP 文件,例如 test.php,内容如下:

    <?php
    echo my_add(10, 20); // 输出 30
    ?>

    在浏览器中访问 test.php,如果看到 30,恭喜你,你的第一个 PHP 扩展就成功了!

第三部分:深入了解 PHP 扩展开发

  1. 参数解析: ZEND_PARSE_PARAMETERS_STARTZEND_PARSE_PARAMETERS_END 是参数解析的核心。Z_PARAM_XXX 系列宏用于指定参数类型。常用的类型有:

    类型 说明
    long Z_PARAM_LONG 整数
    double Z_PARAM_DOUBLE 浮点数
    string Z_PARAM_STRING 字符串,需要手动释放内存
    string (copy) Z_PARAM_STR 字符串,自动拷贝,不需要手动释放内存
    bool Z_PARAM_BOOL 布尔值
    array Z_PARAM_ARRAY 数组
    object Z_PARAM_OBJECT 对象
    resource Z_PARAM_RESOURCE 资源
    null Z_PARAM_NULL NULL 值
    is_null Z_PARAM_ZVAL 任意类型,需要使用 Z_ISNULL 宏判断是否为 NULL

    示例:

    PHP_FUNCTION(my_function)
    {
        zend_long a;
        double b;
        char *str;
        size_t str_len;
    
        ZEND_PARSE_PARAMETERS_START(2, 3)
            Z_PARAM_LONG(a)
            Z_PARAM_DOUBLE(b)
            Z_PARAM_OPTIONAL
            Z_PARAM_STRING(str, str_len)
        ZEND_PARSE_PARAMETERS_END();
    
        // ... 使用 a, b, str ...
    
        efree(str); // 释放字符串内存
    }
    • Z_PARAM_OPTIONAL:表示后面的参数是可选的。
    • Z_PARAM_STRING(str, str_len):获取字符串参数,str 指向字符串内容,str_len 是字符串长度。 注意: 使用 Z_PARAM_STRING 获取的字符串,需要手动使用 efree() 函数释放内存。
  2. 返回值: RETURN_XXX 系列宏用于返回不同类型的值。常用的类型有:

    类型 说明
    long RETURN_LONG 整数
    double RETURN_DOUBLE 浮点数
    string RETURN_STRING 字符串,需要手动拷贝字符串
    string (copy) RETURN_STR 字符串,自动拷贝,不需要手动释放内存
    bool RETURN_TRUE, RETURN_FALSE 布尔值
    array RETURN_ARR 数组,返回的是 zend_array * 指针,需要先创建 zend_array 结构体
    object RETURN_OBJ 对象,返回的是 zend_object * 指针,需要先创建 zend_object 结构体
    resource RETURN_RES 资源,返回的是 zend_resource * 指针,需要先创建 zend_resource 结构体
    null RETURN_NULL NULL 值
    void RETURN_VOID 没有返回值

    示例:

    PHP_FUNCTION(my_string_function)
    {
        char *str = "Hello, world!";
        RETURN_STRING(estrdup(str)); // 使用 estrdup 拷贝字符串
    }
    
    PHP_FUNCTION(my_array_function)
    {
        zval return_value;
        array_init(&return_value); // 初始化数组
    
        add_assoc_string(&return_value, "key1", "value1");
        add_index_long(&return_value, 0, 123);
    
        RETURN_ZVAL(&return_value, 1, 0); // 返回数组,并释放 return_value 内存
    }
    • estrdup():用于拷贝字符串,返回的指针需要手动使用 efree() 释放内存。
    • array_init():初始化一个 zval 类型的变量,作为数组使用。
    • add_assoc_string()add_index_long():向数组中添加元素。
    • RETURN_ZVAL(&return_value, 1, 0):返回 zval 类型的变量,第一个参数是 zval 的指针,第二个参数表示是否拷贝 zval,第三个参数表示是否释放 zval
  3. 资源管理: 在 PHP 扩展中,经常需要操作资源,例如文件句柄、数据库连接等。为了避免内存泄漏,需要正确管理资源。PHP 提供了资源管理机制,使用 zend_resource 结构体来表示资源。

    示例:

    typedef struct {
        FILE *fp;
    } my_resource;
    
    PHP_FUNCTION(my_open_file)
    {
        char *filename;
        size_t filename_len;
        zend_resource *res;
        my_resource *my_res;
    
        ZEND_PARSE_PARAMETERS_START(1, 1)
            Z_PARAM_STRING(filename, filename_len)
        ZEND_PARSE_PARAMETERS_END();
    
        FILE *fp = fopen(filename, "r");
        if (!fp) {
            RETURN_FALSE;
        }
    
        my_res = emalloc(sizeof(my_resource));
        my_res->fp = fp;
    
        res = zend_register_resource(my_res, le_my_resource);
    
        RETURN_RES(res);
        efree(filename);
    }
    
    PHP_FUNCTION(my_read_file)
    {
        zend_resource *res;
        my_resource *my_res;
        char buf[1024];
    
        ZEND_PARSE_PARAMETERS_START(1, 1)
            Z_PARAM_RESOURCE(res)
        ZEND_PARSE_PARAMETERS_END();
    
        my_res = (my_resource *)zend_fetch_resource(res, "my_resource", le_my_resource);
    
        if (!my_res) {
            RETURN_FALSE;
        }
    
        size_t bytes_read = fread(buf, 1, sizeof(buf) - 1, my_res->fp);
        if (bytes_read > 0) {
            buf[bytes_read] = '';
            RETURN_STRINGL(buf, bytes_read);
        } else {
            RETURN_FALSE;
        }
    }
    
    PHP_FUNCTION(my_close_file)
    {
        zend_resource *res;
        my_resource *my_res;
    
        ZEND_PARSE_PARAMETERS_START(1, 1)
            Z_PARAM_RESOURCE(res)
        ZEND_PARSE_PARAMETERS_END();
    
        my_res = (my_resource *)zend_fetch_resource(res, "my_resource", le_my_resource);
    
        if (!my_res) {
            RETURN_FALSE;
        }
    
        fclose(my_res->fp);
        efree(my_res);
        zend_list_delete(Z_RES_HANDLE_P(res));
    
        RETURN_TRUE;
    }
    
    static void my_resource_dtor(zend_resource *rsrc)
    {
        my_resource *my_res = (my_resource *)rsrc->ptr;
        fclose(my_res->fp);
        efree(my_res);
    }
    
    PHP_MINIT_FUNCTION(my_awesome_extension)
    {
        le_my_resource = zend_register_list_destructors_ex(my_resource_dtor, NULL, "my_resource", module_number);
        return SUCCESS;
    }
    • my_resource 结构体:定义了资源的数据结构,这里包含一个文件指针 fp
    • zend_register_resource():注册资源,将 my_resource 结构体和资源类型 le_my_resource 关联起来。
    • zend_fetch_resource():获取资源,从 zend_resource 结构体中获取 my_resource 结构体的指针。
    • zend_list_delete():删除资源,释放资源占用的内存。
    • my_resource_dtor():资源析构函数,在资源被销毁时调用,用于释放资源。
    • zend_register_list_destructors_ex():注册资源析构函数,在 PHP_MINIT_FUNCTION 中调用。
  4. 异常处理: 在 C 代码中,如果发生错误,可以使用 zend_throw_exception 函数抛出 PHP 异常。

    示例:

    PHP_FUNCTION(my_divide)
    {
        zend_long a, b;
    
        ZEND_PARSE_PARAMETERS_START(2, 2)
            Z_PARAM_LONG(a)
            Z_PARAM_LONG(b)
        ZEND_PARSE_PARAMETERS_END();
    
        if (b == 0) {
            zend_throw_exception_ex(zend_ce_exception, 0, "Division by zero");
            RETURN_FALSE;
        }
    
        RETURN_LONG(a / b);
    }
    • zend_throw_exception_ex():抛出异常,第一个参数是异常类,第二个参数是异常代码,第三个参数是异常信息。

第四部分:性能优化技巧

  1. 避免内存拷贝: 尽量避免在 C 代码和 PHP 代码之间拷贝大量数据。可以使用指针传递数据,或者使用共享内存。
  2. 使用缓存: 对于重复计算的结果,可以使用缓存来避免重复计算。可以使用 PHP 的 apcu 扩展或者自己实现缓存。
  3. 减少函数调用: 函数调用会带来额外的开销,尽量减少函数调用次数。可以将一些常用的代码内联到函数中。
  4. 使用位运算: 位运算比算术运算更快,可以使用位运算来优化代码。
  5. 使用编译器优化: 在编译扩展时,可以使用编译器优化选项,例如 -O3,来提高性能。

第五部分:总结与展望

PHP 扩展开发是一项非常有用的技能,可以让你突破 PHP 的限制,实现更强大的功能,提升 PHP 的性能。虽然 C 语言学习曲线比较陡峭,但是只要掌握了基本概念和技巧,就可以轻松开发出高效稳定的 PHP 扩展。

未来的 PHP 扩展开发,将会更加注重性能优化、安全性和易用性。希望大家能够积极参与到 PHP 扩展开发的行列中来,为 PHP 生态系统贡献力量!

一些实用表格总结:

任务 常用命令/函数 说明
生成扩展框架 phpize, ./configure --with-php-config=/path/to/php-config, make 初始化扩展开发环境
参数解析 ZEND_PARSE_PARAMETERS_START, ZEND_PARSE_PARAMETERS_END, Z_PARAM_LONG, Z_PARAM_STRING, Z_PARAM_ARRAY, Z_PARAM_OBJECT 解析 PHP 函数传递的参数
返回值 RETURN_LONG, RETURN_STRING, RETURN_ARR, RETURN_OBJ, RETURN_NULL, RETURN_ZVAL 返回值给 PHP
内存管理 emalloc, efree, estrdup 分配和释放内存,字符串拷贝
资源管理 zend_register_resource, zend_fetch_resource, zend_list_delete, zend_register_list_destructors_ex 管理资源,例如文件句柄、数据库连接
异常处理 zend_throw_exception_ex 抛出 PHP 异常
编译安装 make, sudo make install 编译和安装扩展
配置文件 config.m4, php.ini 扩展配置文件和 PHP 配置文件
扩展生命周期函数 PHP_MINIT_FUNCTION, PHP_MSHUTDOWN_FUNCTION, PHP_RINIT_FUNCTION, PHP_RSHUTDOWN_FUNCTION, PHP_MINFO_FUNCTION 扩展模块初始化、关闭,请求初始化、关闭,模块信息

希望这次的讲座能帮到你!祝你开发顺利,做出牛逼的 PHP 扩展! 拜了个拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注