mini_v8/
mini_v8.rs

1use crate::*;
2use std::any::Any;
3use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::rc::Rc;
6use std::string::String as StdString;
7use std::sync::{Arc, Condvar, Mutex, Once};
8use std::thread;
9use std::time::Duration;
10
11#[derive(Clone)]
12pub struct MiniV8 {
13    interface: Interface,
14}
15
16impl MiniV8 {
17    pub fn new() -> MiniV8 {
18        initialize_v8();
19        let mut isolate = v8::Isolate::new(Default::default());
20        initialize_slots(&mut isolate);
21        MiniV8 { interface: Interface::new(isolate) }
22    }
23
24    /// Returns the global JavaScript object.
25    pub fn global(&self) -> Object {
26        self.scope(|scope| {
27            let global = scope.get_current_context().global(scope);
28            Object {
29                mv8: self.clone(),
30                handle: v8::Global::new(scope, global),
31            }
32        })
33    }
34
35    /// Executes a JavaScript script and returns its result.
36    pub fn eval<S, R>(&self, script: S) -> Result<R>
37    where
38        S: Into<Script>,
39        R: FromValue,
40    {
41        let script = script.into();
42        let isolate_handle = self.interface.isolate_handle();
43        match (self.interface.len() == 1, script.timeout) {
44            (true, Some(timeout)) => {
45                execute_with_timeout(
46                    timeout,
47                    || self.eval_inner(script),
48                    move || { isolate_handle.terminate_execution(); },
49                )?.into(self)
50            },
51            (false, Some(_)) => Err(Error::InvalidTimeout),
52            (_, None) => self.eval_inner(script)?.into(self),
53        }
54    }
55
56    fn eval_inner(&self, script: Script) -> Result<Value> {
57        self.try_catch(|scope| {
58            let source = create_string(scope, &script.source);
59            let origin = script.origin.map(|o| {
60                let name = create_string(scope, &o.name).into();
61                let source_map_url = create_string(scope, "").into();
62                v8::ScriptOrigin::new(
63                    scope,
64                    name,
65                    o.line_offset,
66                    o.column_offset,
67                    false,
68                    0,
69                    source_map_url,
70                    true,
71                    false,
72                    false,
73                )
74            });
75            let script = v8::Script::compile(scope, source, origin.as_ref());
76            self.exception(scope)?;
77            let result = script.unwrap().run(scope);
78            self.exception(scope)?;
79            Ok(Value::from_v8_value(self, scope, result.unwrap()))
80        })
81    }
82
83    /// Inserts any sort of keyed value of type `T` into the `MiniV8`, typically for later retrieval
84    /// from within Rust functions called from within JavaScript. If a value already exists with the
85    /// key, it is returned.
86    pub fn set_user_data<K, T>(&self, key: K, data: T) -> Option<Box<dyn Any>>
87    where
88        K: ToString,
89        T: Any,
90    {
91        self.interface.use_slot(|m: &AnyMap| m.0.borrow_mut().insert(key.to_string(), Box::new(data)))
92    }
93
94    /// Calls a function with a user data value by its key, or `None` if no value exists with the
95    /// key. If a value exists but it is not of the type `T`, `None` is returned. This is typically
96    /// used by a Rust function called from within JavaScript.
97    pub fn use_user_data<F, T: Any, U>(&self, key: &str, func: F) -> U
98    where
99        F: FnOnce(Option<&T>) -> U + 'static,
100    {
101        self.interface.use_slot(|m: &AnyMap| {
102            func(m.0.borrow().get(key).and_then(|d| d.downcast_ref::<T>()))
103        })
104    }
105
106    /// Removes and returns a user data value by its key. Returns `None` if no value exists with the
107    /// key.
108    pub fn remove_user_data(&self, key: &str) -> Option<Box<dyn Any>> {
109        self.interface.use_slot(|m: &AnyMap| m.0.borrow_mut().remove(key))
110    }
111
112    /// Creates and returns a string managed by V8.
113    ///
114    /// # Panics
115    ///
116    /// Panics if source value is longer than `(1 << 28) - 16` bytes.
117    pub fn create_string(&self, value: &str) -> String {
118        self.scope(|scope| {
119            let string = create_string(scope, value);
120            String {
121                mv8: self.clone(),
122                handle: v8::Global::new(scope, string),
123            }
124        })
125    }
126
127    /// Creates and returns an empty `Array` managed by V8.
128    pub fn create_array(&self) -> Array {
129        self.scope(|scope| {
130            let array = v8::Array::new(scope, 0);
131            Array {
132                mv8: self.clone(),
133                handle: v8::Global::new(scope, array),
134            }
135        })
136    }
137
138    /// Creates and returns an empty `Object` managed by V8.
139    pub fn create_object(&self) -> Object {
140        self.scope(|scope| {
141            let object = v8::Object::new(scope);
142            Object {
143                mv8: self.clone(),
144                handle: v8::Global::new(scope, object),
145            }
146        })
147    }
148
149    /// Creates and returns an `Object` managed by V8 filled with the keys and values from an
150    /// iterator. Keys are coerced to object properties.
151    ///
152    /// This is a thin wrapper around `MiniV8::create_object` and `Object::set`. See `Object::set`
153    /// for how this method might return an error.
154    pub fn create_object_from<K, V, I>(&self, iter: I) -> Result<Object>
155    where
156        K: ToValue,
157        V: ToValue,
158        I: IntoIterator<Item = (K, V)>,
159    {
160        let object = self.create_object();
161        for (k, v) in iter {
162            object.set(k, v)?;
163        }
164        Ok(object)
165    }
166
167    /// Wraps a Rust function or closure, creating a callable JavaScript function handle to it.
168    ///
169    /// The function's return value is always a `Result`: If the function returns `Err`, the error
170    /// is raised as a JavaScript exception, which can be caught within JavaScript or bubbled up
171    /// back into Rust by not catching it. This allows using the `?` operator to propagate errors
172    /// through intermediate JavaScript code.
173    ///
174    /// If the function returns `Ok`, the contained value will be converted to a JavaScript value.
175    /// For details on Rust-to-JavaScript conversions, refer to the `ToValue` and `ToValues` traits.
176    ///
177    /// If the provided function panics, the executable will be aborted.
178    pub fn create_function<F, R>(&self, func: F) -> Function
179    where
180        F: Fn(Invocation) -> Result<R> + 'static,
181        R: ToValue,
182    {
183        let func = move |mv8: &MiniV8, this: Value, args: Values| {
184            func(Invocation { mv8: mv8.clone(), this, args })?.to_value(mv8)
185        };
186
187        self.scope(|scope| {
188            let callback = Box::new(func);
189            let callback_info = CallbackInfo { mv8: self.clone(), callback };
190            let ptr = Box::into_raw(Box::new(callback_info));
191            let ext = v8::External::new(scope, ptr as _);
192
193            let v8_func = |
194                scope: &mut v8::HandleScope,
195                fca: v8::FunctionCallbackArguments,
196                mut rv: v8::ReturnValue,
197            | {
198                let data = fca.data();
199                let ext = v8::Local::<v8::External>::try_from(data).unwrap();
200                let callback_info_ptr = ext.value() as *mut CallbackInfo;
201                let callback_info = unsafe { &mut *callback_info_ptr };
202                let CallbackInfo { mv8, callback } = callback_info;
203                let ptr = scope as *mut v8::HandleScope;
204                // We can erase the lifetime of the `v8::HandleScope` safely because it only lives
205                // on the interface stack during the current block:
206                let ptr: *mut v8::HandleScope<'static> = unsafe { std::mem::transmute(ptr) };
207                mv8.interface.push(ptr);
208                let this = Value::from_v8_value(&mv8, scope, fca.this().into());
209                let len = fca.length();
210                let mut args = Vec::with_capacity(len as usize);
211                for i in 0..len {
212                    args.push(Value::from_v8_value(&mv8, scope, fca.get(i)));
213                }
214                match callback(&mv8, this, Values::from_vec(args)) {
215                    Ok(v) => {
216                        rv.set(v.to_v8_value(scope));
217                    },
218                    Err(e) => {
219                        let exception = e.to_value(&mv8).to_v8_value(scope);
220                        scope.throw_exception(exception);
221                    },
222                };
223                mv8.interface.pop();
224            };
225
226            let value = v8::Function::builder(v8_func).data(ext.into()).build(scope).unwrap();
227            // TODO: `v8::Isolate::adjust_amount_of_external_allocated_memory` should be called
228            // appropriately with the following external resource size calculation. This cannot be
229            // done as of now, since `v8::Weak::with_guaranteed_finalizer` does not provide a
230            // `v8::Isolate` to the finalizer callback, and so the downward adjustment cannot be
231            // made.
232            //
233            // let func_size = mem::size_of_val(&func); let ext_size = func_size +
234            // mem::size_of::<CallbackInfo>;
235            let drop_ext = Box::new(move || drop(unsafe { Box::from_raw(ptr) }));
236            add_finalizer(scope, value, drop_ext);
237            Function {
238                mv8: self.clone(),
239                handle: v8::Global::new(scope, value),
240            }
241        })
242    }
243
244    /// Wraps a mutable Rust closure, creating a callable JavaScript function handle to it.
245    ///
246    /// This is a version of `create_function` that accepts a FnMut argument. Refer to
247    /// `create_function` for more information about the implementation.
248    pub fn create_function_mut<F, R>(&self, func: F) -> Function
249    where
250        F: FnMut(Invocation) -> Result<R> + 'static,
251        R: ToValue,
252    {
253        let func = RefCell::new(func);
254        self.create_function(move |invocation| {
255            (&mut *func.try_borrow_mut().map_err(|_| Error::RecursiveMutCallback)?)(invocation)
256        })
257    }
258
259    // Opens a new handle scope in the global context. Nesting calls to this or `MiniV8::try_catch`
260    // will cause a panic (unless a callback is entered, see `MiniV8::create_function`).
261    pub(crate) fn scope<F, T>(&self, func: F) -> T
262    where
263        F: FnOnce(&mut v8::ContextScope<v8::HandleScope>) -> T,
264    {
265        self.interface.scope(func)
266    }
267
268    // Opens a new try-catch scope in the global context. Nesting calls to this or `MiniV8::scope`
269    // will cause a panic (unless a callback is entered, see `MiniV8::create_function`).
270    pub(crate) fn try_catch<F, T>(&self, func: F) -> T
271    where
272        F: FnOnce(&mut v8::TryCatch<v8::HandleScope>) -> T,
273    {
274        self.interface.try_catch(func)
275    }
276
277    pub(crate) fn exception(&self, scope: &mut v8::TryCatch<v8::HandleScope>) -> Result<()> {
278        if scope.has_terminated() {
279            Err(Error::Timeout)
280        } else if let Some(exception) = scope.exception() {
281            Err(Error::Value(Value::from_v8_value(self, scope, exception)))
282        } else {
283            Ok(())
284        }
285    }
286}
287
288#[derive(Clone)]
289struct Interface(Rc<RefCell<Vec<Rc<RefCell<InterfaceEntry>>>>>);
290
291impl Interface {
292    fn len(&self) -> usize {
293        self.0.borrow().len()
294    }
295
296    fn isolate_handle(&self) -> v8::IsolateHandle {
297        self.top(|entry| entry.isolate_handle())
298    }
299
300    // Opens a new handle scope in the global context.
301    fn scope<F, T>(&self, func: F) -> T
302    where
303        F: FnOnce(&mut v8::ContextScope<v8::HandleScope>) -> T,
304    {
305        self.top(|entry| entry.scope(func))
306    }
307
308    // Opens a new try-catch scope in the global context.
309    fn try_catch<F, T>(&self, func: F) -> T
310    where
311        F: FnOnce(&mut v8::TryCatch<v8::HandleScope>) -> T,
312    {
313        self.scope(|scope| func(&mut v8::TryCatch::new(scope)))
314    }
315
316    fn new(isolate: v8::OwnedIsolate) -> Interface {
317        Interface(Rc::new(RefCell::new(vec![Rc::new(RefCell::new(InterfaceEntry::Isolate(isolate)))])))
318    }
319
320    fn push(&self, handle_scope: *mut v8::HandleScope<'static>) {
321        self.0.borrow_mut().push(Rc::new(RefCell::new(InterfaceEntry::HandleScope(handle_scope))));
322    }
323
324    fn pop(&self) {
325        self.0.borrow_mut().pop();
326    }
327
328    fn use_slot<F, T: 'static, U>(&self, func: F) -> U
329    where
330        F: FnOnce(&T) -> U,
331    {
332        self.top(|entry| func(entry.get_slot()))
333    }
334
335    fn top<F, T>(&self, func: F) -> T
336    where
337        F: FnOnce(&mut InterfaceEntry) -> T,
338    {
339        let top = self.0.borrow().last().unwrap().clone();
340        let mut top_mut = top.borrow_mut();
341        func(&mut top_mut)
342    }
343}
344
345enum InterfaceEntry {
346    Isolate(v8::OwnedIsolate),
347    HandleScope(*mut v8::HandleScope<'static>),
348}
349
350impl InterfaceEntry {
351    fn scope<F, T>(&mut self, func: F) -> T
352    where
353        F: FnOnce(&mut v8::ContextScope<v8::HandleScope>) -> T,
354    {
355        match self {
356            InterfaceEntry::Isolate(isolate) => {
357                let global_context = isolate.get_slot::<Global>().unwrap().context.clone();
358                let scope = &mut v8::HandleScope::new(isolate);
359                let context = v8::Local::new(scope, global_context);
360                let scope = &mut v8::ContextScope::new(scope, context);
361                func(scope)
362            },
363            InterfaceEntry::HandleScope(ref ptr) => {
364                let scope: &mut v8::HandleScope = unsafe { &mut **ptr };
365                let scope = &mut v8::ContextScope::new(scope, scope.get_current_context());
366                func(scope)
367            },
368        }
369    }
370
371    fn get_slot<T: 'static>(&self) -> &T {
372        match self {
373            InterfaceEntry::Isolate(isolate) => isolate.get_slot::<T>().unwrap(),
374            InterfaceEntry::HandleScope(ref ptr) => {
375                let scope: &mut v8::HandleScope = unsafe { &mut **ptr };
376                scope.get_slot::<T>().unwrap()
377            },
378        }
379    }
380
381    fn isolate_handle(&self) -> v8::IsolateHandle {
382        match self {
383            InterfaceEntry::Isolate(isolate) => isolate.thread_safe_handle(),
384            InterfaceEntry::HandleScope(ref ptr) => {
385                let scope: &mut v8::HandleScope = unsafe { &mut **ptr };
386                scope.thread_safe_handle()
387            },
388        }
389    }
390}
391
392struct Global {
393    context: v8::Global<v8::Context>,
394}
395
396static INIT: Once = Once::new();
397
398fn initialize_v8() {
399    INIT.call_once(|| {
400        let platform = v8::new_default_platform(0, false).make_shared();
401        v8::V8::initialize_platform(platform);
402        v8::V8::initialize();
403    });
404}
405
406fn initialize_slots(isolate: &mut v8::Isolate) {
407    let scope = &mut v8::HandleScope::new(isolate);
408    let context = v8::Context::new(scope);
409    let scope = &mut v8::ContextScope::new(scope, context);
410    let global_context = v8::Global::new(scope, context);
411    scope.set_slot(Global { context: global_context });
412    scope.set_slot(AnyMap(Rc::new(RefCell::new(BTreeMap::new()))));
413}
414
415fn create_string<'s>(scope: &mut v8::HandleScope<'s>, value: &str) -> v8::Local<'s, v8::String> {
416    v8::String::new(scope, value).expect("string exceeds maximum length")
417}
418
419fn add_finalizer<T: 'static>(
420    isolate: &mut v8::Isolate,
421    handle: impl v8::Handle<Data = T>,
422    finalizer: impl FnOnce() + 'static,
423) {
424    let rc = Rc::new(RefCell::new(None));
425    let weak = v8::Weak::with_guaranteed_finalizer(isolate, handle, Box::new({
426        let rc = rc.clone();
427        move || {
428            let weak = rc.replace(None).unwrap();
429            finalizer();
430            drop(weak);
431        }
432    }));
433    rc.replace(Some(weak));
434}
435
436type Callback = Box<dyn Fn(&MiniV8, Value, Values) -> Result<Value>>;
437
438struct CallbackInfo {
439    mv8: MiniV8,
440    callback: Callback,
441}
442
443struct AnyMap(Rc<RefCell<BTreeMap<StdString, Box<dyn Any>>>>);
444
445// A JavaScript script.
446#[derive(Clone, Debug, Default)]
447pub struct Script {
448    /// The source of the script.
449    pub source: StdString,
450    /// The maximum runtime duration of the script's execution. This cannot be set within a nested
451    /// evaluation, i.e. it cannot be set when calling `MiniV8::eval` from within a `Function`
452    /// created with `MiniV8::create_function` or `MiniV8::create_function_mut`.
453    ///
454    /// V8 can only cancel script evaluation while running actual JavaScript code. If Rust code is
455    /// being executed when the timeout is triggered, the execution will continue until the
456    /// evaluation has returned to running JavaScript code.
457    pub timeout: Option<Duration>,
458    /// The script's origin.
459    pub origin: Option<ScriptOrigin>,
460}
461
462/// The origin, within a file, of a JavaScript script.
463#[derive(Clone, Debug, Default)]
464pub struct ScriptOrigin {
465    /// The name of the file this script belongs to.
466    pub name: StdString,
467    /// The line at which this script starts.
468    pub line_offset: i32,
469    /// The column at which this script starts.
470    pub column_offset: i32,
471}
472
473impl From<StdString> for Script {
474    fn from(source: StdString) -> Script {
475        Script { source, ..Default::default() }
476    }
477}
478
479impl<'a> From<&'a str> for Script {
480    fn from(source: &'a str) -> Script {
481        source.to_string().into()
482    }
483}
484
485fn execute_with_timeout<T>(
486    timeout: Duration,
487    execute_fn: impl FnOnce() -> T,
488    timed_out_fn: impl FnOnce() + Send + 'static,
489) -> T {
490    let wait = Arc::new((Mutex::new(true), Condvar::new()));
491    let timer_wait = wait.clone();
492    thread::spawn(move || {
493        let (mutex, condvar) = &*timer_wait;
494        let timer = condvar.wait_timeout_while(
495            mutex.lock().unwrap(),
496            timeout,
497            |&mut is_executing| is_executing,
498        ).unwrap();
499        if timer.1.timed_out() {
500            timed_out_fn();
501        }
502    });
503
504    let result = execute_fn();
505    let (mutex, condvar) = &*wait;
506    *mutex.lock().unwrap() = false;
507    condvar.notify_one();
508    result
509}