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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В 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/


Интересные статьи

Интересные статьи

SWAP (своп) — это механизм виртуальной памяти, при котором часть данных из оперативной памяти (ОЗУ) перемещается на хранение на HDD (жёсткий диск), SSD (твёрдотельный накоп...
Ранее в одном из наших КП добавление задач обрабатывалось бизнес-процессами, сейчас задач стало столько, что бизнес-процессы стали неуместны, и понадобился инструмент для массовой заливки задач на КП.
Истории по распиливанию монолита часто похожи одна на другую. Был у команды здоровенный неповоротливый монолит, решили его распилить на россыпь правильных и шустреньких микросервисов, все стало...
Golang — отличный язык программирования с широким спектром возможностей. В этой статье показано, как на Go можно написать клиент и сервер для протоколов HTTP/1.1 и HTTP/2. ...
С версии 12.0 в Bitrix Framework доступно создание резервных копий в автоматическом режиме. Задание параметров автоматического резервного копирования производится в Административной части на странице ...