Repository navigation
Expand file tree
/
Copy pathcallback.rs
More file actions
107 lines (97 loc) · 4.22 KB
/
Copy pathcallback.rs
File metadata and controls
107 lines (97 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! `Js\Callback`: a PHP-facing wrapper around a JS function handed to PHP.
//!
//! The JS function itself never leaves the engine; the callback holds only an
//! integer id into the JS-side registry plus a handle back to the engine. When
//! invoked from PHP it re-enters JS — reusing the live context if a host call
//! is already in flight, else acquiring the runtime lock afresh.
use crate::engine::{current_ctx_ptr, Engine};
use crate::marshal::{middle_to_zval, zval_to_middle, MiddleValue};
use ext_php_rs::prelude::*;
use ext_php_rs::types::Zval;
use rquickjs::{Ctx, Function, TypedArray, Value};
use std::rc::Rc;
#[php_class]
#[php(name = "Js\\Callback")]
pub struct JsCallback {
pub id: u64,
pub engine: Rc<Engine>,
}
impl JsCallback {
pub fn new(id: u64, engine: Rc<Engine>) -> Self {
JsCallback { id, engine }
}
}
impl Drop for JsCallback {
/// Queue this callback's registry entry for release. Deletion is deferred
/// (not done here) and flushed at the next eval boundary: a JS function can
/// be round-tripped PHP->JS within a single host call, dropping a transient
/// wrapper while JS still needs the entry, so deleting eagerly would race.
/// Queuing touches no JS and cannot re-enter the engine.
fn drop(&mut self) {
self.engine.state.queue_fn_deletion(self.id);
}
}
impl JsCallback {
/// Invoke the underlying JS function with the given (already PHP-side) args.
fn invoke_inner(&self, args: &[&Zval]) -> PhpResult<Zval> {
let _guard = self.engine.enter().map_err(PhpException::default)?;
let mut middle_args = Vec::with_capacity(args.len());
for a in args {
middle_args.push(zval_to_middle(a, &self.engine.state).map_err(PhpException::default)?);
}
let payload = MiddleValue::Array(middle_args)
.to_msgpack()
.map_err(|e| PhpException::default(e.to_string()))?;
let id = self.id;
let engine = self.engine.clone();
let run = move |ctx: &Ctx<'_>| -> PhpResult<Zval> {
let globals = ctx.globals();
let invoke: Function = globals
.get("__invokeJs")
.map_err(|e| PhpException::default(format!("__invokeJs missing: {e}")))?;
let arg_bytes = TypedArray::new(ctx.clone(), payload.clone())
.map_err(|e| PhpException::default(e.to_string()))?;
// A JS error here re-surfaces a host exception (unwrapped to its
// original PHP class) or becomes a QuickJSEvalException.
let ret: Value = invoke
.call((id as f64, arg_bytes))
.map_err(|e| crate::error::js_error_to_php(ctx, e))?;
let ta = TypedArray::<u8>::from_value(ret).map_err(|e| {
PhpException::default(format!("JS callback did not return bytes: {e}"))
})?;
let bytes = ta
.as_bytes()
.ok_or_else(|| PhpException::default("detached result buffer".to_owned()))?;
let mv = MiddleValue::from_msgpack(bytes)
.map_err(|e| PhpException::default(e.to_string()))?;
middle_to_zval(&mv, &engine.state).map_err(PhpException::default)
};
// Reuse the live context if we are nested inside a host call; otherwise
// acquire the runtime lock on the persistent realm. Reusing avoids a
// deadlock from re-locking.
match current_ctx_ptr() {
Some(ptr) => {
let ctx = unsafe { Ctx::from_raw(ptr) };
run(&ctx)
}
None => match self.engine.shared_ctx() {
Some(ctx) => ctx.with(|c| run(&c)),
// Isolated mode: the realm that owned this callback is gone.
None => Err(PhpException::default(
"JS callback invoked outside its eval (isolated QuickJS instance)".to_owned(),
)),
},
}
}
}
#[php_impl]
impl JsCallback {
/// Invoke the JS callback: `$cb(...$args)`.
pub fn __invoke(&self, args: &[&Zval]) -> PhpResult<Zval> {
self.invoke_inner(args)
}
/// Explicit form: `$cb->call([...$args])`.
pub fn call(&self, args: &[&Zval]) -> PhpResult<Zval> {
self.invoke_inner(args)
}
}