In PHP 7.4 wird FFI angezeigt, d. H. Sie können Bibliotheken in C (oder beispielsweise Rust) direkt verbinden, ohne eine ganze Erweiterung schreiben und die vielen Nuancen verstehen zu müssen.
Versuchen wir, Code in Rust zu schreiben und ihn in einem PHP- Programm zu verwenden
Die Idee, FFI in PHP 7.4 zu implementieren, wurde von LuaJIT und Python übernommen, nämlich: Ein Parser ist in die Sprache integriert, die Deklarationen von Funktionen, Strukturen usw. versteht. C Sprache. Tatsächlich können Sie den gesamten Inhalt der Header-Datei dorthin verschieben und sofort mit der Verwendung beginnen.
Ein Beispiel:
<?php
Das Anschließen der fertigen Personen ist einfach und macht Spaß, aber Sie möchten auch etwas Eigenes schreiben. Zum Beispiel müssen Sie eine Datei schnell analysieren und die Analyseergebnisse von PHP verwenden.
Von den drei Systemsprachen (C, C ++, Rust) wähle ich persönlich die letztere. Der Grund ist einfach: Ich habe nicht genügend Kompetenzen, um sofort ein speichersicheres Programm in C oder C ++ zu schreiben. Rost ist kompliziert, aber in diesem Sinne sieht es zuverlässiger aus. Der Compiler sagt Ihnen sofort, wo Sie falsch liegen. Es ist fast unmöglich, undefiniertes Verhalten zu erreichen.
Haftungsausschluss: Ich bin kein Systemprogrammierer. Den Rest verwenden Sie auf eigenes Risiko.
Beginnen wir damit, etwas ganz Einfaches zu schreiben, eine einfache Funktion zum Hinzufügen von Zahlen. Nur zum Training. Und dann gehen wir zu einer schwierigeren Aufgabe über.
Erstellen Sie ein Projekt als Bibliothek
cargo new hellofromrust --lib
und geben Sie in load.toml an, dass es sich um eine dynamische Bibliothek (dylib) handelt.
…. [lib] name="hellofromrust" crate-type = ["dylib"] ….
Die Funktion selbst auf Rast sieht so aus
#[no_mangle] pub extern "C" fn addNumbers(x: i32, y: i32) -> i32 { x + y }
gut, d.h. normale Funktion, nur ein paar magische Wörter no_mangle und extern "C" werden hinzugefügt
Als nächstes bauen wir Fracht, um die So-Datei zu erhalten (unter Linux)
Kann von PHP verwenden:
<?php $ffi = FFI::cdef("int addNumbers(int x, int y);", './libhellofromrust.so'); print "1+2=" . $ffi->addNumbers(1, 2) . "\n";
Das Hinzufügen von Zahlen ist einfach. Die Funktion verwendet ganzzahlige Argumente als Wert und gibt eine neue Ganzzahl zurück.
Aber was ist, wenn Sie Zeichenfolgen verwenden müssen? Was aber, wenn eine Funktion einen Link zu einem Elementbaum zurückgibt? Und wie kann man bestimmte Konstruktionen von Rast für die Signatur von Funktionen verwenden?
Diese Fragen quälten mich und ich schrieb einen Parser mit arithmetischen Ausdrücken über Rast. Und ich entschied mich, es von PHP aus zu verwenden, um alle Nuancen zu studieren.
Der vollständige Projektcode ist hier: simple-rust-arithmetic-parser . Übrigens habe ich auch ein Docker-Image eingefügt, das PHP (kompiliert mit FFI), Rust, Cbindgen usw. enthält. Alles was du brauchst um zu rennen.
Der Parser macht, wenn wir die reine Rast-Sprache betrachten, Folgendes:
nimmt eine Zeichenfolge der Form " 100500*(2+35)-2*5
" und konvertiert expression.rs in einen 100500*(2+35)-2*5
:
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 ist eine Rast-Aufzählung, und in Rast ist Enum, wie Sie wissen, nicht nur eine Menge von Konstanten, sondern Sie können trotzdem einen Wert an sie binden. Wenn der Knotentyp Expression :: Value ist, wird eine Ganzzahl in ihn geschrieben, z. B. 100500. Für einen Knoten vom Typ Add speichern wir auch zwei Links (Box) zu den Operandenausdrücken dieser Addition.
Ich habe den Parser trotz der begrenzten Kenntnisse von Rust ziemlich schnell geschrieben, aber ich musste mich mit FFI quälen. Wenn in C die Zeichenfolge ein Zeiger auf einen char * -Typ ist, d. H. ein Zeiger auf ein Array von Zeichen, die mit \ 0 enden, dann ist es in Rast ein völlig anderer Typ. Daher müssen Sie die Eingabezeichenfolge wie folgt in den Typ & str konvertieren:
CStr::from_ptr(s).to_str()
Mehr zu CStr
Das ist alles die halbe Mühe. Das eigentliche Problem ist, dass es in C keine Rast-Enums oder Safe-Box-Links gibt. Daher musste ich eine separate ExpressionFfi-Struktur zum Speichern des Ausdrucksbaums im C-Stil erstellen, d. H. über Struktur, Vereinigung und einfache Zeiger ( 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, }
Nun, und eine Methode, um darauf zu konvertieren:
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
verwandelt den Box
Typ in einen Box::into_raw
Infolgedessen sieht die Funktion, die wir nach PHP exportieren, folgendermaßen aus:
#[no_mangle] pub extern "C" fn parse_arithmetic(s: *const c_char) -> *mut ExpressionFfi { unsafe {
Hier ist ein Haufen von unwrap (), was "Panik für jeden Fehler" bedeutet. In einem normalen Produktionscode müssen Fehler natürlich normal behandelt und ein Fehler als Teil der Rückgabe der C-Funktion übergeben werden.
Nun, hier sehen wir einen erzwungenen unsicheren Block, ohne den sich nichts kompiliert hätte. Leider kann der Rust-Compiler zu diesem Zeitpunkt im Programm nicht für die Speichersicherheit verantwortlich sein. Das ist verständlich und natürlich. An der Kreuzung von Rust und C wird dies immer sein. An allen anderen Orten ist jedoch alles absolut kontrolliert und sicher.
Fuf, es ist so, als ob alles kompiliert werden kann. Aber tatsächlich gibt es noch eine Nuance: Sie müssen noch Header-Konstruktionen schreiben, damit PHP die Signaturen von Funktionen und Typen versteht.
Glücklicherweise hat Rast ein praktisches Tool zum Binden. Es sucht automatisch im Rast-Code nach Konstruktionen, die mit extern "C", repr (C) usw. gekennzeichnet sind. und generieren Sie Header-Dateien
Ich musste ein bisschen mit den Einstellungen von cbindgen leiden, sie stellten sich wie folgt heraus ( cbindgen.toml ):
language = "C" no_includes = true style="tag" [parse] parse_deps = true
Ich bin mir nicht sicher, ob ich alle Nuancen klar verstehe, aber es funktioniert.
Startbeispiel:
cbindgen . -o target/testffi.h
Das Ergebnis wird folgendermaßen aussehen:
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);
Also haben wir die h-Datei generiert, die cargo build
Bibliothek kompiliert und Sie können unseren PHP-Code schreiben. Der Code zeigt einfach mit der printExpression-Rekursionsfunktion an, was von unserer Rust-Bibliothek auf dem Bildschirm analysiert wird.
<?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; } }
Nun, das ist es, danke fürs Zuschauen.
Scheiße, da war "alles". Der Speicher muss noch gelöscht werden. Rast kann seine Magie nicht außerhalb des Rast-Codes anwenden.
Fügen Sie eine weitere Zerstörungsfunktion hinzu
#[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);
- konvertiert den Rohzeiger in den Box-Typ. Da das Ergebnis dieser Konvertierung von niemandem verwendet wird, wird der Speicher beim Verlassen des Bereichs automatisch zerstört.
Vergessen Sie nicht, die Header-Datei zu erstellen und zu generieren.
und in php fügen wir unserer funktion einen aufruf hinzu
$cdef->destroy($expression);
Das ist alles. Wenn Sie hinzufügen oder mitteilen möchten, dass ich irgendwo falsch lag, können Sie dies gerne kommentieren.
Ein Repository mit einem vollständigen Beispiel befindet sich unter folgendem Link: [ https://github.com/anton-okolelov/simple-rust-arithmetic-parser ]
PS Wir werden dies in der nächsten Ausgabe des Zinc Prod- Podcasts diskutieren. Abonnieren Sie also unbedingt den Podcast.