От Rust к чему-то большему, чем просто Rust: домен языка PHP

задняя часть Rust

От Rust и дальше: галактика PHP

Этот пост является частью серии «Как распространить Rust на другие языки». Прогресс завершения ржавчины:

Область, которую мы исследуем сегодня, — это область PHP. В этой статье объясняется, что такое PHP и как компилировать программы на Rust в C, а затем преобразовывать их в нативные расширения PHP.

Что такое PHP? Почему это?

PHPДа:

Популярный скриптовый язык общего назначения, особенно в сфере веб-разработки. От личных блогов до самых популярных в мире веб-сайтов PHP обеспечивает быструю, гибкую и полезную функциональность.

Обидно, что в последние годы у PHP плохая репутация, но в недавних выпусках (начиная с PHP 7.0) было представлено много изящных языковых функций, которые прекрасны. PHP также является быстрым языком сценариев и очень гибким. PHP теперь имеет типы, трейты, вариативные параметры, замыкания (с явной областью действия), генераторы и мощные функции обратной совместимости. PHP был разработанRFCsРуководство, весь процесс открыт и демократичен. Project Gutenberg — новый редактор для WordPress. WordPress написан на PHP. Естественно, нам нужно собственное расширение PHP для разбора формата статьи Гутенберга. PHP — этоСпецификацияязык. Его самые популярные виртуальные машины:Zend Engineи некоторые другие виртуальные машины, такие какHHVM(но HHVM недавно отказалась от поддержки PHP в пользу собственного форка PHP их команды, также известного как Hack),PeachpieилиTagua VM(в развитие).在本文中,我们将为 Zend Engine 创建一个扩展。这个虚拟机是 C 语言编写的。恰好跟之前的一篇文章C сериисовпадение.

Ржавчина 🚀 C 🚀 PHP

Чтобы портировать парсер Rust на PHP, нам сначала нужно перенести его на C. Это было достигнуто в предыдущей статье. С этого конца на C есть два файла:libgutenberg_post_parser.aиgutenberg_post_parser.h, которые являются статическими библиотеками и заголовочными файлами соответственно.

Используйте леса для направления

Исходный код 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вне каталога. Поэтому мы используем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.aиgutenberg_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документ. Этот файл отвечает за создание модуля, а код ржавчины, чтобы привязать к 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генерируется, если ты не помнишь,Прочитать статью). Затем мы должны решить, какой API предоставить PHP.AST, сгенерированный парсером Rust, определяется следующим образом:

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_parseилиGutenberg_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;

Запись класса от имени определенного типа. И будет соответствующая программа обработки, связанная с записью класса. Логика какая-то сложная. Если вы хотите узнать больше, я предлагаю вам прочитать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_FUNCTIONиPHP_MINFO_FUNCTIONФункция, ониext_skel.phpСкрипт сгенерирован. Также генерируются входная информация модуля и конфигурация модуля.

gutenberg_post_parseфункция

Теперь мы сосредоточимся наgutenberg_post_parseфункция. Функция получает строку в качестве параметра и возвращает значение, если синтаксический анализ не удался.false, иначе возвращаемый типGutenberg_Parser_BlockилиGutenberg_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функция

В этой функции нет ничего сложного: просто она реализована через API Zend Engine. Объясняем внимательному читателю, как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? Это просто структура с указателем и длиной. Указатель указывает на исходную входную строку, поэтому копии нет (на самом деле это определение среза). Ну, есть 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);
}

это похоже наnamespaceиnameВыполнено. Теперь мы переходим к обсуждению детей.

// 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, выделяющем память для расширений Гутенберга,
  • Перешел на Rust через FFI (статическая библиотека + заголовок),
  • Вернитесь в Zend Engine через расширение Гутенберга
  • генерировать объекты PHP,
  • PHP читает объект.

Ржавчина работает во многих местах! Мы видели в реальном программировании, как кто-то реализует парсер на 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

PHP-расширение парсера Rust в среднем в 5230 раз быстрее, чем фактическая реализация PEG PHP. Средний кратный буст равен 941. Другая проблема заключается в том, что синтаксический анализатор PEG не может обрабатывать слишком много документов Gutenberg из-за нехватки памяти. Конечно, увеличение объема памяти может решить эту проблему, но это не лучшее решение. При использовании синтаксического анализатора Rust в качестве расширения PHP потребление памяти практически не меняется и приближается к размеру анализируемого документа. Я думаю, что мы можем дополнительно оптимизировать это расширение с помощью итераторов вместо массивов. Вот что я хотел бы изучить и влияние профилирования на производительность. В книге ядра PHP естьГлава об итераторе. В следующем разделе этой серии мы увидим, что Rust может помочь во многих областях, и чем больше он распространяется, тем интереснее становится. Спасибо за чтение!