En PHP 7.4, aparecerá FFI, es decir puede conectar bibliotecas en C (o, por ejemplo, Rust) directamente, sin tener que escribir una extensión completa y comprender sus muchos matices.
Intentemos escribir código en Rust y usarlo en un programa PHP
La idea de implementar FFI en PHP 7.4 fue tomada de LuaJIT y Python, a saber: un analizador está integrado en el lenguaje que comprende las declaraciones de funciones, estructuras, etc. Lenguaje C. De hecho, puede deslizar todo el contenido del archivo de encabezado allí e inmediatamente comenzar a usarlo.
Un ejemplo:
<?php
Conectar los completados de alguien es simple y divertido, pero también desea escribir algo propio. Por ejemplo, debe analizar rápidamente un archivo y utilizar los resultados de análisis de php.
De los tres lenguajes del sistema (C, C ++, Rust), personalmente elijo el último. La razón es simple: no tengo suficientes competencias para escribir inmediatamente un programa seguro para la memoria en C o C ++. El óxido es complicado, pero en este sentido parece más confiable. El compilador le dice inmediatamente dónde está equivocado. Es casi imposible lograr un comportamiento indefinido.
Descargo de responsabilidad: no soy un programador del sistema, así que use el resto bajo su propio riesgo.
Comencemos escribiendo algo completamente simple, una función simple para agregar números. Solo para entrenar. Y luego pasemos a una tarea más difícil.
Crea un proyecto como biblioteca
cargo new hellofromrust --lib
e indique en cargo.toml que es una biblioteca dinámica (dylib)
…. [lib] name="hellofromrust" crate-type = ["dylib"] ….
La función en sí misma en Rast se ve así
#[no_mangle] pub extern "C" fn addNumbers(x: i32, y: i32) -> i32 { x + y }
bueno es decir función normal, solo se le agregan un par de palabras mágicas no_mangle y extern "C"
A continuación, hacemos la construcción de carga para obtener el archivo (bajo Linux)
Se puede usar desde php:
<?php $ffi = FFI::cdef("int addNumbers(int x, int y);", './libhellofromrust.so'); print "1+2=" . $ffi->addNumbers(1, 2) . "\n";
Agregar números es fácil. La función toma argumentos enteros por valor y devuelve un nuevo entero.
Pero, ¿qué pasa si necesitas usar cadenas? Pero, ¿qué pasa si una función devuelve un enlace a un árbol de elementos? ¿Y cómo usar construcciones específicas de Rast en la firma de funciones?
Estas preguntas me torturaron, así que escribí un analizador de expresiones aritméticas en Rast. Y decidí usarlo desde PHP para estudiar todos los matices.
El código completo del proyecto está aquí: simple-rust-arithmetic-parser . Por cierto, también puse una imagen acoplable que contiene PHP (compilado con FFI), Rust, Cbindgen, etc. Todo lo que necesitas para correr.
El analizador, si consideramos el lenguaje Rast puro, hace lo siguiente:
toma una cadena de la forma " 100500*(2+35)-2*5
" y convierte expression.rs en una expresión de árbol:
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), }
es una enumeración Rast, y en Rast, como sabes, la enumeración no es solo un conjunto de constantes, sino que aún puedes vincularles un valor. Aquí, si el tipo de nodo es Expression :: Value, se escribe un número entero, por ejemplo, 100500. Para un nodo de tipo Add, también almacenaremos dos enlaces (Box) a las expresiones de operando de esta adición.
Escribí el analizador bastante rápido, a pesar del conocimiento limitado de Rust, pero tuve que atormentarme con FFI. Si en C la cadena es un puntero a un tipo char *, es decir un puntero a una matriz de caracteres que termina en \ 0, luego en Rast es un tipo completamente diferente. Por lo tanto, debe convertir la cadena de entrada al tipo & str de la siguiente manera:
CStr::from_ptr(s).to_str()
Más sobre CStr
Esto es todo la mitad del problema. El verdadero problema es que no hay enlaces Rast Enums o Safe Box en C. Por lo tanto, tuve que crear una estructura ExpressionFfi separada para almacenar el árbol de expresión de estilo C, es decir a través de struct, union y punteros 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, }
Bueno, y un método para convertirlo:
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
convierte el tipo Box
en un puntero sin formato
Como resultado, la función que exportaremos a PHP se ve así:
#[no_mangle] pub extern "C" fn parse_arithmetic(s: *const c_char) -> *mut ExpressionFfi { unsafe {
Aquí hay un montón de unwrap (), que significa "pánico por cualquier error". En un código de producción normal, por supuesto, los errores deben manejarse normalmente y pasar un error como parte de la devolución de la función C.
Bueno, aquí vemos un bloqueo inseguro forzado, sin él, nada se habría compilado. Desafortunadamente, en este punto del programa, el compilador Rust no puede ser responsable de la seguridad de la memoria. Esto es comprensible y natural. En la unión de Rust y C, esto siempre será así. Sin embargo, en todos los demás lugares, todo está absolutamente controlado y seguro.
Fuf, es como si todo se pudiera compilar. Pero en realidad hay un matiz más: aún necesita escribir construcciones de encabezado para que PHP entienda las firmas de funciones y tipos.
Afortunadamente, Rast tiene una práctica herramienta cbindgen. Busca automáticamente en el código Rast las construcciones etiquetadas como externas "C", repr (C), etc. y generar archivos de encabezado
Tuve que sufrir un poco con la configuración de cbindgen, resultaron así ( cbindgen.toml ):
language = "C" no_includes = true style="tag" [parse] parse_deps = true
No estoy seguro de entender claramente todos los matices, pero funciona)
Ejemplo de lanzamiento:
cbindgen . -o target/testffi.h
El resultado será así:
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);
Entonces, generamos el archivo h, compilamos la biblioteca de cargo build
y puedes escribir nuestro código php. El código simplemente muestra lo que analiza nuestra biblioteca Rust en la pantalla con la función de recursión 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; } }
Bueno, eso es todo, gracias por mirar.
Joder, había "todo". La memoria aún necesita ser borrada. Rast no puede aplicar su magia fuera del código Rast.
Agregar otra función de destrucción
#[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);
- convierte el puntero sin formato al tipo Cuadro y, dado que nadie utiliza el resultado de esta conversión, la memoria se destruye automáticamente cuando sale del ámbito.
No olvides construir y generar el archivo de encabezado.
y en php agregamos una llamada a nuestra función
$cdef->destroy($expression);
Ahora eso es todo. Si desea agregar o decir que me equivoqué en algún lugar, no dude en comentar.
En el enlace se encuentra un repositorio con un ejemplo completo: [ https://github.com/anton-okolelov/simple-rust-arithmetic-parser ]
PD: Hablaremos de esto en el próximo número del podcast Zinc Prod , así que asegúrese de suscribirse al podcast.