Skip to main content

quickjs_oxide/engine/api/core/
function.rs

1//! Host functions: Rust closures as JavaScript functions. The core runs the
2//! body this module wraps around the embedder's closure; the closure reads
3//! its call and calls back into the engine through a [`Scope`].
4
5use std::rc::{Rc, Weak};
6
7use oxide_core::vm::native::{HostFn, NativeCall};
8use oxide_core::{Intrinsic, NativeCx};
9
10use super::RuntimeError;
11use super::ops::{JsResult, View};
12use super::runtime::Shared;
13use super::value::{CallableRef, Value};
14
15/// A host function's view of the engine: its call, and every operation of
16/// [`Js`](super::Js). Values it hands out are handles like any other and
17/// outlive the call.
18pub struct Scope<'a> {
19    pub(crate) core: NativeCx<'a>,
20    pub(crate) shared: Rc<Shared>,
21    call: NativeCall,
22}
23
24impl Scope<'_> {
25    /// The operations' view of the engine for one operation.
26    pub(crate) fn view(&mut self) -> View<'_> {
27        View::new(self.core.reborrow(), &self.shared)
28    }
29
30    fn lift(&mut self, value: oxide_core::Value) -> Value {
31        // Lifting fails only when a string cannot be copied out (the heap
32        // cap), which a read of the call reports as `undefined`.
33        self.view().lift(value).unwrap_or(Value::Undefined)
34    }
35
36    /// The receiver: `this` of the call.
37    pub fn this(&mut self) -> Value {
38        let this = self.call.this(&self.core);
39        self.lift(this)
40    }
41
42    /// Argument `index`; `undefined` past [`Scope::argc`].
43    pub fn arg(&mut self, index: usize) -> Value {
44        let arg = self.call.arg(&self.core, index);
45        self.lift(arg)
46    }
47
48    /// The number of arguments passed, whatever the function's `length`.
49    pub fn argc(&self) -> usize {
50        self.call.argc()
51    }
52
53    /// Every argument passed.
54    pub fn args(&mut self) -> Vec<Value> {
55        (0..self.argc()).map(|index| self.arg(index)).collect()
56    }
57
58    /// `new.target`; `undefined` for a call.
59    pub fn new_target(&mut self) -> Value {
60        let target = self.call.new_target();
61        self.lift(target)
62    }
63
64    /// Whether `new` called the function.
65    pub fn is_construct(&self) -> bool {
66        self.call.is_construct()
67    }
68
69    /// The function object called.
70    pub fn callee(&mut self) -> CallableRef {
71        let callee = self.call.callee().as_object().expect("a host function");
72        CallableRef(self.view().lift_object(callee))
73    }
74
75    /// Captured value `slot` ([`FunctionOptions::data`]); `undefined` past
76    /// the last.
77    pub fn data(&mut self, slot: usize) -> Value {
78        if slot >= self.call.data_len(&self.core) {
79            return Value::Undefined;
80        }
81        let value = self.call.data(&self.core, slot);
82        self.lift(value)
83    }
84
85    /// Replaces captured value `slot`, which must exist.
86    pub fn set_data(&mut self, slot: usize, value: Value) -> JsResult<()> {
87        if slot >= self.call.data_len(&self.core) {
88            return Err(RuntimeError::Invariant("a captured value's slot"));
89        }
90        let call = self.call;
91        let mut view = self.view();
92        let value = view.lower(&value)?;
93        call.set_data(&mut view.core, slot, value);
94        Ok(())
95    }
96
97    /// The value a host object receiver holds ([`Js::host_data`](super::Js::host_data) of
98    /// `this`); `None` when the receiver is not a host object of `T`.
99    pub fn this_host_data<T: 'static>(&mut self) -> JsResult<Option<Rc<T>>> {
100        match self.this() {
101            Value::Object(object) => self.host_data::<T>(&object),
102            _ => Ok(None),
103        }
104    }
105}
106
107/// How a host function is made: its `name` and `length`, whether `new` may
108/// call it, and the values it captures ([`Scope::data`]). Captured values are
109/// traced by the collector; a Rust value the closure captures is not.
110#[derive(Clone, Debug, Default)]
111pub struct FunctionOptions {
112    pub name: String,
113    pub length: u32,
114    pub constructor: bool,
115    pub data: Vec<Value>,
116}
117
118impl FunctionOptions {
119    pub fn new(name: impl Into<String>) -> Self {
120        FunctionOptions {
121            name: name.into(),
122            ..FunctionOptions::default()
123        }
124    }
125
126    #[must_use]
127    pub fn length(mut self, length: u32) -> Self {
128        self.length = length;
129        self
130    }
131
132    /// The function may be called by `new` ([`Scope::is_construct`]). It
133    /// has no `prototype` property until the embedder defines one.
134    #[must_use]
135    pub fn constructor(mut self) -> Self {
136        self.constructor = true;
137        self
138    }
139
140    #[must_use]
141    pub fn data(mut self, data: Vec<Value>) -> Self {
142        self.data = data;
143        self
144    }
145}
146
147/// What [`Js::new_function`](super::Js::new_function) takes: a closure over
148/// a [`Scope`]. `Args` is the marker the closure's shape selects on.
149pub trait HostFunction<Args>: 'static {
150    fn call(&self, scope: &mut Scope<'_>) -> JsResult<Value>;
151}
152
153/// The marker of a closure over the [`Scope`] alone.
154pub struct Raw;
155
156impl<F> HostFunction<Raw> for F
157where
158    F: Fn(&mut Scope<'_>) -> JsResult<Value> + 'static,
159{
160    fn call(&self, scope: &mut Scope<'_>) -> JsResult<Value> {
161        self(scope)
162    }
163}
164
165/// Marks the runtime poisoned when a panic unwinds through a host function.
166struct PoisonGuard<'a>(&'a Shared);
167
168impl Drop for PoisonGuard<'_> {
169    fn drop(&mut self) {
170        if std::thread::panicking() {
171            self.0.poisoned.set(true);
172        }
173    }
174}
175
176/// The core's body for `function`: it builds the [`Scope`], calls the
177/// embedder's closure and translates its result into a value or a throw.
178/// The runtime is held weakly, since the body lives inside it.
179pub(crate) fn host_body<Args>(
180    shared: &Rc<Shared>,
181    function: impl HostFunction<Args>,
182) -> Rc<HostFn> {
183    let weak: Weak<Shared> = Rc::downgrade(shared);
184    Rc::new(move |core: &mut NativeCx<'_>, call: NativeCall| {
185        let shared = weak
186            .upgrade()
187            .expect("a host function runs inside its live runtime");
188        let _guard = PoisonGuard(&shared);
189        let mut scope = Scope {
190            core: core.reborrow(),
191            shared: Rc::clone(&shared),
192            call,
193        };
194        let result = function.call(&mut scope);
195        drop(scope);
196        let mut view = View::new(core.reborrow(), &shared);
197        let completion = match result {
198            // A constructor's non-object result passes through, as QuickJS
199            // passes it.
200            Ok(value) => view
201                .lower(&value)
202                .map_err(|error| error_throw(&mut view, error)),
203            Err(error) => Err(error_throw(&mut view, error)),
204        };
205        drop(view);
206        // Handles the closure dropped wait for nothing: a script that calls
207        // a host function many times keeps only one call's handles.
208        shared.drain_released(core);
209        completion
210    })
211}
212
213/// The throw a host function's error becomes.
214fn error_throw(view: &mut View<'_>, error: RuntimeError) -> oxide_core::Throw {
215    let thrown = match error {
216        RuntimeError::Exception(value) => view.lower(&value),
217        // A nested call was interrupted: the interrupt keeps unwinding, the
218        // flag still set.
219        RuntimeError::Interrupted => view
220            .core
221            .new_error(oxide_core::ErrorKind::Internal, "interrupted")
222            .map_err(RuntimeError::from),
223        other => Err(other),
224    };
225    match thrown {
226        Ok(value) => oxide_core::Throw(value),
227        Err(RuntimeError::OutOfMemory) => out_of_memory(view),
228        Err(other) => {
229            let message = other.to_string();
230            match view
231                .core
232                .new_error(oxide_core::ErrorKind::Internal, &message)
233            {
234                Ok(error) => oxide_core::Throw(error),
235                Err(_) => out_of_memory(view),
236            }
237        }
238    }
239}
240
241fn out_of_memory(view: &View<'_>) -> oxide_core::Throw {
242    let error = view.core.intrinsic(Intrinsic::OutOfMemoryError);
243    oxide_core::Throw(oxide_core::Value::object(error))
244}