Contact salesFree trial
Blog

Integration von C-Bibliotheken mit PHP FFI

PHPCLIKonfigurationUpsunify
26 Februar 2020
Teilen Sie

Heute werden wir diese C-Bibliothek in PHP einbinden und das Ganze auf Upsun zum Laufen bringen.

Das Foreign Function Interface von PHP verstehen

FFI, oder Foreign Function Interface, ist eine Funktion vieler Sprachen, die es dieser Sprache ermöglicht, in einer anderen Sprache geschriebenen Code aufzurufen. PHP hat diese Funktionalität in PHP 7.4 erhalten, obwohl es, wie zu erwarten war, einige Probleme gibt.

Ein Problem ist, dass PHP von Natur aus von entfernten Systemen aus zugänglich ist. Das stellt ein natürliches Sicherheitsrisikodar . Wenn man mit FFI irgendeine Lücke in einer Anwendung ausnutzen könnte, dann könnte man potenziell eine Lücke für die Remotecodeausführung von Code auf Systemebene erreichen. Auf einer Sicherheitsskala von 1-10 ist das ein "heilige Scheiße!", daher unterstützt PHP standardmäßig nicht einmal das. FFI ist nur standardmäßig über die CLI oder in vorgeladenem Code aktiviert.

Allerdings gibt es hier einen Vorbehalt. Das Vorladen (ebenfalls neu in PHP 7.4, mehr dazu in Kürze) basiert auf dem Opcache. Das gilt auch für FFI. Der Opcache ist jedoch standardmäßig deaktiviert, da er in der CLI keine Möglichkeit hat, zwischengespeicherte Opcodes von einer Ausführung zur nächsten zu erhalten. Um FFI mit der CLI zu verwenden, müssen wir daher den Opcache manuell aktivieren.

FFI auf dem CLI

Beginnen wir mit etwas Code, um die zuvor erstellte Punktebibliothek zu verwenden. Zunächst benötigen wir eine Struktur in PHP, die die point struct nachahmt. (Das ist nicht erforderlich, aber die Spiegelung von Wertobjekten auf beiden Seiten macht die API einfacher zu verstehen).

class Point { public int $x; public int $y; public function __construct(int $x, int $y) { $this->x = $x; $this->y = $y; } }

Der nächste Schritt besteht darin, PHP über die Bibliothek zu informieren. Es gibt mehrere Möglichkeiten, dies zu tun, aber wir beginnen mit cdef():

$ffi = FFI::cdef(file_get_contents('points.h'), __DIR__ . '/points.so');

FFI::cdef() ist keine schiefgelaufene Alphabetisierungsstunde; es steht für "C-Definition" und nimmt zwei Parameter entgegen: eine Zeichenkette, die die freizulegenden C-Strukturen definiert, und den Pfad zu der .so-Datei, in der sie zu finden sind.

In diesem Fall ist es am einfachsten, die Definition einfach in der .h-Datei zu lesen. Allerdings benutzt PHPs FFI keinen Standard-C-Header-Parser. Es hat seinen eigenen, der zum Zeitpunkt der Erstellung dieses Artikels... eher schwach ist. Er unterstützt viele Syntaxen nicht, die in modernem C-Code völlig legal sind und die in den meisten echten C-Programmen heute üblich sind. Während selbst geschriebene Bibliotheken wie diese ihre Header wiederverwenden können, haben die meisten echten C-Bibliotheken Header, die zu komplex sind, um von PHPs FFI verarbeitet zu werden. Das bedeutet, dass Sie Ihre eigenen Header neu implementieren müssen.

Das ist eigentlich kein Problem; die Kopplung zwischen einer Header-Datei und einer Bibliothek ist viel schwächer als, sagen wir, eine PHP-Schnittstelle und eine Klasse. Die Header-Datei sagt nur: "Es gibt irgendwo eine Funktion namens distance, die 2 Punkte annimmt." Die .so-Datei selbst veröffentlicht "BTW, I've got a function named distance that takes 2 points". Solange diese zum Zeitpunkt des Ladens übereinstimmen, wird es klappen.

Das bedeutet auch, dass wir, wenn wir nur einen kleinen Teil einer größeren Bibliothek offenlegen wollen, eine Header-Datei schreiben können, die nur die Funktionen und Strukturen deklariert, die wir PHP offenlegen wollen, und den Rest unzugänglich lässt. Das kann bei größeren Bibliotheken nützlich sein.

Wir werden hier nicht näher darauf eingehen, aber Anthony Ferrara hat ein Tool namens FFIMe geschrieben, das die FFI-API umhüllt, um die Arbeit damit zu erleichtern, und auch die Vorverarbeitung von C-Headern in die von PHP unterstützte Untermenge der Sprache übernimmt. Es ist nicht perfekt, aber es ist es wert, für ernsthafte FFI-Nutzung untersucht zu werden.
Wir können nun die Variable $ffi verwenden, um auf die Teile der .so-Datei zuzugreifen, die offengelegt werden.

// inline.php // ...

$p1 = new Point(3, 4); $p2 = new Point(7, 9); $cp1 = $ffi->new('struct point'); $cp2 = $ffi->new('struct point'); $cp1->x = $p1->x; $cp1->y = $p1->y; $cp2->x = $p2->x; $cp2->y = $p2->y; $d = $ffi->distance($cp1, $cp2); print "Abstand ist $d\n";

Zuerst erstellen wir zwei Punktobjekte. Dann erstellen wir zwei Variablen, die Brücken zu der Punktstruktur aus der Header-Datei sind. Diese Variablen sind vom Typ FFI\CData und dienen als Adapter zwischen PHP- und C-Land. Da es sich um Objekte handelt, können wir sie als Objekte verwenden, und sie übernehmen die Rückübersetzung nach C, so dass das Kopieren unserer Point-PHP-Objektwerte in diese Variablen ein einfacher und langweiliger Prozess ist.

Schließlich können wir die distance-Methode für das $ffi-Objekt aufrufen, das ein Adapter für die C-Funktion ist. Die Daten werden an die C-Bibliothek weitergegeben, die einen Double (auch Float genannt) erzeugt und zurückgibt. Wenn Sie diesen Code ausführen, wird pflichtgemäß Distance is 6.4031242374328 ausgegeben.

Oder, na ja, fast. Wir haben oben erwähnt, dass FFI einen funktionierenden Opcache benötigt, den die CLI standardmäßig nicht hat. Wir können ihn entweder in der php.ini oder in der Kommandozeile selbst aktivieren. Letzteres ist einfacher:

$ php -d opcache.enable_cli=true inline.php Entfernung ist 6.4031242374328

Jetzt erhalten wir die gewünschte Ausgabe.

Im Voraus einrichten

Hier gibt es allerdings ein Problem. Das Instanziieren der FFI-Bibliothek, das Parsen der Header-Datei und das Einrichten der entsprechenden Adapter im Hintergrund nimmt eine nicht unerhebliche Zeit in Anspruch, insbesondere wenn die C-Bibliothek größer ist als in diesem einfachen Beispiel. Auf der CLI gibt es keine gute Möglichkeit, diese Kosten bei jeder Anfrage zu vermeiden, aber sobald wir diesen Code mit einer Web-Anfrage verbinden, wollen wir diesen Overhead nach Möglichkeit auslassen.

Glücklicherweise bietet FFI eine alternative Möglichkeit, eine Brücke zu einer C-Bibliothek zu schlagen, die keinen Laufzeit-Overhead hat und Preloading nutzt. Preloading ist eine weitere neue Funktion von PHP 7.4, mit der Sie Klassen- und Funktionsdefinitionen einmalig beim Start des Servers in den Speicher von PHP-FPM laden können und sie dann nie wieder neu laden müssen. Wir haben bereits über Preloading geschrieben, und es ist eine wirklich tolle Ergänzung.

Sie können auch eine C-Bibliothek per Preloading initialisieren und im Speicher behalten, um die Initialisierungskosten bei zukünftigen Anfragen zu vermeiden. Standardmäßig ist dies die einzige Möglichkeit, die Bibliothek in einer Webanforderung zu verwenden, und zwar aus den bereits erwähnten Sicherheitsgründen. Es ist möglich, FFI::cdef() in Web-Anfragen für die Entwicklung zu aktivieren, aber bitte tun Sie dies niemals in der Produktion.

Wir werden zuerst das Vorladen auf der Kommandozeile zeigen. Da wir sowieso Preloading verwenden, verschieben wir die Points-Klassendefinition in eine andere Datei namens classes.php, als ob wir uns wieder im Jahr 2006 befinden würden. (Es ist retro.) Als nächstes müssen wir einige zusätzliche Änderungen an der Header-Datei vornehmen, indem wir diese beiden Zeilen am Anfang hinzufügen:

#define FFI_SCOPE "POINTS" #define FFI_LIB "./points.so"

FFI_SCOPE gibt einen eindeutigen Bezeichner an, an dem PHP FFI diese Bibliothek erkennt. FFI_LIB gibt die Bibliotheksdatei an, auf die sie sich bezieht. Beachten Sie, dass der Pfad relativ zum Pfad der Systembibliothek ausgewertet wird, der standardmäßig nicht das aktuelle Verzeichnis enthält. Das bedeutet, dass das ./ erforderlich ist, und wenn es fehlt, kann es nicht geladen werden.

Eine weitere wichtige Warnung für die Header-Datei: Zum jetzigen Zeitpunkt verhindert ein Fehler in PHP, dass die Header-Datei geparst wird, wenn vor einer #define-Zeile eine Kommentarzeile steht. Alle #define-Anweisungen müssen vor jedem Kommentar stehen. Wir haben diesen Fehler an PHP gemeldet (siehe vorheriger Link) und hoffen, dass er in einer zukünftigen Version behoben oder zumindest dokumentiert wird.

Nun können wir uns daran machen, ein Preload-Skript einzurichten. In diesem Fall ist es eine sehr einfache Datei:

<?php // preloader.php declare(strict_types=1); FFI::load(__DIR__ . "/points.h"); opcache_compile_file(__DIR__ . "/classes.php");

Der Aufruf opcache_compile_file() weist den Preloader an, alles in classes.php in den gemeinsamen Speicher von PHP zu laden. Der andere Aufruf, FFI::load(), weist PHP an, die Header-Datei zu laden und dann den FFI-Adapter für diese Definition zu erstellen und zu speichern. Die zu verwendende Bibliotheksdatei und der Bezeichner für diese Bibliothek werden in der Header-Datei angegeben.

Schließlich können wir ein weiteres Testskript einrichten, um die vorgeladene Version in Aktion zu sehen; es kann fast identisch mit der Inline-Version sein, nur mit einer Änderung des FFI-Setup-Codes:

<?php // preload.php $ffi = \FFI::scope("POINTS"); $p1 = new Point(3, 4); $p2 = new Point(7, 9); $cp1 = $ffi->new('struct point'); $cp2 = $ffi->new('struct point'); $cp1->x = $p1->x; $cp1->y = $p1->y; $cp2->x = $p2->x; $cp2->y = $p2->y; $d = $ffi->distance($cp1, $cp2); print "Abstand ist $d\n";

Der $ffi-Adapter wird nun mit FFI::scope() erstellt und gibt die zuvor geladene Bibliothek(POINTS) an, die wir verwenden wollen. Da die Bibliothek bereits im Vorladeschritt in den Speicher geladen wurde, ist dieser Aufruf wesentlich kostengünstiger. Der Rest des Codes ist identisch.

Beachten Sie auch, dass die Datei classes.php nicht mehr benötigt wird, da sie bereits geladen wurde. Sie befindet sich bereits im Speicher und kann verwendet werden.

Um dieses Skript von der Kommandozeile aus zu starten, müssen wir das zu verwendende Preload-Skript angeben, wiederum entweder inline oder über php.ini:

$php -d opcache.preload="preloader.php" -d opcache.enable_cli=true preload.php Entfernung ist 6.4031242374328

Et voilà.

Verbessern der API

Die FFI-API kann sich manchmal etwas unnatürlich anfühlen, und zwar sehr oft, vor allem, wenn man anfängt, die fortgeschrittenen Funktionen zu nutzen, auf die wir hier nicht eingehen werden. Zum Glück haben wir als Programmierer eine Standardlösung für klobige APIs: "Hey, schaut mal, eine Abstraktion!"

Es gibt zwei wesentliche Änderungen, die wir an unserer Codebasis vornehmen werden. Erstens lassen wir Point-Objekte sich selbst in eine FFI-Variable umwandeln:

class Point { public int $x; public int $y; public function __construct(int $x, int $y) { $this->x = $x; $this->y = $y; } public function toStruct($ffi) { $cp = $ffi->new('struct point'); $cp->x = $this->x; $cp->y = $this->y; return $cp; } }

Zweitens werden wir die FFI-Logik in ein anderes Objekt verpacken. Es gibt ein Dutzend Möglichkeiten, dies zu tun, je nach Kontext, in dem man es tut, aber wir entscheiden uns jetzt für die einfache Variante, um ein Zeichen zu setzen (kein Wortspiel beabsichtigt):

class PointApi { private static $ffi = null; public function __construct() { static::$ffi ??= \FFI::scope("POINTS"); } public function distance(Point $p1, Point $p2): float { $cp1 = $p1->toStruct(static::$ffi); $cp2 = $p2->toStruct(static::$ffi); return static::$ffi->distance($cp1, $cp2); }

Ein bestimmter FFI-Bereich kann sicher mehrfach referenziert werden. Anstatt zu versuchen, daraus ein globales Singleton zu machen und es zu injizieren, werden wir es einfach in eine private statische Variable packen.

Jetzt können wir unser Beispielskript auf dieses vereinfachen:

<?php declare(strict_types=1); $p1 = new Point(3, 4); $p2 = new Point(7, 9); $api = new PointApi(); $d2 = $api->distance($p1, $p2); print "Distance is $d2\n";

Kein FFI in Sicht, aber das ist es, was hinter den Kulissen passiert.

Kompilieren der C-Bibliothek für PHP FFI

Die nächste Frage ist natürlich, wie man all diesen Code für Webanfragen zum Laufen bringt. Auf Upsun ist das ziemlich einfach, aber die beweglichen Teile sind im Grunde auf jedem Server die gleichen.

Wir beginnen mit unserem neuesten Code in einem Projekt-Repository und nehmen an, dass die routes.yaml und eine leere services.yaml bereits eingerichtet sind. Dann fügen wir ein Webverzeichnis mit einem einfachen Webskript hinzu, das nur das preload.php-Skript von vorhin ist, mit unserer bereinigten API.

Der interessante Teil befindet sich in der Datei .platform.app.yaml, in der es ein paar bewegliche Teile gibt. Wir werden die gemeinsam genutzte Bibliothek bei jedem Build kompilieren, damit wir immer die neueste Version haben. Glücklicherweise ist gcc bereits auf allen Upsun-Umgebungen verfügbar, so dass dies trivial zu erledigen ist. Hier ist das vollständige Beispiel mit den entsprechenden Kommentaren:

name: app type: php:7.4 # FFI ist eine Erweiterung. Sie muss explizit aktiviert werden. runtime: extensions: - ffi # Das Setzen einer Variable im `php`-Namensraum macht sie zu einer ini-Einstellung. Dieser Block # weist PHP-FPM an, die Datei "preloader.php" beim Start auszuführen. Es ist dieselbe Datei, die wir zuvor gesehen haben. variables: php: opcache.preload: "preloader.php" hooks: build: | # Mit dem Makefile, das wir in Teil 1 definiert haben, kompilieren wir die Datei "points.so". set -e make points.so web: locations: "/": root: "web" passthru: "/index.php" disk: 128


Nun wird bei jedem Git-Push die points-Bibliothek in eine neue .so-Datei neu kompiliert. Zum Zeitpunkt der Bereitstellung startet PHP-FPM, lädt die FFI-Erweiterung und führt das einmalige Skript preloader.php aus, um unseren POINTS-FFI-Bereich zu initialisieren (und alles andere, was wir tun wollen). Die Webkonfiguration ist denkbar einfach: Jede Anfrage geht an unsere Basisdatei index.php, die denselben Code enthält, den wir bereits gesehen haben.

Wenn wir sicherstellen wollen, dass die FFI-Bibliothek von der Kommandozeile aus funktioniert, z. B. für eine Cron-Aufgabe, ist es am einfachsten, dafür zu sorgen, dass der Cron-Befehl sowohl die Direktiven opcache.preload als auch opcache.enable_cli enthält (siehe oben). Beide sind erforderlich.

Aber ist das klug? Das ist iFFI

Puh! Nicht viel Code, aber eine Menge Konzepte. Ist es das wert?

Wie immer lautet die Antwort: "Es kommt darauf an". Die meiste Zeit? Nein, das ist es nicht. In einem trivialen Fall wie diesem ist es kontraproduktiv.

Zum einen ist modernes PHP von sich aus schon verdammt schnell. Die Leistungsvorteile, die sich aus dem Verschieben von PHP-Code nach C ergeben, sind in den meisten Fällen bestenfalls marginal; die Eliminierung von ein oder zwei Datenbankaufrufen führt zu einer viel besseren Geschwindigkeit bei viel weniger Aufwand.

Zum anderen hat FFI seinen eigenen Overhead. Jedes Mal, wenn Sie die PHP/C-Grenze überschreiten, entsteht ein Übersetzungsaufwand bei der Umwandlung von Datenstrukturen von einem Stil in den anderen. Allein der Zugriff auf eine Variable über FFI kann doppelt so langsam sein, als wenn die Variable in PHP wäre. Bei trivialem Code wie diesem Beispiel ist es mit Sicherheit langsamer als die direkte Implementierung von distance() in PHP.

Wann ist es also sinnvoll, Logik von PHP nach C zu übertragen und dabei FFI zu verwenden? Ich sehe zwei Hauptanwendungsfälle:

  • Wenn Sie eine beträchtliche Menge an schwerer Verarbeitung zu erledigen haben. Oft wird das offline in einem Befehlszeilenskript oder einem Arbeitsprozess erledigt, aber es kann auch in einer Webanforderung geschehen, wenn es dynamisch sein muss. Denken Sie an Bildverarbeitung, maschinelles Lernen, umfangreiche Graphenmanipulationen und andere Aufgaben, die Hunderte oder Tausende von Objekten oder Schleifenwiederholungen umfassen können, wenn sie in PHP ausgeführt werden. In einer vorkompilierten Bibliothek sind diese Aufgaben schneller erledigt.
  • Wenn Sie eine bestehende Bibliothek von jemand anderem nutzen wollen. In der Vergangenheit musste dafür eine PHP-Erweiterung geschrieben werden, was viel schwieriger und aufwendiger ist als die Verwendung von FFI. Mit FFI können Sie nun jedoch eine bestehende C-Bibliothek in FFI einpacken und sie in PHP genauso verwenden, als wäre es eine Erweiterung. Die Anwendungsfälle sind ähnlich: Bildverarbeitung, maschinelles Lernen und andere CPU-intensive Aufgaben, für die es bereits eine Fülle von in C oder C++ geschriebenem Code gibt. In diesem Fall müssen Sie höchstwahrscheinlich eine Header-Datei schreiben oder FFIMe verwenden, um eine Brücke zu PHP zu schlagen. Ich würde dringend empfehlen, auch eine schönere API auf FFI zu bauen, wie wir es in diesem Beispiel getan haben, damit es für die Endbenutzer einfacher zu benutzen ist.

Wenn Ihr Anwendungsfall in einen dieser Bereiche fällt, haben Sie jetzt ein neues Werkzeug, mit dem Sie das Problem angehen können.

Gabriel Couto hat eine Reihe von Beispielen für die Verwendung von FFI mit bestehenden Bibliotheken zusammengestellt, bei denen eine Neuimplementierung in PHP überhaupt keinen Sinn machen würde. Nachdem Sie nun die FFI-Grundlagen gut beherrschen, bieten die Beispiele die nächste Stufe nach oben.

Nützliche Links

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

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