Cargo.toml
[package]
name = "php_warp_server"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.15"
# warp v0.4(TLSなし。warp::serve は無いので hyper でサーバを組む)
warp = { version = "0.4", default-features = false } # :contentReference[oaicite:1]{index=1}
tokio = { version = "1", features = ["full"] }
hyper = { version = "1", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }use std::net::SocketAddr;
use ext_php_rs::prelude::*;
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use tokio::net::TcpListener;
use warp::Filter;
fn php_err<E: std::fmt::Display>(e: E) -> PhpException {
PhpException::default(e.to_string())
}
async fn serve_once(host: String, port: u16) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// 例: GET / で "Hello, World!" を返す
let routes = warp::path::end().map(|| "Hello, World!".to_string());
// Filter -> tower::Service へ変換し、さらに hyper 用に変換
// warp v0.4 はここを自分で組む(warp::serve が無い) :contentReference[oaicite:2]{index=2}
let tower_svc = warp::service(routes);
let hyper_svc = TowerToHyperService::new(tower_svc);
let addr: SocketAddr = format!("{host}:{port}").parse()?;
let listener = TcpListener::bind(addr).await?;
eprintln!("listening on http://{addr}");
loop {
let (stream, _peer) = listener.accept().await?;
let io = TokioIo::new(stream);
let svc = hyper_svc.clone();
tokio::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, svc)
.await
{
eprintln!("error serving connection: {err}");
}
});
}
}
#[php_function]
pub fn warp_http_serve(host: String, port: i64) -> PhpResult<()> {
if port < 0 || port > 65535 {
return Err(php_err("port must be in 0..65535"));
}
let port = port as u16;
// PHP 呼び出しをブロックして Rust 側のサーバを回す(最小例)
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(php_err)?;
rt.block_on(async move { serve_once(host, port).await })
.map_err(php_err)?;
Ok(())
}
#[php_module]
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(warp_http_serve))
}cargo build
php -d extension=target/debug/libphp_warp_server.so -r 'warp_http_serve("127.0.0.1", 3000);'
[package]
name = "php_warp_server"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.15"
# warp v0.4(TLSなし。warp::serve は無いので hyper でサーバを組む)
warp = { version = "0.4", default-features = false } # :contentReference[oaicite:1]{index=1}
tokio = { version = "1", features = ["full"] }
hyper = { version = "1", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
bytes = "1"use std::net::SocketAddr;
use ext_php_rs::prelude::*;
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use tokio::net::TcpListener;
use warp::Filter;
use warp::http::HeaderMap;
use bytes::Bytes;
fn php_err<E: std::fmt::Display>(e: E) -> PhpException {
PhpException::default(e.to_string())
}
fn format_request(
method: warp::http::Method,
path: warp::path::FullPath,
headers: HeaderMap,
body: Bytes,
) -> String {
let mut out = String::new();
out.push_str("---- request ----\n");
out.push_str(&format!("method: {}\n", method));
out.push_str(&format!("path: {}\n", path.as_str()));
out.push_str("headers:\n");
for (name, value) in headers.iter() {
match value.to_str() {
Ok(s) => out.push_str(&format!(" {}: {}\n", name, s)),
Err(_) => out.push_str(&format!(" {}: <non-utf8 {:?}>\n", name, value.as_bytes())),
}
}
out.push_str(&format!("body: {} bytes\n", body.len()));
if !body.is_empty() {
out.push_str("body preview:\n");
out.push_str(&String::from_utf8_lossy(body.as_ref()));
out.push('\n');
}
out.push_str("-----------------\n");
out
}
async fn serve_once(
host: String,
port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// /debug に来たリクエストを“解析してそのまま返す”
let debug = warp::path("debug")
.and(warp::path::end())
.and(warp::method())
.and(warp::path::full())
.and(warp::header::headers_cloned())
.and(warp::body::content_length_limit(1024 * 1024).and(warp::body::bytes()))
.map(|method, path, headers, body| {
let text = format_request(method, path, headers, body);
// text/plain で返す
warp::reply::with_header(text, "content-type", "text/plain; charset=utf-8")
});
// それ以外は通常応答
let root = warp::path::end().map(|| "Hello, World!".to_string());
let routes = debug.or(root);
let tower_svc = warp::service(routes);
let hyper_svc = TowerToHyperService::new(tower_svc);
let addr: SocketAddr = format!("{host}:{port}").parse()?;
let listener = TcpListener::bind(addr).await?;
eprintln!("listening on http://{addr}");
loop {
let (stream, _peer) = listener.accept().await?;
let io = TokioIo::new(stream);
let svc = hyper_svc.clone();
tokio::spawn(async move {
if let Err(err) = http1::Builder::new().serve_connection(io, svc).await {
eprintln!("error serving connection: {err}");
}
});
}
}
#[php_function]
pub fn warp_http_serve(host: String, port: i64) -> PhpResult<()> {
if port < 0 || port > 65535 {
return Err(php_err("port must be in 0..65535"));
}
let port = port as u16;
// PHP 呼び出しをブロックして Rust 側のサーバを回す(最小例)
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(php_err)?;
rt.block_on(async move { serve_once(host, port).await })
.map_err(php_err)?;
Ok(())
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(warp_http_serve))
}php -d extension=target/debug/libphp_warp_server.so -r 'warp_http_serve("127.0.0.1", 3000);'
curl -v "http://127.0.0.1:3000/debug?x=1" -H "X-Test: 123" -d "name=masaki"