Rust で PHP 拡張を書く:Debian + ext-php-rs チュートリアル
序文
PHP でウェブ開発をしていると、非同期処理や低レベルの通信仕様(HTTP/2やHTTP/3)について体系的に学ぶ機会は限られています。そこで注目したのが Rust の導入です。
Rust は高速で安全なシステム言語でありながら、近年では WebAssembly や FFI を通じて PHP と組み合わせた開発も現実的になってきました。
本記事では、ext-php-rs
を用いて Rust 製の PHP 拡張モジュールを作成し、PHP 環境に組み込む手順を紹介します。今後、Rust 側で HTTP サーバーや非同期処理を担い、PHP をそのクライアントとして使うようなハイブリッドアーキテクチャも視野に入れています。
特に注目したいのは、既存の PHP アプリを段階的に Rust に置き換えていく戦略です。比較的依存関係がシンプルなドメインロジックを Rust 製の PHP 拡張として書き直すことで、将来的には PHP に依存しない汎用ライブラリとして Rust に切り出すことも可能になります。
また、Cargo による依存関係管理と配布エコシステムは、従来の C/C++ 製 PHP 拡張では難しかったライブラリの再利用・共有を大きく前進させるものです。
さらに近年では、AI の台頭によって Rust を学ぶ際の負担も大きく軽減されるようになってきました。特に ext-php-rs のようなマクロや FFI を多用するライブラリでは、過去であればエラーの原因特定や型の取り扱いに時間がかかりましたが、今ではエラーメッセージをそのまま AI に貼り付ければ、ほとんどの場合で具体的な原因と修正案を提示してくれます。これにより「知らないコードを書いても、あとで原因がわかる」という安心感が得られ、未知の文法や機能にも気軽にチャレンジできるようになっています。
最後にこの記事は ChatGPT と協議しながら生成しました。
✅ 前提環境
- Debian 12
- PHP 8.4(Sury リポジトリ)
- Rust(
rustup
経由で導入) -
libphp.so
は/usr/lib/libphp.so
に存在 - PHP 側で
zend_known_strings
などの内部シンボルが参照可能
1️⃣ パッケージの準備
sudo apt install php8.4-cli php8.4-dev libphp8.4-embed build-essential pkg-config
※ libphp8.4-embed
に libphp.so
が含まれていれば OK
2️⃣ Rust 環境の準備
curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env
cargo-php
のインストール
3️⃣ export PKG_CONFIG_PATH=$HOME/.pkgconfig
export LIBRARY_PATH=/usr/lib
export LD_LIBRARY_PATH=/usr/lib
export RUSTFLAGS="-L /usr/lib -lphp"
cargo install cargo-php
※ .pc
ファイルは $HOME/.pkgconfig/php-8.4.pc
に作成(手動)
prefix=/usr
exec_prefix=${prefix}
includedir=${prefix}/include/php
libdir=/usr/lib
Name: PHP
Description: The PHP scripting language interpreter
Version: 8.4.0
Libs: -L${libdir} -lphp
Cflags: -I${includedir} -I${includedir}/main -I${includedir}/TSRM -I${includedir}/Zend -I${includedir}/ext
4️⃣ Rust 拡張の雛形作成
cargo new hello_ext --lib
cd hello_ext
Cargo.toml
に以下を追記:
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.13"
src/lib.rs
:
#![cfg_attr(windows, feature(abi_vectorcall))]
use ext_php_rs::prelude::*;
#[php_function]
pub fn hello_world(name: String) -> String {
format!("Hello, {}!", name)
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
}
5️⃣ 拡張のビルド
cargo build --release
生成物:target/release/libhello_ext.so
ターミナルから動作を確認できます。
php -d extension=./target/release/libhello_ext.so -r 'var_dump(hello_world("Your Name"));'
6️⃣ PHP 拡張として手動で導入
php-config --extension-dir
# 例: /usr/lib/php/usr/lib/php/20240924
sudo cp target/release/libhello_ext.so /usr/lib/php/usr/lib/php/20240924/hello_ext.so
echo "extension=hello_ext.so" | sudo tee /etc/php/8.4/cli/conf.d/99-hello_ext.ini
7️⃣ 動作確認
php -r 'var_dump(hello_world("Your Name!"));'
# 出力: string(21) "Hello, Your Name!"
8️⃣ (任意)stubs 生成
cargo-php stubs
# ./stubs/hello_ext.php が生成される
コマンドのまとめ
ツール | 用途 |
---|---|
cargo build |
拡張モジュール(.so)をビルド |
cargo-php install |
(root権限必要、手動コピーの方が確実) |
cargo-php stubs |
IDE 補完用の PHP スタブファイル生成 |
🧩 関数のエイリアスや名前空間の登録
ext-php-rs
では、Rust 関数を PHP 側にエクスポートする際に、関数名や名前空間を指定することができます。次のように #[php_function(name = "...")]
属性を使うだけで、PHP 側で MyProject\greet_user()
のように呼び出せる関数を定義できます:
#[php_function(name = "MyProject\\greet_user")]
pub fn hello_world(name: String) -> String {
format!("Hello, {}!", name)
}
ターミナルから次のように確認できます。
php -d extension=./target/release/libhello_ext.so -r 'var_dump(\MyProject\greet_user("Your
Name"));'
string(17) "Hello, Your Name!"
❌ うまくいかなかった方法
当初、module.function("greet_user", hello_world)
のように関数を手動で登録しようとしましたが、現行バージョン(v0.13.1)では FunctionEntry::new(...)
が存在しないため、ビルド時にエラーが発生しました。マクロによる登録が前提の設計になっており、名前の上書きや手動登録は非推奨です。
📚 Rustを学ぶためのおすすめ学習ソース
Rust を使って PHP 拡張を開発するにあたって、以下の2つの学習リソースが非常に役立ちます。特に非同期処理や HTTP/2/3 を視野に入れている方には、どちらも必読といえる内容です。
Rust by Example 日本語訳
🔗- 対象:Rust の基本文法と標準ライブラリの使い方を手を動かしながら学びたい人
- 特徴:実行可能なコード例が豊富で、CやPHPの経験者でもイメージがつきやすい
- 活用法:型、モジュール、エラー処理、FFI など、PHP拡張に必要な基礎を段階的に学べます
💡 たとえば「所有権と借用」の章は、PHPにはないメモリモデルを理解するのに非常に効果的です。
Tokio チュートリアル 日本語訳 (Zenn)
🔗-
対象:Rustで非同期処理やネットワークプログラミングに取り組みたい人
-
特徴:公式 Tokio チュートリアルの翻訳。HTTP クライアント・サーバー、async/await の基礎が網羅
-
活用法:将来的に Rust 側で HTTP/2 サーバーを立て、PHP クライアントと通信する構成に応用できます
💡
tokio::spawn
やasync fn
の概念は、PHP では学びにくい非同期思考を育ててくれます。
🤖 エラーが出ても大丈夫:AIが学習の壁を乗り越えさせてくれる
これらのチュートリアルで紹介されているコードは、執筆時点の Rust に基づいて書かれているため、時が経つにつれて一部がコンパイルエラーになることもあります。
たとえば、ext-php-rs
を使った Hello World 拡張のコードも、バージョンによって wrap_function!
マクロが廃止されていたため、そのままではビルドできませんでした。
ですが、こうした場面でも AI にエラーメッセージを渡すことで正しい修正方法を即座に提案してもらえるため、安心して学習を進めることができます。
✨ 補足:AI × チュートリアル = 最強の学習ブースター
チュートリアルは「道」を示してくれる。AI は「転んだとき」に手を差し伸べてくれる。
このような時代だからこそ、Rust のような“難しいけれど強力な言語”に挑戦するハードルは、以前よりずっと低くなっています。
📘 Rust by Example の使い方:PHPユーザーにとっての“公式マニュアル”的活用法
Rust を初めて学ぶ人にとって、「Rust by Example 日本語訳」は、PHPユーザーにとっての php.net にあたる、最も手軽で実践的な入口です。
このチュートリアル集には、構文の説明とともに短く具体的なサンプルコードが添えられており、それを手元で動かしながら少しずつ構文と挙動を理解できるという特徴があります。
🔁 PHP公式マニュアルと共通する学びのスタイル
たとえば、PHP の array_map()
や preg_match()
を学ぶとき、多くの人は公式マニュアルのサンプルコードをコピーして実行し、「ああ、こうやって使うのか」と実感を得るところから始めます。
Rust でもまったく同じで、たとえば「関数定義」「所有権」「エラーハンドリング」「構造体と impl」など、ext-php-rs
の開発で避けて通れない概念がすべて Rust by Example
に整理されています。
fn main() {
let name = "Your Name";
println!("Hello, {}!", name);
}
このような小さなコード片を何度も動かすことで、「これは PHP でいうところの…」と自分なりの橋渡しができるようになります。
ext-php-rs
に応用するには?
✅ ext-php-rs
は PHP 拡張の定義を Rust の構文で書けるようにするライブラリですが、基盤には Rust の基本構文がそのまま使われます。
たとえば:
Rustの基礎例 | ext-php-rs での応用例 |
---|---|
関数定義 fn greet(name: String)
|
#[php_function] fn greet(name: String) |
構造体 struct User
|
#[php_class] struct User |
Result<T, E> を使ったエラー制御 |
#[php_function] fn might_fail() -> PhpResult<()> |
つまり Rust by Example
を使って基礎を押さえておけば、ext-php-rs
を読む・書く・拡張する力が自然と身につきます。
💡 小さな成功体験の積み重ねが「Rust拡張開発力」になる
-
cargo new
でミニプロジェクトを作る -
Rust by Example
のコードを写経して動かす -
それを
ext-php-rs
の関数に応用する
この繰り返しこそが、Rust を“実務の一部として書ける言語”にしていく王道です。
💬 「1関数動かせたら、次はクラス。次はエラー処理」――PHPのときと同じように、一歩ずつ階段を登っていけます。
✅ まとめ
Rust による PHP 拡張開発は、AI の支援と共により現実的で手の届くものになりつつあります。
今回のような事例が積み重なれば、「PHP から Rust へ」の移行も、段階的かつ低リスクで進められるはずです。
🧠補足: AI 支援が導いた libphp8.4-embed
PHP 拡張を Rust で開発する際、ext-php-rs
と cargo-php
を組み合わせると比較的スムーズに進めることができます。しかし、PHP の C API に依存するライブラリとのリンクには少し工夫が必要でした。
💥 問題:リンク時に zend_known_strings が見つからない
Rust 側で cargo-php をビルドしようとすると、次のようなエラーが発生しました:
undefined reference to zend_known_strings
これは PHP の Zend Engine に含まれる内部シンボルで、FFI を使って Rust から PHP を操作する場合に必要になるものです。しかし、php8.4-dev パッケージだけではこのシンボルが libphp.so に含まれておらず、ビルドに失敗してしまいます。
🤖 解決の糸口:AI が示した embed SAPI の存在
この時、AI にエラーメッセージを共有したところ、
「PHP を --enable-embed オプション付きでビルドして libphp.so に zend_known_strings を含める必要がある」
という指摘がありました。
この一言で、「PHP を自分でビルドしなければならないのか……」という懸念がよぎりましたが、さらに調べてみると libphp8.4-embed
というパッケージが DEB.SURY.ORG で提供されており、それを導入することで libphp.so
に必要なシンボルが含まれるようになることが判明しました。
sudo apt install libphp8.4-embed
その後、RUSTFLAGS="-L /usr/lib -lphp"
を指定して cargo-php
を再ビルドすることで、無事にビルドを成功させることができました。
🧩 embed SAPI はどこで使われているのか?
この embed SAPI(Server API)は、あまり一般的には知られていませんが、Go 言語で開発されている高速 HTTP サーバー「FrankenPHP」 で使われていることで注目を集めています。
FrankenPHP は libphp.so
を使って PHP をプロセス内に直接埋め込むことで、Go アプリケーションの中から高速かつ効率的に PHP を実行できる仕組みを実現しています。
このような流れからも、「embed SAPI を使った Rust/PHP の組み合わせ」は今後より現実的な選択肢となる可能性があります。
🔒 Go で PHP 拡張を開発する場合の制限
Go から PHP を呼び出す(FrankenPHP など)ことは可能でも、Go 自体を PHP 拡張として .so にコンパイルして PHP に読み込ませることは、現実的には困難です。その主な理由は以下の通りです:
- Go で生成される共有ライブラリ(.so)は C ABI に制限がある → PHP の Zend Engine の複雑なマクロや構造体に完全対応するのは難しい
- Go ランタイムがスタンドアロンで動作する設計になっており、PHP 拡張のようなホスト環境への組み込みに向いていない
- PHP の FFI から Go を直接呼び出す方法もあるが、パフォーマンスやメモリ管理の制御が難しい
そのため、Go は PHP と並列で動かすサーバーやミドルウェアの構築には適していても、PHP 拡張そのものの実装には不向きです。
✅ Rust との違い
Rust は cdylib
によって C ABI 準拠の共有ライブラリを安全に生成でき、ext-php-rs
を使えば Zend Engine のインターフェースも簡潔に扱えます。
PHP のエコシステムに“拡張として組み込む”目的において、Rust は Go よりもはるかに適しています。
🎓 教訓:AI × FFIエラーは実践的な学習チャンス
今回のように、未知の低レベルな仕組みに直面したとき、AI にエラーメッセージを渡すことで問題の構造を素早く把握できました。
- 従来であれば調査に数時間かかった可能性のある問題を、数分で整理
- Rust 側の
RUSTFLAGS
設定やリンクエラーの意味をその場で確認 - Debian に既に存在していたパッケージを発見し、ビルド環境の構築に成功
AI による支援が、学習コストを大きく下げるだけでなく、学習モチベーションを保つことにもつながっていると実感した瞬間でした。
🆚 補足: C言語で書く PHP 拡張との比較:Rust の強みとは?
Rust で PHP 拡張を書くことの意義を語るうえで、C 言語との比較は避けて通れません。実際、PHP の標準関数は php-src
に含まれる C のコードで実装されています。
string.c
や array.c
を読む経験から
🔍 C拡張の実態:筆者もかつて、PHP 標準関数の内部構造を理解するために php-src/ext/standard/string.c
や array.c
を読み、ZEND_FUNCTION()
マクロや Z_PARAM_STR()
の使い方を学びました。マクロを駆使して設計されたその構文は強力ですが、初見での理解やデバッグは非常に難解です。
PHP_FUNCTION(strtoupper)
{
zend_string *str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(str)
ZEND_PARSE_PARAMETERS_END();
RETURN_STR(php_strtoupper(str));
}
上記のような記法は C と Zend Engine の前提知識がなければ読みづらく、関数の定義と登録の流れも分散しています。
✅ Rust拡張の明快さ:ext-php-rs の場合
一方、Rust + ext-php-rs
を使えば、同じロジックがより自然な構文で記述できます:
#![cfg_attr(windows, feature(abi_vectorcall))]
use ext_php_rs::prelude::*;
#[php_function]
pub fn hello_world(name: String) -> String {
format!("Hello, {}!", name)
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
}
Rust の構文そのままで PHP 関数を定義でき、型推論・所有権チェック・IDE 補完も効くため、可読性と保守性が飛躍的に向上します。
🔍 ポイント比較表
項目 | C言語 + Zend Engine | Rust + ext-php-rs |
---|---|---|
可読性 | マクロ多用、文脈依存が強い | 関数定義が直感的で明快 |
安全性 | 手動メモリ管理が必要 | 所有権と借用によるコンパイル時安全性 |
開発支援 | IDE 補完が限定的、手動ビルド | 型補完・ドキュメント・テストが豊富 |
配布と依存管理 | ビルド手順が複雑、再利用が難しい | Cargo による依存解決と再配布が容易 |
学習コスト | Zend Engine の前提知識が必要 | Rust 単体でも学習可能、AI支援が活きる |
🧠 補足:Cの知識は今も活きる
とはいえ、Cでの拡張開発経験があることで、Zend Engine の動きや PHP の値の内部表現に対する理解が深まります。Rust で FFI や Zval
を扱う際にも、過去に得た C の知識が補助線として働きます。
💬 「C拡張を知っているからこそ、Rust拡張のありがたみがわかる」
そんな感想を持つ方も多いのではないでしょうか。
🧩 補足: Zend Engine のマクロの学び方
C 言語で PHP 拡張を書く際に避けて通れないのが、Zend Engine によって提供される数多くのマクロ群です。ZEND_FUNCTION
、Z_PARAM_STR
、RETURN_STR
など、関数定義から引数パース、戻り値の返却に至るまで、あらゆる処理がマクロに包まれています。
📂 昔の学び方:用途のないマクロを“探索”して推測する
筆者はかつて、PHP の公式ソースコードリポジトリ(php-src
)の中から 標準関数の実装が集まる ext/standard/string.c
や array.c
を直接読み、未知のマクロの意味を推測しながら学んでいました。
たとえば ZEND_PARSE_PARAMETERS_START()
や Z_PARAM_ARRAY()
のようなマクロについて、ドキュメントが不十分な中でその用途を理解するには、マクロを grep で検索し、用例の中から文脈を探るしかない、という時代もありました。
grep -r Z_PARAM_STR ./ext/standard/
このような探索型学習は、「構文はわかったけど意味が腑に落ちない」「例外的な使われ方が多くて混乱する」といった問題にもつながっていました。
🤖 現代の学び方:AIに聞けば“用例と背景”までわかる
今の時代であれば、知らないマクロを見かけても、AI にマクロ名を入力するだけで用途、使い方、関連マクロ、さらにはリファクタリング例まで提示してくれます。
たとえば:
Z_PARAM_ARRAY_HT()
は何をするマクロ?PHP拡張の中でどのように使われる?
と尋ねるだけで、返される情報は単なる説明にとどまらず、コード例・注意点・バージョン依存性まで網羅されていることも珍しくありません。
🧠 教訓:AIのある時代だからこそ“内部構造への関心”を持てる
AI によるサポートがあることで、「このマクロの中身はどうなってるんだろう?」「Zval って実際どんな構造?」といった素朴な関心が、今ではずっと低コストに探求できるようになりました。
昔は“調べるのに疲れてやめる”ことが多かった。
今は“調べるのが面白くなってくる”時代です。
Discussion