Repository navigation
Expand file tree
/
Copy pathengine.rs
More file actions
172 lines (153 loc) · 5.87 KB
/
Copy pathengine.rs
File metadata and controls
172 lines (153 loc) · 5.87 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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
//! Owns the QuickJS runtime/context and the re-entrancy machinery.
use crate::bridge::BridgeState;
use crate::sandbox;
use crate::transpile::TranspileCache;
use rquickjs::{Context, Ctx, Runtime};
use std::cell::Cell;
use std::ptr::NonNull;
use std::rc::Rc;
use std::time::{Duration, Instant};
/// Maximum nesting depth across the JS<->PHP boundary, guarding runaway mutual
/// recursion (which would otherwise overflow the native stack).
pub const MAX_DEPTH: usize = 200;
/// The QuickJS engine: runtime + context + the shared bridge state.
pub struct Engine {
pub rt: Runtime,
pub state: Rc<BridgeState>,
/// Content-addressed TS->JS transpile cache (source maps kept host-side).
pub transpile: TranspileCache,
/// The persistent realm in shared mode; `None` in isolated mode (a fresh
/// realm is created per eval and discarded afterwards).
shared_ctx: Option<Context>,
depth: Cell<usize>,
/// Per-eval wall-clock deadline; `None` when no eval is in flight.
deadline: Rc<Cell<Option<Instant>>>,
/// Set by the interrupt handler when it aborts on the deadline.
timed_out: Rc<Cell<bool>>,
/// Per-eval timeout; `None` disables the wall-clock guard.
timeout: Option<Duration>,
}
impl Engine {
pub fn new(
memory_limit: usize,
timeout_ms: u64,
max_stack: usize,
isolated: bool,
) -> rquickjs::Result<Rc<Self>> {
let rt = Runtime::new()?;
sandbox::apply_limits(&rt, memory_limit, max_stack);
let deadline = Rc::new(Cell::new(None));
let timed_out = Rc::new(Cell::new(false));
sandbox::install_interrupt(&rt, deadline.clone(), timed_out.clone());
// Shared mode: one persistent realm. Isolated mode: a fresh realm per
// eval (so each eval is its own world; cross-eval state is not kept).
let shared_ctx = if isolated {
None
} else {
Some(Context::full(&rt)?)
};
let state = BridgeState::new();
let engine = Rc::new(Engine {
rt,
state: state.clone(),
transpile: TranspileCache::new(256),
shared_ctx,
depth: Cell::new(0),
deadline,
timed_out,
timeout: (timeout_ms > 0).then(|| Duration::from_millis(timeout_ms)),
});
// Close the cycle so the bridge can reach back into the engine when it
// needs to invoke JS callbacks held by PHP.
state.set_engine(Rc::downgrade(&engine));
Ok(engine)
}
/// Arm the wall-clock deadline for an eval and clear the timeout flag.
pub fn arm_deadline(&self) {
self.timed_out.set(false);
self.deadline.set(self.timeout.map(|t| Instant::now() + t));
}
/// Disarm the deadline once an eval completes.
pub fn disarm_deadline(&self) {
self.deadline.set(None);
}
/// Whether the last eval was aborted by the wall-clock guard.
pub fn timed_out(&self) -> bool {
self.timed_out.get()
}
/// The persistent realm, if this engine has one (shared mode).
pub fn shared_ctx(&self) -> Option<&Context> {
self.shared_ctx.as_ref()
}
/// Run `f` inside an eval realm: the persistent one in shared mode, or a
/// fresh, single-use realm in isolated mode. The realm's `Ctx` is published
/// on the current-context stack for the duration so PHP-side callbacks
/// (and the GC `Drop` cleanup) re-use it instead of re-locking the runtime.
pub fn eval_in<R>(&self, f: impl FnOnce(&Ctx<'_>) -> R) -> rquickjs::Result<R> {
fn run<R>(ctx: &Context, f: impl FnOnce(&Ctx<'_>) -> R) -> R {
ctx.with(|c| {
let _guard = push_ctx(&c);
f(&c)
})
}
match &self.shared_ctx {
Some(ctx) => Ok(run(ctx, f)),
None => {
let ctx = Context::full(&self.rt)?;
Ok(run(&ctx, f))
}
}
}
/// Enter one level of cross-boundary nesting; errors if the cap is hit.
pub fn enter(&self) -> Result<DepthGuard<'_>, String> {
let d = self.depth.get();
if d >= MAX_DEPTH {
return Err(format!(
"maximum bridge re-entrancy depth ({MAX_DEPTH}) exceeded"
));
}
self.depth.set(d + 1);
Ok(DepthGuard { depth: &self.depth })
}
}
/// RAII guard that decrements the depth counter on drop.
pub struct DepthGuard<'a> {
depth: &'a Cell<usize>,
}
impl Drop for DepthGuard<'_> {
fn drop(&mut self) {
self.depth.set(self.depth.get() - 1);
}
}
// ---------------------------------------------------------------------------
// current-context stack
//
// While a host call runs, the live `Ctx` is valid but its `'js` lifetime
// cannot be named in PHP-facing code. We stash the raw pointer so a PHP-held JS
// callback can be invoked *synchronously* during a host call by reusing the
// already-locked context instead of re-locking the runtime (which would
// deadlock). Single-threaded (PHP NTS), so a thread-local stack is sufficient.
// ---------------------------------------------------------------------------
thread_local! {
static CTX_STACK: std::cell::RefCell<Vec<NonNull<rquickjs::qjs::JSContext>>> =
const { std::cell::RefCell::new(Vec::new()) };
}
/// Publish the current context on the stack until the returned guard drops.
#[must_use]
pub fn push_ctx(ctx: &Ctx<'_>) -> CtxGuard {
CTX_STACK.with(|s| s.borrow_mut().push(ctx.as_raw()));
CtxGuard
}
/// RAII guard that pops the current context when dropped (even on unwind).
pub struct CtxGuard;
impl Drop for CtxGuard {
fn drop(&mut self) {
CTX_STACK.with(|s| {
s.borrow_mut().pop();
});
}
}
/// The innermost active context, if a host call is currently on the stack.
pub fn current_ctx_ptr() -> Option<NonNull<rquickjs::qjs::JSContext>> {
CTX_STACK.with(|s| s.borrow().last().copied())
}