folk_runtime_embed/runtime.rs
1//! `Runtime` implementation for embedded PHP.
2//!
3//! PHP module is initialized once. Worker threads attach to it
4//! and only manage per-request lifecycle.
5
6use anyhow::Result;
7use async_trait::async_trait;
8use folk_core::runtime::{Runtime, WorkerHandle};
9use tracing::info;
10
11use crate::handle::EmbedWorkerHandle;
12use crate::php::PhpInstance;
13use crate::worker::spawn_worker_thread;
14
15/// Configuration for the embed runtime.
16#[derive(Debug, Clone)]
17pub struct EmbedConfig {
18 /// Optional PHP bootstrap script to load on each worker.
19 pub script: Option<String>,
20}
21
22/// Embedded PHP runtime — spawns worker threads instead of processes.
23///
24/// PHP module is initialized once in `new()`. Worker threads call
25/// `PhpInstance::attach()` to share the module without re-init.
26pub struct EmbedRuntime {
27 config: EmbedConfig,
28 /// Keeps the module alive — dropped only when `EmbedRuntime` is dropped.
29 _php_module: PhpInstance,
30}
31
32impl EmbedRuntime {
33 /// Initialize the embed runtime.
34 ///
35 /// Calls `folk_sapi_init()` once — sets up PHP module, extensions,
36 /// `OPcache`, signal handlers. Worker threads will attach to this.
37 ///
38 /// PHP module startup is run in a dedicated OS thread (not a tokio
39 /// worker thread) to give PHP a clean stack and standard thread state.
40 pub fn new(config: EmbedConfig) -> Result<Self> {
41 info!("initializing PHP embed module");
42 let php = PhpInstance::boot_custom_sapi()?;
43 info!("PHP embed module ready");
44
45 Ok(Self {
46 config,
47 _php_module: php,
48 })
49 }
50}
51
52#[async_trait]
53impl Runtime for EmbedRuntime {
54 async fn spawn(&self) -> Result<Box<dyn WorkerHandle>> {
55 let (thread, cmd_tx, task_resp_rx, control_rx, worker_id) =
56 spawn_worker_thread(self.config.script.clone());
57
58 Ok(Box::new(EmbedWorkerHandle::new(
59 worker_id,
60 cmd_tx,
61 task_resp_rx,
62 control_rx,
63 thread,
64 )))
65 }
66}