En PHP 7.4, FFI apparaîtra, c'est-à-dire vous pouvez connecter des bibliothèques en C (ou, par exemple, Rust) directement, sans avoir à écrire une extension entière et à comprendre ses nombreuses nuances.
Essayons d'écrire du code dans Rust et de l'utiliser dans un programme PHP
L'idée d'implémenter FFI en PHP 7.4 a été empruntée à LuaJIT et Python, à savoir: un analyseur est intégré dans le langage qui comprend les déclarations de fonctions, structures, etc. Langage C. En fait, vous pouvez y glisser tout le contenu du fichier d'en-tête et commencer immédiatement à l'utiliser.
Un exemple:
<?php
Connecter les fichiers finis de quelqu'un est simple et amusant, mais vous voulez également écrire votre propre article. Par exemple, vous devez analyser rapidement un fichier et utiliser les résultats d'analyse de php.
Parmi les trois langages système (C, C ++, Rust), je choisis personnellement ce dernier. La raison est simple: je n'ai pas assez de compétences pour écrire immédiatement un programme sans mémoire en C ou C ++. La rouille est compliquée, mais en ce sens, elle semble plus fiable. Le compilateur vous indique immédiatement où vous vous trompez. Il est presque impossible d'obtenir un comportement indéfini.
Avis de non-responsabilité: je ne suis pas un programmeur système, alors utilisez le reste à vos risques et périls.
Commençons par écrire quelque chose de complètement simple, une fonction simple pour ajouter des nombres. Juste pour l'entraînement. Et ensuite passons à une tâche plus difficile.
Créer un projet en tant que bibliothèque
cargo new hellofromrust --lib
et indiquer dans cargo.toml qu'il s'agit d'une bibliothèque dynamique (dylib)
…. [lib] name="hellofromrust" crate-type = ["dylib"] ….
La fonction elle-même sur Rast ressemble à ceci
#[no_mangle] pub extern "C" fn addNumbers(x: i32, y: i32) -> i32 { x + y }
eh bien fonction normale, seuls quelques mots magiques no_mangle et extern "C" y sont ajoutés
Ensuite, nous faisons la construction de la cargaison pour obtenir le fichier (sous Linux)
Peut utiliser à partir de php:
<?php $ffi = FFI::cdef("int addNumbers(int x, int y);", './libhellofromrust.so'); print "1+2=" . $ffi->addNumbers(1, 2) . "\n";
L'ajout de numéros est facile. La fonction prend des arguments entiers par valeur et renvoie un nouvel entier.
Mais que faire si vous devez utiliser des chaînes? Mais que se passe-t-il si une fonction renvoie un lien vers un arbre d'éléments? Et comment utiliser des constructions spécifiques de Rast dans la signature de fonctions?
Ces questions m'ont torturé, j'ai donc écrit un analyseur d'expressions arithmétiques sur Rast. Et j'ai décidé de l'utiliser à partir de PHP pour étudier toutes les nuances.
Le code complet du projet est ici: simple-rust-arithmetic-parser . Soit dit en passant, j'ai également mis une image de docker qui contient PHP (compilé avec FFI), Rust, Cbindgen, etc. Tout ce dont vous avez besoin pour courir.
L'analyseur, si nous considérons le langage pur Rast, fait ce qui suit:
prend une chaîne de la forme " 100500*(2+35)-2*5
" et convertit expression.rs en une expression arborescente:
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), }
c'est une énumération Rast, et dans Rast, comme vous le savez, l'énumération n'est pas seulement un ensemble de constantes, mais vous pouvez toujours leur lier une valeur. Ici, si le type de nœud est Expression :: Value, un entier y est écrit, par exemple 100500. Pour un nœud de type Ajouter, nous allons également stocker deux liens (Box) vers les expressions d'opérande de cet ajout.
J'ai écrit l'analyseur assez rapidement, malgré la connaissance limitée de Rust, mais j'ai dû me tourmenter avec FFI. Si en C la chaîne est un pointeur vers un type char *, c'est-à-dire un pointeur vers un tableau de caractères se terminant par \ 0, puis dans Rast, c'est un type complètement différent. Par conséquent, vous devez convertir la chaîne d'entrée en type & str comme suit:
CStr::from_ptr(s).to_str()
Plus sur CStr
C'est la moitié du problème. Le vrai problème est qu'il n'y a pas d'énumérations Rast ou de liens Safe Box en C. Par conséquent, j'ai dû créer une structure ExpressionFfi distincte pour stocker l'arbre d'expression de style C, c'est-à-dire via struct, union et pointeurs 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, }
Eh bien, et une méthode pour le convertir:
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
transforme le type Box
en un pointeur brut
En conséquence, la fonction que nous allons exporter vers PHP ressemble à ceci:
#[no_mangle] pub extern "C" fn parse_arithmetic(s: *const c_char) -> *mut ExpressionFfi { unsafe {
Voici un tas de unwrap (), ce qui signifie «panique pour toute erreur». Dans un code de production normal, bien sûr, les erreurs doivent être traitées normalement et une erreur doit être transmise dans le cadre du retour de la fonction C.
Eh bien, ici, nous voyons un bloc forcé dangereux, sans lui, rien n'aurait été compilé. Malheureusement, à ce stade du programme, le compilateur Rust ne peut pas être responsable de la sécurité de la mémoire. C'est compréhensible et naturel. À la jonction de Rust et C, ce sera toujours le cas. Cependant, dans tous les autres endroits, tout est absolument contrôlé et sûr.
Pouf, c'est comme si tout pouvait être compilé. Mais en réalité, il y a une nuance de plus: vous devez toujours écrire des constructions d'en-tête pour que PHP comprenne les signatures des fonctions et des types.
Heureusement, Rast dispose d'un outil cbindgen pratique. Il recherche automatiquement dans le code Rast les constructions étiquetées extern "C", repr (C), etc. et générer des fichiers d'en-tête
J'ai dû souffrir un peu avec les paramètres de cbindgen, ils se sont révélés comme ça ( cbindgen.toml ):
language = "C" no_includes = true style="tag" [parse] parse_deps = true
Je ne suis pas sûr de bien comprendre toutes les nuances, mais ça marche)
Exemple de lancement:
cbindgen . -o target/testffi.h
Le résultat sera comme ceci:
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);
Nous avons donc généré le fichier h, compilé la bibliothèque de cargo build
et vous pouvez écrire notre code php. Le code affiche simplement ce qui est analysé par notre bibliothèque Rust à l'écran avec la fonction de récursivité 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; } }
Eh bien, c'est tout, merci d'avoir regardé.
Putain, il y avait «tout». La mémoire doit encore être effacée. Rast ne peut pas appliquer sa magie en dehors du code Rast.
Ajouter une autre fonction de destruction
#[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);
- convertit le pointeur brut en type Box, et puisque le résultat de cette conversion n'est utilisé par personne, la mémoire est automatiquement détruite lorsque vous quittez la portée.
N'oubliez pas de construire et de générer le fichier d'en-tête.
et en php nous ajoutons un appel à notre fonction
$cdef->destroy($expression);
Voilà, c'est tout. Si vous voulez ajouter ou dire que je me suis trompé quelque part, n'hésitez pas à commenter.
Un référentiel avec un exemple complet se trouve sur le lien: [ https://github.com/anton-okolelov/simple-rust-arithmetic-parser ]
PS Nous en discuterons dans le prochain numéro du podcast Zinc Prod , alors assurez-vous de vous abonner au podcast.