Home  >  Article  >  Backend Development  >  [Translation] [php extension development and embedding] Chapter 20 - Advanced embedding of php

[Translation] [php extension development and embedding] Chapter 20 - Advanced embedding of php

黄舟
黄舟Original
2017-02-10 10:43:041769browse


Advanced Embedded

php’s embedded can provide more than just synchronous loading and Execute scripts. By understanding how the various parts of PHP's execution module are combined, even giving a script can be called back to your host application. This chapter will cover the benefits of the I/O hooks provided by the SAPI layer, and expand what you have already Learn from the execution module that obtained the information from the previous topic.

Callback to php

In addition to loading External scripts, similar to what you saw in the previous chapter, your PHP embedded application, will implement a command similar to user space eval().

int zend_eval_string(char *str, zval *retval_ptr,
                     char *string_name TSRMLS_DC)

here , str is the actual PHP script code to be executed, and string_name is an arbitrary description information associated with the execution. If an error occurs, PHP will use this description information as the "file name" in the error output. retval_ptr, you should have guessed it , it will be set to the return value produced by the passed code. Try creating a new project with the following code.

#include 

int main(int argc, char *argv[]) {
    PHP_EMBED_START_BLOCK(argc, argv)
        zend_eval_string("echo 'Hello World!';", NULL, "Simple Hello World App" TSRMLS_CC);
    PHP_EMBED_END_BLOCK()
    
    return 0;
}

Now use the command or Chapter 19 "Setting up the hosting environment" Build it (replace embed1 with embed2 in Makefile or command)

Alternative: Include of script file

Predictably, this makes compiling and executing external script files much easier than the previous method, because your application can transform the otherwise complex open/prepare/execute execution sequence into a simplified but more powerful Design alternatives:

##

#include 

int main(int argc, char *argv[]) {
    char    *filename;
    
    if ( argc <= 1 ) {
        fprintf(stderr, "Usage: %s  \n", argv[1]);
        return -1;
    }
    
    filename    = argv[1];
    
    /* 忽略第0个参数 */
    argc --;
    argv ++;
    
    PHP_EMBED_START_BLOCK(argc, argv)
        char    *include_script;
        
        spprintf(&include_script, 0, "include '%s';", filename);
        zend_eval_string(include_script, NULL, filename TSRMLS_CC);
        efree(include_script);
    PHP_EMBED_END_BLOCK()
    
    return 0;
}

## NOTE: This particular approach has one drawback that must be accepted, If the file name contains single quotes, it will cause a parsing error. However this can be solved by using the php_addslashes() API call in ext/standard/php_string.h. Spend some time reading this file and the API reference in the appendix, and you will find Lots of features that allow you to avoid reinventing the wheel in the future.

Calling user space functions

As you can see loading and executing script files, internally there are two ways to call user space functions. The most obvious one now is probably to reuse zend_eval_string(), which organizes the function name and all its parameters into one huge string , and then collect the return value.

    PHP_EMBED_START_BLOCK(argc, argv)
        char    *command;
        zval    retval;

        spprintf(&command, 0, "nl2br('%s');", argv[1]);
        zend_eval_string(command, &retval, "nl2br() execution" TSRMLS_CC);
        efree(command);
        printf("out: %s\n", Z_STRVAL(retval));
        zval_dtor(&retval);
    PHP_EMBED_END_BLOCK()

is very similar to the previous include. This method has a fatal flaw: if the input parameter paramin (the example given by the translator is argv[1 ]) given a wrong data, the function will fail, or worse lead to unexpected results. The solution is to always avoid compiling the runtime piece of code, and call the function directly using the call_user_function() API.

int call_user_function(HashTable *function_table, zval **object_pp,
                       zval *function_name, zval *retval_ptr,
                       zend_uint param_count, zval *params[] TSRMLS_DC);

When actually called from outside the engine, function_table is always EG(function_table). If calling an object or For class methods, object_pp needs to be the calling instance zval of type IS_OBJECT, or the value of IS_STRING for static calls to the class. function_name is usually the value of IS_STRING, containing the name of the function to be called, but it can also be IS_ARRAY, the 0th element Contains an object or class name, and the first element contains the method name.

The result of this function call is to set the return value to the zval pointed to by the incoming retval_ptr. param_count and params play the role of The role of argc/argv. That is, params[0] contains the first parameter passed, and params[param_count - 1] contains the last parameter passed.

下面是用这种方法重新实现上面的例子:

    PHP_EMBED_START_BLOCK(argc, argv)
        zval    *args[1];
        zval    retval, str, funcname;

        ZVAL_STRING(&funcname, "nl2br", 0);
        args[0] = &str;
        ZVAL_STRINGL(args[0], "HELLO WORLD!", sizeof("HELLO WORLD!"), 1);
        call_user_function(EG(function_table), NULL, &funcname, &retval, 1, args TSRMLS_CC);

        printf("out: %s\n", Z_STRVAL(retval));

        zval_dtor(args[0]);
        zval_dtor(&retval);
    PHP_EMBED_END_BLOCK()

尽管代码看起来比较长, 但是工作量会显著降低, 因为这里没有要编译的中间代码, 传递的数据不需要复制, 每个参数都已经在Zend兼容的结构体中. 同时, 要记得原来的例子中在字符串中包含单引号时会有潜在的错误. 而这个版本没有这个问题.

错误处理

当发生错误时, 比如脚本解析错误, php将会进入到bailout模式. 在你已经看到的简单的嵌入式例子中, 这表示它将直接跳到PHP_EMBED_END_BLOCK()宏, 并且绕过所有这个块中的剩余代码. 由于多数潜入php解释器的应用, 目的并不只是为了执行php代码, 因此避免由于php脚本的故障导致整个应用崩溃是有意义的.

有一种方式可以将所有的执行限制到一个非常小的START/END块中, 这样发生崩溃就只影响当前块. 这种方式的缺点是每个START/END块函数都是独立的PHP请求. 因此比如下面START/END块, 虽然从语法逻辑上来看两个块是协同工作的, 但实际上它们之间是不共享公共作用域的.

int main(int argc, char *argv[])
{
    PHP_EMBED_START_BLOCK(argc, argv)
        zend_eval_string("$a = 1;", NULL, "Script Block 1");
    PHP_EMBED_END_BLOCK()
    PHP_EMBED_START_BLOCK(argc, argv)
        /* 将打印出"NULL", 因为变量$a在这个请求中并没有定义. */
        zend_eval_string("var_dump($a);", NULL, "Script Block 2");
    PHP_EMBED_END_BLOCK()
    return 0;
}

还有一种解决方法是将两个zend_eval_string()调用使用Zend特有的伪语言结构zend_try, zend_catch, zend_end_try进行隔离. 使用这些结构, 你的应用就可以按照想要的方式处理错误. 考虑下面的代码:

int main(int argc, char *argv[])
{
    PHP_EMBED_START_BLOCK(argc, argv)
        zend_try {
            /* 尝试执行一些可能失败的代码 */
            zend_eval_string("$1a = 1;", NULL, "Script Block 1a");
        } zend_catch {
            /* 发生错误, 则尝试执行另外一部分代码(一般错误的补救或报告等行为) */
            zend_eval_string("$a = 1;", NULL, "Script Block 1");
        } zend_end_try();
        /* 这里将显示"NULL", 因为变量$a在这个请求中没有定义. */
        zend_eval_string("var_dump($a);", NULL, "Script Block 2");
    PHP_EMBED_END_BLOCK()
    return 0;
}

在这个示例的第二个版本中, zend_try块中将发生解析错误, 但它只影响自己的代码块, 同时在zend_catch块中使用了一段好的代码对错误进行了处理. 同样你也可以尝试自己给var_dump()部分也加上这些块.

译注: 这里对zend_try/zend_catch/zend_end_try解释的不是很清楚, 因此做以下补充说明. 读者阅读这一部分内容需要首先了解sigsetjmp()/siglongjmp()的机制(可以参考a823753b809442f15532a0b488672c3d第10章第15节).

相关的定义如下:

#ifdef HAVE_SIGSETJMP#   define SETJMP(a) sigsetjmp(a, 0)
#   define LONGJMP(a,b) siglongjmp(a, b)
#   define JMP_BUF sigjmp_buf
#else
#   define SETJMP(a) setjmp(a)
#   define LONGJMP(a,b) longjmp(a, b)
#   define JMP_BUF jmp_buf
#endif

#define zend_try                                                \
    {                                                           \
        JMP_BUF *__orig_bailout = EG(bailout);                  \
        JMP_BUF __bailout;                                      \
                                                                \
        EG(bailout) = &__bailout;                               \
        if (SETJMP(__bailout)==0) {
#define zend_catch                                              \
        } else {                                                \
            EG(bailout) = __orig_bailout;
#define zend_end_try()                                          \
        }                                                       \
        EG(bailout) = __orig_bailout;                           \
    }

zend_try {}代码块中的代码是在一个if语句中的, 这个if的条件是SETJMP(__bailout) == 0, SETJMP()是在当前程序执行的点设置一个可回溯的点(保存了当前执行上下文和环境), SETJMP()的返回比较特殊, 它有两种返回: 1) 直接返回, 此时返回值为0; 2) 调用LONGJMP()返回到对应__bailout当时调用SETJMP()的位置, 此时返回值非0.

基于上面的论述, 可以看出, 当zend_try的代码块中调用了LONGJMP()的时候, 程序将回到if ( SETJMP(__bailout) == 0 )的位置开始执行, 并且它的返回值为-1, 因此, 进入到对应的else语句块, 也就是zend_catch语句块的代码.

zend_end_try()则只是一个结尾的花括号.

php中的这个伪语言结构正式这种方式实现的异常处理机制, 在系统的关键点调用zend_bailout()(在Zend/zend.h中定义)即可.

本例中, 译者增加了zend_bailout()调用, 演示了这个伪语言结构的使用.

初始化php

迄今为止, 你看到的PHP_EMBED_START_BLOCK()和PHP_EMBED_END_BLOCK()宏都用于启动, 执行, 终止一个紧凑的院子的php请求. 这样做的优点是任何导致php bailout的错误顶多影响到PHP_EMBED_END_BLOCK()宏之内的当前作用域. 通过将你的代码执行放入到这两个宏之间的小块中, php的错误就不会影响到你的整个应用.

你刚才已经看到了, 这种短小精悍的方法主要的缺点在于每次你建立一个新的START/END块的时候, 都需要创建一个新的请求, 新的符号表, 因此就失去了所有的持久性语义.

要想同时得到两种优点(持久化和错误处理), 就需要将START和END宏分解为它们各自的组件(译注: 如果不明白可以参考这两个宏的定义). 下面是本章开始给出的embed2.c程序, 这一次, 我们对它进行了分解:

#include 

int main(int argc, char *argv[])
{
#ifdef ZTS
    void ***tsrm_ls;
#endif

    php_embed_init(argc, argv PTSRMLS_CC);
    zend_first_try {
        zend_eval_string("echo 'Hello World!';", NULL,
                         "Embed 2 Eval'd string" TSRMLS_CC);
    } zend_end_try();
    php_embed_shutdown(TSRMLS_C);

    return 0;
}

它执行和之前一样的代码, 只是这一次你可以看到打开和关闭的括号包裹了你的代码, 而不是无法分开的START和END块. 将php_embed_init()放到你应用的开始, 将php_embed_shutdown()放到末尾, 你的应用就得到了一个持久的单请求生命周期, 它还可以使用zend_first_try {} zend_end_try(); 结构捕获所有可能导致你整个包装应用跳出末尾的PHP_EMBED_END_BLOCK()宏的致命错误.

这个时候要注意, zend_first_try, 而不是zend_try. 在TRy/catch块外围使用zend_first_try非常重要, 因为zend_first_try执行了一些额外的步骤(清理EG(bailout))

为了看看真实世界环境的这种方法的应用, 我们将本章前面一些的例子的启动和终止处理进行了抽象:

#include 
#ifdef ZTS
    void ***tsrm_ls;
#endif
static void startup_php(void)
{
    /* Create "dummy" argc/argv to hide the arguments
     * meant for our actual application */
    int argc = 1;
    char *argv[2] = { "embed4", NULL };
    php_embed_init(argc, argv PTSRMLS_CC);
}
static void shutdown_php(void)
{
    php_embed_shutdown(TSRMLS_C);
}
static void execute_php(char *filename)
{
    zend_first_try {
        char *include_script;
        spprintf(&include_script, 0, "include '%s';", filename);
        zend_eval_string(include_script, NULL, filename TSRMLS_CC);
        efree(include_script);
    } zend_end_try();
}

int main(int argc, char *argv[])
{

    if (argc <= 1) {
        printf("Usage: embed4 scriptfile");
        return -1;
    }
    startup_php();
    execute_php(argv[1]);
    shutdown_php();
    return 0;
}

类似的概念也可以应用到处理任意代码的执行以及其他任务. 只需要确认在最外部的容器上使用zend_first_try, 则里面的每个容器上使用zend_try即可.

覆写INI_SYSTEM和INI_PERDIR选项

在上一章中, 你曾经使用zend_alter_ini_setting()修改过一些php的ini选项. 由于samp/embed直接将你的脚本推入了运行时模式, 因此许多重要的INI选项在控制返回到你的应用时并没有被修改. 为了修改这些值, 就需要在主引擎启动之后而请求启动之前执行代码.

有一种方式是拷贝php_embed_init()的内容到你的应用中, 在你的本地拷贝中做必要的修改, 接着使用你修改后的版本替代它. 当然这种方式可能会有问题.

首先也是最重要的, 你实际已经对别人的部分代码做了分支, 然而可能别人还会向其中添加新的代码. 现在, 你就不再是只维护自己的应用了, 还需要保持分支出来的代码和主分支保持一致. 幸运的是, 还有几种更简单的方法:

覆写默认的php.ini文件

因为嵌入式和其他的php sapi实现一样都是sapi, 它通过一个sapi_module_struct挂入到引擎中. 嵌入式SAPI定义并设置了这个结构体的一个实例, 你的应用可以在调用php_embed_init()之前访问它.

在这个结构体中, 有一个名为php_ini_path_override的char *类型字段. 为了让嵌入的请求使用你的可选文件扩展php和Zend, 只需要在调用php_embed_init()之前将这个字段设置为NULL终止的字符串. 下面是embed4.c中修改版的startup_php()函数:

static void startup_php(void)
{
    /* Create "dummy" argc/argv to hide the arguments
     * meant for our actual application */
    int argc = 1;
    char *argv[2] = { "embed4", NULL };

    php_embed_module.php_ini_path_override = "/etc/php_embed4.ini";
    php_embed_init(argc, argv PTSRMLS_CC);
}

这就使得每个使用嵌入库的应用可以保持自定义, 而不用将自己的配置暴露给别人. 相反, 如果你想要你的应用不使用php.ini, 只需要设置php_embed_module的php_ini_ignore字段, 这样所有的设置都将使用内建的默认值, 除非由你的应用手动进行修改.

覆写嵌入启动

sapi_module_struct结构还包含一些回调函数, 下面是其中4个在PHP启动和终止阶段比较有用的回调:

/* From main/SAPI.h */
typedef struct _sapi_module_struct {

    ...
    int (*startup)(struct _sapi_module_struct *sapi_module);
    int (*shutdown)(struct _sapi_module_struct *sapi_module);
    int (*activate)(TSRMLS_D);
    int (*deactivate)(TSRMLS_D);
    ...
} sapi_module_struct;

这些方法的名字熟悉吗? 它们对应于扩展的MINIT, MSHUTDOWN, RINIT, RSHUTDOWN, 并且和对应在扩展生命周期中的阶段一致. 要利用这些钩子, 可以如下修改embed4中的startup_php()函数:

static int (*original_embed_startup)(struct _sapi_module_struct *sapi_module);

static int embed4_startup_callback(struct _sapi_module_struct *sapi_module)
{
    /* 首先调用原来的启动回调, 否则环境未就绪 */
    if (original_embed_startup(sapi_module) == FAILURE) {
        /* 这里可以做应用的失败处理 */
        return FAILURE;
    }
    /* 调用原来的embed_startup实际上让我们进入到ACTIVATE阶段而不是STARTUP阶段, 
     * 但是我们仍然可以修改多数INI_SYSTEM和INI_PERDIR选项.
     */
    zend_alter_ini_entry("max_execution_time", sizeof("max_execution_time"),
                 "15", sizeof("15") - 1, PHP_INI_SYSTEM, PHP_INI_STAGE_ACTIVATE);
    zend_alter_ini_entry("safe_mode", sizeof("safe_mode"),
                 "1", sizeof("1") - 1, PHP_INI_SYSTEM, PHP_INI_STAGE_ACTIVATE);
    return SUCCESS;
}

static void startup_php(void)
{
    /* 创建假的argc/argv, 隐藏应用实际的参数 */
    int argc = 1;
    char *argv[2] = { "embed4", NULL };

    /* 使用我们自己的启动函数覆写标准的启动方法, 但是保留了原来的指针, 因此它仍然能够被调用到 */
    original_embed_startup = php_embed_module.startup;
    php_embed_module.startup = embed4_startup_callback;

    php_embed_init(argc, argv PTSRMLS_CC);
}

使用safe_mode, open_basedir这样的选项, 以及其他用以限制独立脚本行为的选项, 可以让你的应用更加安全可靠.

捕获输出

除非你开发的是非常简单的控制台应用, 否则你应该不希望php脚本代码产生的输出直接被扔到激活的终端上. 捕获这些输出和你刚才用以覆写启动处理器的方法类似.

在sapi_module_struct中还有一些有用的回调:

typedef struct _sapi_module_struct {
    ...
    int (*ub_write)(const char *str, unsigned int str_length TSRMLS_DC);
    void (*flush)(void *server_context);
    void (*sapi_error)(int type, const char *error_msg, ...);
    void (*log_message)(char *message);
    ...
} sapi_module_struct;

标准输出: ub_write

所有用户空间的echo和print语句产生的输出, 以及其他内部通过php_printf()或PHPWRITE()产生的输出, 最终都将被发送到激活的SAPI的ub_write()方法. 默认情况, 嵌入式SAPI直接将这些数据交给stdout管道, 而不关心你的应用的输出策略.

假设你的应用想要把所有的输出都发送到一个独立的控制台窗口; 你可能需要实现一个类似于下面伪代码块所描述的回调:

static int embed4_ub_write(const char *str, unsigned int str_length TSRMLS_DC)
{
    output_string_to_window(CONSOLE_WINDOW_ID, str, str_length);
    return str_length;
}

要让这个函数能够处理php产生的内容, 你需要在调用php_embed_init()之前对php_embed_module结构做适当的修改:

php_embed_module.ub_write = embed4_ub_write;

注意: 哪怕你决定你的应用不需要php产生的输出, 也必须为ub_write设置一个回调. 将它的值设置为NULL将导致引擎崩溃, 当然, 你的应用也不能幸免.

缓冲输出: Flush

你的应用可能会使用缓冲php产生的输出进行优化, sapi层提供了一个回调用以通知你的应用"现在请发送你的缓冲区数据", 你的应用并没有义务去实施这个通知; 不过, 由于这个信息通常是由于足够的理由(比如到达请求结束位置)才产生的, 听从这个意见并不会有什么坏处.

下面的这对回调函数, 以256字节缓冲区缓冲数据由引擎安排执行flush.

char buffer[256];
int buffer_pos = 0;
static int embed4_ubwrite(const char *str, unsigned int str_length TSRMLS_DC)
{
    char *s = str;
    char *d = buffer + buffer_pos;
    int consumed = 0;
    /* 缓冲区够用, 直接追加到缓冲区后面 */
    if (str_length < (256 - buffer_pos)) {
        memcpy(d, s, str_length);
        buffer_pos += str_length;
        return str_length;
    }
    consumed = 256 - buffer_pos;
    memcpy(d, s, consumed);
    embed4_output_chunk(buffer, 256);
    str_length -= consumed;
    s += consumed;
    /* 消耗整个传入的块 */
    while (str_length >= 256) {
        embed4_output_chunk(s, 256);
        s += 256;
        consumed += 256;
    }
    /* 重置缓冲区头指针内容 */
    memcpy(buffer, s, str_length);
    buffer_pos = str_length;
    consumed += str_length;
    return consumed;
}
static void embed4_flush(void *server_context)
{
    if (buffer_pos < 0) {
        /* 输出缓冲区中剩下的内容 */
        embed4_output_chunk(buffer, buffer_pos);
        buffer_pos = 0;
    }
}

在startup_php()中增加下面的代码, 这个基础的缓冲机制就就绪了:

php_embed_module.ub_write = embed4_ub_write;
php_embed_module.flush = embed4_flush;

标准错误: log_message

在启用了log_errors INI设置时, 在启动或执行脚本时如果碰到错误, 将激活log_message回调. 默认的php错误处理程序会在处理显示(这里是调用log_message回调)之前, 格式化这些错误消息, 使其称为整齐的, 人类可读的内容.

关于log_message回调, 这里你需要注意的第一件事是它并不包含长度参数, 因此它并不是二进制安全的. 也就是说, 它只是按照NULL终止来处理字符串末尾.

使用它来做错误报告通常不会有什么问题, 实际上, 它可以用于在错误消息的呈现上做更多的事情. 默认情况下, sapi/embed将会通过这个简单的内建回调, 发送这些错误消息到标准错误管道:

static void php_embed_log_message(char *message)
{
    fprintf (stderr, "%s\n", message);
}

如果你想发送这些消息到日志文件, 则可以使用下面的版本替代:

static void embed4_log_message(char *message)
{
    FILE *log;
    log = fopen("/var/log/embed4.log", "a");
    fprintf (log, "%s\n", message);
    fclose(log);
}

特殊错误: sapi_error

少数特殊情况的错误属于某个sapi, 因此将绕过php的主错误处理程序. 这些错误一般是由于使用不当造成的, 比如非web应用不应该使用header()函数, 上传文件到控制台应用程序等.

由于这些情况都离你所开发的sapi/embed应用非常遥远, 因此最好保持这个回调为空. 不过, 如果你非要坚持去捕获每种类型错误的源, 也只需要实现一个回调函数, 并在调用php_embed_init()之前覆写它就可以了.

同时扩展和嵌入

在你的应用中运行php代码固然不错, 但是此刻, php执行环境仍然和你的主应用是隔离的, 它们并没有在真正意义上的一个层级进行交互.

现在你应该对php扩展的开发以及构建启用方面比较熟悉了. 你也已经有完成了嵌入工作的例程, 这样就省去了这份工作. 将扩展代码植入到嵌入式应用中的工作量要比标准扩展小. 下面是一个新的嵌入式项目:

#include 
#ifdef ZTS
    void ***tsrm_ls;
#endif
/* Extension bits */
zend_module_entry php_mymod_module_entry = {
    STANDARD_MODULE_HEADER,
    "mymod", /* extension name */
    NULL, /* function entries */
    NULL, /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
    "1.0", /* version */
    STANDARD_MODULE_PROPERTIES
};
/* Embedded bits */
static void startup_php(void)
{
    int argc = 1;
    char *argv[2] = { "embed5", NULL };
    php_embed_init(argc, argv PTSRMLS_CC);
    zend_startup_module(&php_mymod_module_entry);
}
static void execute_php(char *filename)
{
    zend_first_try {
        char *include_script;
        spprintf(&include_script, 0, "include '%s'", filename);
        zend_eval_string(include_script, NULL, filename TSRMLS_CC);
        efree(include_script);
    } zend_end_try();
]
int main(int argc, char *argv[])
{
    if (argc <= 1) {
        printf("Usage: embed4 scriptfile";);
        return -1;
    }
    startup_php();
    execute_php(argv[1]);
    php_embed_shutdown(TSRMLS_CC);
    return 0;
}

现在, 你可以定义function_entry向量, 启动/终止函数, 定义类, 以及所有你想增加的东西. 现在, 它和你使用用户空间的dl()命令加载这个扩展库一样, 在这一个命令中Zend将自动的处理所有的钩子并对你的模块进行注册, 就绪等待使用.(译注: startup_php()中调用zend_startup_module(&php_mymod_module_entry)进行了模块注册)

小结

In this chapter you have looked at some simple embedded examples in the previous chapter and expanded them. You can already put PHP into various non-threaded applications. Now you have mastered the expansion and embedded Basics, and can work on zval, class, resource, HashTable, you can already really start a real project.

In the remaining appendices, you will see There are many API functions exposed by php, zend and other extensions. You will see some commonly used code snippets and hundreds of open source PECL projects in recent years, which can be used as a reference for your future projects.

The above is the content of [Translation][php extension development and embedded] Chapter 20-Advanced Embedding of PHP. For more related content, please pay attention to the PHP Chinese website ( m.sbmmt.com)!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn