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();
29
30static ZTS_WORKERS: OnceLock<Mutex<Vec<thread::JoinHandle<()>>>> = OnceLock::new();
32
33pub fn register_zts_worker(handle: thread::JoinHandle<()>) {
35 let workers = ZTS_WORKERS.get_or_init(|| Mutex::new(Vec::new()));
36 workers.lock().unwrap().push(handle);
37}
38
39pub fn join_zts_workers() {
41 if let Some(workers) = ZTS_WORKERS.get() {
42 let handles: Vec<_> = workers.lock().unwrap().drain(..).collect();
43 for handle in handles {
44 let _ = handle.join();
45 }
46 tracing::info!("all ZTS worker threads joined");
47 }
48}
49
50pub fn version() -> String {
53 format!("folk-ext {}", env!("CARGO_PKG_VERSION"))
54}
55
56pub fn start_server(config: FolkConfig, plugins: Vec<Box<dyn Plugin>>) -> anyhow::Result<()> {
61 let worker_count = config.workers.count;
62 let is_zts = zts::is_zts();
63
64 if worker_count > 1 && !is_zts {
65 tracing::warn!(
66 worker_count,
67 "multi-worker requested but PHP is NTS; only 1 worker will be used"
68 );
69 }
70
71 let (task_tx, task_rx) = std::sync::mpsc::sync_channel::<bridge::TaskRequest>(8);
73 let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel::<()>(1);
74
75 bridge::init_worker_state(1, task_rx, ready_tx);
77
78 let tx_sides = vec![WorkerTxSide { task_tx, ready_rx }];
80
81 let registry = InProcessRegistry::new();
82 REGISTRY.set(registry.clone()).ok();
83
84 let workers_config = config.workers.clone();
85
86 thread::Builder::new()
87 .name("folk-tokio".into())
88 .spawn(move || {
89 let rt = tokio::runtime::Builder::new_multi_thread()
90 .enable_all()
91 .build()
92 .expect("failed to create tokio runtime");
93
94 TOKIO_HANDLE.set(rt.handle().clone()).ok();
95
96 rt.block_on(async move {
97 let ext_runtime = Arc::new(ExtensionRuntime::new(workers_config, tx_sides));
100
101 let mut server = folk_core::server::FolkServer::new(config, ext_runtime);
102 server.set_rpc_registrar(registry);
103
104 for plugin in plugins {
105 server.register_plugin(plugin);
106 }
107
108 if let Err(e) = server.run().await {
109 tracing::error!(error = ?e, "server error");
110 }
111 });
112 })?;
113
114 std::thread::sleep(std::time::Duration::from_millis(100));
115 info!(
116 worker_count,
117 is_zts, "folk server started, main process is worker #1"
118 );
119 Ok(())
120}
121
122pub fn call_method(method: &str, payload: bytes::Bytes) -> anyhow::Result<bytes::Bytes> {
123 let registry = REGISTRY
124 .get()
125 .ok_or_else(|| anyhow::anyhow!("server not started"))?;
126 let handle = TOKIO_HANDLE
127 .get()
128 .ok_or_else(|| anyhow::anyhow!("runtime not available"))?;
129
130 handle.block_on(registry.call(method, payload))
131}
132
133#[cfg(feature = "standalone")]
136#[php_class]
137#[php(name = "Folk\\Server")]
138#[derive(Debug)]
139pub struct Server {
140 config_path: String,
141}
142
143#[cfg(feature = "standalone")]
144#[php_impl]
145impl Server {
146 pub fn __construct(config_path: String) -> Self {
147 Self { config_path }
148 }
149
150 pub fn start(&self) -> PhpResult<()> {
151 let config = FolkConfig::load_from(&self.config_path)
152 .map_err(|e| PhpException::default(format!("Config error: {e}")))?;
153
154 start_server(config, vec![])
155 .map_err(|e| PhpException::default(format!("Start error: {e}")))?;
156
157 Ok(())
158 }
159}
160
161#[cfg(feature = "standalone")]
162#[php_function]
163pub fn folk_version() -> String {
164 version()
165}
166
167#[cfg(feature = "standalone")]
168#[php_function]
169#[allow(clippy::needless_pass_by_value)] pub fn folk_call(method: String, payload: Binary<u8>) -> PhpResult<Binary<u8>> {
171 let data: Vec<u8> = payload.into();
172 let result = call_method(&method, bytes::Bytes::from(data))
173 .map_err(|e| PhpException::default(format!("folk_call({method}): {e}")))?;
174
175 Ok(Binary::new(result.to_vec()))
176}
177
178#[cfg(feature = "standalone")]
179#[php_function]
180pub fn folk_worker_ready() -> PhpResult<bool> {
181 bridge::do_ready().map_err(|e| PhpException::default(format!("folk_worker_ready: {e}")))
182}
183
184#[cfg(feature = "standalone")]
185#[php_function]
186pub fn folk_worker_recv() -> PhpResult<Option<Vec<Binary<u8>>>> {
187 match bridge::do_recv() {
188 Ok(Some((method, payload))) => Ok(Some(vec![
189 Binary::new(method.into_bytes()),
190 Binary::new(payload),
191 ])),
192 Ok(None) => Ok(None),
193 Err(e) => Err(PhpException::default(format!("folk_worker_recv: {e}"))),
194 }
195}
196
197#[cfg(feature = "standalone")]
198#[php_function]
199pub fn folk_worker_send(result: Binary<u8>) -> PhpResult<()> {
200 let data: Vec<u8> = result.into();
201 bridge::do_send(&data).map_err(|e| PhpException::default(format!("folk_worker_send: {e}")))
202}
203
204#[cfg(feature = "standalone")]
205#[php_function]
206#[allow(clippy::needless_pass_by_value)] pub fn folk_worker_send_error(message: String) -> PhpResult<()> {
208 bridge::do_send_error(&message)
209 .map_err(|e| PhpException::default(format!("folk_worker_send_error: {e}")))
210}
211
212#[cfg(feature = "standalone")]
215#[php_function]
216pub fn folk_is_worker_thread() -> bool {
217 bridge::has_worker_state()
218}
219
220#[cfg(feature = "standalone")]
228#[php_function]
229#[allow(clippy::needless_pass_by_value)]
230pub fn folk_worker_run(dispatch_fn: String) -> PhpResult<()> {
231 bridge::run_dispatch_loop(&dispatch_fn)
232 .map_err(|e| PhpException::default(format!("folk_worker_run: {e}")))
233}
234
235#[cfg(feature = "standalone")]
236#[php_module]
237pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
238 module
239 .class::<Server>()
240 .function(wrap_function!(folk_version))
241 .function(wrap_function!(folk_call))
242 .function(wrap_function!(folk_worker_ready))
243 .function(wrap_function!(folk_worker_recv))
244 .function(wrap_function!(folk_worker_send))
245 .function(wrap_function!(folk_worker_send_error))
246 .function(wrap_function!(folk_is_worker_thread))
247 .function(wrap_function!(folk_worker_run))
248}