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;