FFI: пишем на Rust в PHP-программе

от автора

В PHP 7.4 появится FFI, т.е. можно подключать библиотеки на языке C (или, например, Rust) напрямую, без необходимости писать целый extension и разбираться в его многочисленных нюансах.

Давайте попробуем написать код на Rust, и используем его PHP-программе

Идея реализации FFI в PHP 7.4 была взята из LuaJIT и Python, а именно: в язык встроен парсер, который понимает декларации функций, структур и т.д. языка Си. По факту туда можно подсунуть всё содержимое заголовочного файла и сразу начать использовать его.

Пример:

<?php // вставляем декларацию функции printf на языке Си $ffi = FFI::cdef(     "int printf(const char *format, ...);", // это синтаксис языка Си     "libc.so.6"); //  указываем скомпилированную библиотеку // вызываем printf $ffi->printf("Hello %s!\n", "world");

Подключать чьи-то готовые либы — это просто и весело, но хочется и что-то своё написать. Например, нужно быстро распарсить какой-то файл, и результаты парсинга использовать из php.

Из трех системных языков (C, C++, Rust) лично я выбираю последний. Причина проста: у меня не хватит компетенций, чтобы сходу написать безопасную по памяти программу на C или С++. Rust сложноват, но в этом смысле выглядит надёжнее. Компилятор сразу указывает тебе, где ты неправ. Почти невозможно добиться Undefined Behavior.

Disclaimer: я не являюсь системным программистом, поэтому дальнейшее используйте на свой страх и риск.

Давайте для начала напишем что-то совсем простое, простую функцию для складывания чисел. Просто для тренировки. А потом перейдем к более сложной задаче.

Создаем проект как библиотеку

cargo new hellofromrust --lib

и указываем в cargo.toml, что это динамическая библиотека (dylib)

 …. [lib] name="hellofromrust" crate-type = ["dylib"]  ….

Сама функция на Расте выглядит так

#[no_mangle] pub extern "C" fn addNumbers(x: i32, y: i32) -> i32 { x + y }

ну т.е. обычная функция, только к ней добавлено пара магических слов no_mangle и extern «C»

Далее, делаем cargo build, чтобы получить so-файл (под Линуксом)

Можно использовать из php:

<?php $ffi = FFI::cdef("int addNumbers(int x, int y);", './libhellofromrust.so'); print "1+2=" . $ffi->addNumbers(1, 2) . "\n"; // 1+2=3

Складывать числа просто. Функция принимает целые аргументы по значению, и возвращает новое целое число.

А что если нужно использовать строки? А что если функция возвращает ссылку на дерево элементов? А как использовать специфические конструкции Раста в сигнатуре функций?

Эти вопросы меня замучили, поэтому я написал парсер арифметических выражений на Расте. И решил использовать его из PHP для изучения всех нюансов.

Полный код проекта здесь: simple-rust-ariphmetic-parser. Кстати, туда же я положил docker образ, в котором есть PHP (скомпилированный с FFI), Rust, Cbindgen и т.д. Всё, что нужно для запуска.

Парсер, если рассматривать чистый язык Раст, делает следующее:

берет строку вида «100500(2+35)-25″ и преобразовывает в выражение-дерево expression.rs:

pub enum Expression {     Add(Box<Expression>, Box<Expression>),     Subtract(Box<Expression>, Box<Expression>),     Multiply(Box<Expression>, Box<Expression>),     Divide(Box<Expression>, Box<Expression>),     UnaryMinus(Box<Expression>),     Value(i64), }

это Растовый enum, а в Расте, как известно, enum — это не просто набор констант, но к ним можно еще привязать значение. Здесь если тип узла Expression::Value, то к нему записано целое число, например 100500. Для узла типа Add будем хранить также две ссылки (Box) на выражения-операнды этого сложения.

Парсер я написал довольно быстро, несмотря на ограниченное знание Rust, а вот с FFI пришлось помучиться. Если в C строки — это указатель на тип char *, т.е. указатель на массив символов, заканчивающихся на \0, то в Расте это совсем другой тип. Поэтому необходимо преобразовать входную строку в тип &str следующим образом:

CStr::from_ptr(s).to_str()

Подробнее про CStr

Это всё полбеды. Настоящая проблема в том, что ни Растовых енумов, ни безопасных ссылок типа Box в языке C нет. Поэтому пришлось сделать отдельную структуру ExpressionFfi для хранения дерева выражений в C-стиле, т.е. через struct, union и простые указатели (ffi.rs).

#[repr(C)] pub struct ExpressionFfi {     expression_type: ExpressionType,     data: ExpressionData, }  #[repr(u8)] pub enum ExpressionType {     Add = 0,     Subtract = 1,     Multiply = 2,     Divide = 3,     UnaryMinus = 4,     Value = 5, }  #[repr(C)] pub union ExpressionData {     pair_operands: PairOperands,     single_operand: *mut ExpressionFfi,     value: i64, }  #[derive(Copy, Clone)] #[repr(C)] pub struct PairOperands {     left: *mut ExpressionFfi,     right: *mut ExpressionFfi, }

Ну и метод для преобразования в нее:

impl Expression {     fn convert_to_c(&self) -> *mut ExpressionFfi {         let expression_data = match self {             Value(value) => ExpressionData { value: *value },             Add(left, right)             | Subtract(left, right)             | Multiply(left, right)             | Divide(left, right) => ExpressionData {                 pair_operands: PairOperands {                     left: left.convert_to_c(),                     right: right.convert_to_c(),                 },             },             UnaryMinus(operand) => ExpressionData {                 single_operand: operand.convert_to_c(),             },         };          let expression_ffi = match self {             Add(_, _) => ExpressionFfi {                 expression_type: ExpressionType::Add,                 data: expression_data,             },             Subtract(_, _) => ExpressionFfi {                 expression_type: ExpressionType::Subtract,                 data: expression_data,             },             Multiply(_, _) => ExpressionFfi {                 expression_type: ExpressionType::Multiply,                 data: expression_data,             },             Divide(_, _) => ExpressionFfi {                 expression_type: ExpressionType::Multiply,                 data: expression_data,             },             UnaryMinus(_) => ExpressionFfi {                 expression_type: ExpressionType::UnaryMinus,                 data: expression_data,             },             Value(_) => ExpressionFfi {                 expression_type: ExpressionType::Value,                 data: expression_data,             },         };         Box::into_raw(Box::new(expression_ffi))     } }

Box::into_raw превращает тип Box в сырой «сишный» указатель

В итоге функция, которую мы будем экспортировать в PHP, выглядит так:

#[no_mangle] pub extern "C" fn parse_arithmetic(s: *const c_char) -> *mut ExpressionFfi {     unsafe {         // todo: error handling         let rust_string = CStr::from_ptr(s).to_str().unwrap();         parse(rust_string).unwrap().convert_to_c()     } }

Здесь куча unwrap(), что означает «паникуй при любой ошибке». В нормальном продакшен коде конечно же ошибки нужно обрабатывать нормально и передавать ошибку как часть возврата С-функции.

Ну и мы видим здесь вынужденный блок unsafe, без него бы ничего не скомпилировалось. К сожалению, в этом месте программы компилятор Rust не может отвечать за безопасность памяти. Это понятно и естественно. На стыке Rust и C такое будет всегда. Однако во всех других местах всё абсолютно контролируемо и безопасно.

Фуф, ну вроде всё, можно компилировать. Но вообще-то есть еще один нюанс: надо еще надо написать заголовочные конструкции, чтобы PHP понимал сигнатуры функций и типов.

К счастью, в Раст есть удобная тулза cbindgen. Она автоматически ищет в коде на Раст конструкции, которые помечены extern «C», repr(С) и т.д. и генерит заголовочный файлы

Мне пришлось немного помучиться с настройками cbindgen, они у меня получились такие (cbindgen.toml):

language = "C" no_includes = true style="tag"  [parse] parse_deps = true

Не уверен, что я четко понимаю все нюансы, но это работает )

Пример запуска:

cbindgen . -o target/testffi.h

Результат будет такой:

enum ExpressionType {   Add = 0,   Subtract = 1,   Multiply = 2,   Divide = 3,   UnaryMinus = 4,   Value = 5, }; typedef uint8_t ExpressionType;  struct PairOperands {   struct ExpressionFfi *left;   struct ExpressionFfi *right; };  union ExpressionData {   struct PairOperands pair_operands;   struct ExpressionFfi *single_operand;   int64_t value; };  struct ExpressionFfi {   ExpressionType expression_type;   union ExpressionData data; };  struct ExpressionFfi *parse_arithmetic(const char *s);

Итак,  сгенерировали h-файл, компилируем библиотеку cargo build и можно написать наш php код. Код просто выводит рекурсивной функцией printExpression на экран то, что распаршено нашей Rust-библиотекой

<?php  $cdef = \FFI::cdef(file_get_contents("target/testffi.h"), "target/debug/libexpr_parser.so"); $expression = $cdef->parse_arithmetic("-6-(4+5)+(5+5)*(4-4)");  printExpression($expression);  class ExpressionKind {     const Add = 0;     const Subtract = 1;     const Multiply = 2;     const Divide = 3;     const UnaryMinus = 4;     const Value = 5; }  function printExpression($expression) {     switch ($expression->expression_type) {         case ExpressionKind::Add:         case ExpressionKind::Subtract:         case ExpressionKind::Multiply:         case ExpressionKind::Divide:             $operations = ["+", "-", "*", "/"];             print "(";             printExpression($expression->data->pair_operands->left);             print $operations[$expression->expression_type];             printExpression($expression->data->pair_operands->right);             print ")";             break;         case ExpressionKind::UnaryMinus:             print "-";             printExpression($expression->data->single_operand);             break;         case ExpressionKind::Value:             print $expression->data->value;             break;     } }

Ну вот и всё, спасибо за внимание.

Хрен там был «всё». Память надо еще очистить. Раст не может применить свою магию за пределами Раст-кода.

Добавляем еще одну функцию destroy

#[no_mangle] pub extern "C" fn destroy(expression: *mut ExpressionFfi) {     unsafe {         match (*expression).expression_type {             ExpressionType::Add             | ExpressionType::Subtract             | ExpressionType::Multiply             | ExpressionType::Divide => {                 destroy((*expression).data.pair_operands.right);                 destroy((*expression).data.pair_operands.left);                 Box::from_raw(expression);             }             ExpressionType::UnaryMinus => {                 destroy((*expression).data.single_operand);                 Box::from_raw(expression);             }             ExpressionType::Value => {                 Box::from_raw(expression);             }         };     } }

Box::from_raw(expression); — преобразовывает сырой указатель в тип Box, а так как результат этого преобразования никем не используется, то происходит автоматическое уничтожение памяти при выходе из скоупа.

Не забываем сбилдить и сгенерить заголовочный файл.

и в php добавляем вызов нашей функции

$cdef->destroy($expression);

Вот теперь точно всё. Если вы хотите дополнить или рассказать, что я где-то был не прав, please feel free to comment.

Репозиторий с полным примером находится по ссылке: [https://github.com/anton-okolelov/simple-rust-ariphmetic-parser]

# build docker-compose build docker-compose run php74 cargo build  docker-compose run php74 cbindgen . -o target/testffi.h  #run php docker-compose run php74 php testffi.php 

P.S. Мы обязательно это обсудим в ближайшем выпуске подкаста «Цинковый прод», так что не забудьте подписаться на подкаст.


ссылка на оригинал статьи https://habr.com/ru/post/455614/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *