Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active February 27, 2026 02:22
Show Gist options
  • Select an option

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

Select an option

Save masakielastic/fcc50ae1cffc1b8d10f327faebc8f646 to your computer and use it in GitHub Desktop.
tiny_http で HTTP/1 サーバーの PHP 拡張

tiny_http で HTTP/1 サーバーの PHP 拡張

コード

[package]
name = "tiny_http_php"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
ext-php-rs = "0.15"
tiny_http = "0.12"
#![cfg_attr(windows, feature(abi_vectorcall))]

use ext_php_rs::exception::PhpException;
use ext_php_rs::prelude::*;
use ext_php_rs::types::ZendCallable;

use std::io::Read;
use tiny_http::{Header, Response, Server};

fn sanitize_no_nul(mut s: String) -> String {
    // PHP 側へ例外メッセージとして渡す文字列に NUL が混ざると fatal になるので除去
    s.retain(|c| c != '\0');
    s
}

/// tiny_http で HTTP/1 サーバーを起動し、各リクエストごとに PHP callable を呼び出して本文を返します。
///
/// callable のシグネチャ例:
///   function (string $method, string $path, string $body): string
///
/// @param string $addr 例: "127.0.0.1:8080"
/// @param callable $handler
#[php_function]
pub fn tiny_http_serve(addr: String, handler: ZendCallable) -> Result<(), PhpException> {
    let server = Server::http(&addr)
        .map_err(|e| PhpException::default(sanitize_no_nul(format!("bind failed: {e}"))))?;

    // Content-Type: text/plain; charset=utf-8
    let ct = Header::from_bytes("Content-Type", "text/plain; charset=utf-8")
    .map_err(|e| PhpException::default(... format!("header failed: {e}") ...))?;

    for mut req in server.incoming_requests() {
        let method = req.method().as_str().to_string();
        let path = req.url().to_string();

        // 最大 1MB だけ読む(最小安全策)
        let mut body_buf = Vec::new();
        let _ = req
            .as_reader()
            .take(1024 * 1024)
            .read_to_end(&mut body_buf);

        let body = String::from_utf8_lossy(&body_buf).into_owned();

        // PHP callable を呼ぶ(失敗したら例外が投げられる)
        let z = handler
            .try_call(vec![&method, &path, &body])
            .map_err(|e| PhpException::default(sanitize_no_nul(format!("handler failed: {e}"))))?;

        // 戻り値は「文字列ならそれ」を採用(それ以外は空文字)
        let resp_body = z.string().unwrap_or_default();

        let resp = Response::from_string(resp_body)
            .with_status_code(200)
            .with_header(ct.clone());

        // 送信(失敗しても次へ)
        let _ = req.respond(resp);
    }

    Ok(())
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module.function(wrap_function!(tiny_http_serve))
}
<?php
// 実行例:
// php -d extension=target/debug/libtiny_http_php.so server.php

tiny_http_serve("127.0.0.1:8080", function(string $method, string $path, string $body): string {
    return "method={$method}\npath={$path}\nbody={$body}\n";
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment