TrueAsync Mobile
(Demo-Projekt, experimentell, Repository native-bridge, Android)
Asynchrones PHP passt hervorragend zu UI-Anwendungen: die Oberfläche darf nicht einfrieren, während etwas mit dem Netzwerk spricht, von der Festplatte liest oder auf die nächste Benutzeraktion wartet. TrueAsync hat dafür eine dedizierte C-API: das Trigger Event (ZEND_ASYNC_NEW_TRIGGER_EVENT() in zend_async_API.h). Es ist ein Objekt mit einer einzigen Methode, trigger(), die beliebiger C- oder C++-Code von einem anderen Thread aus aufrufen kann, um den PHP-Reaktor thread-sicher aufzuwecken und ihm die Kontrolle zu übergeben, das Event zu verarbeiten.
native-bridge implementiert genau diese Art von Integration für Android: PHP bettet sich als persistenter Prozess in die App ein, startet einmalig auf einem Hintergrund-Thread, betreibt eine Event-Loop (denselben TrueAsync-Reaktor, der im Rest des Ökosystems verwendet wird) und spricht in beide Richtungen mit Kotlin.
Warum ein persistenter Prozess statt Request/Response
Das übliche PHP-Szenario ist eine Web-Anfrage: der Prozess startet, bearbeitet eine Anfrage und beendet sich. Das passt nicht zu einer mobilen App: PHP muss so lange am Leben bleiben, wie die App geöffnet ist, und auf Benutzer-Events (Taps, Sensoren, Standort) genauso reagieren, wie ein Handler auf eine HTTP-Anfrage reagiert. Genau das liefert native-bridge: PHP startet einmalig beim App-Start und lebt in seinem eigenen Thread, bis es explizit gestoppt wird, während TrueAsync-Coroutinen innerhalb dieses Threads Events und Hintergrundarbeit konkurrent bearbeiten.
Bridge-Architektur
Die Bridge arbeitet in zwei Richtungen:
- Android zu PHP. Kotlin schiebt Events (ein Tap, ein Sensorwert, Standort, ein beliebiges Custom-Event) in eine Queue, und PHP holt sie aus seiner eigenen Loop.
- PHP zu Kotlin. PHP ruft Methoden auf, die auf der Kotlin-Seite implementiert sind (einen Toast anzeigen, vibrieren, Text in die Zwischenablage kopieren und so weiter).
Beide Richtungen laufen über JNI (Java Native Interface), den Standard-Android-Mechanismus, der C-Code den Aufruf von Kotlin-/Java-Code erlaubt und umgekehrt. Keine der beiden Richtungen transportiert Daten über JSON oder ein anderes Textformat: Werte überqueren die Grenze bereits typisiert, ohne zusätzliche Konvertierungen.
PHP läuft auf seinem eigenen OS-Thread und blockiert niemals den UI-Thread von Android. Wenn PHP auf Daten wartet, bleibt der UI-Thread reaktionsfähig, und umgekehrt.
Richtung 1: Events von Android zu PHP
Kotlin sendet Events über JNI in eine Queue, die PHP mit NativeBridge::poll() liest. Ist die Queue leer, liefert poll() sofort null, und die PHP-Anwendung entscheidet selbst, ob sie auf das nächste Event wartet oder in der Zwischenzeit etwas anderes tut (in der Demo-App ist das eine kurze usleep()-Pause, während der TrueAsync Hintergrund-Coroutinen und Timer laufen lassen kann).
Es gibt vier Event-Typen: ein Bildschirm-Touch, Standortdaten, Sensordaten (Beschleunigungsmesser und Ähnliches) und ein beliebiges Event mit einem Namen und einer Text-Payload. Letzteres nutzt die Demo-App, um Button-Klicks zu markieren:
use TrueAsync\NativeBridge;
while (!NativeBridge::shouldStop()) {
$event = NativeBridge::poll();
if ($event === null) {
usleep(1000);
continue;
}
if ($event['type'] === NativeBridge::EVENT_GENERIC) {
match ($event['event_name']) {
'count_a' => $counterA->toggle(),
'fetch' => spawn(fn() => fetchDemo()),
default => null,
};
}
}Die ersten drei Event-Typen (Touch, Standort, Sensoren) benötigen keine String-Allokation und bleiben daher auch bei hoher Aufrufrate günstig (zum Beispiel für einen Strom von Beschleunigungsmesser-Daten).
Richtung 2: Aufrufe von PHP nach Kotlin
Wenn PHP eine Modulmethode aufruft, zum Beispiel Toast::show('Hello', true), gibt es zwei Wege, wie dieser Aufruf Kotlin erreichen kann:
Der generische Pfad
Standardmäßig packt PHP die Argumente in einen kompakten typisierten Puffer (kein String-Format wie JSON, sodass Kotlin ihn ohne Textparsing und ohne zusätzliche Allokationen liest) und verschickt ihn über einen einzigen Aufruf von NativeBridge::invoke(). Ein neues Modul oder eine neue Methode auf diesem Pfad hinzuzufügen, berührt nie C: nur Kotlin und der generierte PHP-Wrapper ändern sich, ein Gradle-Rebuild der Kotlin-Seite genügt, kein Rebuild der nativen Bibliothek nötig.
Der schnelle Pfad: #[FastPath]
Für "heiße" Methoden, die sehr oft aufgerufen werden (zum Beispiel Sensordaten bei jedem Frame), markiert die PHP-Spec die Methode mit dem #[FastPath]-Attribut. Für eine solche Methode erzeugt der Generator eine dedizierte typisierte C-Funktion, die Kotlin direkt über JNI aufruft, ohne Zwischenpuffer. Diese Art von Methode erfordert bei jeder Änderung einen Rebuild der nativen Bibliothek (der .so-Datei), läuft aber schneller und ohne zusätzliche Allokationen. Das Verhalten der Methode ändert sich nicht, nur die Art, wie der Aufruf die PHP/Kotlin-Grenze überquert.
Ein Modul beschreiben: #[BridgeModule]
Der Vertrag eines Moduls wird auf der PHP-Seite als Interface mit dem #[BridgeModule]-Attribut beschrieben:
namespace TrueAsync\Android\Spec;
#[BridgeModule]
interface ToastInterface
{
#[Ui]
public function show(string $text, bool $long): void;
public function batteryLevel(): int;
}- Der Modulname wird aus dem Interface-Namen abgeleitet (
ToastInterfacewird zum ModulToast), oder explizit gesetzt:#[BridgeModule('Clipboard')]. #[Ui]an einer Methode bedeutet, dass die Kotlin-Implementierung auf dem UI-Thread von Android laufen muss (der Generator fügt den Thread-Wechsel für Sie hinzu).#[FastPath]an einer Methode aktiviert den oben beschriebenen schnellen Aufrufpfad.
Was tools/bridge/gen.php generiert
Aus einer PHP-Spec (einem #[BridgeModule]-Interface) baut der Generator bei jedem Lauf neu:
- eine Kotlin-Klasse mit abstrakten Methoden (
ToastSpec); - den Call-Routing-Code (Kotlin);
- einen PHP-Wrapper (
Toast::show(...)), den der restliche PHP-Anwendungscode aufruft; - für mit
#[FastPath]markierte Methoden typisierten C-Code, der Kotlin direkt aufruft.
Lebenszyklus der PHP-Anwendung
- Kotlin startet PHP auf einem Hintergrund-Thread und übergibt ihm den Pfad zum PHP-Einstiegsskript.
- Das PHP-Skript ruft
NativeBridge::init()auf; ab diesem Punkt ist die Bridge bereit, Events und Aufrufe entgegenzunehmen. - Von dort an läuft die Anwendung in einer Schleife: Events über
poll()abholen, behandeln und bei Bedarf Hintergrund-TrueAsync-Coroutinen starten (zum Beispiel für Netzwerkanfragen). - Das Herunterfahren erfolgt geordnet: Kotlin ruft
NativeBridge.stop()auf, die PHP-Schleife erkennt das überNativeBridge::shouldStop(), schließt ab und gibt ihre Ressourcen sauber frei.
Beispiel: ein Zähler an einem Button
Ein vereinfachtes Beispiel basierend auf der Demo-App: ein Button startet und stoppt einen endlosen Zähler, und sein Wert aktualisiert sich direkt in der UI. Start und Stopp werden mit dem einfachen spawn()/cancel() einer TrueAsync-Coroutine implementiert, ohne den UI-Thread zu blockieren:
use TrueAsync\NativeBridge;
use TrueAsync\Android\Ui;
use function Async\spawn;
use function Async\delay;
NativeBridge::init();
$root = Ui::newLinearLayout();
$button = Ui::newButton();
Ui::setText($button, '▶ start counter');
Ui::setOnClickListener($button, 'toggle');
Ui::addView($root, $button);
$label = Ui::newTextView();
Ui::setText($label, 'stopped');
Ui::addView($root, $label);
Ui::setContentView($root);
$counter = null;
function tick(int $label): void
{
$n = 0;
while (true) {
Ui::setText($label, 'tick ' . ++$n);
delay(400);
}
}
while (!NativeBridge::shouldStop()) {
$event = NativeBridge::poll();
if ($event === null) {
usleep(1000);
continue;
}
if ($event['type'] === NativeBridge::EVENT_GENERIC && $event['event_name'] === 'toggle') {
if ($counter === null) {
$counter = spawn(fn() => tick($label));
Ui::setText($button, '■ stop counter');
} else {
$counter->cancel();
$counter = null;
Ui::setText($button, '▶ start counter');
}
}
}Ein zweiter Klick bricht die $counter-Coroutine über cancel() ab, und der Zähler bleibt bei dem Wert stehen, den er erreicht hat. Das vollständige Beispiel mit mehreren unabhängigen Zählern befindet sich in android/app.php des Repositorys.
Status und Einschränkungen
- Nur Android wird unterstützt; iOS-Unterstützung ist geplant, aber noch nicht implementiert.
- Die Bridge transportiert derzeit einfache Typen: Strings, Integers, Floats, Booleans. Das Übergeben zusammengesetzter Objekte (Feld für Feld, weiterhin ohne String-Format) ist geplant.
- Die Richtung PHP zu Kotlin ist synchron: eine Methode liefert ihr Ergebnis sofort; verzögerte (asynchrone) Ergebnisse werden auf dieser Seite noch nicht unterstützt.
- PHPs Opcache ist unter Android zwangsweise deaktiviert: die App-Sandbox erlaubt ihm nicht, die Lock-Datei und den ausführbaren Speicher zu nutzen, die es braucht.
- Ein thread-sicherer (ZTS) PHP-Build ist erforderlich, da PHP auf seinem eigenen OS-Thread läuft statt auf dem Haupt-Thread der App.