全国热线:4006-1777-31

Rust实现一个PHP扩展

我准备为 Rust库实现一个PHP扩展,这里 有一个示例,我还为我的 Rust selecta端口 实现了个 PHP扩展,两个示例都用了相同的外国函数接口foreign function interface(ffi)。我要确保挑选一个用了字符串的例子讲解,因为字符串会包含数字不介绍的额外的复杂性。

开始前的准备工作

注意:我创建了一个可以设置环境的 docker 容器 。

你将需要一个PHP的开发版本,如果有的话可以通过运行来测试它: 

$ which phpize

运行 which命令后能找到像 /usr/local/bin/phpize这样的提示信息的话,说明它是能正常工作的。否则你将需要运行管理器如在CentOS下可输入 yum install php-devel, 在 Debian/Ubuntu下可以输入 apt-get install php5-dev进行安装。当然你也可以自己从网上下载源代码进行手动编译安装。

编译扩展

我们的 Rust 库 公开了一个叫做ext_score的单函数,它有两个* const字符类型的参数,之后返回一个64位的浮点型参数(或双精度型)。为了构建Rust库:

$ cd rust $ cargo build

我们的 PHP 扩展 定义了一个名为score的函数,它可以把我们的PHP用户区和 ext_score Rust函数“粘”在一起。为了构建PHP扩展:

$ cd php-ext $ phpize $ ./configure --with-score=../rust/target/debug
        $ make $ php -d extension=modules/score.so -r "var_dump(score('vim', 'vi'));"

现在我们有了一个工作着的示例,这样可以探索每个文件实际上在做什么。

配置扩展

我要直接跳到autotools,我认为autoconf是“魔法”,而autoconf周围的PHP包装是“黑魔法”。然而,它是PHP扩展能工作的最大障碍,在这里所有的东西都会密集,需要整个博客都涉足“犄角旮旯”,通常,通过你复制、粘贴,它就会工作,但我要试着征服这些羁绊我的东西。如果你在这一节中成功了,接下来的事情将会简单许多。

如果这超过了你所需要的,你想要开始你的扩展硬编码, 我这样觉得 !你可以直接跳到我之后讨论源代码的地方。

这是我为我的扩展写的config.m4文件,让我们看看里面正在发生什么。

PHP_ARG_WITH(score,
    [whether to enable the "score" extension],
    [  --enable-score          Enable "score" extension support])

if test "$PHP_SCORE" != "no"; then

    SEARCH_PATH="/usr/local /usr"
    SEARCH_FOR="libscore.so"
    if test -r $PHP_SCORE/$SEARCH_FOR; then # path given as parameter
      SCORE_LIB_DIR=$PHP_SCORE
    else # search default path list
      AC_MSG_CHECKING([for score files in default path])
      for i in $SEARCH_PATH ; do
        if test -r $i/lib/$SEARCH_FOR; then
          SCORE_LIB_DIR=$i
          AC_MSG_RESULT(found in $i)
        fi
      done
    fi

    if test -z "$SCORE_LIB_DIR"; then
      AC_MSG_RESULT([not found])
      AC_MSG_ERROR([Please reinstall the score rust library])
    fi

    PHP_CHECK_LIBRARY(score, ext_score,
    [
        PHP_ADD_LIBRARY_WITH_PATH(score, $SCORE_LIB_DIR, SCORE_SHARED_LIBADD)
        AC_DEFINE(HAVE_SCORE, 1, [whether ext_score function exists])
    ],[
        AC_MSG_ERROR([ext_score function not found in libscore])
    ],[
        -L$SCORE_LIB_DIR -R$SCORE_LIB_DIR
    ])

    PHP_SUBST(SCORE_SHARED_LIBADD)
    PHP_NEW_EXTENSION(score, score.c, $ext_shared)
fi

Config.m4文件是一个组合,它有一些autoconf(AC)函数和一些自定义PHP函数。高的层次,我们正在编写一些能检测我们的Rust库存在于哪的代码,然后将这些信息添加到一个能自动生成的Makefile,它是从一个叫做configure的脚本中生成的。大多数配置脚本将会通过PHP工具为我们创建,然而,我们需要添加一些扩展的特定信息。

让我们从用PHP_ARG_WITH挂钩扩展到配置脚本开始,PHP_ARG_WITH函数有三个参数:

  • 扩展的名称。这将用来确定我们扩展变量的名称。在这个例子中,使用$PHP_SCORE。
  • 当./configure --with-score运行时,人类可读的字符串出现了。例如: 检查是否启用“score”扩展……是的,共享。
  • 当 / configure——help运行时,人类可读的字符串出现了。这就是为什么字符串的间距有点奇怪的原因。

现在运行./configure --with-score和配置脚本就可以知道我们正在讨论什么了。接下来,需要告诉配置脚本在哪里可以找到我们的库,这样它就可以将这些细节添加到Makefile里了。

没有头文件

PHP假定附带一个描述库公开函数的头文件库,但 Rust的FFI并不提供头文件。如果我们使用一个库,比如 gearman ,那么我们会希望 /usr /include/gearman.h存在。标准的PHP config.m4文件会使用这些头文件来检查一个库是否被安装。为了解决缺乏头文件的问题,我们可以寻找共享对象文件:SEARCH_FOR = " / lib / libscore.so "。现在,我们创建了一个可以起检查作用的Rust兼容文件,我们需要寻找它。

在我们开始检查我们在常用目录中的libscore.so 共享对象(例如/usr和/usr/local),我们想先允许通过./configure --with-score=/path/to/library覆盖。这对我们的Rust库与PHP扩展的结合非常有用。我们可以运行 cargo build ,之后会在/home/herman/projects/selecta/php-ext/target/debug/中安装libscore.so。然后我们可以用./configure --with-score=/home/herman/projects/selecta/php-ext/target/debug/配置PHP扩展。当我指定一个像这样的路径后,路径将会存储在 $ PHP_SCORE变量中。这让我们不用一遍遍地安装Rust库。如果没有指定覆盖,我们可以搜索一些常见的地方,还可以随时添加更多的目录去搜索,例如 /opt/local

链接之前的验证

我们定位了一个名为 libscore.so的文件,但我们需要确保它是一个有效的库文件。PHP_CHECK_LIBRARY函数用于验证我们的共享对象是否包含一个已知函数或符号。PHP_CHECK_LIBRARY函数有五个参数:

  • 库的名称。在我们的例子中编译时将被转换为-lscore。例如:score cc -o conftest -g -O0 -Wl,-rpath,/usr/local/lib -L/usr/local/lib -lscore conftest.c 。
  • 试图在我们的库中找到函数的名字。
  • 如果找到该函数后采取的行动。在我们的例子中,我们增加了 Makefile代码去反编译我们的库,还有定义在编译过程中使用的HAVE_SCORE。
  • 如果函数没有被发现则需要采取的行动。在我们的例子中 ,我们扔了一个带有人类可读错误信息的错误。
  • 设置额外的库定义。在我们的例子中 ,我们确保编译器知道在哪里可以找到共享对象。

PHP_ADD_LIBRARY_WTH_PATH函数接受以下三个参数:

  • 库的名称
  • 到达库的路径
  • 存储库信息的变量名称。我们将使用 PHP_SUBST

最后的步骤

我们就快成功了!

PHP_SUBST函数在 Makefile中添加一个变量的值。

PHP_NEW_EXTENSION函数需要大量的参数,但是我只使用这三个:

  • 扩展的名称
  • 用于构建扩展的源或文件的列表
  •  扩展应该动态加载还是静态编译。 $ext_shared变量会为这点设置适当的值

建立你自己的扩展

通常,你可以使用 ext_skel程序创建一个 PHP扩展,然而ext_skel生成 config.m4文件时做了一些Rust违反的假设。但这仍然是一个很好的起点,你可以改变想要生成的扩展目录,然后运行ext_skel:

$ cd /path/to/projects
$ /path/to/php-src/ext/ext_skel --ext-name=php-rust-ext

这将创建一个有下面文件的 /home/herman/projects/php-rust-ext目录: config.m4 config.w32 tests。我没有浏览 config.w32 因为它是 Windows的,当它们涉及到PHP和Windows时,我就显示出了可悲的无知。 config.m4有很多的评论来帮助你,你可以用我上面的笔记做出任何必要的改变。

修改config.m4

一旦你觉得正确设置了 config.m4文件,就运行 phpize命令。这个程序将为你的目录添加一群自动生成的文件。随时 .gitignore它们,但不要通过版本控制检查它们。最重要的是,它创造了我们将用于生成 Makefile的配置文件。

你需要更改 config.m4 ,以使你特定的扩展和库工作。如果你更改了 config.m4文件,之后也要确保phpize运行。如果你做出改变但之后仅仅运行了 ./configure --with-score,那么将不会在更改后得到任何好处。

扩展头文件

这是标准的 PHP扩展的头文件,该公约使用 php_[extension-name].h做为它的名字,在我们的例子中,写为 php_score.h 

#ifndef PHP_SCORE_H

#define PHP_SCORE_H

#define PHP_SCORE_EXTNAME "score"
#define PHP_SCORE_EXTVER  "1.0"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"

extern zend_module_entry score_module_entry;
#define phpext_score_ptr &score_module_entry

// Define our Rust foreign function interface (ffi) here
extern double ext_score(unsigned char *, unsigned int, char *, unsigned int);

#endif

你可以复制 /粘贴它们中的大多数,用你的扩展名称替换SCORE和scroe。我选择在这里定义分数库函数(score libraries functions),我们告诉编译器将一些外部代码定义为一个名叫 ext_score的函数。当我们使用这个 Rust函数时,这会使我们的代码成功编译。请确保列出了你所有的Rust库公开函数。

扩展的源代码

score.c文件有点长而且大多数都是无趣的,全部的 score.c文件在这里。让我们探索一个名为 score的 PHP用户区函数调用Rust ext_score函数的部分。

PHP_FUNCTION(score)
{
    char *choice;
    int choice_len;
    char *query;
    int query_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &choice, &choice_len, &query, &query_len) == FAILURE) {
        return;
    }

    double s = ext_score(choice, choice_len, query, query_len);

    RETURN_DOUBLE(s);
}

我们宣布了新的 PHP函数,它们使用PHP_FUNCTION宏传递函数名称。如果你正在使用gdb并且想拆散这个函数,这个宏会将它转换成 zif_[func-name]。在我们的例子中:是zif_score。zif代表Zend接口函数(Zend Interface Function)。你会注意到Zend这个词使用了很多次是因为这是PHP virutal机器的名字(也是vm公司创始人的名字)。

我们使用 zend_parse_parameters函数去解析被指定在我们的用户区的函数参量,在这种情况下,我们预计到两个字符串。这个函数可能看起来有点粗糙,因为它就是这样。在最后我将提供一些链接,更详细地解释这个函数是如何工作的。一句话,我们返回了两个non-null型终止char *值和对应其长度的整形数(ints)。

我们可以通过字符串到达 ext_score函数,得到一个结果,然后返回这个值到用户区PHP。我们现在有了一个可以工作的Rust库end-to-end PHP扩展。