Skip to main content

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}