こんにちは!Haxeの世界へようこそ。
今回は、Haxeの大きな魅力である「クロスプラットフォーム・トランスパイル」の中でも、特にサーバーサイドで根強い人気を誇るPHPターゲット(PHP 8.1以降)に焦点を当てます。
テーマは、「Haxeの非同期処理(Future / Promise)をPHPのFiber(ファイバー)で扱う際の仕組みと注意点」です。
JavaScriptのように「イベントループが最初から当たり前にある言語」から来た方や、PHPで長年同期処理を書いてきた方にとって、Haxeが吐き出すPHPコードがどのように非同期を同期っぽく動かしているのかは、少しブラックボックスに見えるかもしれません。
でも安心してください!仕組みとスタック(メモリ)の裏側さえイメージできれば、何も怖いことはありません。「ここをクリアすれば、Haxe×PHPの非同期処理はバッチリマスターできますよ!」というポイントを、優しく丁寧に解き明かしていきますね。
—
1. HaxeのFuture/PromiseとPHPの歴史的ギャップ
まずは基本の整理から始めましょう。
Haxeでは、非同期処理を扱う際にライブラリ(例えば定番の `tink_core` など)で提供される `Future` や `Promise`、あるいはコールバックパターンを使うのが一般的です。
// Haxeでの一般的な非同期イメージ(疑似コード)
function fetchUserData(userId:Int):Promise
return new Promise((resolve, reject) -> {
// 何らかの非同期I/O処理
});
}
しかし、トランスパイル先であるPHPは、もともと「リクエストが来たら上から下まで同期的に実行してプロセスを終了する」という完全な同期的世界観を持っています。
そこで登場したのが PHP 8.1 の「Fiber」!
PHP 8.1で導入された Fiber(ファイバー) は、いわゆる「中断・再開が可能な軽量スレッド(コルーチン)」です。
これを使うと、コールバック地獄(`.then(…)` のネスト)に陥ることなく、「非同期処理の結果が返ってくるまで、その場の実行スタックを一時停止(サスペンド)し、準備ができたら再開(レジューム)する」 という、まるで `async/await` のような処理をPHPネイティブで実現できます。
—
2. 図解:Fiberによるコンテキストスイッチとスタックの裏側
「実行を一時停止して再開する」とは、メモリの中で一体何が起きているのでしょうか?
図解でイメージを掴んでみましょう。
【メインの実行スタック】 【Fiberの独立したスタック】
│ │
├─① Fiber起動 (start/resume) ───────>│ 処理開始(非同期処理呼び出し)
│ │
│ ├─② I/O待ち発生!
│<─③ 一時中断 (Fiber::suspend()) ────┤ スタック状態を丸ごと保存して待機
│ │
├─ メインループ側で他のタスクを消化 │ (停止中...)
│ (またはイベントループがI/O完了を検知) │
│ │
├─④ 再開 (Fiber::resume(結果)) ──────>│ 保存されていたスタックから再開!
│ │ 戻り値を受け取って次の処理へ
│<─⑤ Fiber終了 ──────────────────────┘
▼
ここがポイント!
通常、関数の呼び出しスタック(コールスタック)は1本しかありません。そのため、関数の中で `sleep()` などを呼ぶとプログラム全体が止まってしまいます。
しかしFiberを使うと、「Fiber専用の別の小さなスタック領域(4KB〜)」が確保されます。
HaxeのPromiseが未解決の間、Fiberは `Fiber::suspend()` を呼んで自分のスタックの巻き戻し・退避(コンテキストスイッチ)を行い、CPUの制御権をメインプロセスに戻します。そしてPromiseが解決(Resolve)された瞬間に `Fiber::resume()` で中断した場所へ正確にワープして戻ってくるわけです。
—
3. 実践:HaxeからPHPのFiberを制御してみよう
それでは、実際にHaxeコードからPHP 8.1のFiberを扱う仕組みを書いてみましょう。
PHPのネイティブクラス `\Fiber` をHaxeから呼び出せるように `extern`(型定義)を用意し、シンプルな「非同期を同期化するヘルパー」を作ってみます。
① FiberのExtern定義(Haxe側)
package php;
import php.NativeIndexedArray;
// PHP 8.1の組み込みFiberクラスの型定義
@:native(“Fiber”)
extern class Fiber {
public function __construct(callback:haxe.Constraints.Function);
public function start(args:haxe.Rest
public function resume(value:Dynamic = null):Dynamic;
public function throw(exception:php.Throwable):Dynamic;
public function isStarted():Bool;
public function isSuspended():Bool;
public function isRunning():Bool;
public function isTerminated():Bool;
public static function suspend(value:Dynamic = null):Dynamic;
public static function getCurrent():Null
}
② Promiseを「await」する関数の実装
次に、未解決のPromiseをFiberのサスペンドを使って「その場で待つ(awaitする)」マジックを実装します。
import php.Fiber;
// 簡易的なPromise型(解説用)
class SimplePromise
private var handlers:Array
private var value:Null
private var isResolved:Bool = false;
public function new() {}
public function resolve(val:T):Void {
if (isResolved) return;
isResolved = true;
value = val;
for (h in handlers) h(val);
}
public function handle(callback:T -> Void):Void {
if (isResolved) {
callback(value);
} else {
handlers.push(callback);
}
}
}
class AsyncTool {
/
- Promiseの完了をFiber内で同期的に待ち受ける(await相当)
/
public static function await
// 現在動いているFiberを取得
var currentFiber = Fiber.getCurrent();
// もしFiberの外(メインスタック)で呼ばれたらエラー!
if (currentFiber == null) {
throw new haxe.Exception(“await() must be called inside a Fiber context!”);
}
// Promiseが解決されたら、このFiberを再開(resume)するように仕込む
promise.handle(function(result:T) {
if (currentFiber.isSuspended()) {
currentFiber.resume(result);
}
});
// 値が返ってくるまで、現在のスタックを一時停止して処理を抜ける
return Fiber.suspend();
}
}
③ 実際に動かしてみよう
class Main {
static function main() {
trace(“— プログラム開始 —“);
// Fiberを作って非同期フローを実行
var fiber = new Fiber(function() {
trace(“[Fiber内] 非同期タスクを開始します…”);
var promise = new SimplePromise
// 擬似的な非同期処理(後からresolveされる)
// 実際はReactPHPやSwoole、cURLマルチハンドルのイベント等で発火します
fakeAsyncIo(function(data) {
promise.resolve(data);
});
// ここでサスペンド!結果が来るまで処理が一時停止します
var result = AsyncTool.await(promise);
// resumeされると、何事もなかったかのようにここから再開!
trace(‘[Fiber内] 結果を受信しました: $result’);
});
// Fiberの実行を開始
fiber.start();
trace(“— メインスレッド:Fiberが一時停止中なので別の仕事ができます —“);
}
static function fakeAsyncIo(callback:String->Void):Void {
// 今回はデモのため即時ではなく、何らかのキューに登録されたと想定
// 実際にはイベントループが後からcallbackを呼びます
callback(“サーバーからのレスポンスデータ (200 OK)”);
}
}
—
4. 現場でハマりがちな注意点と落とし穴
HaxeからPHPのFiberを扱う際、コンパイラを通っても実行時にクラッシュする落とし穴がいくつか存在します。先輩エンジニアとして、特に気をつけてほしい3つのポイントをお伝えしますね。
① 「Fiberの外」で `await` を呼んでしまう(実行時エラー)
`Fiber::suspend()` は、現在実行中のコードがFiberのスタック上にない場合、PHPの `\FiberError` をスローします。
Haxeのコード上ではただの関数呼び出しに見えるため、「その関数がFiber内で呼ばれる前提なのか、通常の同期スタックなのか」 が型レベルで区別しにくいという弱点があります。
> Tips: Haxeの「抽象型(Abstract)」やカスタムマクロを使って、Fiber内でしか呼べない `FiberContext` 型を要求するシグネチャにしておくと、コンパイル時に事故を防げて安心ですよ!
② 例外(Exception)の伝播境界
Fiberの内部でキャッチされなかった例外は、`fiber.start()` または `fiber.resume()` を呼び出した側の呼び出し元スタックへ飛んでいきます。
Fiber内部で throw ──> (スタック境界を越える) ──> fiber.resume() を呼んだ場所で発症!
Haxe側で `try…catch` を書くときは、「非同期処理の内部で起きたエラー」をPromiseのReject経由で受けてFiberへ `fiber.throw()` で注入し直すのか、それともイベントループ側でキャッチするのか、エラーハンドリングの設計を統一しておきましょう。
③ スタックサイズとGC(ガベージコレクション)
Fiberはそれぞれ独立したメモリ(Cレベルのスタック領域)を確保します。
Haxeで大きなローカル配列や巨大なオブジェクトをFiber内のスコープで大量に生成し、サスペンドしたまま放置すると、PHPプロセスのメモリ使用量が跳ね上がる原因になります。
「用が済んだFiberは確実に最後まで走らせて終了(Terminated)させる」ことを意識してくださいね。
—
まとめ
今回の内容をおさらいしましょう!
1. Fiberは独立したスタックを持つ軽量スレッド:Haxeの非同期を「中断(suspend)」と「再開(resume)」で同期コードのように扱える。
2. `AsyncTool.await` の裏側:Promiseの解決コールバックに `fiber.resume()` を仕込み、自身は `Fiber.suspend()` で退避する。
3. 境界を意識する:Fiberの外からの呼び出しや、例外の飛び越えに注意する。
HaxeからPHP 8.1のFiberを自在に操れるようになると、Node.jsのような非同期I/Oの高速性と、PHPのシンプルなライフサイクル運用の良いとこ取りができるようになります。
一見難しそうに見えるトランスパイルの裏側ですが、メモリとスタックの動きが頭の中でアニメーションできるようになれば、もう一人前のHaxe/PHPアーキテクトです。ぜひご自身のプロジェクトでも試してみてくださいね!応援しています!