Cargo.toml
[package]
name = "php_warp_server"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.15"
warp = { version = "0.4", default-features = false, features = ["server"] }
tokio = { version = "1", features = ["full"] }
hyper = { version = "1", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
bytes = "1.11.1"
src/lib.rs
use std::{cell::RefCell, collections::BTreeMap, convert::Infallible, net::SocketAddr};
use bytes::Bytes;
use ext_php_rs::prelude::*;
use ext_php_rs::types::{ArrayKey, ZendCallable, ZendHashTable};
use ext_php_rs::boxed::ZBox;
use ext_php_rs::types::Zval;
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use tokio::net::TcpListener;
use warp::Filter;
use warp::Reply;
thread_local! {
static PHP_HANDLER: RefCell<Option<ZendCallable<'static>>> = const { RefCell::new(None) };
}
fn php_err<E: std::fmt::Display>(e: E) -> PhpException {
PhpException::default(e.to_string())
}
fn call_php_handler(req: ZBox<ZendHashTable>) -> Result<warp::reply::Response, PhpException> {
PHP_HANDLER.with(|h| {
let h = h.borrow();
let handler = h.as_ref().ok_or_else(|| php_err("handler is not set"))?;
// ZendCallable::try_call は Vec<&dyn IntoZvalDyn> を受ける想定なので参照で渡す
let resp = handler.try_call(vec![&req]).map_err(php_err)?;
// 返り値が配列でなければ body 扱い
let Some(resp_ht) = resp.array() else {
let body = resp.str().unwrap_or("").to_string();
return Ok(warp::reply::with_status(body, warp::http::StatusCode::OK).into_response());
};
let status = resp_ht.get("status").and_then(|z| z.long()).unwrap_or(200);
let status = warp::http::StatusCode::from_u16(status as u16)
.unwrap_or(warp::http::StatusCode::OK);
let body = resp_ht
.get("body")
.and_then(|z| z.str())
.unwrap_or("")
.to_string();
// まず status + body
let mut out = warp::reply::with_status(body, status).into_response();
// headers(連想配列)
if let Some(hz) = resp_ht.get("headers").and_then(|z| z.array()) {
for (k, v) in hz.iter() {
let key = match k {
ArrayKey::String(s) => s.to_string(),
ArrayKey::Str(s) => s.to_string(),
ArrayKey::Long(i) => i.to_string(),
};
if let Some(val) = v.str() {
if let Ok(name) = warp::http::header::HeaderName::from_bytes(key.as_bytes()) {
if let Ok(value) = warp::http::header::HeaderValue::from_str(val) {
out.headers_mut().insert(name, value);
}
}
}
}
}
Ok(out)
})
}
async fn serve_forever(
host: String,
port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let route = warp::any()
.and(warp::method())
.and(warp::path::full())
.and(warp::query::raw().or_else(|_| async {
Ok::<(String,), Infallible>((String::new(),))
}))
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and_then(
|method: warp::http::Method,
full_path: warp::path::FullPath,
query: String,
headers: warp::http::HeaderMap,
body: Bytes| async move {
let internal = |msg: String| {
Ok::<warp::reply::Response, Infallible>(
warp::reply::with_status(
msg,
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
)
.into_response(),
)
};
// req array
let mut req = ZendHashTable::new();
if let Err(e) = req.insert("method", method.as_str()) {
return internal(format!("insert method failed: {:?}", e));
}
if let Err(e) = req.insert("path", full_path.as_str()) {
return internal(format!("insert path failed: {:?}", e));
}
if let Err(e) = req.insert("query", query) {
return internal(format!("insert query failed: {:?}", e));
}
// headers を map で(IntoZval 的に安全)
let mut hmap = BTreeMap::<String, String>::new();
for (name, value) in headers.iter() {
hmap.insert(
name.as_str().to_ascii_lowercase(),
value.to_str().unwrap_or("").to_string(),
);
}
if let Err(e) = req.insert("headers", hmap) {
return internal(format!("insert headers failed: {:?}", e));
}
// body は生文字列(lossy UTF-8)
let body_str = String::from_utf8_lossy(&body).to_string();
if let Err(e) = req.insert("body", body_str) {
return internal(format!("insert body failed: {:?}", e));
}
match call_php_handler(req) {
Ok(resp) => Ok::<warp::reply::Response, Infallible>(resp),
Err(e) => internal(format!("PHP handler error: {:?}", e)),
}
},
);
let tower_svc = warp::service(route);
let hyper_svc = TowerToHyperService::new(tower_svc);
let addr: SocketAddr = format!("{host}:{port}").parse()?;
eprintln!("listening on http://{addr}");
let listener = TcpListener::bind(addr).await?;
loop {
let (stream, _peer) = listener.accept().await?;
let io = TokioIo::new(stream);
// 逐次処理:この接続が終わるまで次 accept しない
if let Err(err) = http1::Builder::new()
.serve_connection(io, hyper_svc.clone())
.await
{
eprintln!("error serving connection: {err}");
}
}
}
#[php_function]
pub fn warp_http_serve(host: String, port: i64, handler: &mut Zval) -> PhpResult<()> {
if port < 0 || port > 65535 {
return Err(php_err("port must be in 0..65535"));
}
let port = port as u16;
// handler を owned callable にして thread_local に保存(寿命問題を解消)
let owned_callable = ZendCallable::new_owned(handler.shallow_clone()).map_err(php_err)?;
PHP_HANDLER.with(|h| *h.borrow_mut() = Some(owned_callable));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(php_err)?;
rt.block_on(async move { serve_forever(host, port).await })
.map_err(php_err)?;
Ok(())
}
#[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 server.php
server.php
final class Handler {
public function __invoke(array $req): array {
return ["status" => 200,
"headers" => ["content-type" => "text/plain; charset=utf-8"],
"body" => "Hello\n"];
}
}
$h = new Handler(); // 変数に入れる
warp_http_serve("127.0.0.1", 18080, $h); // OK