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
12pub (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
27unsafe 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}