前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从Rust到远方:PHP星系

从Rust到远方:PHP星系

作者头像
MikeLoveRust
发布2019-07-09 13:36:23
1.1K0
发布2019-07-09 13:36:23
举报

来源:https://mnt.io/2018/10/29/from-rust-to-beyond-the-php-galaxy/

译注:原作者换工作到Wasmer,后续绑定相关文章没有再更新。

这篇博客文章是这一系列解释如何将Rust发射到地球以外的许多星系的文章的一部分:

  • 前奏,
  • WebAssembly 星系
  • ASM.js星系
  • C星系
  • PHP星系(当前这一集)
  • NodeJS 星系

今天将要探索的是PHP星系。这篇文章会解释什么是PHP,以及如何将任何的Rust程序编译为C进而制作PHP的原生扩展。

什么是PHP,为什么?

PHP 是:

流行的通用脚本语言,特别适合Web开发。从您的博客到世界上最流行的网站,PHP提供了快速、灵活和实用的功能。

令人遗憾的是,PHP多年来名声不佳,但是最近的版本(主要是从PHP 7.0开始的)引入了简洁的语言特性和许多清理优化,这些特性都被讨厌它的人过分忽略了。PHP也是一种快速脚本语言,并且非常灵活。PHP现在已经有了声明类型、特征、可变参数、闭包(带有显式范围!)、生成器等特性和强大的向后兼容能力。PHP的开发是由RFC主导的,过程开放、民主。Gutenberg项目是WordPress的一个新编辑器,因为Wordpress是用PHP编写的,很自然的我们需要一个PHP的原生扩展来解析Gutenberg博客格式。PHP是一种有规范的语言(意味着可以有不同的虚拟机实现方案)。最流行的虚拟机是Zend Engine, 其他虚拟机也存在,比如HHVM(但是PHP支持最近被放弃,转而支持它们自己的PHP fork,称为Hack)、Peachpie或Tagua VM(正在开发中)。在本文中,我们将为Zend Engine创建一个扩展。注意这个虚拟机是用C语言编写的,很棒的是我们已经在前面一篇文章登陆了C星系!

Rust ? C ? PHP

要将Rust解析器移植到PHP中,我们首先需要将它移植到C。这在上一节中已经完成。移植到C的结果就是两个文件: libgutenberg_post_parser.agutenberg_post_parser.h,分别为静态库和头文件。

从脚手架开始

PHP附带一个脚本来创建一个扩展框架模板或者说脚手架,叫做ext_skel.php。这个脚本可以从Zend引擎虚拟机的源代码找到(我们把它叫做php-src)。可以像这样调用脚本

$ 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树之外。所以我们将使用phpize, phpize是一个可执行文件,是随php一起安装的, 还有如php-cgi, phpdbg, php-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所有需要的东西:

#include "php.h"
#include "ext/standard/info.h"
#include "php_gutenberg_post_parser.h"
#include "gutenberg_post_parser.h"

最后一行include由Rust生成的gutenberg_post_parser.h 文件(更准确地说,是由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>)
}

C版本AST和这个非常相似(具有更多的结构,但是思想几乎相同)。在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_Parser_BlockGutenberg_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表示特定的类类型。 会有一个handlerclass_entry相关联。逻辑有点复杂。如果您需要更多详细信息,我建议您阅读PHP内部原理这本书。然后,让我们创建一个函数来即时处理这些对象

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;

    // Declare 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);

    // Declare the create handler.
    gutenberg_parser_block_class_entry->create_object = create_parser_node_object;

    // The class is final.
    gutenberg_parser_block_class_entry->ce_flags |= ZEND_ACC_FINAL;

    // Declare the `namespace` public attribute,
    // with an empty string for the default value.
    zend_declare_property_string(gutenberg_parser_block_class_entry, "namespace", sizeof("namespace") - 1, "", ZEND_ACC_PUBLIC);

    // Declare the `name` public attribute,
    // with an empty string for the default value.
    zend_declare_property_string(gutenberg_parser_block_class_entry, "name", sizeof("name") - 1, "", ZEND_ACC_PUBLIC);

    // Declare the `attributes` public attribute,
    // with `NULL` for the default value.
    zend_declare_property_null(gutenberg_parser_block_class_entry, "attributes", sizeof("attributes") - 1, ZEND_ACC_PUBLIC);

    // Declare the `children` public attribute,
    // with `NULL` for the default value.
    zend_declare_property_null(gutenberg_parser_block_class_entry, "children", sizeof("children") - 1, ZEND_ACC_PUBLIC);

    // Declare the Gutenberg_Parser_Block.

    … skip …

    // Declare Gutenberg parser node object handlers.

    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_FUNCTION 函数和PHP_MINFO_FUNCTION函数,这些函数已经由ext_skel.php脚本生成。对于模块定义和其他模块配置细节也是如此。

gutenberg_post_parse函数

现在我们将关注gutenberg_post_parse这个PHP函数。该函数需要一个字符串类型的参数,如果解析失败,则返回false,否则返回Gutenberg_Parser_Block 或者 Gutenberg_Parser_Phrase的对象数组。让我们写下来!注意,它是用PHP函数宏声明的。

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

    // Read the input as a string.
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &input, &input_len) == FAILURE) {
        return;
    }

在此步骤中,参数已被声明类型为字符串(“s”)。字符串值在input中,字符串长度在input_len中。下一步是解析输入。(不需要字符串的长度)。这就是我们要调用Rust代码的地方!我们来写一下:

    // Parse the input.
    Result parser_result = parse(input);

    // If parsing failed, then return false.
    if (parser_result.tag == Err) {
        RETURN_FALSE;
    }

    // Else map the Rust AST into a PHP array.
    const Vector_Node nodes = parse_result.ok._0;

Result类型和parse函数来自Rust。如果你不记得这些类型,请阅读前一集关于C星系的内容。Zend Engine有一个名为RETURN_FALSE的宏来返回false!很贴心是不是?最后如果一起顺利,我们会得到一个节点集合,节点类型为Vector_Node。下一步是要映射这些Rust/C类型到PHP的类型,也就是Gutenberg类的数组。开始:

    // Note: return_value is a “magic” variable that holds the value to be returned.
    //
    // Allocate an array.
    array_init_size(return_value, nodes.length);

    // Map the Rust AST.
    into_php_objects(return_value, &nodes);
}

完成!等一下。。。into_php_objects好像还没有写!

into_php_objects函数

这个函数并不十分复杂:它只是像预期的那样充满了特定于Zend引擎的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;
    }

    // Iterate over all nodes.
    for (uintptr_t nth = 0; nth < number_of_nodes; ++nth) {
        const Node node = nodes->buffer[nth];

        if (node.tag == Block) {
            // Map Block into Gutenberg_Parser_Block.
        } else if (node.tag == Phrase) {
            // Map Phrase into Gutenberg_Parser_Phrase.
        }
    }
}

映射block的过程如下:

  1. blocknamespace分配一个PHP字符串,block的名字也需要这样,
  2. 分配一个对象,
  3. 设置block namespaceblock name相应的属性,
  4. 为必要block属性的PHP字符串,
  5. 设置block属性到对应的对象,
  6. 如果有子节点,初始化一个数组, 然后用child节点和新数组调用into_php_objects函数,
  7. 设置children到对应的对象,
  8. 最后,把block对象追加到将要返回的数组里面。
const Block_Body block = node.block;
zval php_block, php_block_namespace, php_block_name;

// 1. Prepare the PHP strings.
ZVAL_STRINGL(&php_block_namespace, block.namespace.pointer, block.namespace.length);
ZVAL_STRINGL(&php_block_name, block.name.pointer, block.name.length);

您还记得namespacename和其他类似的数据都属于Slice_c_char类型吗?它只是一个有指针和长度的结构。指针指向原始的输入字符串,因此没有副本(实际上这是Slice的定义)。Zend Engine有一个ZVAL_STRINGL宏,它允许从指针和长度创建字符串,太棒了!不幸的是,对于我们来说,Zend Engine在后台做了一个复制使得没有办法只保留指针和长度,但是它做到了只用很小复制的数量。我认为它之所以需要完全拥有数据所有权,是因为垃圾回收需要这个。

// 2. Create the Gutenberg_Parser_Block object.
object_init_ex(&php_block, gutenberg_parser_block_class_entry);

对象已经用gutenberg_parser_block_class_entry表示的类进行了实例化。

// 3. Set the namespace and the name.
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);
The zval_ptr_dtor adds 1 to the reference counter. This is required for the garbage collector.
// 4. Deal with block attributes if some.
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. Set the attributes.
    add_property_zval(&php_block, "attributes", &php_block_attributes);

    zval_ptr_dtor(&php_block_attributes);
}

namespace, name的操作一样,我们完成children

// 6. Handle children.
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);

    // Recursion.
    into_php_objects(&php_children_array, children);

    // 7. Set the children.
    add_property_zval(&php_block, "children", &php_children_array);

    Z_DELREF(php_children_array);
}

free((void*) children);

最后,追加这个block示例到返回数组:

// 8. Insert the object in the collection.
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的配置文件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的string,
  • 在Gutenberg扩展中分配属于Zend Engine,
  • 通过FFI传递给Rust(静态库 + 头文件),
  • 从Gutenberg扩展回到Zend Engine,
  • 生成PHP对象,
  • PHP得到对象。

到处都适用Rust!我们已经看到在现实世界中如何用Rust编写一个解析器,如何将其绑定到C然后编译到一个静态库和C头文件,如何创建一个PHP扩展暴露一个函数和两个对象,如何将C绑定集成到PHP中,以及如何在PHP中使用这个扩展。提醒一下,C绑定大约有150行代码。PHP扩展大约有300行代码,但是减去自动生成的修饰后(声明和管理扩展的样板代码),PHP扩展减少到大约200行代码。再一次,可以看到我们需要review的代码面是很小的,因为考虑到解析器仍然是用Rust编写的,修改解析器不会影响绑定(除非AST明显更新)! PHP是一种带有垃圾收集器的语言。这解释了为什么要复制所有字符串,以便它们都属于PHP本身。然而,Rust不复制任何数据的事实节省了内存分配和释放,这在大多数情况下是最大的成本。Rust也提供了安全。考虑到我们要处理的绑定数量,可以对这个属性提出疑问: Rust到C到PHP: 这还安全么?从Rust的角度来看,答案是肯定的,但是在C或PHP中发生的所有事情都必须被认为是不安全的。在C绑定中必须特别注意处理所有情况。还快吗?我们来做个基准测试。我想提醒您,这个实验的第一个目标是解决原始PEG.js解析器的性能问题。在JavaScript方面,WASM和ASM.js已经显示出了非常快的速度(参见WebAssembly galaxy和ASM.js galaxy)。对于PHP,我们使用phpegjs:它读取为PEG.js编写的语法并将其编译到PHP。我们来比较一下

file

PEG PHP parser (ms)

Rust parser as a PHP extension (ms)

speedup

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扩展,内存保持大小不变,并且和被解析文档的大小接近。我认为我们可以进一步优化扩展来生成迭代器而不是数组,这是我想探索东西以及分析其对性能的影响。The PHP Internals Book中就有一章是关于迭代器的。我们将在本系列的下一集看到Rust可以到达很多星系,Rust越多的往后旅行,也会变得更加有趣。谢谢你的阅读。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust语言学习交流 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是PHP,为什么?
  • Rust ? C ? PHP
  • 从脚手架开始
    • 模块/扩展
      • 声明类
        • gutenberg_post_parse函数
          • into_php_objects函数
          • PHP扩展 ? PHP用户态
          • 结论
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档