kg_js/
interop.rs

1use crate::{ConsoleFunc, DukContext, JsError, Return};
2use log::log;
3use std::any::TypeId;
4
5pub trait JsInterop: std::any::Any + std::fmt::Debug + 'static {
6    fn call(&mut self, engine: &mut DukContext, func_name: &str) -> Result<Return, JsError>;
7
8    unsafe fn alloc(&mut self, size: usize) -> *mut u8 {
9        super::alloc::alloc(size)
10    }
11
12    unsafe fn realloc(&mut self, ptr: *mut u8, size: usize) -> *mut u8 {
13        super::alloc::realloc(ptr, size)
14    }
15
16    unsafe fn free(&mut self, ptr: *mut u8) {
17        super::alloc::free(ptr)
18    }
19
20    fn fatal(&mut self, msg: &str) -> ! {
21        panic!("Duktape fatal error: {}", msg);
22    }
23
24    fn console(&mut self, func: ConsoleFunc, msg: &str) {
25        log!(func.level(), "JS: {}", msg);
26    }
27}
28
29impl dyn JsInterop {
30    pub fn downcast_ref<T: JsInterop>(&self) -> Option<&T> {
31        if self.type_id() == TypeId::of::<T>() {
32            unsafe { Some(&*(self as *const dyn JsInterop as *const T)) }
33        } else {
34            None
35        }
36    }
37
38    pub fn downcast_mut<T: JsInterop>(&mut self) -> Option<&mut T> {
39        let t = (self as &dyn JsInterop).type_id();
40        if t == TypeId::of::<T>() {
41            unsafe { Some(&mut *(self as *mut dyn JsInterop as *mut T)) }
42        } else {
43            None
44        }
45    }
46}
47
48#[derive(Debug)]
49pub struct NoopInterop;
50
51impl JsInterop for NoopInterop {
52    fn call(&mut self, _ctx: &mut DukContext, _func_name: &str) -> Result<Return, JsError> {
53        Ok(Return::Undefined)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use std::sync::Arc;
60    use crate::JsEngine;
61    use super::*;
62
63    #[derive(Debug)]
64    struct Interop {
65        stdout: String,
66        number: f64,
67        pub tracker: Arc<std::sync::Mutex<alloc_tracker::AllocTracker>>,
68    }
69
70    impl Interop {
71        pub fn new() -> Self {
72            Self {
73                stdout: String::new(),
74                number: 0.,
75                tracker: Arc::new(std::sync::Mutex::new(alloc_tracker::AllocTracker::new())),
76            }
77        }
78    }
79
80    impl JsInterop for Interop {
81        fn call(&mut self, ctx: &mut DukContext, func_name: &str) -> Result<Return, JsError> {
82            match func_name {
83                "add" => {
84                    let a = ctx.get_number(0);
85                    let b = ctx.get_number(1);
86                    let res = a + b;
87                    ctx.push_number(res);
88                    Ok(Return::Top)
89                }
90                "sub" => {
91                    let a = ctx.get_number(0);
92                    let b = ctx.get_number(1);
93                    let res = a - b;
94                    ctx.push_number(res);
95                    Ok(Return::Top)
96                }
97                "put_number" => {
98                    let n = ctx.get_number(0);
99                    self.number = n;
100                    Ok(Return::Undefined)
101                }
102                "get_number" => {
103                    ctx.push_number(self.number);
104                    Ok(Return::Top)
105                }
106                _ => unreachable!(),
107            }
108        }
109
110
111        #[cfg(test)]
112        unsafe fn alloc(&mut self, size: usize) -> *mut u8 {
113            let ptr = crate::alloc::alloc(size);
114            self.tracker.lock().unwrap().alloc(ptr, size);
115            ptr
116        }
117
118
119        #[cfg(test)]
120        unsafe fn realloc(&mut self, ptr: *mut u8, size: usize) -> *mut u8 {
121            let new_ptr = crate::alloc::realloc(ptr, size);
122            self.tracker.lock().unwrap().realloc(ptr, new_ptr, size);
123            new_ptr
124        }
125
126        #[cfg(test)]
127        unsafe fn free(&mut self, ptr: *mut u8) {
128            self.tracker.lock().unwrap().free(ptr);
129            crate::alloc::free(ptr)
130        }
131
132
133        fn console(&mut self, _func: ConsoleFunc, msg: &str) {
134            self.stdout.push_str(msg);
135            self.stdout.push('\n');
136        }
137    }
138
139    fn init() -> JsEngine {
140        let e = JsEngine::with_interop(Interop::new()).unwrap();
141        e.init_console();
142        e.put_global_function("add", 2);
143        e.put_global_function("sub", 2);
144        e.put_global_function("put_number", 1);
145        e.put_global_function("get_number", 0);
146        e
147    }
148
149    #[test]
150    fn call_rust_function() {
151        let e = init();
152
153        e.eval("var a = add(10, 11); put_number(a);").unwrap();
154        assert_eq!(21f64, e.interop_as::<Interop>().number);
155
156        e.eval("var b = sub(12, 10); put_number(b);").unwrap();
157        assert_eq!(2f64, e.interop_as::<Interop>().number);
158
159        e.eval("put_number(123.5); console.log(get_number());")
160            .unwrap();
161        assert_eq!("123.5\n", e.interop_as::<Interop>().stdout);
162    }
163
164    #[test]
165    fn test_changed_duk_context() {
166        let e = init();
167
168        //language=javascript
169        e.eval("typeof add === 'function'").unwrap();
170        assert!(e.get_boolean(-1));
171        e.pop();
172
173        let new_idx = e.push_thread_new_globalenv();
174        let ctx = e.get_context(new_idx).unwrap();
175
176        // This function should not be available in the new context
177        //language=javascript
178        ctx.eval("typeof add === 'undefined'").unwrap();
179
180        // Register the function in the new context
181        ctx.put_global_function("add", 2);
182
183        // Check if the function is available in the new context
184        //language=javascript
185        ctx.eval("add(2, 3)").unwrap();
186        assert_eq!(5.0, ctx.get_number(-1));
187
188        // Create function in the new context
189        //language=javascript
190        ctx.eval("var f = function (a, b) { return a * b; }; f").unwrap();
191        ctx.put_global_string("multiply");
192
193        // Check if the function is available in the new context
194        //language=javascript
195        ctx.eval("multiply(2, 5)").unwrap();
196        assert_eq!(10.0, ctx.get_number(-1));
197
198        // Drop context and remove it from the stack
199        drop(ctx);
200        e.pop();
201
202        // Check if the function is still available in the original context
203        //language=javascript
204        e.eval("add(2, 3)").unwrap();
205        assert_eq!(5.0, e.get_number(-1));
206        e.pop();
207
208        // Check if multiply function is not available in the original context
209        //language=javascript
210        e.eval("typeof multiply === 'undefined'").unwrap();
211        assert!(e.get_boolean(-1));
212        e.pop();
213
214        assert!(e.get_stack_dump().contains("ctx: top=0"));
215    }
216
217
218    #[test]
219    fn test_eval_allocations() {
220        let engine = init();
221        let tracker = engine.interop_as::<Interop>().tracker.clone();
222        assert_eq!(tracker.lock().unwrap().total_bytes(), 102591);
223
224        //language=javascript
225        engine.eval(r#"100 + 2"#).unwrap();
226        assert_eq!(engine.get_number(-1), 102.);
227        assert_eq!(tracker.lock().unwrap().total_bytes(), 102591);
228
229        engine.gc();
230        assert_eq!(tracker.lock().unwrap().total_bytes(), 101686);
231
232        drop(engine);
233
234        assert_eq!(tracker.lock().unwrap().total_bytes(), 0);
235    }
236
237
238    pub mod alloc_tracker {
239        use std::collections::HashMap;
240
241        #[derive(Debug)]
242        pub struct AllocTracker {
243            allocs: HashMap<usize, usize>,
244            alloc_count: u64,
245        }
246
247        impl AllocTracker {
248            pub fn new() -> Self {
249                Self {
250                    allocs: HashMap::new(),
251                    alloc_count: 0,
252                }
253            }
254
255            pub fn alloc(&mut self, ptr: *mut u8, size: usize) {
256                self.alloc_count += 1;
257                self.allocs.insert(ptr as usize, size);
258            }
259
260            pub fn realloc(&mut self, old_ptr: *mut u8, new_ptr: *mut u8, size: usize) {
261                self.alloc_count += 1;
262                self.allocs.remove(&(old_ptr as usize));
263                self.allocs.insert(new_ptr as usize, size);
264            }
265
266            pub fn free(&mut self, ptr: *mut u8) {
267                self.allocs.remove(&(ptr as usize));
268            }
269
270            pub fn total_bytes(&self) -> usize {
271                self.allocs.values().sum()
272            }
273
274            pub fn alloc_count(&self) -> u64 {
275                self.alloc_count
276            }
277        }
278    }
279}