【译】从 Rust 到不仅是 Rust:PHP 语言领域

From Rust to beyond: The PHP galaxy 译文php

这篇博客文章是“如何将 Rust 传播到其余语言领域”系列文章之一。Rust 完成进度:html

咱们今天探索的领域是 PHP 领域。这个文章解释了什么是 PHP,如何将 Rust 程序编译成 C 再转换成 PHP 原生扩展。node

PHP 是什么?为何是它?

PHP 是:git

受欢迎的通用脚本语言,尤为是在 web 开发领域。从我的博客到世界上最流行的网站,PHP 提供了快速、灵活而且实用的功能。github

使人遗憾的是,PHP 近年来名声不佳,可是最近的发行版(从 PHP 7.0 开始)引入了许多简洁的语言特性,这些特性使人喜好。PHP 也是一种快速脚本语言,而且很是灵活。PHP 如今已经具有了类型、性征、可变参数、闭包(带有显式范围)、生成器和强大的向后兼容特性。PHP 的开发由 RFCs 领导,整个过程是开放和民主的。Gutenberg 项目是 WordPress 的新编辑器。WordPress 是用 PHP 编写的。很天然的,咱们须要一个 PHP 的本地扩展来解析 Gutenberg 文章格式。PHP 是一种具备规范的语言。其最流行的虚拟机是 Zend Engine,还有一些其余虚拟机,好比 HHVM(但 HHVM 最近已经放弃对 PHP 的支持,转而支持他们团队本身的 PHP 分支,也称为 Hack),PeachpieTagua VM(正在开发中)。在本文中,咱们将为 Zend Engine 建立一个扩展。这个虚拟机是 C 语言编写的。刚好跟以前的一篇文章 C 系列 相契合。web

Rust 🚀 C 🚀 PHP

要将 Rust 解析器移植到 PHP 中,咱们首先须要将它移植到 C。这在上一篇文章中已经实现了。从这一端到 C 有两个文件:libgutenberg_post_parser.agutenberg_post_parser.h,分别是静态库和头文件。chrome

使用脚手架引导

PHP 源码中自带了一个建立扩展的脚手架/模板,是 ext_skel.php。这个脚本能够从 Zend Engine 虚拟机的源代码中找到。能够这样使用它:编程

$ cd php-src/ext/
$ ./ext_skel.php \
      --ext gutenberg_post_parser \
      --author 'Ivan Enderlin' \
      --dir /path/to/extension \
      --onlyunix
$ cd /path/to/extension
$ ls gutenberg_post_parser
tests/
.gitignore
CREDITS
config.m4
gutenberg_post_parser.c
php_gutenberg_post_parser.h
复制代码

ext_skel.php 脚本建议以以下步骤使用: - 从新构建 PHP 源码配置(在 php-src 根目录下运行 ./buildconf), - 从新配置构建系统以启用扩展,如 ./configure --enable-gutenberg_post_parser, - 使用 make 构建 - 完成数组

可是咱们的扩展极可能位于 php-src 之外的目录。因此咱们使用 phpizephpizephpphp-cgiphpdbgphp-config 等相似,是一个可执行文件。它让咱们根据已编译的 php 二进制文件去编译扩展,这很符合咱们的例子。咱们像下面这样使用它:安全

$ cd /path/to/extension/gutenberg_post_parser

$ # Get the bin directory for PHP utilities.
$ PHP_PREFIX_BIN=$(php-config --prefix)/bin

$ # Clean (except if it is the first run).
$ $PHP_PREFIX_BIN/phpize --clean

$ # “phpize” the extension.
$ $PHP_PREFIX_BIN/phpize

$ # Configure the extension for a particular PHP version.
$ ./configure --with-php-config=$PHP_PREFIX_BIN/php-config

$ # Compile.
$ make install
复制代码

在这篇文章中,咱们将再也不展现相关的代码修改,而是将重点放在扩展绑定上。全部的相关源码能够在这里找到,简单的说,这是 config.m4 文件的配置:

PHP_ARG_ENABLE(gutenberg_post_parser, whether to enable gutenberg_post_parser support,
[  --with-gutenberg_post_parser          Include gutenberg_post_parser support], no)

if  test "$PHP_GUTENBERG_POST_PARSER" != "no"; then
  PHP_SUBST(GUTENBERG_POST_PARSER_SHARED_LIBADD)

  PHP_ADD_LIBRARY_WITH_PATH(gutenberg_post_parser, ., GUTENBERG_POST_PARSER_SHARED_LIBADD)

  PHP_NEW_EXTENSION(gutenberg_post_parser, gutenberg_post_parser.c, $ext_shared)
fi
复制代码

它的做用主要有如下这些: - 在构建系统中注册 --with-gutenberg_post_parser 选项,而且 - 声明要编译的静态库以及扩展源代码。

我么必须在同一级目录(连接符号是可用的)下添加 libgutenberg_post_parser.agutenberg_post_parser.h 文件,而后能够获得以下的目录结构:

$ ls gutenberg_post_parser
tests/                       # from ext_skel
.gitignore                   # from ext_skel
CREDITS                      # from ext_skel
config.m4                    # from ext_skel (edited)
gutenberg_post_parser.c      # from ext_skel (will be edited)
gutenberg_post_parser.h      # from Rust
libgutenberg_post_parser.a   # from Rust
php_gutenberg_post_parser.h  # from ext_skel
复制代码

扩展的核心是 gutenberg_post_parser.c 文件。这个文件负责建立模块,而且将 Rust 代码绑定到 PHP。

模块即扩展

如前所述,咱们将在 gutenberg_post_parser.c 中实现咱们的逻辑。首先,引入所须要的文件:

#include "php.h"
#include "ext/standard/info.h"
#include "php_gutenberg_post_parser.h"
#include "gutenberg_post_parser.h"
复制代码

最后一行引入的 gutenberg_post_parser.h 文件由 Rust 生成(准确的说是 cbindgen 生成的,若是你不记得,阅读上一篇文章)。接着,咱们必须决定好向 PHP 暴露的 API,Rust 解析器生成的 AST 定义以下:

pub enum Node<'a> {
    Block {
        name: (Input<'a>, Input<'a>),
        attributes: Option<Input<'a>>,
        children: Vec<Node<'a>>
    },
    Phrase(Input<'a>)
}
复制代码

AST 的 C 变体与上方的版本是相似的(具备不少结构,但思路几乎相同)。因此在 PHP 中,选择以下结构:

class Gutenberg_Parser_Block {
    public string $namespace;
    public string $name;
    public string $attributes;
    public array $children;
}

class Gutenberg_Parser_Phrase {
    public string $content;
}

function gutenberg_post_parse(string $gutenberg_post): array;
复制代码

gutenberg_post_parse 函数输出一个对象数组,对象类型是 gutenberg_post_parseGutenberg_Parser_Phrase,也就是咱们的 AST。咱们须要声明这些类。

类的声明

注意:后面的 4 个代码块不是本文的核心,它只是须要编写的代码,若是你不打算编写 PHP 扩展,能够跳过它

zend_class_entry *gutenberg_parser_block_class_entry;
zend_class_entry *gutenberg_parser_phrase_class_entry;
zend_object_handlers gutenberg_parser_node_class_entry_handlers;

typedef struct _gutenberg_parser_node {
    zend_object zobj;
} gutenberg_parser_node;
复制代码

一个 class entry 表明一个特定的类型。并会有对应的处理程序与 class entry 相关联。逻辑有些复杂。若是你想了解更多内容,我建议你阅读 PHP Internals Book。接着,咱们建立一个函数来实例化这些对象:

static zend_object *create_parser_node_object(zend_class_entry *class_entry) {
    gutenberg_parser_node *gutenberg_parser_node_object;

    gutenberg_parser_node_object = ecalloc(1, sizeof(*gutenberg_parser_node_object) + zend_object_properties_size(class_entry));

    zend_object_std_init(&gutenberg_parser_node_object->zobj, class_entry);
    object_properties_init(&gutenberg_parser_node_object->zobj, class_entry);

    gutenberg_parser_node_object->zobj.handlers = &gutenberg_parser_node_class_entry_handlers;

    return &gutenberg_parser_node_object->zobj;
}
复制代码

而后,咱们建立一个函数来释放这些对象。它的工做有两步:调用对象的析构函数(在用户态)来析构对象,而后将其释放(在虚拟机中):

static void destroy_parser_node_object(zend_object *gutenberg_parser_node_object) {
    zend_objects_destroy_object(gutenberg_parser_node_object);
}

static void free_parser_node_object(zend_object *gutenberg_parser_node_object) {
    zend_object_std_dtor(gutenberg_parser_node_object);
}
复制代码

而后,咱们初始化这个“模块”,也就是扩展。在初始化过程当中,咱们将在用户空间中建立类,并声明它的属性等。

PHP_MINIT_FUNCTION(gutenberg_post_parser)
{
    zend_class_entry class_entry;

    // 声明 Gutenberg_Parser_Block.
    INIT_CLASS_ENTRY(class_entry, "Gutenberg_Parser_Block", NULL);
    gutenberg_parser_block_class_entry = zend_register_internal_class(&class_entry TSRMLS_CC);

    // 声明 create handler.
    gutenberg_parser_block_class_entry->create_object = create_parser_node_object;

    // 类是 final 的(不能被继承)
    gutenberg_parser_block_class_entry->ce_flags |= ZEND_ACC_FINAL;

    // 使用空字符串做为默认值声明 `namespace` 公共属性,
    zend_declare_property_string(gutenberg_parser_block_class_entry, "namespace", sizeof("namespace") - 1, "", ZEND_ACC_PUBLIC);

    // 使用空字符串做为默认值声明 `name` 公共属性
    zend_declare_property_string(gutenberg_parser_block_class_entry, "name", sizeof("name") - 1, "", ZEND_ACC_PUBLIC);

    // 使用 `NULL` 做为默认值声明 `attributes` 公共属性
    zend_declare_property_null(gutenberg_parser_block_class_entry, "attributes", sizeof("attributes") - 1, ZEND_ACC_PUBLIC);

    // 使用 `NULL` 做为默认值,声明 `children` 公共属性
    zend_declare_property_null(gutenberg_parser_block_class_entry, "children", sizeof("children") - 1, ZEND_ACC_PUBLIC);

    // 声明 Gutenberg_Parser_Block.

    … 略 …

    // 声明 Gutenberg 解析器节点对象 handler

    memcpy(&gutenberg_parser_node_class_entry_handlers, zend_get_std_object_handlers(), sizeof(gutenberg_parser_node_class_entry_handlers));

    gutenberg_parser_node_class_entry_handlers.offset = XtOffsetOf(gutenberg_parser_node, zobj);
    gutenberg_parser_node_class_entry_handlers.dtor_obj = destroy_parser_node_object;
    gutenberg_parser_node_class_entry_handlers.free_obj = free_parser_node_object;

    return SUCCESS;
}
复制代码

若是你还在阅读,首先我表示感谢,其次,恭喜!接着,代码中有 PHP_RINIT_FUNCTIONPHP_MINFO_FUNCTION 函数,它们是由 ext_skel.php 脚本生成的。模块条目信息和模块配置也是这样生成的。

gutenberg_post_parse 函数

如今咱们将重点介绍 gutenberg_post_parse 函数。该函数接收一个字符串做为参数,若是解析失败,则返回 false,不然返回类型为 Gutenberg_Parser_BlockGutenberg_Parser_Phrase 的对象数组。咱们开始编写它!注意它是由 PHP_FUNCTION声明的.

PHP_FUNCTION(gutenberg_post_parse)
{
    char *input;
    size_t input_len;

    // 将 input 做为字符串读入
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &input, &input_len) == FAILURE) {
        return;
    }
复制代码

在这个步骤中,参数已经做为字符串("s")被声明和引入了。字符串值在 input 中,字符串长度存储在 input_len。下一步就是解析 input。(实际上不须要字符串长度)。这就是咱们要调用 Rust 代码的地方!咱们能够这样作:

// 解析 input
    Result parser_result = parse(input);

    // 若是解析失败,则返回 false.
    if (parser_result.tag == Err) {
        RETURN_FALSE;
    }

    // 不然将 Rust 的 AST 映射到 PHP 的数组中
    const Vector_Node nodes = parse_result.ok._0;
复制代码

Result 类型和 parse 函数来自 Rust 中。若是你不记得这些类型,能够阅读前一篇关于 C 领域的文章。Zend Engine 有一个 RETURN_FALSE 宏,用于返回 false!很方即是吗?最后,若是顺利,咱们将获得 Vector_Node 类型的节点集合。下一步是将它们映射到 PHP 类型中,如 Gutenberg 类型的数组。咱们开始干吧:

// 注意:return_value 是一个"魔术"变量,它用于存放返回值
    //
    // 分配一个数组空间
    array_init_size(return_value, nodes.length);

    // 映射 Rust AST
    into_php_objects(return_value, &nodes);
}
复制代码

完事了 😁!噢,等等 …… 还要实现 into_php_objects函数!

into_php_objects 函数

这个函数并不复杂:只是它是经过 Zend Engine 的 API 实现。咱们会向勤奋的读者阐释如何将 Block 映射为 Gutenberg_Parser_Block 对象,以及让 Phrase 映射为 Gutenberg_Parser_Phrase。咱们开始吧:

void into_php_objects(zval *php_array, const Vector_Node *nodes) {
    const uintptr_t number_of_nodes = nodes->length;

    if (number_of_nodes == 0) {
        return;
    }

    // 遍历全部节点
    for (uintptr_t nth = 0; nth < number_of_nodes; ++nth) {
        const Node node = nodes->buffer[nth];

        if (node.tag == Block) {
            // 将 Block 映射为 Gutenberg_Parser_Block
        } else if (node.tag == Phrase) {
            // 将 Phrase 映射为 Gutenberg_Parser_Phrase
        }
    }
}
复制代码

如今,咱们开始实现映射一个内存区块(如下简称块)。主要过程以下:

  1. 为块名称空间和块名称分配 PHP 字符串,
  2. 分配对象,
  3. 将块名称空间和块名称设定为各自的独享属性
  4. 为块属性分配一个 PHP 字符串
  5. 把块属性设定为对应的对象属性
  6. 若是有子节点,初始化一个数组,并使用子节点和新数组调用 into_php_objects
  7. 把子节点设定为对应的对象属性
  8. 最后,在返回的数组中添加块对象
const Block_Body block = node.block;
zval php_block, php_block_namespace, php_block_name;

// 1. 准备 PHP 字符串
ZVAL_STRINGL(&php_block_namespace, block.namespace.pointer, block.namespace.length);
ZVAL_STRINGL(&php_block_name, block.name.pointer, block.name.length);
复制代码

你还记得名称空间、名称和其余相似数据的类型是 Slice_c_char 吗?它就是一个带有指针和长度的结构体。指针指向原始的输入字符串,所以没有副本(这实际上是 slice 的定义)。好了,Zend Engine 中有名为 ZVAL_STRINGL 的宏,它的功能是经过“指针”和“长度”建立字符串,很棒!可不幸的是,Zend Engine 在底层作了拷贝…… 没有办法只保留指针和长度,可是它保证拷贝的数量很小。我想应该为了获取数据的所有全部权,这是垃圾回收所必需的。

// 2. 建立 Gutenberg_Parser_Block 对象
object_init_ex(&php_block, gutenberg_parser_block_class_entry);
复制代码

使用 gutenberg_parser_block_class_entry 所表明的类实例化对象。

// 3. 设定命名空间和名称
add_property_zval(&php_block, "namespace", &php_block_namespace);
add_property_zval(&php_block, "name", &php_block_name);

zval_ptr_dtor(&php_block_namespace);
zval_ptr_dtor(&php_block_name);
复制代码

zval_ptr_dtor 的做用是给引用计数加 1。便于垃圾回收。

// 4. 处理一些内存块属性
if (block.attributes.tag == Some) {
    Slice_c_char attributes = block.attributes.some._0;
    zval php_block_attributes;

    ZVAL_STRINGL(&php_block_attributes, attributes.pointer, attributes.length);

    // 5. 设置属性
    add_property_zval(&php_block, "attributes", &php_block_attributes);

    zval_ptr_dtor(&php_block_attributes);
}
复制代码

它相似于 namespacename 所作的。如今咱们继续讨论 children。

// 6. 处理子节点
const Vector_Node *children = (const Vector_Node*) (block.children);

if (children->length > 0) {
    zval php_children_array;

    array_init_size(&php_children_array, children->length);

    // 递归
    into_php_objects(&php_children_array, children);

    // 7. 设置 children
    add_property_zval(&php_block, "children", &php_children_array);

    Z_DELREF(php_children_array);
}

free((void*) children);
复制代码

最后,将块实例增长到返回的数组中:

// 8. 在集合中加入对象
add_next_index_zval(php_array, &php_block);
复制代码

完整代码点此查看

PHP 扩展 🚀 PHP 用户态

如今扩展写好了,咱们必须编译它。能够直接重复前面提到的使用 phpize 等展现的命令集。一旦扩展被编译,就会在本地的扩展存放目录中生成 generated gutenberg_post_parser.so 文件。使用如下命令能够找到该目录:

$ php-config --extension-dir
复制代码

例如,在个人计算机中,扩展目录是 /usr/local/Cellar/php/7.2.11/pecl/20170718。而后,要使用扩展须要先启用它,你必须这样作:

$ php -d extension=gutenberg_post_parser -m | \
      grep gutenberg_post_parser
复制代码

或者,针对全部的脚本执行启用扩展,你须要使用命令 php --ini 定位到 php.ini 文件,并编辑,向其中追加如下内容:

extension=gutenberg_post_parser
复制代码

完成!如今,咱们使用一些反射来检查扩展是否被 PHP 正确加载和处理:

$ php --re gutenberg_post_parser
Extension [ <persistent> extension #64 gutenberg_post_parser version 0.1.0 ] {

  - Functions {
    Function [ <internal:gutenberg_post_parser> function gutenberg_post_parse ] {

      - Parameters [1] {
        Parameter #0 [ <required> $gutenberg_post_as_string ]
      }
    }
  }

  - Classes [2] {
    Class [ <internal:gutenberg_post_parser> final class Gutenberg_Parser_Block ] {

      - Constants [0] {
      }

      - Static properties [0] {
      }

      - Static methods [0] {
      }

      - Properties [4] {
        Property [ <default> public $namespace ]
        Property [ <default> public $name ]
        Property [ <default> public $attributes ]
        Property [ <default> public $children ]
      }

      - Methods [0] {
      }
    }

    Class [ <internal:gutenberg_post_parser> final class Gutenberg_Parser_Phrase ] {

      - Constants [0] {
      }

      - Static properties [0] {
      }

      - Static methods [0] {
      }

      - Properties [1] {
        Property [ <default> public $content ]
      }

      - Methods [0] {
      }
    }
  }
}
复制代码

看起来没什么问题:有一个函数和两个预约义的类。如今,咱们来编写本文的 PHP 代码!

<?php

var_dump(
    gutenberg_post_parse(
        '<!-- wp:foo /-->bar<!-- wp:baz -->qux<!-- /wp:baz -->'
    )
);

/** * Will output: * array(3) { * [0]=> * object(Gutenberg_Parser_Block)#1 (4) { * ["namespace"]=> * string(4) "core" * ["name"]=> * string(3) "foo" * ["attributes"]=> * NULL * ["children"]=> * NULL * } * [1]=> * object(Gutenberg_Parser_Phrase)#2 (1) { * ["content"]=> * string(3) "bar" * } * [2]=> * object(Gutenberg_Parser_Block)#3 (4) { * ["namespace"]=> * string(4) "core" * ["name"]=> * string(3) "baz" * ["attributes"]=> * NULL * ["children"]=> * array(1) { * [0]=> * object(Gutenberg_Parser_Phrase)#4 (1) { * ["content"]=> * string(3) "qux" * } * } * } * } */
复制代码

它正确执行了!

结语

主要过程:

  • 获取 PHP 字符串
  • 在 中 Zend Engine 为 Gutenberg 扩展分配内存,
  • 经过 FFI(静态库 + header)传递到 Rust,
  • 经过 Gutenberg 扩展返回数据到 Zend Engine
  • 生成 PHP 对象,
  • PHP 读取该对象。

Rust 适用于不少地方!咱们已经看到在实际编程中已经有人实现如何用 Rust 实现解析器,如何将其绑定到 C 语言并生成除了 C 头文件以外的静态库,如何建立一个 PHP 扩展并暴露一个函数接口和两个对象,如何把“C 绑定”集成到 PHP,以及如何在 PHP 中使用该扩展。提醒一下,“C 绑定”大概有 150 行代码。PHP 扩展大概有 300 行代码,可是减去自动生成的“代码修饰”(一些声明和管理扩展的模板文件),PHP 扩展将减小到大约 200 行代码。一样,考虑到解析器仍然是用 Rust 编写的,修改解析器不会影响绑定(除非 AST 发生了较大更新),我发现整个实现过程只是一小部分代码。PHP 是一个有垃圾回收的语言。这就解释了为什么须要拷贝全部的字符串,这样数据都能被 PHP 拥有。然而,Rust 中不拷贝任何数据的事实代表能够减小内存分配和释放,这些开销刚好在大多数状况下是最大的时间成本。Rust 还提供了安全性。考虑到咱们要进行绑定的数量,这个特性可能受到质疑:Rust 到 C 到 PHP,这种安全性还存在吗?从 Rust 的角度看,答案是肯定的,但在 C 或 PHP 中发生的全部操做都被认为是不安全的。在 C 绑定中必须特别谨慎处理全部状况。这样还快吗?好吧,让咱们进行基准测试。我想提醒你,这个实验的首要目标是解决原始的 PEG.js 解析器性能问题。在 JavaScript 的基础上,WASM 和 ASM.js 方案已经被证实要快的多(参见 WebAssembly 领域ASM.js 领域)。对于 PHP,使用 phpegjs:它读取为 PEG.js 编写的语法并将其编译到 PHP。咱们来比较一下:

文件名 PEG PHP parser (ms) Rust parser as a PHP extension (ms) 提高倍数
demo-post.html 30.409 0.0012 × 25341
shortcode-shortcomings.html 76.39 0.096 × 796
redesigning-chrome-desktop.html 225.824 0.399 × 566
web-at-maximum-fps.html 173.495 0.275 × 631
early-adopting-the-future.html 280.433 0.298 × 941
pygmalian-raw-html.html 377.392 0.052 × 7258
moby-dick-parsed.html 5,437.630 5.037 × 1080

Rust 解析器的 PHP 扩展比实际的 PEG PHP 实现平均快 5230 倍。提高倍数的中位数是 941。另外一个问题是 PEG 解析器因为内存限制没法处理过多的 Gutenberg 文档。固然,增大内存的大小可能解决这个问题,但并非最佳方案。使用 Rust 解析器做为 PHP 扩展,内存消耗基本保持不变,而且接近解析文档的大小。我认为咱们能够经过迭代器而非数组的方式来进一步优化该扩展。这是我想探索的东西以及分析对性能的影响。PHP 内核书籍有个迭代器章节。咱们将在本系列的下一节看到 Rust 能够助力于不少领域,并且传播的越多,就越有趣味。感谢你的阅读!

相关文章
相关标签/搜索