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

Integration von C-Bibliotheken mit PHP FFI

PHPCLIKonfigurationUpsunify
26 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.

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

PHPs Foreign Function Interface 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, einige Stolpersteine gibt.

Ein Problem ist, dass auf PHP naturgemäß von Remote-Systemen zugegriffen wird. Das birgt ein offensichtliches Sicherheitsrisiko. Mit FFI könntest du, wenn du irgendeine Schwachstelle in einer Anwendung ausnutzen könntest, potenziell eine Lücke für die Remote-Code-Ausführung auf Systemebene schaffen. Auf einer Sicherheitsskala von 1 bis 10 ist das ein „Oh mein Gott!“, daher unterstützt PHP das standardmäßig gar nicht. FFI ist standardmäßig nur über die CLI oder in vorgeladenem Code aktiviert.

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

FFI in der CLI

Fangen wir mit etwas Programmieren an, um die points Bibliothek, die wir zuvor erstellt haben. Zunächst benötigen wir eine Struktur in PHP, die die point struct nachzubilden. (Das ist nicht zwingend erforderlich, aber die Spiegelung von Value-Objekten auf beiden Seiten macht die API übersichtlicher.)

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 fehlgeschlagene Alphabetstunde; es steht für „C-Definition“ und benötigt zwei Parameter: eine Zeichenkette, die die C-Strukturen definiert, die verfügbar gemacht werden sollen, und den Pfad zu der .so Datei, in der sie zu finden sind.

In diesem Fall ist es am einfachsten, einfach die .h Datei mit der Definition einzulesen. Allerdings verwendet PHPs FFI keinen Standard-C-Header-Parser. Es hat einen eigenen, der zum Zeitpunkt des Verfassens dieses Artikels… eher schwach ist. Er unterstützt viele Syntaxelemente nicht, die in modernem C-Code völlig zulässig und in den meisten echten C-Programmen heute sogar recht verbreitet sind. Während selbst geschriebene Bibliotheken wie diese ihre Header wiederverwenden können, haben die meisten echten C-Produktionsbibliotheken Header, die für PHPs FFI zu komplex sind. Das bedeutet, dass du deine eigenen neu implementieren musst.

Das ist eigentlich kein Problem; die Kopplung zwischen einer Header-Datei und einer Bibliothek ist viel schwächer als beispielsweise zwischen einer PHP-Schnittstelle und einer Klasse. Die Header-Datei sagt lediglich: „Irgendwo gibt es eine Funktion namens distance , die 2 points.“ Die .so Datei selbst gibt bekannt: „Übrigens, ich habe eine Funktion namens distance , die 2 points.“ Solange diese beim Laden übereinstimmen, klappt es.

Das bedeutet auch: Wenn wir nur einen kleinen Teil einer größeren Bibliothek verfügbar machen wollen, können wir eine Header-Datei schreiben, die nur die Funktionen und Strukturen deklariert, die wir PHP zur Verfügung stellen wollen, während der Rest unzugänglich bleibt. 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 vereinfachen, und das auch die Vorverarbeitung von C-Headern in die von PHP unterstützte Sprachuntermenge übernimmt. Es ist nicht perfekt, aber für den ernsthaften FFI-Einsatz eine Untersuchung wert.
Wir können nun diese $ffi Variable nutzen, um auf die Teile der .so Datei, die verfügbar sind.

// 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 "Distance is $d\n";

Zuerst erstellen wir zwei Point Objekte. Dann erstellen wir zwei Variablen, die als Brücken zur point Struktur aus der Header-Datei dienen. Diese Variablen sind vom Typ FFI\CData und fungieren als Adapter zwischen der PHP- und der C-Welt. Da es sich um Objekte handelt, können wir sie als Objekte verwenden, und sie übernehmen die Rückübersetzung nach C, sodass das Kopieren unserer Point PHP-Objektwerte in sie hinein ist ein einfacher und langweiliger Vorgang.

Schließlich können wir die distance Methode auf dem $ffi Objekt aufrufen, das als Adapter für die C-Funktion dient. Die Daten werden an die C-Bibliothek übergeben, die einen double (auch bekannt als float) und gibt sie zurück. Wenn du dieses Programmierprogramm ausführst, wird pflichtbewusst Distance is 6.4031242374328.

Oder, nun ja, fast. Erinnere dich daran, dass wir oben gesagt haben, dass FFI einen funktionierenden Opcache benötigt, über den die CLI standardmäßig nicht verfügt. Wir können ihn entweder in php.ini oder direkt in der Befehlszeile aktivieren. Letzteres ist einfacher:

$ php -d opcache.enable_cli=true inline.php
Distance is 6.4031242374328

Jetzt erhalten wir die gewünschte Ausgabe.

Vorab-Einrichtung

Es gibt hier 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 dieses einfache Beispiel. Auf der CLI gibt es keine gute Möglichkeit, diesen Aufwand bei jeder Anfrage zu vermeiden, aber sobald wir dieses Programm mit einer Webanfrage verbinden, möchten wir diesen Overhead nach Möglichkeit überspringen.

Glücklicherweise bietet FFI eine alternative Möglichkeit, eine Brücke zu einer C-Bibliothek zu schlagen, die keinen Laufzeit-Overhead verursacht und das Preloading nutzt. Preloading ist eine weitere neue Funktion in PHP 7.4, mit der du Klassen- und Funktionsdefinitionen beim Serverstart einmalig in den Speicher von PHP-FPM laden und sie danach nie wieder neu laden musst. Wir haben bereits über Preloading geschrieben, und es ist eine wirklich tolle Ergänzung.

Du kannst eine C-Bibliothek auch per Preloading initialisieren und im Speicher behalten, wodurch du die Initialisierungskosten bei zukünftigen Anfragen vermeidest. Standardmäßig ist das aus den oben genannten Sicherheitsgründen die einzige Möglichkeit, sie in einer Webanfrage zu nutzen. Es ist möglich, FFI::cdef() für Webanfragen in der Entwicklung zu aktivieren, aber bitte tu das niemals in der Produktivumgebung.

Wir zeigen das Preloading zunächst in der Befehlszeile. Da wir das Preloading ohnehin nutzen, verschieben wir die Points Klassendefinition in eine andere Datei namens classes.php, als wäre es wieder 2006. (Das 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"

Das FFI_SCOPE gibt eine eindeutige Kennung an, anhand derer PHP FFI diese Bibliothek erkennt. FFI_LIB gibt die Bibliotheksdatei an, auf die sie verweist. Beachte, dass der Pfad relativ zum Systembibliothekspfad ausgewertet wird, der standardmäßig das aktuelle Verzeichnis nicht enthält. Das bedeutet, dass ./ erforderlich ist und das Laden fehlschlägt, wenn er fehlt.

Ein weiterer wichtiger Hinweis zur Header-Datei: Zum Zeitpunkt des Verfassens dieses Artikels verhindert ein Fehler in PHP das Parsen der Header-Datei, wenn vor einer #define Zeile stehen. Alle #define Anweisungen müssen vor etwaigen Kommentaren 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.

Jetzt können wir ein Preload-Skript einrichten. 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 opcache_compile_file() Aufruf weist den Preloader an, alles classes.php in den gemeinsamen Speicher von PHP zu laden. Der andere Aufruf, FFI::load(), weist PHP an, die Header-Datei zu laden und anschließend 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 im FFI-Setup-Programm:

<?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 "Distance is $d\n";

Der $ffi Adapter wird nun erstellt, indem FFI::scope() und unter Angabe der zuvor geladenen Bibliothek (POINTS), die wir verwenden möchten. Da die Bibliothek bereits im Vorladeschritt in den Speicher geladen wurde, ist dies ein wesentlich kostengünstigerer Aufruf. Der Rest des Codes ist identisch.

Beachte außerdem, dass wir, da wir sie vorladen, nicht require die classes.php Datei laden. Sie befindet sich bereits im Speicher und ist einsatzbereit.

Um dieses Skript über die Befehlszeile auszuführen, müssen wir das zu verwendende Vorladeskript angeben, wiederum entweder inline oder über php.ini:

$php -d opcache.preload="preloader.php" -d opcache.enable_cli=true preload.php
Distance is 6.4031242374328

Et voilà.

Verbesserung der API

Die FFI-API kann sich manchmal etwas unnatürlich anfühlen; eigentlich sogar ziemlich oft, besonders wenn man anfängt, ihre fortgeschritteneren Features zu nutzen, auf die wir hier nicht näher eingehen werden. Glücklicherweise haben wir als Programmierer eine Standardlösung für klobige APIs: „Hey, schau mal, eine Abstraktion!“

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

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. Je nach Kontext gibt es Dutzende Möglichkeiten, dies zu tun, aber wir entscheiden uns jetzt für eine einfache Variante, nur um einen Punkt zu verdeutlichen (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 bedenkenlos mehrfach referenziert werden. Anstatt also zu versuchen, ihn zu einem globalen Singleton zu machen und einzubinden, speichern wir ihn einfach in einer privaten statischen Variablen.

Jetzt können wir unser Beispielskript auf Folgendes 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 genau das passiert hinter den Kulissen.

Kompilieren der C-Bibliothek für PHP FFI

Die nächste Frage ist natürlich, wie man all diese Programme für Webanfragen zum Laufen bringt. Auf Upsun ist das ziemlich einfach, aber die wesentlichen Schritte sind auf jedem Server im Grunde dieselben.

Wir beginnen mit unserem neuesten Programm in einem Projekt-Repository und gehen davon aus, dass routes.yaml und ein leeres services.yaml bereits eingerichtet sind. Dann fügen wir ein web Verzeichnis mit einem einfachen Webskript hinzu, das genau das preload.php Skript von vorhin ist, mit unserer bereinigten API.

Der interessante Teil befindet sich in der .platform.app.yaml Datei, wo es ein paar bewegliche Teile gibt. Wir werden die Shared Library bei jedem Build kompilieren, damit wir immer die neueste Version davon haben. Glücklicherweise gcc ist bereits auf allen Upsun-Umgebungen verfügbar, sodass das ganz einfach ist. Hier ist das vollständige Beispiel mit den entsprechenden Kommentaren:

name: app

type: php:7.4

# FFI is an extension. It must be explicitly enabled.
runtime:
  extensions:
    - ffi

# Setting a variable in the `php` namespace makes it an ini setting. This block
# tells PHP-FPM to run `preloader.php` on startup. It's the same file we saw before.
variables:
  php:
    opcache.preload: "preloader.php"

hooks:
  build: |
    # Using the Makefile we defined in part 1, compile the `points.so` file.
    set -e
    make points.so

web:
  locations:
    "/":
      root: "web"
      passthru: "/index.php"

disk: 128


Jetzt wird bei jedem git push wird die Punkte-Bibliothek in eine neue .so Datei kompiliert. Beim Deployment startet dann PHP-FPM, lädt die FFI-Erweiterung und führt das einmalige preloader.php Skript aus, um unseren POINTS FFI-Bereich zu initialisieren (und was auch immer wir sonst noch tun wollen). Die Webkonfiguration ist denkbar einfach; jede Anfrage geht an unsere Basis- index.php Datei, die denselben Code enthält, den wir bereits gesehen haben.

Wenn wir sicherstellen wollten, dass die FFI-Bibliothek über die Befehlszeile funktioniert, etwa für eine cron Aufgabe, ist der einfachste Weg, dafür zu sorgen, dass der cron Befehl sowohl den opcache.preload und opcache.enable_cli Direktiven wie oben enthält. Beide sind erforderlich.

Aber ist das sinnvoll? Das ist iFFI

Puh! Nicht viel Programmieren, aber viele Konzepte. Lohnt sich das?

Wie so oft lautet die Antwort: „Es kommt darauf an.“ Meistens? Nein, das tut es nicht. Bei einem trivialen Fall wie diesem ist es kontraproduktiv.

Zum einen ist modernes PHP an sich schon verdammt schnell. Die Performance-Vorteile, die sich aus der Verlagerung von alltäglichem PHP-Code nach C ergeben, sind in den meisten Fällen bestenfalls marginal; ein oder zwei Datenbankaufrufe zu vermeiden, bringt eine viel größere Beschleunigung bei viel geringerem Aufwand.

Zum anderen hat FFI seinen eigenen Overhead. Jedes Mal, wenn du über die PHP/C-Grenze hinweg aufrufst, entsteht ein Übersetzungs-Overhead, der Datenstrukturen von einem Stil in den anderen konvertiert. 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 in diesem Beispiel ist es fast sicher langsamer, als die Funktion distance() direkt in PHP.

Wann ist es also sinnvoll, Logik mithilfe von FFI von PHP nach C zu verlagern? Ich sehe zwei Hauptanwendungsfälle:

  • Wenn du eine erhebliche Menge an rechenintensiven Aufgaben zu bewältigen hast. Oft wird das offline in einem Kommandozeilen-Skript oder einem Worker-Prozess erledigt, aber es kann auch in einer Webanfrage erfolgen, wenn es dynamisch sein muss. Denk an Bildverarbeitung, maschinelles Lernen, aufwendige Graphmanipulation und andere Aufgaben, die Hunderte oder Tausende von Objekten oder Schleifeniterationen erfordern könnten, wenn sie in PHP ausgeführt würden. Diese laufen in einer vorkompilierten Bibliothek schneller.
  • Wenn du eine bestehende Bibliothek von jemand anderem hast, die du nutzen möchtest. Früher hätte das das Schreiben einer PHP-Erweiterung erfordert, was viel schwieriger und aufwendiger ist als die Verwendung von FFI. Mit FFI kannst du nun jedoch eine bestehende C-Bibliothek nehmen, sie in FFI einbinden und sie von PHP aus nutzen, genau wie es bei einer Erweiterung der Fall wäre. Die Anwendungsfälle sind ähnlich: Bildverarbeitung, maschinelles Lernen und andere CPU-intensive Aufgaben, für die es bereits eine Fülle von Programmen gibt, die in C oder C++ geschrieben wurden. In diesem Fall musst du mit ziemlicher Sicherheit eine eigene Header-Datei schreiben oder FFIMe verwenden, um eine Brücke zu PHP zu schlagen. Ich würde dir dringend empfehlen, zusätzlich eine benutzerfreundlichere API auf Basis von FFI zu erstellen, wie wir es in diesem Beispiel getan haben, um die Nutzung für Endbenutzer zu vereinfachen.

Wenn dein Anwendungsfall in einen dieser Bereiche fällt, hast du nun ein neues Werkzeug, mit dem du das Problem angehen kannst.

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. Jetzt, da du die Grundlagen von FFI gut verstanden hast, bieten die dortigen Beispiele den nächsten Schritt.

Nützliche Links

Bleiben Sie auf dem Laufenden

Abonnieren Sie unseren monatlichen Newsletter.

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

Kostenloser Test