【譯】從 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 能夠助力於不少領域,並且傳播的越多,就越有趣味。感謝你的閱讀!

相關文章
相關標籤/搜索