Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active June 22, 2025 18:33
Show Gist options
  • Select an option

  • Save masakielastic/4a3fe426b520a9280da5d466ace76f0d to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/4a3fe426b520a9280da5d466ace76f0d to your computer and use it in GitHub Desktop.
Rust tokio の HTTP/1 サーバーを PHP 拡張機能で利用する

Rust tokio の HTTP/1 サーバーを PHP 拡張機能で利用する

構成

  • Cargo.toml
  • src/main.rs

ビルド

cargo build --release

実行

php -d extension=target/release/libtokio_http.so simple_server.php
[package]
name = "tokio_http"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.13"
tokio = { version = "1.4", features = ["full"] }
hyper = { version = "1.6", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
http-body-util = "0.1"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors"] }
[package.metadata.php-extension]
name = "tokio_http"
version = "1.0.0"
author = "Masaki Kagaya"
description = "Tokio HTTP Server Extension"
use ext_php_rs::prelude::*;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{body::Bytes, Request, Response};
use hyper_util::rt::TokioIo;
use http_body_util::Full;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use tokio::net::TcpListener;
// グローバルなサーバー状態管理
static SERVER_RUNNING: AtomicBool = AtomicBool::new(false);
static SERVER_HANDLE: Mutex<Option<tokio::task::JoinHandle<()>>> = Mutex::new(None);
// シンプルなHTTPレスポンスハンドラー
async fn handle_request(
_req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
let response_body = r#"
<!DOCTYPE html>
<html>
<head>
<title>Tokio HTTP Server</title>
</head>
<body>
<h1>Hello from Tokio HTTP Server!</h1>
<p>This server is running from a PHP extension written in Rust.</p>
<p>Powered by ext-php-rs + Tokio + Hyper</p>
</body>
</html>
"#;
Ok(Response::builder()
.status(200)
.header("Content-Type", "text/html; charset=utf-8")
.header("Server", "tokio-http-php-extension/0.1.0")
.body(Full::new(Bytes::from(response_body)))
.unwrap())
}
/// PHP関数: start_tokio_server(host, port)
/// TokioベースのHTTP/1サーバーを起動する
#[php_function]
pub fn start_tokio_server(host: Option<String>, port: Option<i64>) -> PhpResult<bool> {
let host = host.unwrap_or_else(|| "127.0.0.1".to_string());
let port = port.unwrap_or(8080) as u16;
// 既にサーバーが動いている場合は停止
if SERVER_RUNNING.load(Ordering::Relaxed) {
stop_tokio_server()?;
}
// 新しいTokioランタイムを作成してサーバーを起動
let runtime = tokio::runtime::Runtime::new()
.map_err(|e| PhpException::default(format!("Failed to create Tokio runtime: {}", e)))?;
let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
.map_err(|e| PhpException::default(format!("Invalid address: {}", e)))?;
// サーバータスクをスポーン
let handle = runtime.spawn(async move {
if let Err(e) = run_server_with_graceful_shutdown(addr).await {
eprintln!("Server error: {}", e);
}
});
// ハンドルを保存
if let Ok(mut handle_guard) = SERVER_HANDLE.lock() {
*handle_guard = Some(handle);
}
SERVER_RUNNING.store(true, Ordering::Relaxed);
// ランタイムを別スレッドで実行
std::thread::spawn(move || {
runtime.block_on(async {
// サーバーが停止するまで待機
while SERVER_RUNNING.load(Ordering::Relaxed) {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
});
});
Ok(true)
}
/// Go の ListenAndServe 風の実装:グレースフルシャットダウン付き
async fn run_server_with_graceful_shutdown(addr: SocketAddr) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind(addr).await?;
println!("Tokio HTTP server listening on http://{}", addr);
loop {
tokio::select! {
// 新しい接続を受け付け
result = listener.accept() => {
match result {
Ok((stream, _)) => {
let io = TokioIo::new(stream);
tokio::task::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await
{
eprintln!("Error serving connection: {:?}", err);
}
});
}
Err(e) => {
eprintln!("Error accepting connection: {}", e);
}
}
}
// Ctrl+C でグレースフルシャットダウン(クロスプラットフォーム対応)
_ = tokio::signal::ctrl_c() => {
println!("\nReceived Ctrl+C, shutting down gracefully...");
SERVER_RUNNING.store(false, Ordering::Relaxed);
break;
}
// サーバー停止フラグをチェック
_ = async {
while SERVER_RUNNING.load(Ordering::Relaxed) {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
} => {
println!("Server stop requested");
break;
}
}
}
println!("Server shutdown complete");
Ok(())
}
/// PHP関数: stop_tokio_server()
/// 実行中のTokioサーバーを停止する
#[php_function]
pub fn stop_tokio_server() -> PhpResult<bool> {
if !SERVER_RUNNING.load(Ordering::Relaxed) {
return Ok(false); // 既に停止している
}
SERVER_RUNNING.store(false, Ordering::Relaxed);
// サーバーハンドルをabort
if let Ok(mut handle_guard) = SERVER_HANDLE.lock() {
if let Some(handle) = handle_guard.take() {
handle.abort();
}
}
println!("Tokio HTTP server stopped");
Ok(true)
}
/// PHP関数: is_tokio_server_running()
/// サーバーの実行状態を確認する
#[php_function]
pub fn is_tokio_server_running() -> bool {
SERVER_RUNNING.load(Ordering::Relaxed)
}
/// PHP関数: start_tokio_server_forever(host, port)
/// Go の ListenAndServe 風:グレースフルシャットダウン付きで永続実行
#[php_function]
pub fn start_tokio_server_forever(host: Option<String>, port: Option<i64>) -> PhpResult<bool> {
let host = host.unwrap_or_else(|| "127.0.0.1".to_string());
let port = port.unwrap_or(8080) as u16;
// 既にサーバーが動いている場合は停止
if SERVER_RUNNING.load(Ordering::Relaxed) {
stop_tokio_server()?;
// 少し待ってから新しいサーバーを起動
std::thread::sleep(std::time::Duration::from_millis(500));
}
let addr: SocketAddr = format!("{}:{}", host, port)
.parse()
.map_err(|e| PhpException::default(format!("Invalid address: {}", e)))?;
SERVER_RUNNING.store(true, Ordering::Relaxed);
// ブロッキング実行(Go の ListenAndServe と同様)
let runtime = tokio::runtime::Runtime::new()
.map_err(|e| PhpException::default(format!("Failed to create Tokio runtime: {}", e)))?;
println!("Starting server on http://{}:{} (Press Ctrl+C to stop)", host, port);
// ブロッキング実行
runtime.block_on(async move {
if let Err(e) = run_server_with_graceful_shutdown(addr).await {
eprintln!("Server error: {}", e);
}
});
SERVER_RUNNING.store(false, Ordering::Relaxed);
Ok(true)
}
// PHPエクステンションのモジュール定義
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
}
<?php
// simple_server.php - Go の ListenAndServe 風のシンプルなサーバー
echo "=== Simple Tokio HTTP Server (ListenAndServe style) ===\n";
// 利用可能な関数を確認
$available_functions = [
'start_tokio_server',
'start_tokio_server_forever',
'stop_tokio_server',
'is_tokio_server_running',
'get_tokio_server_info'
];
echo "Checking available functions:\n";
foreach ($available_functions as $func) {
$exists = function_exists($func);
echo " $func: " . ($exists ? "✓" : "✗") . "\n";
}
echo "\n";
// サーバー情報を表示(関数が存在する場合のみ)
if (function_exists('get_tokio_server_info')) {
$info = get_tokio_server_info();
echo "Server Info:\n";
foreach ($info as $item) {
echo " {$item[0]}: {$item[1]}\n";
}
echo "\n";
}
// 引数でホストとポートを指定可能
$host = $argv[1] ?? "127.0.0.1";
$port = isset($argv[2]) ? intval($argv[2]) : 8080;
echo "Starting Tokio HTTP server on $host:$port\n";
echo "This will run forever until you press Ctrl+C\n";
echo "Visit http://$host:$port in your browser\n";
echo "\n";
// 関数の存在確認
if (function_exists('start_tokio_server_forever')) {
// Go の http.ListenAndServe と同様にブロッキング実行
// Ctrl+C でグレースフルシャットダウン
start_tokio_server_forever($host, $port);
echo "Server has been stopped.\n";
} else {
echo "Error: start_tokio_server_forever function not found!\n";
echo "Please check if the extension is properly loaded.\n";
exit(1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment