如何基於 PHP-X 快速開發一個 PHP 擴充套件

韓天峰發表於2019-02-16

PHP-X是我在2017年年初建立的一個新專案。這個專案的目標就是讓有一定工作經驗的PHP程式都能夠具備擴充套件開發的能力

0x00 初衷

2012年開始編寫swoole,現在算來已經有5個年頭了。我發現編寫一個 PHP 擴充套件這個工作非常艱難。PHP 程式設計師群體中,甚至可以說 100 人中都很難找出一個會編寫 PHP 擴充套件的人來。PHP 官方對擴充套件開發者非常不友好,原始碼中提供的Zend API極其難用,API複雜而且凌亂,充斥著各種巨集的寫法。Zend API坑非常多,普通開發者很容易踩到坑裡。出現各種莫名其妙的core dump問題。Zend API幾乎沒有任何文件,開發者如果要真正掌握這項技能需要付出大量的學習時間。

於是我今年就冒出一個新的想法,基於我編寫swoole擴充套件超過5年的經驗,我試圖在Zend APIC++之間建立一個包裝層,讓PHP擴充套件開發變得簡單。有一定C++基礎的PHPer都可以輕鬆得開發一個PHP擴充套件。

PHP-X這個專案就這樣誕生了,開發只用了一個月的時間。它的開發效率非常高,在我公司中一個只工作了3年的 PHP 程式設計師,都可以做出一個擴充套件來。接下來陸續在公司的幾個專案中進行了快速驗證。在3個的時間裡修復了大量崩潰和記憶體洩漏問題。目前穩定性、效能、健壯性均已達到工業級水準。

0x01 起步

PHP-X本身基於C++11開發,使用cmake進行編譯配置。首先,你需要確定所有依賴項已安裝好。包括:

  • gcc-4.8 或更高版本
  • php-7.0 或更高版本,需要php7-dev
  • cmake-2.8 或更高版本

然後安裝PHP-X

git clone https://github.com/swoole/PHP-X.git
cd PHP-X
cmake .
make -j 4
sudo make install

未出現任何編譯錯誤,會成功編譯出libphpx.so,並安裝到系統的lib目錄。標頭檔案會複製到系統的include目錄。這時需要執行 sudo ldconfig重新整理so檔案快取。

0x02 新建工程

使用任意開發工具,新建一個test.cc原始檔。首先需要引入phpx.h標頭檔案。然後使用using引入phpx的名稱空間。PHP官方未使用C++,因此phpx直接使用了php作為名稱空間。

#include <phpx.h>
using namespace std;
using namespace php;

建立擴充套件使用PHPX_EXTENSION巨集來實現。在這巨集中只需要new Extension即可建立擴充套件。構造方法接受2個引數,第一個是擴充套件的名稱,第二個是擴充套件的版本號。在PHPX_EXTENSION巨集中return這個擴充套件物件的指標。

PHPX_EXTENSION()
{
    Extension *ext = new Extension("test", "0.0.1");
    return ext;
}

這裡必須使用 new Extension,而不能直接在棧上建立物件

0x03 增加函式

一個PHP擴充套件的主要作用就是提供擴充套件函式,擴充套件函式由於是用C/C++程式碼實現,因此它的效能會比PHP使用者函式效能高几十甚至上百倍。在phpx中實現函式非常簡單。使用PHPX_FUNCTION來實現擴充套件函式,然後呼叫Extension::registerFunction來註冊擴充套件函式。

PHPX_FN是一個助手巨集,實際上展開就是"cpp_hello_world", cpp_hello_world
PHPX_FUNCTION展開後,包含了2個變數,第一個是引數args,第二個是返回值retval
通過操作argsretval兩個變數,就可以實現函式的輸入和輸出

這裡我們的程式碼非常簡單,cpp_test($str, $n),呼叫這個函式返回一個$n$str的陣列。

#include <phpx.h>
using namespace std;
using namespace php;

//宣告函式
PHPX_FUNCTION(cpp_test);

PHPX_EXTENSION()
{
    Extension *ext = new Extension("test", "0.0.1");
    ext->registerFunction(PHPX_FN(cpp_test));
    return ext;
}

//實現函式
PHPX_FUNCTION(cpp_test)
{
    //args[1] 就是這個擴充套件函式的第 2 個引數
    long n = args[1].toInt();
    
    //將返回值 retval 初始化為陣列
    Array _array(retval);
    
    for(int i = 0; i < n; i++)
    {
        //args[0] 就是這個擴充套件函式的第 1 個引數
        //append 方法表示向陣列中追加元素
        _array.append(args[0]);
    }
}

0x04 編譯擴充套件

編寫一個Makefile檔案。內容如下:

PHP_INCLUDE = `php-config --includes`
PHP_LIBS = `php-config --libs`
PHP_LDFLAGS = `php-config --ldflags`
PHP_INCLUDE_DIR = `php-config --include-dir`
PHP_EXTENSION_DIR = `php-config --extension-dir`

test.so: test.cc
    c++ -DHAVE_CONFIG_H -g -o test.so -O0 -fPIC -shared test.cc -std=c++11 ${PHP_INCLUDE} -I${PHP_INCLUDE_DIR} -lphpx
    
install: test.so
    cp test.so ${PHP_EXTENSION_DIR}/
clean:
    rm *.so

php-config 這個工具是PHP提供的,使用php-config可以得到PHP的安裝路徑、標頭檔案目錄、擴充套件目錄、其他額外的編譯引數等等。

這個Makefile支援了3個指令,make編譯,make clean清理,make install安裝到擴充套件目錄中。

這裡可能需要root許可權,使用sudo make install進行安裝
直接從網頁複製,可能會出現tab製表符被替換為空格,請手工編輯一下Makefile使用tab縮排
MacOS下需要在c++編譯引數中增加-undefined dynamic_lookup

編寫好之後執行make install,就會編譯擴充套件並將擴充套件test.so安裝到PHP的擴充套件目錄中。這時需要修改php.ini加入extension=test.so載入擴充套件。

使用php -m來觀察你的擴充套件是否正常載入。

php -m
[PHP Modules]
Core
ctype
curl
date
dom
fileinfo
filter
gd
hash
iconv
inotify
json
libxml
mbstring
mcrypt
memcached
mongodb
mysqli
mysqlnd
openssl
pcntl
pcre
PDO
pdo_mysql
pdo_sqlite
Phar
posix
redis
Reflection
session
SimpleXML
sockets
SPL
sqlite3
standard
swoole
test
tokenizer
xml
xmlreader
xmlwriter
yac
zlib
zmq

[Zend Modules]

這裡看到test,表明你的擴充套件已經載入成功了,現在就可以呼叫cpp_test這個擴充套件函式了。

0x05 執行

編寫一個test.php,內容為:

<?php
var_dump(cpp_test("hello", 3));

執行test.php

php test.php
array(3) {
  [0]=>
  string(5) "hello"
  [1]=>
  string(5) "hello"
  [2]=>
  string(5) "hello"
}

可以看到執行結果符合預期。那麼恭喜你,現在你已經成功地開發了一個PHP擴充套件了。是不是很簡單?

0x06 更多

上面的例子還比較簡單,只是編寫了一個擴充套件函式。要真正在實際專案中使用PHP-X你還有很多工作要做。

  • 需要C++的功底
  • 瞭解更多PHP-XAPI

另外配合使用EclipseIDE工具,可以實現API自動提示和補齊,開發起來會更順手。

相比Zend APIPHP-X要簡單易用地多了,相信你不會花太多時間就可以掌握此項技能。在接下來我會撰寫更多教程,教大家如何使用PHP-X實現擴充套件類、資源、回撥函式等更復雜的功能。

相關文章