Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active February 24, 2026 20:05
Show Gist options
  • Select an option

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

Select an option

Save masakielastic/c0c0e28f3865cc8f322ddddcc332c652 to your computer and use it in GitHub Desktop.
warp v0.4 で TLS なしの HTTP サーバー | PHP 拡張、ext-php-rs

warp v0.4 で TLS なしの HTTP サーバー | PHP 拡張、ext-php-rs

コード

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"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment