1pub mod bridge;
7pub mod registry;
8pub mod runtime;
9pub mod worker;
10pub mod zts;
11pub mod zval_convert;
12
13use std::sync::{Arc, Mutex, OnceLock};
14use std::thread;
15
16use ext_php_rs::binary::Binary;
17use ext_php_rs::prelude::*;
18use folk_api::Plugin;
19use folk_core::config::FolkConfig;
20use tracing::info;
21
22use crate::registry::InProcessRegistry;
23use crate::runtime::{ExtensionRuntime, WorkerTxSide};
24
25pub use folk_core;
26
27static REGISTRY: OnceLock<Arc<InProcessRegistry>> = OnceLock::new();
28static TOKIO_HANDLE: OnceLock<tokio::runtime::Handle> = OnceLock::new();
29static PROJECT_ROOT: OnceLock<std::path::PathBuf> = OnceLock::new();
30
31pub fn project_root() -> Option<&'static std::path::Path> {
33 PROJECT_ROOT.get().map(|p| p.as_path())
34}
35
36static ZTS_WORKERS: OnceLock<Mutex<Vec<thread::JoinHandle<()>>>> = OnceLock::new();
38
39pub fn register_zts_worker(handle: thread::JoinHandle<()>) {
41 let workers = ZTS_WORKERS.get_or_init(|| Mutex::new(Vec::new()));
42 workers.lock().unwrap().push(handle);
43}
44
45pub fn join_zts_workers() {
47 if let Some(workers) = ZTS_WORKERS.get() {
48 let handles: Vec<_> = workers.lock().unwrap().drain(..).collect();
49 for handle in handles {
50 let _ = handle.join();
51 }
52 tracing::info!("all ZTS worker threads joined");
53 }
54}
55
56pub fn version() -> String {
59 format!("folk-ext {}", env!("CARGO_PKG_VERSION"))
60}
61
62pub fn start_server(config: FolkConfig, plugins: Vec<Box<dyn Plugin>>) -> anyhow::Result<()> {
67 let _ = PROJECT_ROOT.set(std::env::current_dir().unwrap_or_default());
69
70 let worker_count = config.workers.count;
71 let is_zts = zts::is_zts();
72
73 if worker_count > 1 && !is_zts {
74 tracing::warn!(
75 worker_count,
76 "multi-worker requested but PHP is NTS; only 1 worker will be used"
77 );
78 }
79
80 let (task_tx, task_rx) = std::sync::mpsc::sync_channel::<bridge::TaskRequest>(8);
82 let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel::<()>(1);
83
84 bridge::init_worker_state(1, task_rx, ready_tx);
86
87 let tx_sides = vec![WorkerTxSide { task_tx, ready_rx }];
89
90 let registry = InProcessRegistry::new();
91 REGISTRY.set(registry.clone()).ok();
92
93 let workers_config = config.workers.clone();
94
95 thread::Builder::new()
96 .name("folk-tokio".into())
97 .spawn(move || {
98 let rt = tokio::runtime::Builder::new_multi_thread()
99 .enable_all()
100 .build()
101 .expect("failed to create tokio runtime");
102
103 TOKIO_HANDLE.set(rt.handle().clone()).ok();
104
105 rt.block_on(async move {
106 let ext_runtime = Arc::new(ExtensionRuntime::new(workers_config, tx_sides));
109
110 let mut server = folk_core::server::FolkServer::new(config, ext_runtime);
111 server.set_rpc_registrar(registry);
112
113 for plugin in plugins {
114 server.register_plugin(plugin);
115 }
116
117 if let Err(e) = server.run().await {
118 tracing::error!(error = ?e, "server error");
119 }
120 });
121 })?;
122
123 std::thread::sleep(std::time::Duration::from_millis(100));
124 info!(
125 worker_count,
126 is_zts, "folk server started, main process is worker #1"
127 );
128 Ok(())
129}
130
131pub fn call_method(method: &str, payload: bytes::Bytes) -> anyhow::Result<bytes::Bytes> {
132 let registry = REGISTRY
133 .get()
134 .ok_or_else(|| anyhow::anyhow!("server not started"))?;
135 let handle = TOKIO_HANDLE
136 .get()
137 .ok_or_else(|| anyhow::anyhow!("runtime not available"))?;
138
139 handle.block_on(registry.call(method, payload))
140}
141
142#[cfg(feature = "standalone")]
145#[php_class]
146#[php(name = "Folk\\Server")]
147#[derive(Debug)]
148pub struct Server {
149 config_path: String,
150}
151
152#[cfg(feature = "standalone")]
153#[php_impl]
154impl Server {
155 pub fn __construct(config_path: String) -> Self {
156 Self { config_path }
157 }
158
159 pub fn start(&self) -> PhpResult<()> {
160 let config = FolkConfig::load_from(&self.config_path)
161 .map_err(|e| PhpException::default(format!("Config error: {e}")))?;
162
163 start_server(config, vec![])
164 .map_err(|e| PhpException::default(format!("Start error: {e}")))?;
165
166 Ok(())
167 }
168}
169
170#[cfg(feature = "standalone")]
171#[php_function]
172pub fn folk_version() -> String {
173 version()
174}
175
176#[cfg(feature = "standalone")]
177#[php_function]
178#[allow(clippy::needless_pass_by_value)] pub fn folk_call(method: String, payload: Binary<u8>) -> PhpResult<Binary<u8>> {
180 let data: Vec<u8> = payload.into();
181 let result = call_method(&method, bytes::Bytes::from(data))
182 .map_err(|e| PhpException::default(format!("folk_call({method}): {e}")))?;
183
184 Ok(Binary::new(result.to_vec()))
185}
186
187#[cfg(feature = "standalone")]
188#[php_function]
189pub fn folk_worker_ready() -> PhpResult<bool> {
190 bridge::do_ready().map_err(|e| PhpException::default(format!("folk_worker_ready: {e}")))
191}
192
193#[cfg(feature = "standalone")]
194#[php_function]
195pub fn folk_worker_recv() -> PhpResult<Option<Vec<Binary<u8>>>> {
196 match bridge::do_recv() {
197 Ok(Some((method, payload))) => Ok(Some(vec![
198 Binary::new(method.into_bytes()),
199 Binary::new(payload),
200 ])),
201 Ok(None) => Ok(None),
202 Err(e) => Err(PhpException::default(format!("folk_worker_recv: {e}"))),
203 }
204}
205
206#[cfg(feature = "standalone")]
207#[php_function]
208pub fn folk_worker_send(result: Binary<u8>) -> PhpResult<()> {
209 let data: Vec<u8> = result.into();
210 bridge::do_send(&data).map_err(|e| PhpException::default(format!("folk_worker_send: {e}")))
211}
212
213#[cfg(feature = "standalone")]
214#[php_function]
215#[allow(clippy::needless_pass_by_value)] pub fn folk_worker_send_error(message: String) -> PhpResult<()> {
217 bridge::do_send_error(&message)
218 .map_err(|e| PhpException::default(format!("folk_worker_send_error: {e}")))
219}
220
221#[cfg(feature = "standalone")]
224#[php_function]
225pub fn folk_is_worker_thread() -> bool {
226 bridge::has_worker_state()
227}
228
229#[cfg(feature = "standalone")]
237#[php_function]
238#[allow(clippy::needless_pass_by_value)]
239pub fn folk_worker_run(dispatch_fn: String) -> PhpResult<()> {
240 bridge::run_dispatch_loop(&dispatch_fn)
241 .map_err(|e| PhpException::default(format!("folk_worker_run: {e}")))
242}
243
244#[cfg(feature = "standalone")]
245#[php_module]
246pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
247 module
248 .class::<Server>()
249 .function(wrap_function!(folk_version))
250 .function(wrap_function!(folk_call))
251 .function(wrap_function!(folk_worker_ready))
252 .function(wrap_function!(folk_worker_recv))
253 .function(wrap_function!(folk_worker_send))
254 .function(wrap_function!(folk_worker_send_error))
255 .function(wrap_function!(folk_is_worker_thread))
256 .function(wrap_function!(folk_worker_run))
257}