От Rust и дальше: галактика PHP
- Оригинальный адрес:Пробный вопрос.IO/2018/10/29/…
- Оригинальный склад:GitHub.com/HYplay/Костяной суп на…
- Оригинальный автор:Ivan Enderlin
- Перевод с:GitHub.com/Сестра Су Ханью/…
- Постоянная ссылка на эту статью:GitHub.com/Сестра Су Ханью/…
- Переводчик:suhanyujie
- Бюро переводы неуместно, пожалуйста, также укажите, спасибо!
- теги: Разработка расширений для PHP с помощью Rust, Расширение возможностей PHP с помощью Rust
Этот пост является частью серии «Как распространить Rust на другие языки». Прогресс завершения ржавчины:
- предисловие,
- Сфера WebAssembly,
- Область ASM.js,
- Поле С,
- поле PHP (текущая глава) и
- Область NodeJS
Область, которую мы исследуем сегодня, — это область 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
}
}
}
Теперь мы начинаем отображать блок памяти (далее блок). Основной процесс выглядит следующим образом:
- Назначьте строки PHP для пространств имен блоков и имен блоков,
- назначать объекты,
- Задайте для пространства имен и имени блока собственные уникальные свойства.
- Назначить строку PHP свойству блока
- Установите свойство блока на соответствующее свойство объекта
- Если есть дочерние элементы, инициализируйте массив и вызовите с дочерними элементами и новым массивом
into_php_objects
- Установите дочерний узел в соответствующее свойство объекта
- Наконец, добавьте объект блока в возвращаемый массив.
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 может помочь во многих областях, и чем больше он распространяется, тем интереснее становится. Спасибо за чтение!