kg_js/
ctx.rs

1use std::ops::DerefMut;
2use super::*;
3
4macro_rules! try_exec_success {
5    ($res:expr) => {
6        if $res != DUK_EXEC_SUCCESS {
7            return Err($res)
8        }
9    }
10}
11
12/// Wrapper for Duktape context
13#[derive(Debug)]
14pub struct DukContext {
15    pub (crate) ctx: *mut duk_context,
16}
17
18impl DukContext {
19    pub (crate) unsafe fn from_raw(ctx: *mut duk_context) -> Self {
20        Self { ctx }
21    }
22
23    #[inline]
24    pub fn normalize_index(&self, index: i32) -> i32 {
25        unsafe {
26            duk_normalize_index(self.ctx, index)
27        }
28    }
29
30    #[inline]
31    pub fn get_top(&self) -> i32 {
32        unsafe { duk_get_top(self.ctx) }
33    }
34
35    #[inline]
36    pub fn dup(&self, index: i32) {
37        unsafe {
38            duk_dup(self.ctx, index);
39        }
40    }
41
42    #[inline]
43    pub fn remove(&self, index: i32) {
44        unsafe {
45            duk_remove(self.ctx, index);
46        }
47    }
48
49    #[inline]
50    pub fn pop(&self) {
51        unsafe {
52            duk_pop(self.ctx);
53        }
54    }
55
56    #[inline]
57    pub fn pop_n(&self, n: i32) {
58        unsafe {
59            duk_pop_n(self.ctx, n);
60        }
61    }
62
63    #[inline]
64    pub fn swap(&self, idx1: i32, idx2: i32) {
65        unsafe {
66            duk_swap(self.ctx, idx1, idx2);
67        }
68    }
69
70    #[inline]
71    pub fn push_this(&self) {
72        unsafe { duk_push_this(self.ctx); }
73    }
74
75    #[inline]
76    pub fn push_thread(&self) -> i32 {
77        unsafe { duk_push_thread_raw(self.ctx, 0) }
78    }
79
80    #[inline]
81    pub fn push_thread_new_globalenv(&self) -> i32 {
82        unsafe { duk_push_thread_raw(self.ctx, DukThreadFlags::DUK_THREAD_NEW_GLOBAL_ENV.bits()) }
83    }
84
85    #[inline]
86    pub fn push_global_object(&self) {
87        unsafe { duk_push_global_object(self.ctx); }
88    }
89
90    #[inline]
91    pub fn push_boolean(&self, value: bool) {
92        unsafe { duk_push_boolean(self.ctx, value as i32) }
93    }
94
95    #[inline]
96    pub fn push_null(&self) {
97        unsafe { duk_push_null(self.ctx) }
98    }
99
100    #[inline]
101    pub fn push_undefined(&self) {
102        unsafe { duk_push_undefined(self.ctx) }
103    }
104
105    #[inline]
106    pub fn push_i32(&self, value: i32) {
107        unsafe { duk_push_int(self.ctx, value) }
108    }
109
110    #[inline]
111    pub fn push_u32(&self, value: u32) {
112        unsafe { duk_push_uint(self.ctx, value) }
113    }
114
115    #[inline]
116    pub fn push_number(&self, value: f64) {
117        unsafe { duk_push_number(self.ctx, value) }
118    }
119
120    #[inline]
121    pub fn push_string(&self, value: &str) {
122        unsafe {
123            duk_push_lstring(self.ctx, value.as_ptr() as *const c_char, value.len());
124        }
125    }
126
127    #[inline]
128    pub fn push_object(&self) -> i32 {
129        unsafe { duk_push_object(self.ctx) }
130    }
131
132    #[inline]
133    pub fn push_ext_buffer(&self, data: &[u8]) {
134        unsafe {
135            duk_push_buffer_raw(self.ctx, 0, (DukBufFlags::DUK_BUF_FLAG_DYNAMIC | DukBufFlags::DUK_BUF_FLAG_EXTERNAL).bits());
136            duk_config_buffer(self.ctx, -1, data.as_ptr() as *mut c_void, data.len());
137        }
138    }
139
140    #[inline]
141    pub fn push_array(&self) -> i32 {
142        unsafe { duk_push_array(self.ctx) }
143    }
144
145    pub fn push_function(&self, func_name: &str, nargs: i32) {
146        unsafe {
147            duk_push_c_function(self.ctx, Some(func_dispatch), nargs);
148            duk_push_lstring(self.ctx, FUNC_NAME_PROP.as_ptr() as *const c_char, FUNC_NAME_PROP.len());
149            duk_push_lstring(self.ctx, func_name.as_ptr() as *const c_char, func_name.len());
150            duk_def_prop(self.ctx, -3, (DukDefpropFlags::DUK_DEFPROP_ENUMERABLE | DukDefpropFlags::DUK_DEFPROP_HAVE_VALUE).bits())
151        }
152    }
153
154    pub fn put_prop_function(&self, obj_index: i32, func_name: &str, nargs: i32) {
155        let obj_index = self.normalize_index(obj_index);
156        self.push_function(func_name, nargs);
157        unsafe {
158            duk_put_prop_lstring(self.ctx, obj_index, func_name.as_ptr() as *const c_char, func_name.len());
159        }
160    }
161
162    pub fn put_global_function(&self, func_name: &str, nargs: i32) {
163        self.push_function(func_name, nargs);
164        self.put_global_string(func_name);
165    }
166
167    #[inline]
168    pub fn get_type(&self, index: i32) -> DukType {
169        DukType::from(unsafe { duk_get_type(self.ctx, index) })
170    }
171
172    #[inline]
173    pub fn is_string(&self, index: i32) -> bool {
174        unsafe { duk_is_string(self.ctx, index) == 1 }
175    }
176
177    #[inline]
178    pub fn is_number(&self, index: i32) -> bool {
179        unsafe { duk_is_number(self.ctx, index) == 1 }
180    }
181
182    #[inline]
183    pub fn is_object(&self, index: i32) -> bool {
184        unsafe { duk_is_object(self.ctx, index) == 1 }
185    }
186
187    #[inline]
188    pub fn is_array(&self, index: i32) -> bool {
189        unsafe { duk_is_array(self.ctx, index) == 1 }
190    }
191
192    #[inline]
193    pub fn is_pure_object(&self, index: i32) -> bool {
194        unsafe {
195            duk_is_object(self.ctx, index) == 1
196                && duk_is_array(self.ctx, index) == 0
197                && duk_is_function(self.ctx, index) == 0
198                && duk_is_thread(self.ctx, index) == 0
199        }
200    }
201
202    #[inline]
203    pub fn get_string(&self, index: i32) -> &str {
204        use std::str;
205        use std::slice;
206        unsafe {
207            let mut len: usize = 0;
208            let ptr = duk_get_lstring(self.ctx, index, Some(&mut len)) as *const u8;
209            str::from_utf8_unchecked(slice::from_raw_parts(ptr, len))
210        }
211    }
212
213    #[inline]
214    pub fn get_buffer(&self, index: i32) -> &[u8] {
215        use std::slice;
216        unsafe {
217            let mut len: usize = 0;
218            let ptr = duk_get_buffer(self.ctx, index, Some(&mut len)) as *const u8;
219            slice::from_raw_parts(ptr, len)
220        }
221    }
222
223    #[inline]
224    pub fn get_number(&self, index: i32) -> f64 {
225        unsafe { duk_get_number(self.ctx, index) }
226    }
227
228    #[inline]
229    pub fn get_boolean(&self, index: i32) -> bool {
230        unsafe { duk_get_boolean(self.ctx, index) != 0 }
231    }
232
233    pub fn get_context(&self, index: i32) -> Result<DukContextGuard, JsError> {
234        let new_ctx = unsafe { duk_get_context(self.ctx, index) };
235        if new_ctx.is_null() {
236            return Err(JsError::from(format!("could not get context from index {}", index)));
237        }
238        Ok(DukContextGuard::new(unsafe { DukContext::from_raw(new_ctx) }))
239    }
240
241    #[inline]
242    pub fn get_prop(&self, obj_index: i32) -> bool {
243        unsafe { duk_get_prop(self.ctx, obj_index) == 1 }
244    }
245
246    #[inline]
247    pub fn put_prop(&self, obj_index: i32) {
248        unsafe { duk_put_prop(self.ctx, obj_index); }
249    }
250
251    #[inline]
252    pub fn get_prop_string(&self, obj_index: i32, key: &str) -> bool {
253        unsafe {
254            duk_get_prop_lstring(self.ctx, obj_index, key.as_ptr() as *const c_char, key.len()) == 1
255        }
256    }
257
258    #[inline]
259    pub fn put_prop_string(&self, obj_index: i32, key: &str) {
260        unsafe {
261            duk_put_prop_lstring(self.ctx,
262                                 obj_index,
263                                 key.as_ptr() as *const c_char,
264                                 key.len());
265        }
266    }
267
268    #[inline]
269    pub fn get_prop_index(&self, obj_index: i32, index: u32) -> bool {
270        unsafe { duk_get_prop_index(self.ctx, obj_index, index) == 1 }
271    }
272
273    #[inline]
274    pub fn put_prop_index(&self, obj_index: i32, index: u32) {
275        unsafe {
276            duk_put_prop_index(self.ctx, obj_index, index);
277        }
278    }
279
280    #[inline]
281    pub fn get_global_string(&self, key: &str) -> bool {
282        unsafe {
283            duk_get_global_lstring(self.ctx, key.as_ptr() as *const c_char, key.len()) == 1
284        }
285    }
286
287    #[inline]
288    pub fn put_global_string(&self, key: &str) {
289        unsafe {
290            duk_put_global_lstring(self.ctx, key.as_ptr() as *const c_char, key.len());
291        }
292    }
293
294    #[inline]
295    pub fn get_length(&self, obj_index: i32) -> usize {
296        unsafe {
297            duk_get_length(self.ctx, obj_index)
298        }
299    }
300
301    #[inline]
302    pub fn enum_indices(&self, obj_index: i32) {
303        unsafe {
304            duk_enum(self.ctx, obj_index, DukEnumFlags::DUK_ENUM_ARRAY_INDICES_ONLY.bits());
305        }
306    }
307
308    #[inline]
309    pub fn enum_keys(&self, obj_index: i32) {
310        unsafe {
311            duk_enum(self.ctx, obj_index, DukEnumFlags::DUK_ENUM_OWN_PROPERTIES_ONLY.bits());
312        }
313    }
314
315    #[inline]
316    pub fn next(&self, obj_index: i32) -> bool {
317        unsafe {
318            duk_next(self.ctx, obj_index, 1) == 1
319        }
320    }
321
322    #[inline]
323    pub fn call_prop(&self, obj_index: i32, nargs: usize) {
324        unsafe {
325            duk_call_prop(self.ctx, obj_index, nargs as i32);
326        }
327    }
328
329    #[inline]
330    pub fn pcall(&self, nargs: usize) -> Result<(), i32> {
331        let res = unsafe {
332            duk_pcall(self.ctx, nargs as i32)
333        };
334        try_exec_success!(res);
335        Ok(())
336    }
337
338    #[inline]
339    pub fn pcall_method(&self, nargs: usize) -> Result<(), i32> {
340        let res = unsafe {
341            duk_pcall_method(self.ctx, nargs as i32)
342        };
343        try_exec_success!(res);
344        Ok(())
345    }
346
347    #[inline]
348    pub fn pcall_prop(&self, obj_index: i32, nargs: usize) -> Result<(), i32> {
349        let res = unsafe {
350            duk_pcall_prop(self.ctx, obj_index, nargs as i32)
351        };
352        try_exec_success!(res);
353        Ok(())
354    }
355
356    #[inline]
357    pub fn safe_to_lstring(&self, obj_index: i32) -> String {
358        unsafe {
359            let mut len: usize = 0;
360            let msg = duk_safe_to_lstring(self.ctx, obj_index, &mut len);
361            String::from(std::str::from_utf8_unchecked(std::slice::from_raw_parts(msg as *const u8, len)))
362        }
363    }
364
365    #[inline]
366    pub fn throw(&self) {
367        unsafe {
368            duk_throw_raw(self.ctx);
369        }
370    }
371
372    #[inline]
373    pub fn push_context_dump(&self) {
374        unsafe {
375            duk_push_context_dump(self.ctx);
376        }
377    }
378
379    pub fn get_stack_dump(&self) -> String {
380        self.push_context_dump();
381        unsafe {
382            let dump = CStr::from_ptr(duk_to_string(self.ctx, -1)).to_string_lossy().to_string();
383            duk_pop(self.ctx);
384            dump
385        }
386    }
387
388    /// Propagate JS error to Rust, popping the error from the stack.
389    /// js_res: Result<(), i32> - JS result returned by protected call functions.
390    /// If it is an error, it will be converted to JsError.
391    /// This method should be called immediately after a protected call to handle the error.
392    pub fn propagate_js_error<T>(&self, js_res: Result<T, i32>) -> Result<T, JsError> {
393        unsafe {
394            match js_res {
395                Ok(v) => Ok(v),
396                Err(_err) => {
397                    let mut len: usize = 0;
398                    let msg = duk_safe_to_lstring(self.ctx, -1, &mut len);
399                    let s = String::from(std::str::from_utf8_unchecked(std::slice::from_raw_parts(msg as *const u8, len)));
400                    duk_pop(self.ctx);
401                    Err(JsError::from(s))
402                }
403            }
404        }
405    }
406
407    #[inline]
408    pub fn eval(&self, code: &str) -> Result<(), JsError> {
409        unsafe {
410            if duk_eval_raw(self.ctx,
411                            code.as_ptr() as *const c_char,
412                            code.len(),
413                            0 | (DukCompileFlags::DUK_COMPILE_SAFE | DukCompileFlags::DUK_COMPILE_NOSOURCE | DukCompileFlags::DUK_COMPILE_NOFILENAME).bits()) != 0 {
414                let mut len: usize = 0;
415                let msg = duk_safe_to_lstring(self.ctx, -1, &mut len);
416                let s = String::from(std::str::from_utf8_unchecked(std::slice::from_raw_parts(msg as *const u8, len)));
417                duk_pop(self.ctx);
418                Err(JsError::from(s))
419            } else {
420                Ok(())
421            }
422        }
423    }
424
425    #[inline]
426    pub fn eval_file(&self, filename: &str, code: &str) -> Result<(), JsError> {
427        unsafe {
428            duk_push_lstring(self.ctx, filename.as_ptr() as *const c_char, filename.len());
429            if duk_eval_raw(self.ctx,
430                            code.as_ptr() as *const c_char,
431                            code.len(),
432                            1 | (DukCompileFlags::DUK_COMPILE_SAFE | DukCompileFlags::DUK_COMPILE_NOSOURCE).bits()) != 0 {
433                let s = self.safe_to_lstring(-1);
434                duk_pop(self.ctx);
435                Err(JsError::from(s))
436            } else {
437                Ok(())
438            }
439        }
440    }
441
442    #[inline]
443    pub fn compile(&self, code: &str) -> Result<(), JsError> {
444        unsafe {
445            if duk_compile_raw(self.ctx,
446                               code.as_ptr() as *const c_char,
447                               code.len(),
448                               0 | (DukCompileFlags::DUK_COMPILE_NORESULT | DukCompileFlags::DUK_COMPILE_NOFILENAME).bits()) != 0 {
449                let s = self.safe_to_lstring(-1);
450                duk_pop(self.ctx);
451                Err(JsError::from(s))
452            } else {
453                Ok(())
454            }
455        }
456    }
457
458    #[inline]
459    pub fn compile_file(&self, filename: &str, code: &str) -> Result<(), JsError> {
460        unsafe {
461            duk_push_lstring(self.ctx, filename.as_ptr() as *const c_char, filename.len());
462            if duk_compile_raw(self.ctx,
463                               code.as_ptr() as *const c_char,
464                               code.len(),
465                               1 | DukCompileFlags::DUK_COMPILE_NORESULT.bits()) != 0 {
466                let s = self.safe_to_lstring(-1);
467                duk_pop(self.ctx);
468                Err(JsError::from(s))
469            } else {
470                Ok(())
471            }
472        }
473    }
474
475    #[inline]
476    pub fn write<O: WriteJs>(&self, obj: &O) -> Result<(), JsError> {
477        obj.write_js(self)
478    }
479
480    #[inline]
481    pub fn read<O: ReadJs>(&self, obj_index: i32) -> Result<O, JsError> {
482        let obj_index = self.normalize_index(obj_index);
483        O::read_js(self, obj_index)
484    }
485
486    #[inline]
487    pub fn read_top<O: ReadJs>(&self) -> Result<O, JsError> {
488        self.read( -1)
489    }
490
491    /// Initialize console functions.
492    #[inline]
493    pub fn init_console(&self) {
494        unsafe {
495            duk_api_console_init(self.ctx, Some(console_func));
496        }
497    }
498
499    #[inline]
500    pub fn xcopy_top(&self, from: &DukContext, count: i32) {
501        unsafe {
502            duk_xcopymove_raw(self.ctx, from.ctx, count, 1);
503        }
504    }
505
506    #[inline]
507    pub fn xmove_top(&self, from: &mut DukContext, count: i32) {
508        unsafe {
509            duk_xcopymove_raw(self.ctx, from.ctx, count, 0);
510        }
511    }
512
513    #[inline]
514    pub fn check_stack(&self, extra: i32) -> Result<(), JsError> {
515        let res = unsafe {
516            duk_check_stack(self.ctx, extra)
517        };
518
519        if res {
520            Ok(())
521        } else {
522            Err(JsError::from("failed to reserve enough stack space".to_string()))
523        }
524    }
525
526    #[inline]
527    pub fn check_stack_top(&self, top: i32) -> Result<(), JsError> {
528        let res = unsafe {
529            duk_check_stack_top(self.ctx, top)
530        };
531        if res {
532            Ok(())
533        } else {
534            Err(JsError::from("failed to reserve enough stack space".to_string()))
535        }
536    }
537
538    #[inline]
539    pub fn set_global_object(&self) {
540        unsafe {
541            duk_set_global_object(self.ctx);
542        }
543    }
544
545    pub fn gc(&self) {
546        unsafe {
547            duk_gc(self.ctx, DukGcFlags::NONE.bits());
548        }
549    }
550}
551
552pub struct DukContextGuard<'a> {
553    ctx: DukContext,
554    _marker: std::marker::PhantomData<&'a JsEngine>,
555}
556
557impl std::fmt::Debug for DukContextGuard<'_> {
558    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559        f.debug_struct("DukContextGuard").finish()
560    }
561}
562
563impl Deref for DukContextGuard<'_> {
564    type Target = DukContext;
565
566    fn deref(&self) -> &Self::Target {
567        &self.ctx
568    }
569}
570
571impl DerefMut for DukContextGuard<'_> {
572    fn deref_mut(&mut self) -> &mut Self::Target {
573        &mut self.ctx
574    }
575}
576
577impl <'a> DukContextGuard<'a> {
578    pub fn new(ctx: DukContext) -> Self {
579        Self {
580            ctx,
581            _marker: std::marker::PhantomData,
582        }
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use serde::Deserialize;
589    use super::*;
590
591    #[test]
592    fn test_eval() {
593        let engine = JsEngine::new().unwrap();
594        //language=js
595        engine.eval(r#" var tmp =  {
596            "foo": 1,
597            "bar": "baz"
598        }
599        tmp
600        "#).unwrap();
601
602        #[derive(Deserialize)]
603        struct TestStruct {
604            foo: i32,
605        }
606        assert_eq!(engine.read_top::<TestStruct>().unwrap().foo, 1);
607        engine.pop();
608    }
609
610    #[test]
611    fn test_get_invalid_context() {
612        let engine = JsEngine::new().unwrap();
613        let res = engine.get_context(0);
614        assert!(res.is_err());
615    }
616
617    #[test]
618    fn test_push_thread() {
619        let engine = JsEngine::new().unwrap();
620        let new_idx = engine.push_thread();
621        let new_ctx = engine.get_context(new_idx).unwrap();
622        new_ctx.push_string("test");
623        assert_eq!(new_ctx.get_string(-1), "test");
624        new_ctx.pop();
625        assert!(new_ctx.get_stack_dump().contains("ctx: top=0"));
626
627        drop(new_ctx);
628        engine.pop();
629        assert!(engine.get_stack_dump().contains("ctx: top=0"));
630    }
631
632    #[test]
633    fn test_nested_push_thread() {
634        let engine = JsEngine::new().unwrap();
635        let new_idx = engine.push_thread();
636        let new_ctx = engine.get_context(new_idx).unwrap();
637
638        let nested_id = new_ctx.push_thread();
639        let nested_ctx = new_ctx.get_context(nested_id).unwrap();
640        nested_ctx.push_string("test");
641
642        assert_eq!(nested_ctx.get_string(-1), "test");
643
644        drop(nested_ctx);
645        drop(new_ctx);
646
647        engine.pop();
648
649        assert!(engine.get_stack_dump().contains("ctx: top=0"));
650    }
651
652    #[test]
653    fn test_push_thread_new_globalenv() {
654        let engine = JsEngine::new().unwrap();
655
656        let new_idx = engine.push_thread_new_globalenv();
657        let new_idx2 = engine.push_thread_new_globalenv();
658
659        let new_ctx = engine.get_context(new_idx).unwrap();
660        let new_ctx2 = engine.get_context(new_idx2).unwrap();
661
662        // Test first context
663        new_ctx.push_string("test");
664        assert_eq!(new_ctx.get_string(-1), "test");
665        new_ctx.pop();
666        assert!(new_ctx.get_stack_dump().contains("ctx: top=0"));
667
668        // Test second context
669        new_ctx2.push_string("test2");
670        new_ctx2.push_string("test2");
671        assert_eq!(new_ctx2.get_string(-1), "test2");
672        // Pop only one string
673        new_ctx2.pop();
674        assert!(new_ctx2.get_stack_dump().contains("ctx: top=1"));
675
676        drop(new_ctx);
677        drop(new_ctx2);
678
679        // Pop both contexts
680        engine.pop_n(2);
681
682        assert!(engine.get_stack_dump().contains("ctx: top=0"));
683    }
684
685    #[test]
686    fn test_to_lstring_safety() {
687        let engine = JsEngine::new().unwrap();
688        engine.push_string("test");
689        let s = engine.safe_to_lstring(-1);
690        assert_eq!(s, "test");
691        engine.pop();
692        assert_eq!(s, "test");
693        drop(engine);
694        assert_eq!(s, "test");
695    }
696
697    #[test]
698    fn test_check_stack_error() {
699        let engine = JsEngine::new().unwrap();
700        let res = engine.check_stack(i32::MAX);
701        assert!(res.is_err());
702    }
703
704    #[test]
705    fn test_xcopy_top() {
706        let engine = JsEngine::new().unwrap();
707
708        //language=javascript
709        engine.eval("GLOBAL_TEST = { a: 1, b: 2 }").unwrap();
710
711        let ctx1_idx = engine.push_thread_new_globalenv();
712        let ctx1 = engine.get_context(ctx1_idx).unwrap();
713        // Create a new global variable
714        assert!(engine.get_global_string("GLOBAL_TEST"));
715
716        ctx1.check_stack(1).unwrap();
717        // Copy the global variable to the new context
718        ctx1.xcopy_top(&engine, 1);
719        ctx1.put_global_string("GLOBAL_TEST");
720
721        // Check if the global variable is available in the new context
722        ctx1.eval("GLOBAL_TEST.b").unwrap();
723        assert_eq!(ctx1.get_number(-1), 2.0);
724
725        // Change the value of the global variable in the new context
726        ctx1.eval("GLOBAL_TEST.b = 5").unwrap();
727
728        // Check if the value has changed in original context
729        engine.eval("GLOBAL_TEST.b").unwrap();
730        assert_eq!(engine.get_number(-1), 5.0);
731        engine.pop();
732    }
733
734    #[test]
735    fn test_set_global_object() {
736        let engine = JsEngine::new().unwrap();
737
738        engine.eval("Math.abs(-1)").unwrap();
739        assert_eq!(engine.get_number(-1), 1.0);
740
741        engine.pop();
742
743        //language=javascript
744        let _: () = engine.eval("var obj = { a: 1, b: 2 }; obj").unwrap();
745        engine.set_global_object();
746
747        engine.eval("typeof Math").unwrap();
748
749        assert_eq!(engine.get_string(-1), "undefined");
750
751    }
752
753    #[test]
754    fn test_gc() {
755        let engine = JsEngine::new().unwrap();
756
757        engine.eval("Math.abs(-1)").unwrap();
758        assert_eq!(engine.get_number(-1), 1.0);
759        engine.pop();
760        engine.gc();
761    }
762
763    #[test]
764    fn tes_propagate_js_error() {
765        let engine = JsEngine::new().unwrap();
766        //language=js
767        engine.eval(r#" var tmp =  {
768            "foo": 1,
769            "bar": "baz",
770            "some_fn": function() {
771                throw new Error("test error");
772            }
773        }
774        tmp
775        "#).unwrap();
776
777        engine.push_string("some_fn");
778
779        let call_res = engine.pcall_prop(0, 0);
780        assert!(call_res.is_err());
781        let res = engine.propagate_js_error(call_res);
782
783        assert!(res.is_err());
784        let err = res.unwrap_err();
785        assert!(err.to_string().contains("test error"));
786    }
787}
788