kg_js/
engine.rs

1use std::ffi::CStr;
2use std::ops::{Deref, DerefMut};
3use std::os::raw::c_void;
4use std::pin::Pin;
5use once_cell::sync::Lazy;
6use smallbox::{SmallBox, smallbox};
7use smallbox::space::S8;
8use crate::bindings::{alloc_func, duk_api_git_branch, duk_api_git_commit, duk_api_git_describe, duk_api_version, duk_create_heap, duk_destroy_heap, fatal_handler, free_func, realloc_func};
9use crate::ctx::{DukContext};
10use crate::{NoopInterop, JsInterop, JsError};
11
12// using SmallBox with trait pointer to avoid generics in JsEngine definition
13pub (crate) type InteropRef = SmallBox<dyn JsInterop, S8>;
14
15#[derive(Debug)]
16pub (crate) struct Userdata {
17    pub (crate) interop: InteropRef,
18}
19
20#[derive(Debug)]
21pub struct JsEngine {
22    ctx: DukContext,
23    inner: Pin<Box<Userdata>>,
24}
25
26
27/// SAFETY: JsEngine is Send and Sync since it owns Duktape heap.
28/// A Duktape heap can only be accessed by one native thread at a time [(thread-safety)](https://github.com/svaarala/duktape/blob/master/doc/threading.rst#only-one-active-native-thread-at-a-time-per-duktape-heap)
29/// Rust ownership system ensures that JsEngine is not shared between threads without synchronization.
30unsafe impl Send for JsEngine {}
31unsafe impl Sync for JsEngine {}
32
33impl JsEngine {
34    pub fn new() -> Result<Self, JsError> {
35        Self::with_interop(NoopInterop)
36    }
37
38    pub fn with_interop<I: JsInterop>(interop: I) -> Result<Self, JsError> {
39        let userdata = Box::pin(Userdata {
40            interop: smallbox!(interop),
41        });
42        let udata = &(*userdata.as_ref()) as *const Userdata;
43
44        let ctx = unsafe {
45            duk_create_heap(
46                Some(alloc_func),
47                Some(realloc_func),
48                Some(free_func),
49                udata as *mut c_void,
50                Some(fatal_handler))
51        };
52
53        if ctx.is_null() {
54            return Err(JsError::from("Could not create duktape context".to_string()));
55        }
56
57        let e = JsEngine {
58            ctx: unsafe { DukContext::from_raw(ctx) },
59            inner: userdata,
60        };
61
62        Ok(e)
63    }
64
65    pub fn version() -> u32 {
66        static DUK_VERSION: Lazy<u32> = Lazy::new(|| {
67            unsafe { duk_api_version() }
68        });
69        *DUK_VERSION
70    }
71
72    pub fn version_info() -> &'static str {
73        static DUK_VERSION_INFO: Lazy<String> = Lazy::new(|| {
74            unsafe {
75                format!(
76                    "{} ({}/{})",
77                    CStr::from_ptr(duk_api_git_describe()).to_str().unwrap(),
78                    CStr::from_ptr(duk_api_git_branch()).to_str().unwrap(),
79                    &(CStr::from_ptr(duk_api_git_commit()).to_str().unwrap())[0..9])
80            }
81        });
82        &*DUK_VERSION_INFO
83    }
84
85    pub fn interop(&self) -> Pin<&dyn JsInterop> {
86        unsafe { self.inner.as_ref().map_unchecked(|r| &*((*r).interop)) }
87    }
88
89    pub fn interop_as<I: JsInterop>(&self) -> Pin<&I> {
90        unsafe { self.interop().map_unchecked(|r| r.downcast_ref::<I>().unwrap()) }
91    }
92
93    pub fn interop_mut(&mut self) -> Pin<&mut dyn JsInterop> {
94        unsafe { self.inner.as_mut().map_unchecked_mut(|r| &mut *((*r).interop)) }
95    }
96
97    pub fn interop_as_mut<I: JsInterop>(&mut self) -> Pin<&mut I> {
98        unsafe { self.interop_mut().map_unchecked_mut(|r| r.downcast_mut::<I>().unwrap()) }
99    }
100
101    pub fn ctx(&mut self) -> &mut DukContext {
102        &mut self.ctx
103    }
104}
105
106impl Drop for JsEngine {
107    fn drop(&mut self) {
108        if !self.ctx.ctx.is_null() {
109            unsafe { duk_destroy_heap(self.ctx.ctx); }
110            self.ctx.ctx = std::ptr::null_mut();
111        }
112    }
113}
114
115impl Deref for JsEngine {
116    type Target = DukContext;
117
118    fn deref(&self) -> &Self::Target {
119        &self.ctx
120    }
121}
122
123impl DerefMut for JsEngine {
124    fn deref_mut(&mut self) -> &mut Self::Target {
125        &mut self.ctx
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use crate::JsEngine;
132
133    #[test]
134    fn test_trait_bounds() {
135        fn is_send_sync<T: Send + Sync>() {}
136        is_send_sync::<JsEngine>();
137    }
138
139    #[test]
140    fn test_version() {
141        let version = JsEngine::version();
142        assert_eq!(version, 20700);
143    }
144
145    #[test]
146    fn test_version_info() {
147        let version_info = JsEngine::version_info();
148        assert_eq!(version_info, "03d4d72-dirty (HEAD/03d4d728f)");
149    }
150
151    #[test]
152    fn test_move_to_other_thread() {
153        let mut engine = JsEngine::new().unwrap();
154        engine.push_string("Hello, World!");
155        engine = std::thread::spawn(move || {
156            assert_eq!(engine.get_string(-1), "Hello, World!");
157            engine.push_string("Hello, Again!");
158            assert!(engine.get_stack_dump().contains("ctx: top=2"));
159            engine
160        }).join().unwrap();
161
162        assert_eq!(engine.get_string(-1), "Hello, Again!");
163        assert_eq!(engine.get_string(-2), "Hello, World!");
164
165        assert!(engine.get_stack_dump().contains("ctx: top=2"));
166        engine.pop_n(2);
167        assert!(engine.get_stack_dump().contains("ctx: top=0"));
168    }
169}