PHP原始碼系列之擴充套件的原理與開發

Kuper發表於2021-09-20

1、本文使用原始碼版本為PHP-7.1.19
2、本文安裝的PHP版本為7.1.19
3、使用電腦為Mac,作業系統資訊如下

Darwin Kernel Version 18.0.0: root:xnu-4903.201.2~1/RELEASE_X86_64 x86_64

4、本文提到的擴充套件開發是PHP擴充套件,而不是Zend擴充套件
5、文章原文來自於部落格,關注獲取最新文章

1、什麼是PHP擴充套件

/php-src/Zend/zend_modules.h標頭檔案中定義了_zend_module_entry結構體,這個結構體就是PHP擴充套件的結構體,稱為module,除了一些基本資訊外,主要提供了以下個鉤子函式

  • MINT :模組初始化時被呼叫

  • MSHUTDOWN :模組結束化時被呼叫

  • RINT :每一次請求時被呼叫

  • RSHUTDOWN :每一次請求結束後被呼叫

    struct _zend_module_entry {
      // 基本資訊一般通過STANDARD_MODULE_HEADER常量填充即可
      unsigned short size;
      unsigned int zend_api;
      const struct _zend_ini_entry *ini_entry;
      int module_started;
      int module_number;
    
      // 擴充套件的名稱
      const char *name;
    
      // 擴充套件的函式指標, 用於獲取擴充套件的函式列表
      const struct _zend_function_entry *functions;
    
      // MINT鉤子函式
      int (*module_startup_func)(INIT_FUNC_ARGS);
    
      // MSHUTDOWN鉤子函式
      int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
    
      // RINT鉤子函式
      int (*request_startup_func)(INIT_FUNC_ARGS);
    
      // RSHUTDOWN鉤子函式
      int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
    
      // 呼叫phpinfo()時列印擴充套件資訊
      void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
    };

    2、什麼是Zend擴充套件

    /php-src/Zend/zend_extensions.h標頭檔案中定義了_zend_extension結構體,這個結構體就是Zend擴充套件的結構體,稱為extension。相比PHP擴充套件,主要提供了更底層的鉤子函式,如下所示

    struct _zend_extension {
      // 一些基本資訊
      char *name;
      char *version;
      char *author;
      char *URL;
      char *copyright;
    
      /*Zend生命週期內的一些鉤子函式*/
      startup_func_t startup;
      shutdown_func_t shutdown;
      activate_func_t activate;
      deactivate_func_t deactivate;
    
      message_handler_func_t message_handler;
    
      op_array_handler_func_t op_array_handler;
    
      statement_handler_func_t statement_handler;
      fcall_begin_handler_func_t fcall_begin_handler;
      fcall_end_handler_func_t fcall_end_handler;
    
      op_array_ctor_func_t op_array_ctor;
      op_array_dtor_func_t op_array_dtor;
      /*Zend生命週期內的一些鉤子函式*/
    };

    3、舉例

    (1) Json擴充套件

    Json擴充套件定義結構體為zend_module_entry,可知它是PHP擴充套件

image.png

(2) Opcache擴充套件

Opcache擴充套件定義結構體為zend_extension,可知它是Zend擴充套件

image.png

(3) Xdebug擴充套件

Xdebug擴充套件必須在Zend生命週期內Hook才能實現對程式碼的除錯,所以Xdebug是Zend擴充套件

4、總結

擴充套件是區分php擴充套件和zend擴充套件的,在PHP原始碼中也嚴格區分moduleextension這兩個定義

  • module表示php extension,即PHP的擴充套件,通過extension=*載入
  • extension表示zend extension,即Zend的擴充套件,通過zend_extension=*載入

1、目錄結構

在原始碼中的php-src/ext目錄就是擴充套件目錄,如jsonmysqlipdo等常用的擴充套件,其中每個擴充套件都主要由以下檔案組成

  • tests :單元測試目錄
  • config.m4 :擴充套件的編譯配置檔案(Unix系統)
  • config.w32 :擴充套件的編譯配置檔案(Windows系統)
  • php_{$module}.h :擴充套件的標頭檔案
  • {$module}.c :擴充套件原始檔
  • {$module}.php :擴充套件的測試檔案

2、程式碼結構

(1) 單元測試

在編譯擴充套件成功後,會在擴充套件目錄下生成一個run-test.php指令碼檔案,這個指令碼會自動執行tests目錄下的所有單元測試。

此外在擴充套件目錄下還會自動生成一個{$module}.php擴充套件的測試檔案,可以方便的測試擴充套件是否可以正常載入和使用

(2) 編譯配置檔案

擴充套件下載後只有原始碼,需要進行編譯生成.so擴充套件檔案後才可以被PHP使用,config.m4config.w32這兩個檔案就是在後續執行phpize階段會使用到的編譯配置檔案。

m4是一種巨集處理檔案,主要由PHP_ARG_WITHPHP_ARG_ENABLE兩部分構成,一般使用第二部分即可,用於開啟指定的擴充套件。這樣在編譯階段,就會判斷$PHP_PHP_HELLO變數不是no,從而執行此擴充套件的編譯。

其中dnl巨集會刪除本行多餘的字元,可以簡單的理解為註釋,如下所示,如果需要編譯php_hello這個擴充套件,把PHP_ARG_ENABLE部分最前面的dnl巨集都去掉即可

dnl If your extension references something external, use with:

dnl PHP_ARG_WITH(php_hello, for php_hello support,
dnl Make sure that the comment is aligned:
dnl [  --with-php_hello             Include php_hello support])

dnl Otherwise use enable:

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

if test "$PHP_PHP_HELLO" != "no"; then
  PHP_NEW_EXTENSION(php_hello, php_hello.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi

(3) 擴充套件標頭檔案

一般名為php_{$module}.h的是擴充套件標頭檔案,一般用於定義需要的常量和函式等

(4) 擴充套件原始檔

一般名稱為{$module}.c的是擴充套件原始檔,主要由以下部分組成

  • zend_module_entry :定義擴充套件的結構體
  • PHP_FUNCTION :定義擴充套件的函式
  • Hook_FUNCTION :如 PHP_MINIT_FUNCTION

(1) 原始碼註釋

php-src/main/main.c檔案中,php_module_startup()函式會執行擴充套件的載入與初始化。

int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules)
{

    // Zend引擎初始化
    zend_startup(&zuf, NULL);

    // 註冊常量
    php_output_register_constants();
    php_rfc1867_register_constants();

    // 註冊ini配置
    if (php_init_config() == FAILURE) {
        return FAILURE;
    }
    REGISTER_INI_ENTRIES();
    zend_register_standard_ini_entries();

    // 註冊$_GET/$_POST/$_SERVER/$_REQUEST等全域性變數
    php_startup_auto_globals()

    // 載入靜態編譯的擴充套件這些擴充套件包含在main/internal_functions.c中
    if (php_register_internal_extensions_func(TSRMLS_C) == FAILURE) {
        php_printf("Unable to start builtin modules\n");
        return FAILURE;
    }

    // 註冊SAPI的擴充套件模組,即additional_modules中的擴充套件
    php_register_extensions_bc(additional_modules, num_additional_modules TSRMLS_CC);

    // 根據ini配置,先載入Zend擴充套件, 再載入PHP擴充套件
    php_ini_register_extensions(TSRMLS_C);

    // 擴充套件初始化, 觸發MINT()鉤子
    zend_startup_modules(TSRMLS_C);
    zend_startup_extensions();
}

(2) php_ini_register_extensions函式

在此函式中,extension_lists是一個儲存著解析ini配置後獲得的所有擴充套件(包括PHP擴充套件和Zend擴充套件)的連結串列,通過使用&extension_lists.engine&extension_lists.functions獲取PHP擴充套件列表和Zend擴充套件連結串列,然後通過php_load_zend_extension_cb()php_load_php_extension_cb()分別完成不同型別擴充套件的載入

void php_ini_register_extensions(void)
{
    //註冊zend擴充套件
    zend_llist_apply(&extension_lists.engine, php_load_zend_extension_cb);
    //註冊php擴充套件
    zend_llist_apply(&extension_lists.functions, php_load_php_extension_cb);

    zend_llist_destroy(&extension_lists.engine);
    zend_llist_destroy(&extension_lists.functions);
}

(3) 擴充套件的生命週期

如在PHP擴充套件與Zend擴充套件一節中看到的,這兩種擴充套件分別提供了不同的鉤子函式,這些函式在PHP生命週期內的呼叫順序如下圖所示

JBd05JU8k4.png

1、獲取PHP原始碼

獲取PHP原始碼後,切換至7.1.19版本,按如下命令操作

git clone https://github.com/php/php-src
git checkout remotes/origin/PHP-7.1.19

2、生成擴充套件的基礎檔案

切換到ext擴充套件目錄下,在此目錄下,有一個ext_skel指令碼,可以用來自動生成擴充套件的基礎檔案。比如建立一個print_hello的擴充套件,按如下命令操作

cd php-src/ext/
./ext_skel --extname=print_hello

執行成功後,會得到如下提示

img.png

回到在ext目錄下,發現已經成功生成print_hello目錄,主要包含如下檔案

  • tests :單元測試目錄
  • config.m4 :擴充套件的編譯配置檔案(Unix系統)
  • config.w32 :擴充套件的編譯配置檔案(Windows系統)
  • php_print_hello.h :擴充套件的標頭檔案
  • print_hello.c :擴充套件原始檔
  • print_hello.php :擴充套件的測試檔案

image.png

3、修改編譯配置檔案

開啟config.m4配置檔案,如下所示

image.png

找到PHP_ARG_ENABLE這段程式碼,刪掉前面的dnl巨集,如下所示

# 修改前
dnl PHP_ARG_ENABLE(print_hello, whether to enable print_hello support,
dnl Make sure that the comment is aligned:
dnl [  --enable-print_hello           Enable print_hello support])

# 修改後
PHP_ARG_ENABLE(print_hello, whether to enable print_hello support,
Make sure that the comment is aligned:
[  --enable-print_hello           Enable print_hello support])

4、修改print_hello.c檔案

找到PHP_FUNCTION(表示定義的擴充套件函式),在如下confirm_print_hello_compiled函式中新增一句輸出hello world的程式碼

PHP_FUNCTION(confirm_print_hello_compiled)
{
    char *arg = NULL;
    size_t arg_len, len;
    zend_string *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    strg = strpprintf(0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "print_hello", arg);

  // 新增下面輸出hello world的程式碼
  php_printf("hello world!\n");

  RETURN_STR(strg);
}

5、編譯擴充套件

通過執行以下命令執行對擴充套件的編譯處理

cd print_hello
phpize
./configure --with-php-config=/usr/bin/php-config
make
sudo make

執行make命令成功後如下所示

img.png

執行sudo make install命令成功後如下所示

image.png

6、執行擴充套件測試指令碼

測試指令碼會先動態載入print_hello擴充套件,並輸出擴充套件中所有提供的函式,最後執行在print_hello.c原始檔中定義的confirm_print_hello_compiled 函式,如果正常執行則說明擴充套件載入且執行成功

$br = (php_sapi_name() == "cli")? "":"<br>";

// 判斷擴充套件是否已載入
if(!extension_loaded('print_hello')) {
    // 在執行時動態載入擴充套件庫
    // 如果載入失敗,需要修改php.ini配置檔案,直接開啟動態載入擴充套件的選項enable_dl = On即可,在命令列下執行不需要重啟PHP
    dl('print_hello.' . PHP_SHLIB_SUFFIX);
}
$module = 'print_hello';

// 依次輸出擴充套件提供的所有函式
$functions = get_extension_funcs($module);
echo "Functions available in the test extension:$br\n";
foreach($functions as $func) {
    echo $func."$br\n";
}
echo "$br\n";

// 如果擴充套件載入成功, 則執行 confirm_print_hello_compiled 函式
$function = 'confirm_' . $module . '_compiled';
if (extension_loaded($module)) {
    $str = $function($module);
} else {
    $str = "Module $module is not compiled into PHP";
}

echo "$str\n";

指令碼執行成功後如下所示

image.png

7、結束

到目前為止,簡單的print_hello擴充套件就已經開發完成,當然還可以在print_hello.c原始檔中定義更多的擴充套件函式,做更多有趣的事情!不過篇幅有限就不再講解,關於擴充套件的高階使用請關注部落格,獲取最新文章!

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章