bext_php/bridge.rs
1//! Shared memory bridge — bidirectional PHP ↔ JSC communication.
2//!
3//! ## PHP → JSC (bext_render)
4//!
5//! PHP: $html = bext_render("Dashboard", '{"orders":42}')
6//! → C FFI → Rust → JSC pool → HTML → PHP
7//!
8//! ## JS → PHP (php_call)
9//!
10//! JS: const data = bext.php("GET", "/api/products", '{"category":"Books"}')
11//! → Rust → PHP worker pool → JSON → JS
12//!
13//! Both directions use the same process memory — no HTTP, no sockets.
14
15use std::sync::{Arc, OnceLock};
16
17/// Global reference to the JSC render pool.
18/// Set once at server startup, read by all PHP worker threads.
19static JSC_POOL: OnceLock<Arc<dyn JscBridge + Send + Sync>> = OnceLock::new();
20
21/// Trait abstracting the JSC render pool so bext-php doesn't depend on bext-core.
22pub trait JscBridge: Send + Sync {
23 /// Render a full page given props JSON.
24 /// Returns HTML on success, error string on failure.
25 fn render_page(&self, props_json: String) -> Result<String, String>;
26
27 /// Render a single component.
28 fn render_component(&self, component_id: String, props_json: String) -> Result<String, String>;
29}
30
31/// Register the JSC pool for PHP threads to use.
32/// Called once at server startup after the JSC pool is initialized.
33pub fn set_jsc_bridge(bridge: Arc<dyn JscBridge + Send + Sync>) {
34 let _ = JSC_POOL.set(bridge);
35}
36
37/// Check if a JSC bridge is available.
38pub fn has_jsc_bridge() -> bool {
39 JSC_POOL.get().is_some()
40}
41
42/// Render via the JSC bridge. Called from the C SAPI's `bext_render()` function.
43///
44/// Returns the rendered HTML, or an error message wrapped in a div.
45pub fn jsc_render(props_json: &str) -> String {
46 match JSC_POOL.get() {
47 Some(pool) => match pool.render_page(props_json.to_string()) {
48 Ok(html) => html,
49 Err(e) => format!("<div style=\"color:red\">JSC render error: {}</div>", e),
50 },
51 None => "<div style=\"color:red\">JSC pool not available. \
52 Configure [render] bundle_path in bext.config.toml</div>"
53 .to_string(),
54 }
55}
56
57/// Render a named component via the JSC bridge.
58pub fn jsc_render_component(component_id: &str, props_json: &str) -> String {
59 match JSC_POOL.get() {
60 Some(pool) => match pool.render_component(component_id.to_string(), props_json.to_string())
61 {
62 Ok(html) => html,
63 Err(e) => format!("<div style=\"color:red\">JSC component error: {}</div>", e),
64 },
65 None => "<div style=\"color:red\">JSC pool not available</div>".into(),
66 }
67}
68
69// ─── JS → PHP bridge ────────────────────────────────────────────────────
70
71/// Global reference to the PHP pool for JS→PHP calls.
72static PHP_POOL: OnceLock<Arc<dyn PhpBridge + Send + Sync>> = OnceLock::new();
73
74/// Trait abstracting the PHP pool so JSC/Bun can call PHP.
75pub trait PhpBridge: Send + Sync {
76 /// Execute a PHP request and return the response body.
77 ///
78 /// This dispatches to the PHP worker pool, executes the request
79 /// through PHP's front controller (or worker handler), and returns
80 /// the response body as a string.
81 fn call(&self, method: &str, uri: &str, body: Option<&str>) -> Result<PhpCallResult, String>;
82}
83
84/// Result of a JS→PHP call.
85#[derive(Debug, Clone)]
86pub struct PhpCallResult {
87 pub status: u16,
88 pub body: String,
89 pub content_type: String,
90}
91
92/// Register the PHP pool for JS threads to use.
93pub fn set_php_bridge(bridge: Arc<dyn PhpBridge + Send + Sync>) {
94 let _ = PHP_POOL.set(bridge);
95}
96
97/// Check if a PHP bridge is available.
98pub fn has_php_bridge() -> bool {
99 PHP_POOL.get().is_some()
100}
101
102/// Call PHP from JS. Returns (status, body, content_type).
103///
104/// Used by JSC's `globalThis.__bext_php()` function and the NAPI export.
105pub fn php_call(method: &str, uri: &str, body: Option<&str>) -> Result<PhpCallResult, String> {
106 match PHP_POOL.get() {
107 Some(pool) => pool.call(method, uri, body),
108 None => Err("PHP pool not available".into()),
109 }
110}