Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save masakielastic/b45fe0b0bac935d7146fb738bb37d16e to your computer and use it in GitHub Desktop.
poll/await 風のコード | PHP 拡張 ext-php-rs

poll/await 風のコード | PHP 拡張 ext-php-rs

Cargo.toml

[package]
name = "async-project"
version = "0.1.0"
edition = "2024"


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

[dependencies]
ext-php-rs = "0.15.6"
once_cell = "1"

src/lib.rs

use ext_php_rs::prelude::*;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use std::time::{Duration, Instant};

fn dummy_waker() -> Waker {
    // poll を手動で回すだけなので、起床通知は不要。最小のダミー Waker を作る。
    unsafe fn clone(_: *const ()) -> RawWaker {
        RawWaker::new(std::ptr::null(), &VTABLE)
    }
    unsafe fn wake(_: *const ()) {}
    unsafe fn wake_by_ref(_: *const ()) {}
    unsafe fn drop(_: *const ()) {}

    static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);

    unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}

struct PollSleep {
    deadline: Instant,
}

impl Future for PollSleep {
    type Output = String;

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.deadline {
            Poll::Ready("done".to_string())
        } else {
            Poll::Pending
        }
    }
}

type BoxFutureString = Pin<Box<dyn Future<Output = String> + 'static>>;

#[php_class]
pub struct Task {
    future: Arc<Mutex<Option<BoxFutureString>>>,
    result: Arc<Mutex<Option<String>>>,
}

#[php_impl]
impl Task {
    pub fn poll(&mut self) -> bool {
        // すでに完了しているなら ready 扱い
        if self.result.lock().unwrap().is_some() {
            return true;
        }

        let mut future_guard = self.future.lock().unwrap();
        let Some(fut) = future_guard.as_mut() else {
            return true;
        };

        let waker = dummy_waker();
        let mut cx = Context::from_waker(&waker);

        match fut.as_mut().poll(&mut cx) {
            Poll::Ready(val) => {
                *self.result.lock().unwrap() = Some(val);
                *future_guard = None;
                true
            }
            Poll::Pending => false,
        }
    }

    // PHP 側の呼び方に合わせて camelCase 名にしたいなら `#[php(name = "...")]` を付けますが、
    // まずは素直に snake_case でいきます。
    pub fn is_ready(&self) -> bool {
        self.result.lock().unwrap().is_some()
    }

    pub fn result(&self) -> PhpResult<String> {
        self.result
            .lock()
            .unwrap()
            .clone()
            .ok_or_else(|| PhpException::default("Not ready".to_string()))
    }
}

#[php_function]
pub fn async_sleep(ms: i64) -> PhpResult<Task> {
    if ms < 0 {
        return Err(PhpException::default("ms must be >= 0".to_string()));
    }

    let fut = PollSleep {
        deadline: Instant::now() + Duration::from_millis(ms as u64),
    };

    Ok(Task {
        future: Arc::new(Mutex::new(Some(Box::pin(fut)))),
        result: Arc::new(Mutex::new(None)),
    })
}


#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module
        .class::<Task>()
        .function(wrap_function!(async_sleep))
}
cargo build
<?php
$t = async_sleep(500);

while (!$t->isReady()) {
    $t->poll();
    usleep(10_000);
}

echo $t->result(), PHP_EOL;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment