• Contact us
  • Documentation
  • Login
Watch a demoFree trial
Blog
Blog
BlogProduktFallstudienNachrichtenInsights
Blog

PHP-Spaß mit FFI: Gerade genug C

PHPFeaturesperformanceCLI
20 Februar 2020
Larry Garfield
Larry Garfield
Direktor für Entwicklererfahrung
Teilen
Diese Seite wurde von unseren Experten auf Englisch verfasst und mithilfe einer KI übersetzt, um einen schnellen Zugriff zu ermöglichen! Die Originalversion findest du hier.

Eine der neuen und spannenden Features von PHP 7.4 ist die Unterstützung für Foreign Function Interface, kurz PHP FFI. PHP FFI bietet eine viel einfachere Möglichkeit, Code aus anderen, schnelleren Sprachen in PHP zu laden, als eine Erweiterung zu schreiben. Einfacher heißt aber nicht trivial. Es ist etwas Arbeit damit verbunden, und es ist nicht in allen Situationen geeignet. 

Aufbau einer C-Bibliothek

Die gängigste Sprache für die FFI-Integration ist C, zum einen wegen ihrer Allgegenwärtigkeit und zum anderen, weil es die beliebteste Sprache für Leute ist, die das Maximum an performance aus ihrem Computer herausholen wollen. C funktioniert aber nicht wie PHP, also lass uns ein paar Grundlagen von C durchgehen.

Um es klar zu sagen: Dies ist kein Tutorial über C. Es ist ein Tutorial zum Erstellen von C und deckt gerade so viel ab, dass du mit PHP FFI loslegen kannst. How Stuff Works bietet einen guten Crashkurs zu C selbst, falls du daran interessiert bist.

C ist eine kompilierte Sprache. Das bedeutet, dass du den Quellcode in einer Datei schreibst und dann einen Kompilierungsbefehl darauf anwendest, wodurch eine separate, nur maschinenlesbare Datei entsteht. Das steht im Gegensatz zu PHP und anderen interpretierten Sprachen, die die Quelldatei im laufenden Betrieb in ausführbaren Code kompilieren, diesen im Speicher halten und nach Fertigstellung wieder verwerfen. 

C-Quellcode befindet sich in einer Datei mit der Endung „.c“. Das Programm kann in verschiedene Formen kompiliert werden:

  • Eine eigenständige ausführbare Datei oder „Binärdatei“
  • 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 gemeinsam genutzte Bibliothek. Wir werden zu Demonstrationszwecken beide erstellen.

Ein weiterer Aspekt von C ist, dass die Quelldatei allein nicht ausreicht. C verfügt auch über „Header-Dateien“, die auf .h enden und die Schnittstelle eines Pakets definieren. Das Konzept ähnelt den Schnittstellen in C, ist aber allgemeiner. Die Header-Datei definiert die Funktionen und Datentypen, die eine Bibliothek anderen Bibliotheken zur Verfügung stellt. PHP hat nicht wirklich ein Äquivalent, aber es ähnelt in gewisser Weise den „Exports“ für JavaScript-Module; nicht jede Funktion oder jeder Datentyp muss öffentlich gemacht werden.

Schreib deine eigene

Beginnen wir mit einer einfachen Header-Datei, um zu zeigen, wie das Ganze funktioniert. Hier ist unser erstes „points.h“:

struct point {
   int     x;
   int     y;
};

double distance(struct point first, struct point second);

Diese Datei definiert eine Struktur namens „point“. Eine Struktur ähnelt einer Klasse, hat aber keine Methoden. (Historisch gesehen ist eine Klasse eher „eine Struktur mit Methoden“ als umgekehrt.) Diese Struktur besteht aus zwei Ganzzahlen: „x“ und „y“.

Der Header deklariert außerdem eine öffentliche Funktion, distance, die zwei point-Strukturen entgegennimmt und eine double zurückgibt. Das ist C-Sprache für „Gleitkommazahl mit doppelter Genauigkeit“, was PHP-Entwickler als „float“ erkennen werden. Genau wie bei einer PHP-Schnittstelle definiert sie nicht den Körper, sondern nur die Signatur. Der Sinn der Header-Datei besteht darin, anderem Code mitzuteilen: „So werde ich aussehen, wenn ich kompiliert werde.“

Wir kommen auf die Header-Datei zurück, wenn wir über FFI sprechen, aber das reicht fürs 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 Zeilen unter „#include“ 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 gerade erst geschrieben. Die zweite, „<math.h>“, bezieht sich auf die Header-Datei „math.h“, die im Standardbibliotheksverzeichnis des Systems verfügbar ist. Die komplexen mathematischen Funktionen der Standard-C-Bibliothek werden in einer von der Basisbibliothek getrennten Bibliothek bereitgestellt, da zu der Zeit, als sie in den 1980er Jahren erstmals definiert wurden, mathematische Features auf CPUs teuer und selten waren, sodass man sie oft übersprang und nur die wenigen Teile neu implementierte, die man selbst benötigte. Das ist heute nicht mehr der Fall, aber alte Standards halten sich hartnäckig.

Die Methode `distance` berechnet den Abstand zwischen zwei Punkten mithilfe unseres alten Bekannten, des Satzes des Pythagoras. Die Funktionen `pow()` und `sqrt()` stammen aus der Mathematikbibliothek. (Randbemerkung: Die Funktion `sqrt()` wird „squirt“ ausgesprochen. Ich dulde in diesem Punkt keine Meinungsverschiedenheiten.)

Schließlich haben wir noch eine dritte Datei, die die Funktion „distance()“ nutzt. Eine eigenständige ausführbare Datei muss eine Funktion namens „main()“ enthalten, die ausgeführt wird, wenn das Programm läuft. Unsere sieht so 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("Distance is: %f\n\n", distance(p1, p2));

   return 0;
}

Das sollte dir mittlerweile ziemlich vertraut sein. „#include <stdio.h>“ stellt die Funktionssignaturen für die Standard-Ein-/Ausgabebibliothek bereit, während „#include "./points.h"“ dieser Datei Zugriff auf die Funktionen aus unserer Punktebibliothek gewährt. Die Funktion „main()“ erstellt zwei Punkte und berechnet und gibt dann deren Abstand aus. „printf()“ ist Teil der Bibliothek „stdio“ und funktioniert im Grunde genauso wie in PHP. (Oder besser gesagt: PHPs „printf()“ ist nur eine dünne Hülle um die C-Funktionen.)

Erstellen

Nun, da wir unsere Quelldateien haben, müssen wir sie kompilieren. Dies ist in C eigentlich ein mehrstufiger Prozess. Natürlich benötigst du einen C-Compiler und andere Build-Tools. Auf einem typischen Linux-System gibt es ein Paket namens „build-essentials“ (oder ähnlich), das du installieren kannst und das alles enthält, was wir hier erwähnen. Für andere Plattformen schau bitte in deren Dokumentation nach, da die Installation selbst etwas aufwendig sein kann.

Da mehrere Schritte erforderlich sind, werden wir eine „Make-Datei“ verwenden. GNU Make ist der Urvater aller Task-Runner; Ant, Phing, Grunt, Gulp, DoIt und die anderen sind allesamt Neuimplementierungen von Make. Make selbst ist, im Guten wie im Schlechten, eine sehr dünne Schicht über Shell-Skripten. (Ich würde dich gerne auf die Dokumentation verweisen, aber leider ist diese erschreckend unverständlich, wenn man bedenkt, wie einfach Make selbst ist.)

Wir beginnen mit einer Datei namens „Makefile“ (Groß- und Kleinschreibung beachten) mit einem einzigen „Build-Ziel“:

# Makefile

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

Das erstellt ein einziges Build-Ziel, „points.o“, das von zwei Dateien abhängt: „points.h“ und „points.c“. Es ist üblich, dass Build-Ziele den Namen der Datei tragen, die sie erzeugen. Unter dieser Zeile stehen eingerückt ein oder mehrere auszuführende Shell-Befehle. (Hinweis: Für die Einrückung muss ein Tabulatorzeichen verwendet werden, keine Leerzeichen. Das sind die Regeln, ich habe sie nicht aufgestellt.)

gcc ist der GNU Compiler Collection und der am weitesten verbreitete 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 „Diese Datei kompilieren, aber keine der anderen Schritte ausführen.“ Die Ausgabe dieses Befehls ist eine neue Datei namens (Überraschung!) points.o. Dies ist die „Objektdatei“ oder die rohe Maschinencode-Version 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 Programmierer nicht an Vokale glaubten.)

0000000000000000 T distance
             	U _GLOBAL_OFFSET_TABLE_
             	U pow
             	U sqrt

Das listet vier Funktionen auf, die in die Objektdatei programmiert sind: Unsere Funktion `distance` ist mit ihrem Quellcode enthalten (daher das T), während `pow`, `sqrt` und einige interne Verwaltungsfunktionen ebenfalls erwähnt, aber intern nicht definiert sind. Das ist ein Hinweis auf 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 unseren Punktcode in eine gemeinsam genutzte Bibliothek umwandeln. Zur Veranschaulichung machen wir beides:

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

Dieser Befehl „gcc“ bedeutet: „Kompiliere die Ausgabedatei (-o) „main“ unter Verwendung der Objektdateien „main.o“ und „points.o“ und verknüpfe sie mit der Bibliothek „m“. Das Ziel „main“ hängt von den anderen Zielen „main.o“ und „points.o“ ab, sodass diese Ziele bei Bedarf ausgeführt werden, um die Dateien zu erstellen, falls sie noch nicht existieren.

Das m verdient eine gesonderte Erwähnung; es lässt sich so lesen: „Hier ist eine weitere einzubindende Objektdatei namens libm im Standard-Bibliotheksverzeichnis des Systems.“ Der „lib“-Teil des Namens wird im Befehl -l weggelassen. (Das ist ein kleines L, für diejenigen, die dies in einer serifenlosen Schrift lesen.) m ist in diesem Fall die zuvor erwähnte C-Standard-Mathematikbibliothek. (Siehe vorherige Anmerkung darüber, dass Programmierer in den 80ern allergisch darauf waren, ganze Wörter zu tippen.)

Der Vorgang, bei dem mehrere Objektdateien miteinander verbunden werden, nennt man „Linking“. Wenn sie alle zu einer einzigen ausführbaren Datei zusammengefügt werden, spricht man von „statischem Linking“.

Wir können nun „make main“ in der Befehlszeile ausführen. Das bewirkt Folgendes:

  • points.c zu einer Maschinencode-Datei points.o kompilieren.
  • main.c zu einer Maschinencode-Datei main.o kompiliert.
  • Diese beiden Dateien werden zusammen mit der C-Standard-Mathematikbibliothek zu einem Ganzen zusammengefügt und in ein Format verpackt, das das Betriebssystem ausführen kann.

Wenn du nun „./main“ in der Befehlszeile ausführst, sollte Folgendes ausgegeben werden:

$ ./main
Distance is: 6.403124

Juhu.

Es sind jedoch noch zwei weitere Schritte erforderlich. Erstens ist main für PHP-FFI nicht geeignet. Dafür benötigen wir eine Shared Library. Dazu ist ein weiteres Build-Ziel in Makefile erforderlich:

points.so: points.o
	# Wrap the object file into a shared object.
	gcc -shared -o points.so points.o -lm

Dieser Befehl lautet: „Erstelle eine gemeinsam genutzte Bibliothek, Ausgabe unter points.so, durch Kombination von points.o und der Bibliothek libm.“ Er ist dem Befehl für main sehr ähnlich, aber dieses Mal erzeugen wir eine .so-Datei, auch „Shared Object“-Datei genannt. Diese verwendet denselben Code wie die eigenständige Binärdatei, verpackt ihn jedoch anders, mit Hooks für andere C-Programme, damit diese ihn separat in den Speicher laden und bei Bedarf darauf zugreifen können. Es ist die .so-Datei, die wir für PHP FFI benötigen.

Es gibt noch ein weiteres Ziel, das wir benötigen, um vollständig zu sein, und das ist „clean“:

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

Jedes Mal, wenn du programmierst, möchtest du deine kompilierten Versionen löschen und von vorne beginnen können. In diesem Fall führen wir einfach „rm“ für alle kompilierten Dateien aus. Stelle sicher, dass du „.o“ und „.so“ ebenfalls zu deiner „.gitignore“-Datei hinzufügst, da du diese nicht in Git committen möchtest.

Hier ist unser bisheriges endgültiges „Makefile“:

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.o points.o -lm
  chmod +x main

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

Weiter zu PHP!

Das Erstellen eines C-Programms kann schnell wesentlich aufwendiger und komplexer werden als dieses einfache Beispiel. Das Ziel heute war es, erste Erfahrungen zu sammeln und die Struktur 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 letztendlich points.so und points.h sein, die wir beide für PHP benötigen.

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

Bleiben Sie auf dem Laufenden

Abonnieren Sie unseren monatlichen Newsletter.

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

Kostenloser Test