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 работает в отдельном системном потоке, а не в основном потоке приложения.