Contact salesFree trial
Blog

PHP-Spaß mit FFI: Gerade genug C

PHPMerkmaleLeistungCLI
20 Februar 2020
Teilen Sie

Eine der neuen und aufregenden Funktionen von PHP 7.4 ist die Unterstützung für das Foreign Function Interface, kurz PHP FFI. PHP FFI bietet eine viel einfachere Möglichkeit, Code aus anderen, schnelleren Sprachen in PHPzu laden , als eine Erweiterung zu schreiben. Allerdings bedeutet "einfacher" nicht "trivial". Es ist mit einigem Aufwand verbunden und nicht in allen Situationen sinnvoll.

Anatomie einer C-Bibliothek

Die gebräuchlichste Sprache für die FFI-Integration ist C, zum einen, weil sie allgegenwärtig ist, und zum anderen, weil es die gebräuchlichste Sprache für Leute ist, die die maximale Leistung aus ihrem Computer herausholen wollen. C funktioniert jedoch nicht wie PHP, also lassen Sie uns einige Grundlagen von C durchgehen.

Um das klarzustellen, dies ist kein Tutorium über C. Es ist ein Tutorium über die Entwicklung von C und deckt gerade genug ab, um Sie mit PHP FFI zum Laufen zu bringen. How Stuff Works hat einen vernünftigen Crashkurs über C selbst, falls Sie interessiert sind.

C ist eine kompilierte Sprache. Das bedeutet, dass man den Quellcode in eine Datei schreibt und dann einen Kompilierbefehl ausführt, der eine separate, nur für Maschinen lesbare Datei erzeugt. Dies steht im Gegensatz zu PHP und anderen interpretierten Sprachen, die den Quellcode sofort in ausführbaren Code umwandeln und ihn im Speicher halten, um ihn dann zu verwerfen.

C-Quellcode befindet sich in einer Datei, die auf .c endet. Der Code kann in verschiedene Formen kompiliert werden:

  • Eine eigenständige ausführbare Datei, oder "Binary"
  • eine statische Bibliothek, die dann mit anderen statischen Bibliotheken zu einer eigenständigen ausführbaren Datei kombiniert werden kann
  • eine gemeinsam genutzte Bibliothek, die zur Laufzeit von einer eigenständigen ausführbaren Datei oder mehreren eigenständigen ausführbaren Dateien geladen werden kann

Eine einzelne Binärdatei ist am einfachsten zu erstellen, aber für PHP FFI benötigen wir eine Shared Library. Zu Demonstrationszwecken werden wir beides bauen.

Ein weiterer Aspekt von C ist, dass die Quelldatei allein nicht ausreicht. C hat auch "Header-Dateien", die auf .h enden und die Schnittstelle eines Pakets definieren. Das ist ein ähnliches Konzept wie die Schnittstellen in C, nur allgemeiner. Die Header-Datei definiert die Funktionen und Datentypen, die eine Bibliothek für andere Bibliotheken bereitstellt. PHP hat nicht wirklich ein Äquivalent, aber es ist ähnlich wie "exports" für Javascript-Module; nicht jede Funktion oder jeder Datentyp muss öffentlich gemacht werden.

Schreiben Sie Ihre eigene

Beginnen wir mit einer einfachen Header-Datei, um zu zeigen, wie das Ganze funktioniert. Hier ist unsere anfängliche points.h:

struct point { int x; int y; }; double distance(struct point first, struct point second);

Diese Datei definiert eine Struktur namens point. Eine struct ist ähnlich wie eine Klasse, hat aber keine Methoden. (Historisch gesehen ist eine Klasse "ein struct mit Methoden" und nicht umgekehrt). Diese Struktur besteht aus 2 Ganzzahlen, x und y.

Der Header deklariert auch eine öffentliche Funktion, distance, die zwei point structs annimmt und einen double zurückgibt. Das ist C-Sprache für "double-precision floating point number", was PHP-Entwickler als "float" erkennen werden. Genau wie eine PHP-Schnittstelle definiert sie nicht den Körper, sondern nur die Signatur. Der Sinn der Header-Datei ist es, anderen Code mitzuteilen: "So werde ich aussehen, wenn ich kompiliert werde".

Wir werden auf die Header-Datei zurückkommen, wenn wir über FFI sprechen, aber das ist genug für C.

Jetzt brauchen wir eine Datei für die Implementierung von distance:

#include "points.h" #include <math.h> double distance(struct point first, struct point second) { double a_squared = pow((second.x - first.x), 2); double b_squared = pow((second.y - first.y), 2); return sqrt(a_squared + b_squared); }

Die #include-Zeilen geben an, welche Abhängigkeiten diese Bibliothek hat. Die erste, in Anführungszeichen, verweist auf die Datei points.h im selben Verzeichnis. Das haben wir soeben geschrieben. Die zweite, <math.h>, steht für die Header-Datei math.h, die sich im Verzeichnis der Standardbibliothek des Systems befindet. Die komplexen mathematischen Funktionen der C-Standardbibliothek werden in einer separaten Bibliothek zur Verfügung gestellt, denn als sie in den 1980er Jahren erstmals definiert wurden, waren mathematische Funktionen auf CPUs teuer und selten, so dass man sie oft überging und nur die wenigen Bits, die man brauchte, selbst neu implementierte. Das ist heute nicht mehr der Fall, aber alte Standards sind unverwüstlich.

Die distance-Methode berechnet den Abstand zwischen zwei Punkten mit Hilfe unseres alten Freundes, des Satzes von Pythagoras. Die Funktionen pow() und sqrt() stammen aus der mathematischen Bibliothek. (Nebenbei bemerkt: Die Funktion sqrt() wird "squirt" ausgesprochen. In diesem Punkt dulde ich keine Meinungsverschiedenheiten.)

Schließlich haben wir noch eine dritte Datei, die die Funktion distance() verwendet. Eine eigenständige ausführbare Datei muss eine Funktion namens main() haben, die ausgeführt wird, wenn das Programm läuft. Unsere sieht wie folgt aus:

#include <stdio.h> #include "./points.h" int main() { struct point p1; struct point p2; p1.x = 3; p1.y = 4; p2.x = 7; p2.y = 9; printf("Abstand ist: %f\n\n", distance(p1, p2)); return 0; }

#include <stdio.h> liefert die Funktionssignaturen für die Standard-Ein-/Ausgabebibliothek, während #include "./points.h" dieser Datei Zugriff auf die Funktionen unserer Punktbibliothek gibt. Die Funktion main() erzeugt zwei Punkte und berechnet dann deren Abstand und gibt ihn aus. printf() ist Teil der stdio-Bibliothek und funktioniert im Grunde genauso wie in PHP. (Oder besser gesagt, PHP's printf() ist nur ein dünner Wrapper auf C's.)

Machen Sie

Nun, da wir unsere Quelldateien haben, müssen wir sie erstellen. Das ist in C eigentlich ein mehrstufiger Prozess. Natürlich brauchen Sie einen C-Compiler und andere Build-Tools. Auf einem typischen Linux-System gibt es ein Paket namens build-essentials (oder ähnlich), das Sie installieren können und das alles enthält, was wir hier erwähnen. Für andere Plattformen konsultieren Sie deren Dokumentation, da die Installation selbst etwas kompliziert sein kann.

Da es mehrere Schritte gibt, werden wir eine "Make-Datei" verwenden. GNU Make ist der ursprüngliche Granddaddy unter den Task-Runnern; Ant, Phing, Grunt, Gulp, DoIt und der Rest sind allesamt Neuimplementierungen von Make. Make selbst ist eine sehr dünne Schicht über dem Shell-Scripting, im Guten wie im Schlechten. (Ich würde Sie auf die Dokumentation verweisen, aber leider ist die Dokumentation schockierend undurchsichtig, wenn man bedenkt, wie einfach Make selbst ist).

Wir beginnen mit einer Datei namens Makefile (Großbuchstaben sind wichtig) mit einem einzigen "Build-Target":

# Makefile points.o: points.h points.c gcc -c points.c

Das erzeugt ein einziges Build-Target, points.o, das von zwei Dateien abhängt: points.h und points.c. Es ist typisch für Build-Targets, dass sie den Namen der Datei tragen, die sie erzeugen. Unter dieser Zeile stehen eingerückt ein oder mehrere Shell-Befehle, die ausgeführt werden sollen. (Hinweis: Die Einrückung muss mit einem Tabulatorzeichen erfolgen, nicht mit Leerzeichen. Das sind die Regeln, ich habe sie nicht gemacht.)

gcc ist die GNU Compiler Collection und ist der am meisten verwendete C-Compiler, aber nicht der einzige. Er hat etwa 14 Millionen mögliche Optionen, von denen wir etwa vier verwenden werden. Der Schalter -c bedeutet: "Kompiliere diese Datei, aber führe keinen der anderen Schritte aus." Die Ausgabe dieses Befehls ist eine neue Datei mit dem Namen (Überraschung!) points.o. Dies ist die "Objektdatei" oder die rohe Maschinencodeversion von points.c.

Wenn wir nm points.o ausführen, listet es die "Symbole" in der Objektdatei auf. (nm ist die Abkürzung für "name" und stammt aus einer Zeit, in der der Speicher so knapp und die Tastaturen so schwer zu bedienen waren, dass die Programmierer nicht an Vokale glaubten).

0000000000000000 T distance U _GLOBAL_OFFSET_TABLE_ U pow U sqrt

Das sind vier Funktionen, die in der Objektdatei kodiert sind: Der Quellcode unserer Funktion distance ist enthalten (daher das T), während pow, sqrt` und einige interne Buchhaltungsfunktionen ebenfalls erwähnt werden, aber nicht intern definiert sind. Das ist ein Hinweis für den nächsten Schritt: "Hey, ich brauche diese Dinge."

Wir wollen auch ein Ziel, um die Datei main.c zu kompilieren:

main.o: main.c gcc -c main.c

Als nächstes haben wir die Wahl. Wir können eine eigenständige ausführbare Datei für main erstellen, oder wir können unseren Punktecode in eine gemeinsam genutzte Bibliothek verwandeln. Zu Demonstrationszwecken machen wir beides:

main: main.o points.o gcc -o main main main.o points.o -lm

Dieser gcc-Befehl lautet: "Kompiliere in die Ausgabedatei (-o) main unter Verwendung der Objektdateien main.o und points.o, und linke gegen die Bibliothek m." Das Ziel main hängt von den anderen Zielen main.o und points.o ab, so dass diese Ziele bei Bedarf ausgeführt werden, um diese Dateien zu erzeugen, wenn sie noch nicht vorhanden sind.

Das m verdient besondere Erwähnung; es kann gelesen werden als "Hier ist eine weitere Objektdatei mit dem Namen libm, die in das Standardbibliotheksverzeichnis des Systems eingebunden werden soll." Der "lib"-Teil des Namens wird im Befehl -l weggelassen. (Das ist ein kleingeschriebenes L, für diejenigen, die dies in einer serifenlosen Schriftart lesen.) m ist in diesem Fall die C-Standardbibliothek für Mathematik, die wir bereits erwähnt haben. (Siehe die vorherige Bemerkung über Programmierer in den 80ern, die allergisch auf die Eingabe ganzer Wörter reagierten).

Der Prozess, bei dem mehrere Objektdateien miteinander verbunden werden, wird "Linking" genannt. Wenn sie alle zu einer einzigen ausführbaren Datei zusammengefügt werden, nennt man das "statisches Linken".

Wir können nun make main auf der Kommandozeile ausführen. Das wird:

  • Kompiliert points.c zu einer Maschinencode-Datei points.o.
  • Kompiliert main.c zu einer Maschinencodedatei main.o.
  • Diese beiden Dateien werden zusammen mit der mathematischen C-Standardbibliothek zusammengeklebt und in ein Format verpackt, das das Betriebssystem ausführen kann.

Wenn Sie nun ./main auf der Kommandozeile ausführen, sollte die folgende Ausgabe erscheinen:

$ ./main Die Entfernung beträgt: 6.403124

Juhu.

Es sind jedoch noch zwei weitere Schritte nötig. Erstens ist main nicht für PHP FFI geeignet. Dafür brauchen wir eine Shared Library. Das erfordert ein weiteres Build-Target im Makefile:

points.so: points.o # Die Objektdatei in ein Shared Object umwandeln. gcc -shared -o points.so points.o -lm

Dieser Befehl lautet: "Erstellen Sie eine gemeinsam genutzte Bibliothek, die nach points.so ausgegeben wird, indem Sie points.o und die libm-Bibliothek kombinieren." Er ist dem Befehl für main sehr ähnlich, aber dieses Mal erzeugen wir eine .so-Datei, oder "Shared Object"-Datei. Diese enthält denselben Code wie die eigenständige Binärdatei, ist aber anders verpackt und bietet anderen C-Programmen die Möglichkeit, ihn separat in den Speicher zu laden und bei Bedarf darauf zuzugreifen. Es ist die .so-Datei, die wir für PHP FFI benötigen.

Es gibt noch ein weiteres Ziel, das wir brauchen, um vollständig zu sein, und das ist clean:

clean: rm -f *.o *.so main

Jedes Mal, wenn Sie Code kompilieren, wollen Sie in der Lage sein, Ihre kompilierten Versionen zu löschen und mit einer neuen Version zu beginnen. In diesem Fall löschen wir einfach alle kompilierten Dateien mit rm. Stellen Sie sicher, dass Sie auch .o und .so zu Ihrer .gitignore-Datei hinzufügen, da Sie diese nicht an Git übergeben wollen.

Hier ist unser endgültiges Makefile bis jetzt:

points.o: points.h points.c gcc -c points.c points.so: points.o gcc -shared -o points.so points.o -lm main.o: main.c gcc -c main.c main: main.o points.o gcc -o main main main.o points.o -lm chmod +x main clean: rm -f *.o *.so main

Vorwärts zu PHP!

Die Erstellung eines C-Programms kann sehr viel komplizierter und komplexer werden als dieses triviale Beispiel. Das Ziel des heutigen Tages war es, uns die Füße nass zu machen und die Anatomie einer C-Bibliothek so weit zu erklären, dass wir damit beginnen können, sie in die FFI-Schicht von PHP einzubinden. Die wichtigsten Teile werden points.so und points.h sein, die wir beide für PHP brauchen werden.

(Vielen Dank an Anthony Ferrara für seine Hilfe bei der Erklärung, wie man C baut).

Ihr größtes Werk
steht vor der Tür

Kostenloser Test
Discord
© 2025 Platform.sh. All rights reserved.