TrueAsync Mobile
(демонстраційний проєкт, експериментально, репозиторій native-bridge, Android)
Асинхронний PHP чудово підходить саме для UI-застосунків: інтерфейс не повинен зависати, поки десь триває взаємодія з мережею чи завантаження по мережі, читання з диска або очікування наступної дії користувача. У TrueAsync для цього є окремий C-API: Trigger Event (ZEND_ASYNC_NEW_TRIGGER_EVENT() у zend_async_API.h). Це об'єкт з єдиним методом trigger(), який будь-який код на C чи C++ може викликати з іншого потоку, щоб потокобезпечно розбудити реактор PHP і передати йому керування на обробку події.
native-bridge реалізує таку інтеграцію для Android: PHP вбудовується в застосунок як постійний процес, стартує один раз у фоновому потоці, крутить цикл подій (той самий TrueAsync reactor, що і в решті проєктів екосистеми) і спілкується з Kotlin в обидва боки.
Навіщо постійний процес, а не запит-відповідь
Звичайний сценарій PHP: веб-запит, процес піднявся, обробив один запит, завершився. Для мобільного застосунку це не підходить: PHP повинен жити весь той час, поки відкрито застосунок, і реагувати на події користувача (натискання, датчики, геолокацію) так само, як обробник реагує на HTTP-запит. native-bridge дає саме це: PHP піднімається один раз при старті застосунку і живе у своєму потоці до явної зупинки, а корутини TrueAsync усередині цього потоку обробляють події і фонові завдання конкурентно.
Архітектура моста
Міст працює у двох напрямках:
- Android → PHP. Kotlin кладе події (натискання, датчик, геолокацію, довільну користувацьку подію) у чергу, PHP забирає їх зі свого циклу.
- PHP → Kotlin. PHP викликає методи, які реалізовані на боці Kotlin (показати Toast, завібрувати, скопіювати текст у буфер обміну і так далі).
Обидва напрямки працюють через JNI (Java Native Interface): це стандартний механізм Android, який дозволяє коду на C викликати код на Kotlin/Java і навпаки. Жодного разу дані не проходять через JSON чи інший текстовий формат: значення передаються у типізованому вигляді, без зайвих перетворень.
PHP працює у власному системному потоці і ніколи не блокує потік інтерфейсу Android (UI-потік). Якщо PHP чекає на дані, відповідає потік інтерфейсу, і навпаки.
Напрямок 1: події від Android до PHP
Kotlin надсилає події через JNI у чергу, до якої PHP звертається методом NativeBridge::poll(). Коли черга порожня, poll() одразу повертає null, і PHP-застосунок сам вирішує, чекати наступну подію чи зайнятися чимось іншим (у демо-застосунку це коротка пауза usleep(), під час якої TrueAsync встигає виконати фонові корутини і таймери).
Є чотири типи подій: дотик до екрана, дані геолокації, дані датчиків (акселерометр і подібні) та довільна подія з ім'ям і текстовим payload'ом. Саме такою подією в демо-застосунку позначені натискання кнопок:
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,
};
}
}Перші три типи подій (дотик, геолокація, датчики) не потребують виділення пам'яті під рядки, тому обходяться дешево навіть за високої частоти викликів (наприклад, для потоку даних з акселерометра).
Напрямок 2: виклики PHP → Kotlin
Коли PHP викликає метод модуля, наприклад Toast::show('Привіт', true), є два варіанти, як цей виклик дістанеться до Kotlin.
Звичайний шлях
За замовчуванням PHP пакує аргументи в компактний типізований буфер (без рядкового формату на кшталт JSON, тому Kotlin читає його без розбору тексту і без зайвих виділень пам'яті) і передає його через один виклик NativeBridge::invoke(). Додавання нового модуля чи методу на цьому шляху не потребує правок на C: змінюються лише Kotlin і згенерована PHP-обгортка, а отже достатньо перезібрати тільки Kotlin-частину застосунку (Gradle-збірку), без перезбирання нативної бібліотеки.
Швидкий шлях: #[FastPath]
Для «гарячих» методів, які викликаються дуже часто (наприклад, приймання даних із датчика на кожному кадрі), у PHP-специфікації метод позначається атрибутом #[FastPath]. Для нього генератор створює окрему типізовану функцію на C, яка викликає Kotlin напряму через JNI, без проміжного буфера. Такий метод потребує перезбирання нативної бібліотеки (.so-файлу) за кожної зміни, зате виконується швидше і без зайвих виділень пам'яті. Поведінка методу при цьому не змінюється, змінюється лише спосіб передачі виклику через межу PHP/Kotlin.
Як описується модуль: #[BridgeModule]
Контракт модуля описується на боці PHP як інтерфейс з атрибутом #[BridgeModule]:
namespace TrueAsync\Android\Spec;
#[BridgeModule]
interface ToastInterface
{
#[Ui]
public function show(string $text, bool $long): void;
public function batteryLevel(): int;
}- Ім'я модуля виводиться з імені інтерфейсу (
ToastInterface→ модульToast), або задається явно:#[BridgeModule('Clipboard')]. #[Ui]над методом означає, що реалізація на Kotlin повинна виконуватися в UI-потоці Android (генератор сам додає перемикання на потрібний потік).#[FastPath]над методом вмикає швидкий шлях виклику, описаний вище.
Що генерує tools/bridge/gen.php
З PHP-специфікації (інтерфейсу з #[BridgeModule]) генератор заново збирає при кожному запуску:
- Kotlin-клас з абстрактними методами (
ToastSpec); - код маршрутизації виклику до потрібного методу (Kotlin);
- PHP-обгортку (
Toast::show(...)), яку використовує решта коду застосунку на PHP; - для методів з
#[FastPath]додатково типізований код на C, який викликає Kotlin напряму.
Життєвий цикл PHP-застосунку
- Kotlin запускає PHP у фоновому потоці і передає йому шлях до вхідного PHP-скрипту.
- PHP-скрипт викликає
NativeBridge::init(), після цього міст готовий приймати події і виклики. - Далі застосунок працює в циклі: забирає події через
poll(), обробляє їх, за потреби запускає фонові корутини TrueAsync (наприклад, для мережевих запитів). - Зупинка відбувається штатно: Kotlin викликає
NativeBridge.stop(), PHP-цикл бачить це черезNativeBridge::shouldStop(), завершує роботу і коректно звільняє ресурси.
Приклад: лічильник на кнопці
Спрощений приклад на основі демо-застосунку: кнопка запускає і зупиняє нескінченний лічильник, а його значення оновлюється прямо в інтерфейсі. Запуск і зупинка реалізовані через spawn()/ cancel() звичайної корутини TrueAsync, без блокування потоку інтерфейсу:
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');
}
}
}Другий клік по кнопці скасовує корутину $counter через cancel(), лічильник зупиняється на тому значенні, на якому був. Повний приклад із кількома незалежними лічильниками міститься в android/app.php репозиторію.
Статус і обмеження
- Підтримується тільки Android; підтримка iOS у планах, поки не реалізована.
- Через міст наразі передаються прості типи: рядки, цілі числа, числа з рухомою комою, булеві значення. Передача складених об'єктів (за полями, без рядкового формату) поки в планах.
- Напрямок PHP → Kotlin синхронний: метод повертає результат одразу, відкладені (асинхронні) результати з цього боку поки не підтримуються.
- Кеш PHP (opcache) на Android примусово вимкнено: пісочниця застосунку не дає йому використовувати потрібний для роботи файл блокування і виконувану пам'ять.
- Потрібна потокобезпечна (ZTS) збірка PHP, оскільки PHP працює в окремому системному потоці, а не в основному потоці застосунку.