No PHP 7.4, o FFI aparecerá, ou seja, você pode conectar bibliotecas em C (ou, por exemplo, Rust) diretamente, sem a necessidade de escrever uma extensão inteira e entender suas muitas nuances.
Vamos tentar escrever código no Rust e usá-lo em um programa PHP
A idéia de implementar o FFI no PHP 7.4 foi tirada de LuaJIT e Python, a saber: um analisador é incorporado à linguagem que compreende declarações de funções, estruturas etc. Linguagem C. De fato, você pode colocar todo o conteúdo do arquivo de cabeçalho lá e começar a usá-lo imediatamente.
Um exemplo:
<?php
Conectar os terminados de alguém é simples e divertido, mas você também deseja escrever algo de sua preferência. Por exemplo, você precisa analisar rapidamente um arquivo e usar os resultados da análise do php.
Das três linguagens do sistema (C, C ++, Rust), eu pessoalmente escolho a última. O motivo é simples: não tenho competências suficientes para escrever imediatamente um programa com segurança de memória em C ou C ++. A ferrugem é complicada, mas, nesse sentido, parece mais confiável. O compilador informa imediatamente onde você está errado. É quase impossível alcançar um comportamento indefinido.
Isenção de responsabilidade: eu não sou programador de sistemas, portanto, use o resto por sua conta e risco.
Vamos começar escrevendo algo completamente simples, uma função simples para adicionar números. Apenas para treinamento. E então vamos para uma tarefa mais difícil.
Crie um projeto como uma biblioteca
cargo new hellofromrust --lib
e indique no cargo.toml que é uma biblioteca dinâmica (dylib)
…. [lib] name="hellofromrust" crate-type = ["dylib"] ….
A função em si no Rast se parece com isso
#[no_mangle] pub extern "C" fn addNumbers(x: i32, y: i32) -> i32 { x + y }
bem, isto é função normal, apenas algumas palavras mágicas no_mangle e extern "C" são adicionadas a ela
Em seguida, construímos carga para obter o arquivo so (no Linux)
Pode usar do php:
<?php $ffi = FFI::cdef("int addNumbers(int x, int y);", './libhellofromrust.so'); print "1+2=" . $ffi->addNumbers(1, 2) . "\n";
Adicionar números é fácil. A função aceita argumentos inteiros por valor e retorna um novo número inteiro.
Mas e se você precisar usar strings? Mas e se uma função retornar um link para uma árvore de elementos? E como usar construções específicas do Rast em uma assinatura de função?
Essas perguntas me torturaram, então escrevi um analisador de expressões aritméticas no Rast. E eu decidi usá-lo do PHP para estudar todas as nuances.
O código completo do projeto está aqui: simple-rust-arithmetic-parser . A propósito, também coloquei uma imagem do docker que contém PHP (compilado com FFI), Rust, Cbindgen, etc. Tudo o que você precisa para executar.
O analisador, se considerarmos a linguagem Rast pura, faz o seguinte:
pega uma sequência no formato " 100500*(2+35)-2*5
" e converte expression.rs em uma expressão em árvore:
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), }
é um enum Rast e, em Rast, como você sabe, enum não é apenas um conjunto de constantes, mas você ainda pode vincular um valor a elas. Aqui, se o tipo de nó for Expression :: Value, um número inteiro será gravado nele, por exemplo 100500. Para um nó do tipo Add, também armazenaremos dois links (Box) nas expressões de operandos dessa adição.
Escrevi o analisador rapidamente, apesar do conhecimento limitado de Rust, mas tive que me atormentar com a FFI. Se em C a string é um ponteiro para um tipo de char *, ou seja, um ponteiro para uma matriz de caracteres que termina em \ 0 e, em Rast, é um tipo completamente diferente. Portanto, você deve converter a string de entrada no tipo & str da seguinte maneira:
CStr::from_ptr(s).to_str()
Mais sobre CStr
Isso é metade do problema. O verdadeiro problema é que não há enumerações Rast ou links de caixa segura em C. Portanto, tive que criar uma estrutura ExpressionFfi separada para armazenar a árvore de expressão no estilo C, ou seja, via struct, união e ponteiros simples ( 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, }
Bem, e um método para converter para ele:
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
transforma o tipo de Box
em um ponteiro bruto
Como resultado, a função que exportaremos para o PHP é mais ou menos assim:
#[no_mangle] pub extern "C" fn parse_arithmetic(s: *const c_char) -> *mut ExpressionFfi { unsafe {
Aqui está um monte de desembrulhar (), que significa "entre em pânico por qualquer erro". Em um código de produção normal, é claro, os erros devem ser tratados normalmente e um erro passado como parte do retorno da função C.
Bem, aqui vemos um bloco inseguro forçado, sem ele, nada teria sido compilado. Infelizmente, neste ponto do programa, o compilador Rust não pode ser responsável pela segurança da memória. Isso é compreensível e natural. Na junção de Rust e C, sempre será. No entanto, em todos os outros lugares, tudo é absolutamente controlado e seguro.
Fuf, é como se tudo pudesse ser compilado. Mas na verdade há mais uma nuance: você ainda precisa escrever construções de cabeçalho para que o PHP entenda as assinaturas de funções e tipos.
Felizmente, o Rast possui uma ferramenta cbindgen conveniente. Ele procura automaticamente no código Rast por construções rotuladas como externas "C", repr (C) etc. e gerar arquivos de cabeçalho
Eu tive que sofrer um pouco com as configurações do cbindgen, elas ficaram assim ( cbindgen.toml ):
language = "C" no_includes = true style="tag" [parse] parse_deps = true
Não sei se entendi claramente todas as nuances, mas funciona)
Exemplo de lançamento:
cbindgen . -o target/testffi.h
O resultado será assim:
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);
Então, geramos o arquivo h, compilamos a biblioteca de cargo build
e você pode escrever nosso código php. O código simplesmente exibe o que é analisado pela nossa biblioteca Rust na tela com a função de recursão printExpression.
<?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; } }
Bem, é isso, obrigado por assistir.
Foda-se, havia "tudo". A memória ainda precisa ser limpa. Rast não pode aplicar sua mágica fora do código Rast.
Adicione outra função de destruição
#[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);
- converte o ponteiro bruto no tipo Caixa e, como o resultado dessa conversão não é usado por ninguém, a memória é destruída automaticamente quando você sai do escopo.
Não esqueça de criar e gerar o arquivo de cabeçalho.
e no php adicionamos uma chamada à nossa função
$cdef->destroy($expression);
Agora isso é tudo. Se você deseja adicionar ou dizer que eu estava errado em algum lugar, não hesite em comentar.
Um repositório com um exemplo completo está localizado no link: [ https://github.com/anton-okolelov/simple-rust-arithmetic-parser ]
PS Discutiremos isso na próxima edição do podcast Zinc Prod , portanto, assine o podcast.