├── src ├── quickjs_utils │ ├── class_ids.rs │ ├── runtime.rs │ ├── iterators.rs │ ├── atoms.rs │ ├── properties.rs │ ├── interrupthandler.rs │ ├── dates.rs │ ├── bigints.rs │ ├── primitives.rs │ ├── arrays.rs │ ├── json.rs │ ├── sets.rs │ ├── mod.rs │ ├── modules.rs │ └── compile.rs ├── jsutils │ ├── jsproxies.rs │ ├── helper_tasks.rs │ ├── modules.rs │ ├── mod.rs │ └── promises.rs ├── features │ ├── mod.rs │ ├── setimmediate.rs │ └── console.rs ├── lib.rs ├── builder.rs └── quickjsvalueadapter.rs ├── test_module.mes ├── test.mes ├── .gitignore ├── valgrind.sh ├── LICENSE ├── .github └── workflows │ ├── rust_PR.yml │ └── rust.yml ├── Cargo.toml ├── README.md └── CHANGELOG.md /src/quickjs_utils/class_ids.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test_module.mes: -------------------------------------------------------------------------------- 1 | export function mltpl(a, b) { 2 | return a * b; 3 | } -------------------------------------------------------------------------------- /src/jsutils/jsproxies.rs: -------------------------------------------------------------------------------- 1 | use crate::reflection::Proxy; 2 | 3 | pub type JsProxy = Proxy; 4 | 5 | pub type JsProxyInstanceId = usize; 6 | -------------------------------------------------------------------------------- /test.mes: -------------------------------------------------------------------------------- 1 | import {mltpl} from "test_module.mes"; 2 | 3 | console.info("hello world from test.mes, 5 * 6 = %s", mltpl(5, 6)); 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .idea 13 | 14 | *.log -------------------------------------------------------------------------------- /src/quickjs_utils/runtime.rs: -------------------------------------------------------------------------------- 1 | use libquickjs_sys as q; 2 | 3 | /// create new class id 4 | /// # Safety 5 | /// make sure the runtime param is from a live JsRuntimeAdapter instance 6 | pub unsafe fn new_class_id(_runtime: *mut q::JSRuntime) -> u32 { 7 | let mut c_id: u32 = 0; 8 | 9 | #[cfg(feature = "bellard")] 10 | let class_id: u32 = q::JS_NewClassID(&mut c_id); 11 | 12 | #[cfg(feature = "quickjs-ng")] 13 | let class_id: u32 = q::JS_NewClassID(_runtime, &mut c_id); 14 | 15 | log::trace!("got class id {}", class_id); 16 | 17 | class_id 18 | } 19 | -------------------------------------------------------------------------------- /valgrind.sh: -------------------------------------------------------------------------------- 1 | cargo clean 2 | cargo test 3 | #find ./target/debug/deps/quickjs_runtime-* -maxdepth 1 -type f -executable | xargs valgrind --leak-check=full --error-exitcode=1 4 | test_exe=$(find ./target/debug/deps/quickjs_runtime-* -maxdepth 1 -type f -executable) 5 | echo "exe = ${test_exe}" 6 | test_output=$($test_exe) 7 | #echo "test_output = ${test_output}" 8 | echo "${test_output}" | while read test_line; do 9 | if [[ $test_line == test* ]] && [[ $test_line == *ok ]] ; 10 | then 11 | test_line=${test_line#test } 12 | test_line=${test_line% ... ok} 13 | echo "testing: ${test_line}" 14 | valgrind --leak-check=full --error-exitcode=1 $test_exe $test_line 15 | echo "done: ${test_line}" 16 | fi 17 | done -------------------------------------------------------------------------------- /src/jsutils/helper_tasks.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use hirofa_utils::task_manager::TaskManager; 3 | use lazy_static::lazy_static; 4 | use tokio::task::JoinError; 5 | 6 | lazy_static! { 7 | /// a static Multithreaded task manager used to run rust ops async and multithreaded ( in at least 2 threads) 8 | static ref HELPER_TASKS: TaskManager = TaskManager::new(std::cmp::max(2, num_cpus::get())); 9 | } 10 | 11 | /// add a task the the "helper" thread pool 12 | pub fn add_helper_task(task: T) 13 | where 14 | T: FnOnce() + Send + 'static, 15 | { 16 | log::trace!("adding a helper task"); 17 | HELPER_TASKS.add_task(task); 18 | } 19 | 20 | /// add an async task the the "helper" thread pool 21 | pub fn add_helper_task_async + Send + 'static>( 22 | task: T, 23 | ) -> impl Future> { 24 | log::trace!("adding an async helper task"); 25 | HELPER_TASKS.add_task_async(task) 26 | } 27 | -------------------------------------------------------------------------------- /src/features/mod.rs: -------------------------------------------------------------------------------- 1 | //! contains engine features like console, setTimeout, setInterval and setImmediate 2 | 3 | use crate::facades::QuickJsRuntimeFacade; 4 | use crate::jsutils::JsError; 5 | #[cfg(feature = "console")] 6 | pub mod console; 7 | #[cfg(any(feature = "settimeout", feature = "setinterval"))] 8 | pub mod set_timeout; 9 | #[cfg(feature = "setimmediate")] 10 | pub mod setimmediate; 11 | 12 | #[cfg(any( 13 | feature = "settimeout", 14 | feature = "setinterval", 15 | feature = "console", 16 | feature = "setimmediate" 17 | ))] 18 | pub fn init(es_rt: &QuickJsRuntimeFacade) -> Result<(), JsError> { 19 | log::trace!("features::init"); 20 | 21 | es_rt.exe_rt_task_in_event_loop(move |q_js_rt| { 22 | #[cfg(feature = "console")] 23 | console::init(q_js_rt)?; 24 | #[cfg(feature = "setimmediate")] 25 | setimmediate::init(q_js_rt)?; 26 | 27 | #[cfg(any(feature = "settimeout", feature = "setinterval"))] 28 | set_timeout::init(q_js_rt)?; 29 | Ok(()) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 HiRoFa Open Source 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/jsutils/modules.rs: -------------------------------------------------------------------------------- 1 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 2 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 3 | use std::sync::Arc; 4 | 5 | pub trait ScriptModuleLoader { 6 | fn normalize_path( 7 | &self, 8 | realm: &QuickJsRealmAdapter, 9 | ref_path: &str, 10 | path: &str, 11 | ) -> Option; 12 | fn load_module(&self, realm: &QuickJsRealmAdapter, absolute_path: &str) -> String; 13 | } 14 | 15 | pub trait CompiledModuleLoader { 16 | fn normalize_path( 17 | &self, 18 | realm: &QuickJsRealmAdapter, 19 | ref_path: &str, 20 | path: &str, 21 | ) -> Option; 22 | fn load_module(&self, realm: &QuickJsRealmAdapter, absolute_path: &str) -> Arc>; 23 | } 24 | 25 | pub trait NativeModuleLoader { 26 | fn has_module(&self, realm: &QuickJsRealmAdapter, module_name: &str) -> bool; 27 | fn get_module_export_names(&self, realm: &QuickJsRealmAdapter, module_name: &str) -> Vec<&str>; 28 | fn get_module_exports( 29 | &self, 30 | realm: &QuickJsRealmAdapter, 31 | module_name: &str, 32 | ) -> Vec<(&str, QuickJsValueAdapter)>; 33 | } 34 | -------------------------------------------------------------------------------- /src/quickjs_utils/iterators.rs: -------------------------------------------------------------------------------- 1 | //! utils for the iterator protocol 2 | 3 | use crate::jsutils::JsError; 4 | use crate::quickjs_utils::{functions, objects, primitives}; 5 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 6 | use libquickjs_sys as q; 7 | 8 | /// iterate over an object conforming to the [iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol) protocol 9 | /// # Safety 10 | /// please ensure that the QuickjsContext corresponding to the passed JSContext is still valid 11 | pub unsafe fn iterate Result, R>( 12 | ctx: *mut q::JSContext, 13 | iterator_ref: &QuickJsValueAdapter, 14 | consumer_producer: C, 15 | ) -> Result, JsError> { 16 | let mut res = vec![]; 17 | 18 | loop { 19 | let next_obj = functions::invoke_member_function(ctx, iterator_ref, "next", &[])?; 20 | if primitives::to_bool(&objects::get_property(ctx, &next_obj, "done")?)? { 21 | break; 22 | } else { 23 | let next_item = objects::get_property(ctx, &next_obj, "value")?; 24 | res.push(consumer_producer(next_item)?); 25 | } 26 | } 27 | 28 | Ok(res) 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/rust_PR.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: -D warnings 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Prepare 17 | run: | 18 | sudo apt update 19 | sudo apt install ccache llvm autoconf2.13 automake clang -y 20 | - name: Cache cargo registry 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.cargo/registry 24 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }} 25 | - name: Cache cargo index 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cargo/git 29 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.toml') }} 30 | - name: Ccache 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.ccache 34 | key: ${{ runner.OS }}-ccache-${{ hashFiles('**\Cargo.toml') }} 35 | - name: Build 36 | run: | 37 | export SHELL=/bin/bash 38 | export CC=/usr/bin/clang 39 | export CXX=/usr/bin/clang++ 40 | ccache -z 41 | CCACHE=$(which ccache) cargo build --verbose 42 | ccache -s 43 | - name: Run tests 44 | run: cargo test --verbose 45 | - name: Format 46 | run: | 47 | cargo fmt --all -- --check 48 | - name: Clippy check 49 | uses: actions-rs/clippy-check@v1 50 | with: 51 | token: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /src/features/setimmediate.rs: -------------------------------------------------------------------------------- 1 | use crate::facades::QuickJsRuntimeFacade; 2 | use crate::jsutils::JsError; 3 | use crate::quickjs_utils; 4 | use crate::quickjs_utils::{functions, get_global_q, objects, parse_args}; 5 | use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter; 6 | use libquickjs_sys as q; 7 | 8 | /// provides the setImmediate methods for the runtime 9 | /// # Example 10 | /// ```rust 11 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 12 | /// use quickjs_runtime::jsutils::Script; 13 | /// use std::time::Duration; 14 | /// let rt = QuickJsRuntimeBuilder::new().build(); 15 | /// rt.eval_sync(None, Script::new("test_immediate.es", "setImmediate(() => {console.log('immediate logging')});")).expect("script failed"); 16 | /// std::thread::sleep(Duration::from_secs(1)); 17 | /// ``` 18 | pub fn init(q_js_rt: &QuickJsRuntimeAdapter) -> Result<(), JsError> { 19 | log::trace!("setimmediate::init"); 20 | 21 | q_js_rt.add_context_init_hook(|_q_js_rt, q_ctx| { 22 | let set_immediate_func = 23 | functions::new_native_function_q(q_ctx, "setImmediate", Some(set_immediate), 1, false)?; 24 | 25 | let global = get_global_q(q_ctx); 26 | 27 | objects::set_property2_q(q_ctx, &global, "setImmediate", &set_immediate_func, 0)?; 28 | Ok(()) 29 | })?; 30 | Ok(()) 31 | } 32 | 33 | unsafe extern "C" fn set_immediate( 34 | context: *mut q::JSContext, 35 | _this_val: q::JSValue, 36 | argc: ::std::os::raw::c_int, 37 | argv: *mut q::JSValue, 38 | ) -> q::JSValue { 39 | log::trace!("> set_immediate"); 40 | 41 | let args = parse_args(context, argc, argv); 42 | 43 | QuickJsRuntimeAdapter::do_with(move |q_js_rt| { 44 | let q_ctx = q_js_rt.get_quickjs_context(context); 45 | if args.is_empty() { 46 | return q_ctx.report_ex("setImmediate requires at least one argument"); 47 | } 48 | if !functions::is_function(context, &args[0]) { 49 | return q_ctx.report_ex("setImmediate requires a functions as first arg"); 50 | } 51 | 52 | QuickJsRuntimeFacade::add_local_task_to_event_loop(move |_q_js_rt| { 53 | let func = &args[0]; 54 | 55 | match functions::call_function(context, func, &args[1..], None) { 56 | Ok(_) => {} 57 | Err(e) => { 58 | log::error!("setImmediate failed: {}", e); 59 | } 60 | }; 61 | }); 62 | 63 | quickjs_utils::new_null() 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /src/quickjs_utils/atoms.rs: -------------------------------------------------------------------------------- 1 | //JS_AtomToCString(ctx: *mut JSContext, atom: JSAtom) -> *const ::std::os::raw::c_char 2 | use crate::jsutils::JsError; 3 | use crate::quickjs_utils::primitives; 4 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 5 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 6 | use libquickjs_sys as q; 7 | use std::ffi::CString; 8 | 9 | #[allow(clippy::upper_case_acronyms)] 10 | pub struct JSAtomRef { 11 | context: *mut q::JSContext, 12 | atom: q::JSAtom, 13 | } 14 | 15 | impl JSAtomRef { 16 | pub fn new(context: *mut q::JSContext, atom: q::JSAtom) -> Self { 17 | Self { context, atom } 18 | } 19 | pub(crate) fn get_atom(&self) -> q::JSAtom { 20 | self.atom 21 | } 22 | 23 | pub(crate) fn increment_ref_ct(&self) { 24 | unsafe { q::JS_DupAtom(self.context, self.atom) }; 25 | } 26 | pub(crate) fn decrement_ref_ct(&self) { 27 | unsafe { q::JS_FreeAtom(self.context, self.atom) }; 28 | } 29 | } 30 | 31 | impl Drop for JSAtomRef { 32 | fn drop(&mut self) { 33 | // free 34 | self.decrement_ref_ct(); 35 | } 36 | } 37 | 38 | pub fn to_string_q(q_ctx: &QuickJsRealmAdapter, atom_ref: &JSAtomRef) -> Result { 39 | unsafe { to_string(q_ctx.context, atom_ref) } 40 | } 41 | 42 | /// # Safety 43 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 44 | pub unsafe fn to_string( 45 | context: *mut q::JSContext, 46 | atom_ref: &JSAtomRef, 47 | ) -> Result { 48 | let val = q::JS_AtomToString(context, atom_ref.atom); 49 | let val_ref = QuickJsValueAdapter::new(context, val, false, true, "atoms::to_string"); 50 | primitives::to_string(context, &val_ref) 51 | } 52 | 53 | pub fn to_string2_q(q_ctx: &QuickJsRealmAdapter, atom: &q::JSAtom) -> Result { 54 | unsafe { to_string2(q_ctx.context, atom) } 55 | } 56 | 57 | /// # Safety 58 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 59 | pub unsafe fn to_string2(context: *mut q::JSContext, atom: &q::JSAtom) -> Result { 60 | let val = q::JS_AtomToString(context, *atom); 61 | let val_ref = QuickJsValueAdapter::new(context, val, false, true, "atoms::to_string"); 62 | primitives::to_string(context, &val_ref) 63 | } 64 | 65 | pub fn from_string_q(q_ctx: &QuickJsRealmAdapter, string: &str) -> Result { 66 | unsafe { from_string(q_ctx.context, string) } 67 | } 68 | 69 | /// # Safety 70 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 71 | pub unsafe fn from_string(context: *mut q::JSContext, string: &str) -> Result { 72 | let s = CString::new(string).ok().unwrap(); 73 | 74 | let len = string.len(); 75 | 76 | let atom = q::JS_NewAtomLen(context, s.as_ptr(), len as _); 77 | Ok(JSAtomRef::new(context, atom)) 78 | } 79 | -------------------------------------------------------------------------------- /src/quickjs_utils/properties.rs: -------------------------------------------------------------------------------- 1 | use crate::jsutils::JsError; 2 | use crate::quickjs_utils::atoms; 3 | use crate::quickjs_utils::atoms::JSAtomRef; 4 | use libquickjs_sys as q; 5 | #[cfg(feature = "bellard")] 6 | use std::os::raw::c_int; 7 | 8 | #[allow(clippy::upper_case_acronyms)] 9 | /// this is a wrapper struct for JSPropertyEnum struct in quickjs 10 | /// it used primarily as a result of objects::get_own_property_names() 11 | pub struct JSPropertyEnumRef { 12 | context: *mut q::JSContext, 13 | property_enum: *mut q::JSPropertyEnum, 14 | length: u32, 15 | } 16 | 17 | impl JSPropertyEnumRef { 18 | pub fn new( 19 | context: *mut q::JSContext, 20 | property_enum: *mut q::JSPropertyEnum, 21 | length: u32, 22 | ) -> Self { 23 | Self { 24 | context, 25 | property_enum, 26 | length, 27 | } 28 | } 29 | /// get a raw ptr to an Atom 30 | /// # Safety 31 | /// do not drop the JSPropertyEnumRef while still using the ptr 32 | pub unsafe fn get_atom_raw(&self, index: u32) -> *mut q::JSAtom { 33 | if index >= self.length { 34 | panic!("index out of bounds"); 35 | } 36 | let prop: *mut q::JSPropertyEnum = self.property_enum.offset(index as isize); 37 | let atom: *mut q::JSAtom = (*prop).atom as *mut q::JSAtom; 38 | atom 39 | } 40 | pub fn get_atom(&self, index: u32) -> JSAtomRef { 41 | let atom: *mut q::JSAtom = unsafe { self.get_atom_raw(index) }; 42 | let atom_ref = JSAtomRef::new(self.context, atom as q::JSAtom); 43 | atom_ref.increment_ref_ct(); 44 | atom_ref 45 | } 46 | pub fn get_name(&self, index: u32) -> Result { 47 | let atom: *mut q::JSAtom = unsafe { self.get_atom_raw(index) }; 48 | let atom = atom as q::JSAtom; 49 | unsafe { atoms::to_string2(self.context, &atom) } 50 | } 51 | pub fn is_enumerable(&self, index: u32) -> bool { 52 | if index >= self.length { 53 | panic!("index out of bounds"); 54 | } 55 | unsafe { 56 | let prop: *mut q::JSPropertyEnum = self.property_enum.offset(index as isize); 57 | #[cfg(feature = "bellard")] 58 | { 59 | let is_enumerable: c_int = (*prop).is_enumerable; 60 | is_enumerable != 0 61 | } 62 | #[cfg(feature = "quickjs-ng")] 63 | (*prop).is_enumerable 64 | } 65 | } 66 | pub fn len(&self) -> u32 { 67 | self.length 68 | } 69 | pub fn is_empty(&self) -> bool { 70 | self.length == 0 71 | } 72 | } 73 | 74 | impl Drop for JSPropertyEnumRef { 75 | fn drop(&mut self) { 76 | unsafe { 77 | for index in 0..self.length { 78 | let prop: *mut q::JSPropertyEnum = self.property_enum.offset(index as isize); 79 | q::JS_FreeAtom(self.context, (*prop).atom); 80 | } 81 | 82 | q::js_free(self.context, self.property_enum as *mut std::ffi::c_void); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/quickjs_utils/interrupthandler.rs: -------------------------------------------------------------------------------- 1 | use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter; 2 | use libquickjs_sys as q; 3 | use std::ffi::c_void; 4 | use std::os::raw::c_int; 5 | 6 | // 7 | 8 | /// set an interrupt handler for the runtime 9 | /// # Safety 10 | /// be safe 11 | pub unsafe fn set_interrupt_handler(runtime: *mut q::JSRuntime, handler: q::JSInterruptHandler) { 12 | q::JS_SetInterruptHandler(runtime, handler, std::ptr::null_mut()); 13 | } 14 | 15 | pub(crate) fn init(q_js_rt: &QuickJsRuntimeAdapter) { 16 | unsafe { set_interrupt_handler(q_js_rt.runtime, Some(interrupt_handler)) }; 17 | } 18 | 19 | unsafe extern "C" fn interrupt_handler(_rt: *mut q::JSRuntime, _opaque: *mut c_void) -> c_int { 20 | QuickJsRuntimeAdapter::do_with(|q_js_rt| { 21 | let handler = q_js_rt.interrupt_handler.as_ref().unwrap(); 22 | i32::from(handler(q_js_rt)) 23 | }) 24 | } 25 | 26 | #[cfg(test)] 27 | pub mod tests { 28 | use crate::builder::QuickJsRuntimeBuilder; 29 | use crate::jsutils::Script; 30 | use crate::quickjs_utils::get_script_or_module_name_q; 31 | 32 | use std::cell::RefCell; 33 | use std::panic; 34 | use std::sync::{Arc, Mutex}; 35 | 36 | #[test] 37 | fn test_interrupt_handler() { 38 | log::info!("interrupt_handler test"); 39 | 40 | let called = Arc::new(Mutex::new(RefCell::new(false))); 41 | let called2 = called.clone(); 42 | 43 | /*panic::set_hook(Box::new(|panic_info| { 44 | let backtrace = Backtrace::new(); 45 | println!("thread panic occurred: {panic_info}\nbacktrace: {backtrace:?}"); 46 | log::error!( 47 | "thread panic occurred: {}\nbacktrace: {:?}", 48 | panic_info, 49 | backtrace 50 | ); 51 | }));*/ 52 | 53 | //simple_logging::log_to_file("esruntime.log", LevelFilter::max()) 54 | // .expect("could not init logger"); 55 | 56 | let rt = QuickJsRuntimeBuilder::new() 57 | .set_interrupt_handler(move |qjs_rt| { 58 | log::debug!("interrupt_handler called / 1"); 59 | let script_name = get_script_or_module_name_q(qjs_rt.get_main_realm()); 60 | match script_name { 61 | Ok(script_name) => { 62 | log::debug!("interrupt_handler called: {}", script_name); 63 | } 64 | Err(_) => { 65 | log::debug!("interrupt_handler called"); 66 | } 67 | } 68 | let lck = called2.lock().unwrap(); 69 | *lck.borrow_mut() = true; 70 | false 71 | }) 72 | .build(); 73 | 74 | match rt.eval_sync( 75 | None, 76 | Script::new( 77 | "test_interrupt.es", 78 | "for (let x = 0; x < 10000; x++) {console.log('x' + x);}", 79 | ), 80 | ) { 81 | Ok(_) => {} 82 | Err(err) => { 83 | panic!("err: {}", err); 84 | } 85 | } 86 | 87 | rt.create_context("newctx").expect("ctx crea failed"); 88 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 89 | let ctx = q_js_rt.get_context("newctx"); 90 | match ctx.eval(Script::new( 91 | "test_interrupt.es", 92 | "for (let x = 0; x < 10000; x++) {console.log('x' + x);}", 93 | )) { 94 | Ok(_) => {} 95 | Err(err) => { 96 | panic!("err: {}", err); 97 | } 98 | } 99 | }); 100 | 101 | let lck = called.lock().unwrap(); 102 | assert!(*lck.borrow()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/quickjs_utils/dates.rs: -------------------------------------------------------------------------------- 1 | //! Utils for working with Date objects 2 | 3 | use crate::jsutils::JsError; 4 | use crate::quickjs_utils; 5 | #[cfg(feature = "bellard")] 6 | use crate::quickjs_utils::class_ids::JS_CLASS_DATE; 7 | use crate::quickjs_utils::{functions, primitives}; 8 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 9 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 10 | use libquickjs_sys as q; 11 | #[cfg(feature = "bellard")] 12 | use libquickjs_sys::JS_GetClassID; 13 | #[cfg(feature = "quickjs-ng")] 14 | use libquickjs_sys::JS_IsDate; 15 | 16 | /// create a new instance of a Date object 17 | pub fn new_date_q(context: &QuickJsRealmAdapter) -> Result { 18 | unsafe { new_date(context.context) } 19 | } 20 | 21 | /// create a new instance of a Date object 22 | /// # Safety 23 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 24 | pub unsafe fn new_date(context: *mut q::JSContext) -> Result { 25 | let constructor = quickjs_utils::get_constructor(context, "Date")?; 26 | let date_ref = functions::call_constructor(context, &constructor, &[])?; 27 | Ok(date_ref) 28 | } 29 | 30 | /// check if a JSValueRef is an instance of Date 31 | pub fn is_date_q(context: &QuickJsRealmAdapter, obj_ref: &QuickJsValueAdapter) -> bool { 32 | unsafe { is_date(context.context, obj_ref) } 33 | } 34 | 35 | /// check if a JSValueRef is an instance of Date 36 | /// # Safety 37 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 38 | #[allow(unused_variables)] 39 | pub unsafe fn is_date(ctx: *mut q::JSContext, obj: &QuickJsValueAdapter) -> bool { 40 | #[cfg(feature = "bellard")] 41 | { 42 | JS_GetClassID(*obj.borrow_value()) == JS_CLASS_DATE 43 | } 44 | #[cfg(feature = "quickjs-ng")] 45 | { 46 | JS_IsDate(*obj.borrow_value()) 47 | } 48 | } 49 | 50 | /// set the timestamp for a Date object 51 | pub fn set_time_q( 52 | context: &QuickJsRealmAdapter, 53 | date_ref: &QuickJsValueAdapter, 54 | timestamp: f64, 55 | ) -> Result<(), JsError> { 56 | unsafe { set_time(context.context, date_ref, timestamp) } 57 | } 58 | 59 | /// set the timestamp for a Date object 60 | /// # Safety 61 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 62 | pub unsafe fn set_time( 63 | context: *mut q::JSContext, 64 | date_ref: &QuickJsValueAdapter, 65 | timestamp: f64, 66 | ) -> Result<(), JsError> { 67 | functions::invoke_member_function( 68 | context, 69 | date_ref, 70 | "setTime", 71 | &[primitives::from_f64(timestamp)], 72 | )?; 73 | Ok(()) 74 | } 75 | /// get the timestamp from a Date object 76 | pub fn get_time_q( 77 | context: &QuickJsRealmAdapter, 78 | date_ref: &QuickJsValueAdapter, 79 | ) -> Result { 80 | unsafe { get_time(context.context, date_ref) } 81 | } 82 | /// get the timestamp from a Date object 83 | /// # Safety 84 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 85 | pub unsafe fn get_time( 86 | context: *mut q::JSContext, 87 | date_ref: &QuickJsValueAdapter, 88 | ) -> Result { 89 | let time_ref = functions::invoke_member_function(context, date_ref, "getTime", &[])?; 90 | if time_ref.is_f64() { 91 | primitives::to_f64(&time_ref) 92 | } else if time_ref.is_i32() { 93 | primitives::to_i32(&time_ref).map(|i| i as f64) 94 | } else { 95 | Err(JsError::new_string(format!( 96 | "could not get time, val was a {}", 97 | time_ref.get_js_type() 98 | ))) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | pub mod tests { 104 | use crate::facades::tests::init_test_rt; 105 | use crate::quickjs_utils::dates; 106 | use crate::quickjs_utils::dates::{get_time_q, is_date_q, set_time_q}; 107 | 108 | #[test] 109 | fn test_date() { 110 | let rt = init_test_rt(); 111 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 112 | let q_ctx = q_js_rt.get_main_realm(); 113 | let date_ref = dates::new_date_q(q_ctx).expect("new_date failed"); 114 | assert!(is_date_q(q_ctx, &date_ref)); 115 | 116 | set_time_q(q_ctx, &date_ref, 1746776901898f64).expect("could not set time"); 117 | log::info!( 118 | "date_str={}", 119 | date_ref.to_string().expect("could not get date_ref string") 120 | ); 121 | let gt_res = get_time_q(q_ctx, &date_ref); 122 | match gt_res { 123 | Ok(t) => { 124 | assert_eq!(t, 1746776901898f64); 125 | } 126 | Err(e) => { 127 | panic!("get time failed: {}", e); 128 | } 129 | } 130 | 131 | set_time_q(q_ctx, &date_ref, 2f64).expect("could not set time"); 132 | let gt_res = get_time_q(q_ctx, &date_ref); 133 | match gt_res { 134 | Ok(t) => { 135 | assert_eq!(t, 2f64); 136 | } 137 | Err(e) => { 138 | panic!("get time 2 failed: {}", e); 139 | } 140 | } 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quickjs_runtime" 3 | version = "0.17.0" 4 | authors = ["Andries Hiemstra "] 5 | edition = "2021" 6 | description = "Wrapper API and utils for the QuickJS JavaScript engine with support for Promise, Modules, Async/await" 7 | homepage = "https://github.com/HiRoFa/quickjs_es_runtime" 8 | keywords = ["quickjs", "javascript", "runtime", "async", "engine"] 9 | repository = "https://github.com/HiRoFa/quickjs_es_runtime" 10 | license = "MIT" 11 | documentation = "https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/index.html" 12 | readme = "README.md" 13 | categories = ["development-tools"] 14 | 15 | [features] 16 | default = ["console", "setimmediate", "setinterval", "settimeout", "typescript", "bellard"] 17 | console = [] 18 | settimeout = [] 19 | setinterval = [] 20 | setimmediate = [] 21 | typescript = ["swc", "swc_common", "swc_atoms", "swc_cached", "swc_macros_common", "swc_eq_ignore_macros", "swc_visit", "swc_visit_macros", "swc_config", "swc_config_macro", "swc_ecma_codegen", "swc_ecma_ast", "swc_ecma_codegen_macros", "swc_ecma_utils", "swc_ecma_visit", "swc_ecma_loader", "swc_ecma_transforms_base", "swc_ecma_transforms_compat", "swc_ecma_transforms_classes", "swc_ecma_transforms_optimization", "swc_ecma_transforms_proposal", "swc_ecma_transforms_macros", "swc_ecma_transforms_react", "swc_ecma_transforms_typescript", "swc_graph_analyzer", "swc_bundler", "swc_ecma_lexer", "swc_ecma_parser", "swc_sourcemap", "swc_trace_macro", "swc_node_comments"] 22 | bellard = ["libquickjs-sys/bellard"] 23 | quickjs-ng = ["libquickjs-sys/quickjs-ng"] 24 | 25 | [dependencies] 26 | hirofa_utils = "0.7" 27 | #hirofa_utils = {path="../utils"} 28 | #hirofa_utils = {git="https://github.com/SreeniIO/utils.git"} 29 | #hirofa_utils = {git="https://github.com/HiRoFa/utils"} 30 | backtrace = "0.3" 31 | 32 | #libquickjs-sys = {package="hirofa-quickjs-sys", git='https://github.com/HiRoFa/quickjs-sys'} 33 | #libquickjs-sys = { package = "hirofa-quickjs-sys", path = '../quickjs-sys', default-features = false } 34 | libquickjs-sys = { package = "hirofa-quickjs-sys", version = "0.12", default-features = false } 35 | lazy_static = "1.5.0" 36 | log = "0.4" 37 | num_cpus = "1" 38 | rand = "0.8" 39 | thread-id = "5" 40 | futures = "0.3" 41 | tokio = { version = "1", features = ["rt", "rt-multi-thread"] } 42 | serde_json = "1.0" 43 | #serde = { version = "=1.0.219", features = ["derive"] } 44 | serde = { version = "1", features = ["derive"] } 45 | string_cache = "0.8" 46 | flume = { version = "0.11", features = ["async"] } 47 | either = "1" 48 | lru = "0.14.0" 49 | anyhow = "1" 50 | #swc 51 | # like the good people at denoland said: 52 | # "swc's version bumping is very buggy and there will often be patch versions 53 | # published that break our build, so we pin all swc versions to prevent 54 | # pulling in new versions of swc crates" 55 | # see https://github.com/denoland/deno_ast/blob/main/Cargo.toml 56 | swc = { version = "=35.0.0", optional = true } 57 | swc_config = { version = "=3.1.2", optional = true } 58 | swc_config_macro = { version = "=1.0.1", optional = true } 59 | swc_ecma_ast = { version = "=15.0.0", features = ["serde-impl"], optional = true } 60 | swc_atoms = { version = "=7.0.0", optional = true } 61 | swc_cached = { version = "=2.0.0", optional = true } 62 | swc_eq_ignore_macros = { version = "=1.0.1", optional = true } 63 | swc_visit_macros = { version = "=0.5.13", optional = true } 64 | swc_common = { version = "=14.0.4", optional = true, features = ["tty-emitter"] } 65 | swc_ecma_codegen = { version = "=17.0.0", optional = true } 66 | swc_ecma_codegen_macros = { version = "=2.0.2", optional = true } 67 | swc_ecma_loader = { version = "=14.0.0", optional = true } 68 | swc_ecma_lexer = { version = "=23.0.1", optional = true } 69 | swc_ecma_parser = { version = "=23.0.0", optional = true } 70 | swc_ecma_transforms_base = { version = "=25.0.0", features = ["inline-helpers"], optional = true } 71 | swc_ecma_transforms_classes = { version = "=25.0.0", optional = true } 72 | swc_ecma_transforms_compat = { version = "=27.0.0", optional = true } 73 | swc_ecma_transforms_macros = { version = "=1.0.1", optional = true } 74 | swc_ecma_transforms_optimization = { version = "=26.0.0", optional = true } 75 | swc_ecma_transforms_proposal = { version = "=25.0.0", optional = true } 76 | swc_ecma_transforms_react = { version = "=28.0.0", optional = true } 77 | swc_ecma_transforms_typescript = { version = "=28.0.0", optional = true } 78 | swc_ecma_utils = { version = "=21.0.0", optional = true } 79 | swc_ecma_visit = { version = "=15.0.0", optional = true } 80 | swc_bundler = { version = "=29.0.0", optional = true } 81 | swc_graph_analyzer = { version = "=14.0.1", optional = true } 82 | swc_macros_common = { version = "=1.0.1", optional = true } 83 | swc_sourcemap = { version = "9.3.4", optional = true } 84 | swc_trace_macro = { version = "=2.0.2", optional = true } 85 | swc_visit = { version = "=2.0.1", optional = true } 86 | swc_node_comments = { version = "=14.0.0", optional = true } 87 | 88 | 89 | [dev-dependencies] 90 | #green_copper_runtime = { git = 'https://github.com/HiRoFa/GreenCopperRuntime', branch="main", features = ["console"]} 91 | serde_json = "1" 92 | tracing = "0.1" 93 | tracing-log = "0.1" 94 | tracing-gelf = "0.7" 95 | simple-logging = "2.0.2" 96 | tokio = { version = "1", features = ["macros"] } 97 | 98 | 99 | [dev-dependencies.cargo-husky] 100 | version = "1.5.0" 101 | default-features = false # Disable features which are enabled by default 102 | 103 | 104 | # features = ["precommit-hook", "run-cargo-test", "run-cargo-clippy"] 105 | -------------------------------------------------------------------------------- /src/quickjs_utils/bigints.rs: -------------------------------------------------------------------------------- 1 | use crate::jsutils::JsError; 2 | use crate::quickjs_utils; 3 | use crate::quickjs_utils::{functions, primitives}; 4 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 5 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 6 | #[cfg(feature = "bellard")] 7 | use crate::quickjsvalueadapter::TAG_BIG_INT; 8 | use libquickjs_sys as q; 9 | 10 | pub fn new_bigint_i64_q( 11 | context: &QuickJsRealmAdapter, 12 | int: i64, 13 | ) -> Result { 14 | unsafe { new_bigint_i64(context.context, int) } 15 | } 16 | 17 | pub fn new_bigint_u64_q( 18 | context: &QuickJsRealmAdapter, 19 | int: u64, 20 | ) -> Result { 21 | unsafe { new_bigint_u64(context.context, int) } 22 | } 23 | 24 | #[allow(dead_code)] 25 | /// # Safety 26 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 27 | pub unsafe fn new_bigint_i64( 28 | context: *mut q::JSContext, 29 | int: i64, 30 | ) -> Result { 31 | let res_val = q::JS_NewBigInt64(context, int); 32 | let ret = QuickJsValueAdapter::new(context, res_val, false, true, "new_bigint_i64"); 33 | 34 | #[cfg(feature = "bellard")] 35 | { 36 | #[cfg(debug_assertions)] 37 | if ret.get_tag() == TAG_BIG_INT { 38 | assert_eq!(ret.get_ref_count(), 1); 39 | } 40 | } 41 | Ok(ret) 42 | } 43 | 44 | #[allow(dead_code)] 45 | /// # Safety 46 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 47 | pub unsafe fn new_bigint_u64( 48 | context: *mut q::JSContext, 49 | int: u64, 50 | ) -> Result { 51 | let res_val = q::JS_NewBigUint64(context, int); 52 | let ret = QuickJsValueAdapter::new(context, res_val, false, true, "new_bigint_u64"); 53 | 54 | #[cfg(feature = "bellard")] 55 | { 56 | #[cfg(debug_assertions)] 57 | if ret.get_tag() == TAG_BIG_INT { 58 | assert_eq!(ret.get_ref_count(), 1); 59 | } 60 | } 61 | Ok(ret) 62 | } 63 | 64 | pub fn new_bigint_str_q( 65 | context: &QuickJsRealmAdapter, 66 | input_str: &str, 67 | ) -> Result { 68 | unsafe { new_bigint_str(context.context, input_str) } 69 | } 70 | 71 | #[allow(dead_code)] 72 | /// # Safety 73 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 74 | pub unsafe fn new_bigint_str( 75 | context: *mut q::JSContext, 76 | input_str: &str, 77 | ) -> Result { 78 | let global_ref = quickjs_utils::get_global(context); 79 | let str_ref = primitives::from_string(context, input_str)?; 80 | let bigint_ref = functions::invoke_member_function(context, &global_ref, "BigInt", &[str_ref])?; 81 | let ret = bigint_ref; 82 | 83 | #[cfg(feature = "bellard")] 84 | assert_eq!(ret.get_ref_count(), 1); 85 | Ok(ret) 86 | } 87 | 88 | pub fn to_string_q( 89 | context: &QuickJsRealmAdapter, 90 | big_int_ref: &QuickJsValueAdapter, 91 | ) -> Result { 92 | unsafe { to_string(context.context, big_int_ref) } 93 | } 94 | 95 | #[allow(dead_code)] 96 | /// # Safety 97 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 98 | pub unsafe fn to_string( 99 | context: *mut q::JSContext, 100 | big_int_ref: &QuickJsValueAdapter, 101 | ) -> Result { 102 | if !big_int_ref.is_big_int() { 103 | return Err(JsError::new_str("big_int_ref was not a big_int")); 104 | } 105 | functions::call_to_string(context, big_int_ref) 106 | } 107 | 108 | #[cfg(test)] 109 | pub mod tests { 110 | use crate::facades::tests::init_test_rt; 111 | use crate::jsutils::Script; 112 | use crate::quickjs_utils::bigints; 113 | use crate::quickjs_utils::bigints::new_bigint_str_q; 114 | 115 | #[test] 116 | fn test_bigint() { 117 | let rt = init_test_rt(); 118 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 119 | let q_ctx = q_js_rt.get_main_realm(); 120 | 121 | let res = q_ctx 122 | .eval(Script::new("createABigInt.js", "BigInt(1234567890)")) 123 | .expect("script failed"); 124 | log::info!( 125 | "script bi was {} {}", 126 | res.get_tag(), 127 | res.to_string().expect("could not toString") 128 | ); 129 | 130 | let bi_ref = bigints::new_bigint_u64_q(q_ctx, 659863456456) 131 | .expect("could not create bigint from u64"); 132 | 133 | unsafe { 134 | if let Some(e) = crate::quickjs_utils::errors::get_exception(q_ctx.context) { 135 | log::error!("ex: {}", e); 136 | } 137 | } 138 | 139 | let to_str = bigints::to_string_q(q_ctx, &bi_ref).expect("could not tostring bigint"); 140 | assert_eq!(to_str, "659863456456"); 141 | let bi_ref = bigints::new_bigint_i64_q(q_ctx, 659863456457) 142 | .expect("could not create bigint from u64"); 143 | let to_str = bigints::to_string_q(q_ctx, &bi_ref).expect("could not tostring bigint"); 144 | assert_eq!(to_str, "659863456457"); 145 | 146 | let bi_ref = 147 | new_bigint_str_q(q_ctx, "345346345645234564536345345345345456534783448567") 148 | .expect("could not create bigint from str"); 149 | 150 | log::debug!("bi_ref.get_js_type is {}", bi_ref.get_js_type()); 151 | 152 | let to_str = bigints::to_string_q(q_ctx, &bi_ref).expect("could not tostring bigint"); 153 | assert_eq!(to_str, "345346345645234564536345345345345456534783448567"); 154 | }); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/jsutils/mod.rs: -------------------------------------------------------------------------------- 1 | //! This contains abstract traits and structs for use with different javascript runtimes 2 | //! the Adapter traits are use in the worker thread (EventLoop) of the Runtime and thus are not Send, they should never leave the thread 3 | //! The facade classes are for use outside the worker thread, they are Send 4 | //! 5 | 6 | use crate::values::JsValueFacade; 7 | use backtrace::Backtrace; 8 | use std::fmt::{Debug, Display, Error, Formatter}; 9 | 10 | pub mod helper_tasks; 11 | pub mod jsproxies; 12 | pub mod modules; 13 | pub mod promises; 14 | 15 | pub trait ScriptPreProcessor { 16 | fn process(&self, script: &mut Script) -> Result<(), JsError>; 17 | } 18 | 19 | /// the JsValueType represents the type of value for a JSValue 20 | #[derive(PartialEq, Copy, Clone, Eq)] 21 | pub enum JsValueType { 22 | I32, 23 | F64, 24 | String, 25 | Boolean, 26 | Object, 27 | Function, 28 | BigInt, 29 | Promise, 30 | Date, 31 | Null, 32 | Undefined, 33 | Array, 34 | Error, 35 | } 36 | 37 | impl Display for JsValueType { 38 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | JsValueType::I32 => f.write_str("I32"), 41 | JsValueType::F64 => f.write_str("F64"), 42 | JsValueType::String => f.write_str("String"), 43 | JsValueType::Boolean => f.write_str("Boolean"), 44 | JsValueType::Object => f.write_str("Object"), 45 | JsValueType::Function => f.write_str("Function"), 46 | JsValueType::BigInt => f.write_str("BigInt"), 47 | JsValueType::Promise => f.write_str("Promise"), 48 | JsValueType::Date => f.write_str("Date"), 49 | JsValueType::Null => f.write_str("Null"), 50 | JsValueType::Undefined => f.write_str("Undefined"), 51 | JsValueType::Array => f.write_str("Array"), 52 | JsValueType::Error => f.write_str("Error"), 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug)] 58 | pub struct JsError { 59 | name: String, 60 | message: String, 61 | stack: String, 62 | cause: Option>, 63 | } 64 | 65 | impl JsError { 66 | pub fn new(name: String, message: String, stack: String) -> Self { 67 | Self { 68 | name, 69 | message, 70 | stack, 71 | cause: None, 72 | } 73 | } 74 | pub fn new2(name: String, message: String, stack: String, cause: JsValueFacade) -> Self { 75 | Self { 76 | name, 77 | message, 78 | stack, 79 | cause: Some(Box::new(cause)), 80 | } 81 | } 82 | pub fn new_str(err: &str) -> Self { 83 | Self::new_string(err.to_string()) 84 | } 85 | pub fn new_string(err: String) -> Self { 86 | let bt = Backtrace::new(); 87 | JsError { 88 | name: "Error".to_string(), 89 | message: err, 90 | stack: format!("{bt:?}"), 91 | cause: None, 92 | } 93 | } 94 | pub fn get_message(&self) -> &str { 95 | self.message.as_str() 96 | } 97 | pub fn get_stack(&self) -> &str { 98 | self.stack.as_str() 99 | } 100 | pub fn get_name(&self) -> &str { 101 | self.name.as_str() 102 | } 103 | pub fn get_cause(&self) -> &Option> { 104 | &self.cause 105 | } 106 | } 107 | 108 | impl std::error::Error for JsError { 109 | fn description(&self) -> &str { 110 | self.get_message() 111 | } 112 | } 113 | 114 | impl From for JsError { 115 | fn from(err: anyhow::Error) -> Self { 116 | JsError::new_string(format!("{err:?}")) 117 | } 118 | } 119 | impl std::fmt::Display for JsError { 120 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { 121 | let e = format!("{}: {}\n{}", self.name, self.message, self.stack); 122 | f.write_str(e.as_str()) 123 | } 124 | } 125 | 126 | impl From for JsError { 127 | fn from(e: Error) -> Self { 128 | JsError::new_string(format!("{e:?}")) 129 | } 130 | } 131 | 132 | pub struct Script { 133 | path: String, 134 | code: String, 135 | transpiled_code: Option, 136 | map: Option, 137 | } 138 | 139 | impl Debug for Script { 140 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 141 | f.write_str(format!("Script:{}", self.path.as_str()).as_str()) 142 | } 143 | } 144 | 145 | impl Script { 146 | pub fn new(absolute_path: &str, script_code: &str) -> Self { 147 | Self { 148 | path: absolute_path.to_string(), 149 | code: script_code.to_string(), 150 | transpiled_code: None, 151 | map: None, 152 | } 153 | } 154 | pub fn get_path(&self) -> &str { 155 | self.path.as_str() 156 | } 157 | pub fn get_code(&self) -> &str { 158 | self.code.as_str() 159 | } 160 | pub fn get_runnable_code(&self) -> &str { 161 | if let Some(t_code) = self.transpiled_code.as_ref() { 162 | t_code.as_str() 163 | } else { 164 | self.code.as_str() 165 | } 166 | } 167 | pub fn set_code(&mut self, code: String) { 168 | self.code = code; 169 | } 170 | pub fn set_transpiled_code(&mut self, transpiled_code: String, map: Option) { 171 | self.transpiled_code = Some(transpiled_code); 172 | self.map = map; 173 | } 174 | pub fn get_map(&self) -> Option<&str> { 175 | self.map.as_deref() 176 | } 177 | } 178 | 179 | impl Clone for Script { 180 | fn clone(&self) -> Self { 181 | Self { 182 | path: self.path.clone(), 183 | code: self.code.clone(), 184 | transpiled_code: self.transpiled_code.clone(), 185 | map: self.map.clone(), 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/quickjs_utils/primitives.rs: -------------------------------------------------------------------------------- 1 | use crate::jsutils::JsError; 2 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 3 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 4 | use core::ptr; 5 | use libquickjs_sys as q; 6 | use std::os::raw::c_char; 7 | 8 | pub fn to_bool(value_ref: &QuickJsValueAdapter) -> Result { 9 | if value_ref.is_bool() { 10 | let r = value_ref.borrow_value(); 11 | let raw = unsafe { r.u.int32 }; 12 | let val: bool = raw > 0; 13 | Ok(val) 14 | } else { 15 | Err(JsError::new_str("value is not a boolean")) 16 | } 17 | } 18 | 19 | pub fn from_bool(b: bool) -> QuickJsValueAdapter { 20 | let raw = unsafe { q::JS_NewBool(ptr::null_mut(), b) }; 21 | QuickJsValueAdapter::new_no_context(raw, "primitives::from_bool") 22 | } 23 | 24 | pub fn to_f64(value_ref: &QuickJsValueAdapter) -> Result { 25 | if value_ref.is_f64() { 26 | let r = value_ref.borrow_value(); 27 | let val = unsafe { r.u.float64 }; 28 | Ok(val) 29 | } else { 30 | Err(JsError::new_str("value was not a float64")) 31 | } 32 | } 33 | 34 | pub fn from_f64(f: f64) -> QuickJsValueAdapter { 35 | #[cfg(feature = "bellard")] 36 | let raw = unsafe { q::JS_NewFloat64(ptr::null_mut(), f) }; 37 | 38 | #[cfg(feature = "quickjs-ng")] 39 | let raw = unsafe { q::JS_NewNumber(ptr::null_mut(), f) }; 40 | 41 | QuickJsValueAdapter::new_no_context(raw, "primitives::from_f64") 42 | } 43 | 44 | pub fn to_i32(value_ref: &QuickJsValueAdapter) -> Result { 45 | if value_ref.is_i32() { 46 | let r = value_ref.borrow_value(); 47 | let val: i32 = unsafe { r.u.int32 }; 48 | Ok(val) 49 | } else { 50 | Err(JsError::new_str("val is not an int")) 51 | } 52 | } 53 | 54 | pub fn from_i32(i: i32) -> QuickJsValueAdapter { 55 | let raw = unsafe { q::JS_NewInt32(ptr::null_mut(), i) }; 56 | QuickJsValueAdapter::new_no_context(raw, "primitives::from_i32") 57 | } 58 | 59 | pub fn to_string_q( 60 | q_ctx: &QuickJsRealmAdapter, 61 | value_ref: &QuickJsValueAdapter, 62 | ) -> Result { 63 | unsafe { to_string(q_ctx.context, value_ref) } 64 | } 65 | /// # Safety 66 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 67 | pub unsafe fn to_string( 68 | context: *mut q::JSContext, 69 | value_ref: &QuickJsValueAdapter, 70 | ) -> Result { 71 | //log::trace!("primitives::to_string on {}", value_ref.borrow_value().tag); 72 | 73 | assert!(value_ref.is_string()); 74 | 75 | let mut len = 0; 76 | 77 | #[cfg(feature = "bellard")] 78 | let ptr: *const c_char = q::JS_ToCStringLen2(context, &mut len, *value_ref.borrow_value(), 0); 79 | #[cfg(feature = "quickjs-ng")] 80 | let ptr: *const c_char = 81 | q::JS_ToCStringLen2(context, &mut len, *value_ref.borrow_value(), false); 82 | 83 | if len == 0 { 84 | return Ok("".to_string()); 85 | } 86 | 87 | if ptr.is_null() { 88 | return Err(JsError::new_str( 89 | "Could not convert string: got a null pointer", 90 | )); 91 | } 92 | 93 | let bytes = std::slice::from_raw_parts(ptr as *const u8, len); 94 | 95 | // Convert to String (validate UTF-8). 96 | let s = String::from_utf8_lossy(bytes).into_owned(); 97 | 98 | // Free the c string. 99 | q::JS_FreeCString(context, ptr); 100 | 101 | Ok(s) 102 | } 103 | 104 | pub fn from_string_q(q_ctx: &QuickJsRealmAdapter, s: &str) -> Result { 105 | unsafe { from_string(q_ctx.context, s) } 106 | } 107 | /// # Safety 108 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 109 | pub unsafe fn from_string( 110 | context: *mut q::JSContext, 111 | s: &str, 112 | ) -> Result { 113 | let qval = q::JS_NewStringLen(context, s.as_ptr() as *const c_char, s.len() as _); 114 | let ret = QuickJsValueAdapter::new(context, qval, false, true, "primitives::from_string qval"); 115 | if ret.is_exception() { 116 | return Err(JsError::new_str("Could not create string in runtime")); 117 | } 118 | 119 | Ok(ret) 120 | } 121 | 122 | #[cfg(test)] 123 | pub mod tests { 124 | use crate::facades::tests::init_test_rt; 125 | use crate::jsutils::Script; 126 | 127 | #[tokio::test] 128 | async fn test_emoji() { 129 | let rt = init_test_rt(); 130 | 131 | let res = rt.eval(None, Script::new("testEmoji.js", "'hi'")).await; 132 | 133 | match res { 134 | Ok(fac) => { 135 | assert_eq!(fac.get_str(), "hi"); 136 | } 137 | Err(e) => { 138 | panic!("script failed: {}", e); 139 | } 140 | } 141 | 142 | let res = rt.eval(None, Script::new("testEmoji.js", "'👍'")).await; 143 | 144 | match res { 145 | Ok(fac) => { 146 | assert_eq!(fac.get_str(), "👍"); 147 | } 148 | Err(e) => { 149 | panic!("script failed: {}", e); 150 | } 151 | } 152 | 153 | let res = rt.eval(None, Script::new("testEmoji.js", "'pre👍'")).await; 154 | 155 | match res { 156 | Ok(fac) => { 157 | assert_eq!(fac.get_str(), "pre👍"); 158 | } 159 | Err(e) => { 160 | panic!("script failed: {}", e); 161 | } 162 | } 163 | 164 | let res = rt.eval(None, Script::new("testEmoji.js", "'👍post'")).await; 165 | 166 | match res { 167 | Ok(fac) => { 168 | assert_eq!(fac.get_str(), "👍post"); 169 | } 170 | Err(e) => { 171 | panic!("script failed: {}", e); 172 | } 173 | } 174 | 175 | let res = rt 176 | .eval(None, Script::new("testEmoji.js", "'pre👍post'")) 177 | .await; 178 | 179 | match res { 180 | Ok(fac) => { 181 | assert_eq!(fac.get_str(), "pre👍post"); 182 | } 183 | Err(e) => { 184 | panic!("script failed: {}", e); 185 | } 186 | } 187 | 188 | let res = rt 189 | .eval( 190 | None, 191 | Script::new("testEmoji.js", "JSON.stringify({c: '👍'})"), 192 | ) 193 | .await; 194 | 195 | match res { 196 | Ok(fac) => { 197 | assert_eq!(fac.get_str(), "{\"c\":\"👍\"}"); 198 | } 199 | Err(e) => { 200 | panic!("script failed: {}", e); 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quickjs_runtime 2 | 3 | Quickjs_runtime is a library for quickly getting started with embedding a javascript engine in your rust project. 4 | 5 | Relies on [hirofa-quickjs-sys](https://github.com/HiRoFa/quickjs-sys) to support quickjs-ng as well as the original 6 | quickjs 7 | 8 | Quickjs_runtime runs all javascript action in a single thread using an EventLoop. This means you can call javascript 9 | safely from several threads by adding tasks to the EventLoop. 10 | 11 | # quickjs or quickjs-ng 12 | 13 | Quickjs_runtime supports both the original quickjs and the quickjs-ng project. 14 | 15 | You can use quickjs-ng by adding the dep to quickjs_runtime like this: 16 | 17 | ```toml 18 | quickjs_runtime = { version = "0.16", features = ["console", "setimmediate", "setinterval", "settimeout", "typescript", "quickjs-ng"], default-features = false } 19 | ``` 20 | 21 | # OS support 22 | 23 | | features | linux | mac | windows | 24 | |----------------|-------|-----|----------------| 25 | | **bellard** | yes | yes | mingW only | 26 | | **quickjs-ng** | yes | yes | mingW and MSVC | 27 | 28 | # Usage and Features 29 | 30 | An example on how to embed a script engine in rust using this lib can be found 31 | here: [github.com/andrieshiemstra/ScriptExtensionLayerExample](https://github.com/andrieshiemstra/ScriptExtensionLayerExample). 32 | It was published in TWIR as a walkthrough. 33 | 34 | Quickjs_runtime focuses on making [quickjs](https://bellard.org/quickjs/) easy to use and does not add any additional 35 | features, that's where these projects come in: 36 | 37 | * A more feature-rich (e.g. fetch api support, http based module loader and much more) 38 | runtime: [GreenCopperRuntime](https://github.com/HiRoFa/GreenCopperRuntime). 39 | * The commandline client: [GreenCopperCmd](https://github.com/HiRoFa/GreenCopperCmd). 40 | 41 | Please see the [DOCS](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/index.html) for all inner workings 42 | 43 | # This lib serves two main goals: 44 | 45 | ## 1. Provide simple utils for working with quickjs (these are located in the quickjs_utils mod) 46 | 47 | * The QuickJsRuntime struct, this is to be used from a single thread 48 | * E.g. objects::set_property(), functions::invoke_func() 49 | * Wrap JSValue to provide reference counting (+1 on init, -1 on 50 | drop) ([QuickJsValueAdapter](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/quickjsvalueadapter/struct.QuickJsValueAdapter.html)) 51 | * Pass a module loader 52 | 53 | ## 2. Wrap quickjs for use as a ready to go JavaScript Runtime 54 | 55 | * Start at the QuickjsRuntimeFacade, it provides an EventQueue which has a thread_local QuickJsRuntimeAdapter 56 | * All values are copied or abstracted in a JsValueFacades 57 | * So no need to worry about Garbage collection 58 | * Evaluate script and invoke functions while waiting for results blocking or with async/await 59 | * Get Promise result blocking or with async/await 60 | 61 | # What works? 62 | 63 | ## Script and Modules 64 | 65 | * Typescript (via SWC) 66 | * Console ( 67 | .log/info/debug/trace/error) ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/features/console/index.html)) 68 | * Eval 69 | script ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/facades/struct.QuickJsRuntimeFacade.html#method.eval)) 70 | * Create promises in JavaScript which execute async 71 | * Eval 72 | modules ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/facades/struct.QuickJsRuntimeFacade.html#method.eval_module)) 73 | * Load modules (dynamic and 74 | static) ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/builder/struct.QuickJsRuntimeBuilder.html#method.script_module_loader)) 75 | * Fetch api (impl in [GreenCopperRuntime](https://github.com/HiRoFa/GreenCopperRuntime)) 76 | * setImmediate 77 | * setTimeout/Interval (and clear) 78 | * Script preprocessing (impls for ifdef/macro's/typescript can be found 79 | in [GreenCopperRuntime](https://github.com/HiRoFa/GreenCopperRuntime)) 80 | 81 | ## Rust-Script interoperability 82 | 83 | * Return Promises from rust functions and resolve them from 84 | rust ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/jsutils/promises/fn.new_resolving_promise.html)) 85 | * Add functions from 86 | rust ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/quickjsrealmadapter/struct.QuickJsRealmAdapter.html#method.install_function)) 87 | * Invoke JS functions from 88 | rust ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/facades/struct.QuickJsRuntimeFacade.html#method.invoke_function)) 89 | * Pass primitives, objects and arrays from and to 90 | rust ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/quickjs_utils/primitives/index.html)) 91 | * Create Classes from 92 | rust ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/reflection/struct.Proxy.html)) 93 | * Async/await support on eval/call_function/promise 94 | resolution ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/values/struct.CachedJsPromiseRef.html#method.get_promise_result)) 95 | * Import native Modules (e.g. dynamic loading of rust functions or Proxy 96 | classes) ([docs](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/builder/struct.QuickJsRuntimeBuilder.html#method.native_module_loader)) 97 | 98 | # Goals 99 | 100 | Embedding a script engine in a rust project seems a very tedious job which involves learning a lot about the inner 101 | workings of that engine. 102 | 103 | The main goal of this project is to make that job easy! 104 | 105 | The manner in which this is achieved is primarily focused on abstracting the workings of the engine from the 106 | implementor, therefore some functionality may not be the fastest way of getting things done. 107 | 108 | So a second goal is to make implementing a fast and efficient integration doable for the uninitiated, the most common 109 | tasks you do with the engine should be doable with the utils in this package and working examples should be provided in 110 | the test modules. 111 | 112 | The reason I chose QuickJS as the engine is that I've been dealing with less modern engines in my java projects and not 113 | being able to use the latest and greatest ECMA-script features becomes quite disappointing at times. 114 | 115 | The fun stuff about QuickJS: 116 | 117 | * small footprint 118 | * fast compilation / startup 119 | * great JS compatibility 120 | 121 | # examples 122 | 123 | Cargo.toml 124 | 125 | ```toml 126 | [dependencies] 127 | quickjs_runtime = "0.16" 128 | ``` 129 | 130 | Here are some quickstarts: 131 | 132 | * start by reading the [DOCS](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/index.html) 133 | * [eval a script](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/facades/struct.QuickJsRuntimeFacade.html#method.eval) 134 | 135 | The quickjs Api utils: 136 | 137 | * [quickjs_utils](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/quickjs_utils/index.html) 138 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: -D warnings 10 | 11 | jobs: 12 | build-test-win: 13 | runs-on: windows-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: Install stable toolchain (windows) 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | target: x86_64-pc-windows-gnu 23 | override: true 24 | 25 | - name: Setup (windows) 26 | run: | 27 | $env:PATH = "C:\msys64\mingw64\bin;C:\msys64\usr\bin;$env:PATH" 28 | echo "PATH=${env:PATH}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 29 | echo "CARGO_BUILD_TARGET=x86_64-pc-windows-gnu" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 30 | 31 | - name: Build (windows) 32 | run: | 33 | cargo clean 34 | cargo build --verbose 35 | 36 | - name: Test (windows) 37 | run: | 38 | cargo test --verbose -- --test-threads 1 --nocapture 39 | 40 | build-test-win-ng: 41 | runs-on: windows-latest 42 | steps: 43 | - uses: actions/checkout@v1 44 | 45 | - name: Install stable toolchain (windows) 46 | uses: actions-rs/toolchain@v1 47 | with: 48 | profile: minimal 49 | toolchain: stable 50 | target: x86_64-pc-windows-gnu 51 | override: true 52 | 53 | - name: Setup (windows) 54 | run: | 55 | $env:PATH = "C:\msys64\mingw64\bin;C:\msys64\usr\bin;$env:PATH" 56 | echo "PATH=${env:PATH}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 57 | echo "CARGO_BUILD_TARGET=x86_64-pc-windows-gnu" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 58 | 59 | - name: Build (windows) 60 | run: | 61 | cargo clean 62 | cargo build --verbose --no-default-features --features quickjs-ng,console 63 | - name: Test (windows) 64 | run: | 65 | cargo test --verbose --no-default-features --features quickjs-ng,console -- --test-threads 1 --nocapture 66 | 67 | build-test-win-msvc-ng: 68 | runs-on: windows-latest 69 | steps: 70 | - uses: actions/checkout@v1 71 | 72 | - name: Install stable toolchain (windows) 73 | uses: actions-rs/toolchain@v1 74 | with: 75 | profile: minimal 76 | toolchain: stable 77 | target: x86_64-pc-windows-msvc 78 | override: true 79 | 80 | - name: Setup (windows) 81 | run: | 82 | echo "CARGO_BUILD_TARGET=x86_64-pc-windows-msvc" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 83 | 84 | - name: Build (windows) 85 | run: | 86 | cargo clean 87 | cargo build --verbose --no-default-features --features quickjs-ng,console 88 | 89 | - name: Test (windows) 90 | run: | 91 | cargo test --verbose --no-default-features --features quickjs-ng,console -- --test-threads 1 --nocapture 92 | 93 | build-test-mac: 94 | runs-on: macOS-latest 95 | steps: 96 | - uses: actions/checkout@v3 97 | - name: Build 98 | run: cargo build 99 | - name: Run tests 100 | run: cargo test --verbose -- --test-threads 1 --nocapture 101 | 102 | build-test-mac-ng: 103 | runs-on: macOS-latest 104 | steps: 105 | - uses: actions/checkout@v3 106 | - name: Build 107 | run: cargo build --verbose --no-default-features --features quickjs-ng,console 108 | - name: Run tests 109 | run: cargo test --verbose --no-default-features --features quickjs-ng,console -- --test-threads 1 --nocapture 110 | 111 | build-ubuntu-quickjs-ng: 112 | runs-on: ubuntu-latest 113 | steps: 114 | - uses: actions/checkout@v2 115 | - name: Prepare 116 | run: | 117 | sudo apt update 118 | sudo apt install llvm autoconf2.13 automake clang -y 119 | - name: Run tests 120 | run: cargo test --no-default-features --features "quickjs-ng,console setimmediate setinterval settimeout typescript" -- --test-threads 1 --nocapture 121 | 122 | build: 123 | runs-on: ubuntu-latest 124 | steps: 125 | - uses: actions/checkout@v2 126 | - name: Prepare 127 | run: | 128 | sudo apt update 129 | sudo apt install llvm autoconf2.13 automake clang valgrind -y 130 | - name: Build 131 | run: | 132 | export SHELL=/bin/bash 133 | export CC=/usr/bin/clang 134 | export CXX=/usr/bin/clang++ 135 | cargo build 136 | - name: Format 137 | run: | 138 | cargo fmt 139 | - name: Commit fmt files 140 | run: | 141 | git config --local user.email "action@github.com" 142 | git config --local user.name "GitHub Action" 143 | git commit -m "autofmt" -a || true 144 | git push "https://${{ github.actor }}:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git" HEAD:${{ github.ref }} || true 145 | - name: Doc 146 | run: | 147 | cargo doc 148 | - name: Commit docs 149 | run: | 150 | cp -r ./target/doc /tmp 151 | cd /tmp/doc 152 | git init 153 | echo 'Redirect

Redirecting

' >> index.html 154 | git add . 155 | git remote add origin https://github.com/${{ github.repository }}.git 156 | git config --local user.email "action@github.com" 157 | git config --local user.name "GitHub Action" 158 | git commit -m "doc" -a || true 159 | git push "https://${{ github.actor }}:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git" HEAD:gh-pages --force || true 160 | - name: Deploy to gh-pages 161 | run: | 162 | curl -X POST https://api.github.com/repos/${{ github.repository }}/pages/builds -H "Accept: application/vnd.github.mister-fantastic-preview+json" -u ${{ github.actor }}:${{ secrets.GH_TOKEN }} 163 | - name: Run tests 164 | run: cargo test --verbose -- --test-threads 1 --nocapture 165 | - name: Clippy check 166 | uses: actions-rs/clippy-check@v1 167 | with: 168 | token: ${{ secrets.GITHUB_TOKEN }} 169 | 170 | test-and-valgrind: 171 | if: ${{ false }} 172 | runs-on: ubuntu-latest 173 | steps: 174 | - uses: actions/checkout@v2 175 | - name: Prepare 176 | run: | 177 | sudo apt update 178 | sudo apt install llvm autoconf2.13 automake clang valgrind -y 179 | - name: Build 180 | run: | 181 | export SHELL=/bin/bash 182 | export CC=/usr/bin/clang 183 | export CXX=/usr/bin/clang++ 184 | cargo build --verbose 185 | - name: Run tests 186 | run: cargo test --verbose 187 | - name: Valgrind 188 | run: | 189 | find ./target/debug/deps/quickjs_runtime-* -maxdepth 1 -type f -executable | xargs valgrind --leak-check=full --error-exitcode=1 -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # quickjs_runtime 2 | //! This crate consists of two main parts: 3 | //! * thread-safe utils and wrappers 4 | //! you can call these from any thread, all logic is directed to a single worker-thread(EventLoop) which invokes the quickjs API 5 | //! * quickjs bindings and utils 6 | //! these talk to the quickjs API directly and need to run in the same thread as the Runtime 7 | //! 8 | //! ## Noteworthy structs 9 | //! 10 | //! These are the structs you'll use the most 11 | //! 12 | //! | Thread safe (Facades) | Runtime Thread-local (Adapters) | 13 | //! | --- | --- | 14 | //! | [QuickJsRuntimeFacade](facades/struct.QuickJsRuntimeFacade.html) the 'starting point' | [QuickJsRuntimeAdapter](quickjsruntimeadapter/struct.QuickJsRuntimeAdapter.html) the wrapper for all things quickjs | 15 | //! | - | [QuickJsRealmAdapter](quickjsrealmadapter/struct.QuickJsRealmAdapter.html) a realm or context | 16 | //! | [JsValueFacade](https://hirofa.github.io/utils/hirofa_utils/js_utils/facades/values/enum.JsValueFacade.html) copy of- or reference to a value in the JsRuntimeAdapter | [QuickJsValueAdapter](quickjsvalueadapter/struct.QuickJsValueAdapter.html) reference counting pointer to a Value | 17 | //! 18 | //! ## Doing something in the runtime worker thread 19 | //! 20 | //! You always start with building a new [QuickjsRuntimeFacade](facades/struct.QuickjsRuntimeFacade.html) 21 | //! 22 | //! ```dontrun 23 | //! use quickjs_runtime::builder::QuickJsRuntimeBuilder; 24 | //! let rt: JsRuntimeFacade = QuickJsRuntimeBuilder::new().js_build(); 25 | //! ``` 26 | //! 27 | //! [QuickJsRuntimeFacade](facades/struct.QuickJsRuntimeFacade.html) has plenty public methods you can check out but one of the things you'll need to understand is how to communicate with the [QuickJsRuntimeAdapter](quickjsruntimeadapter/struct.QuickJsRuntimeAdapter.html) and the [QuickJsRealmAdapter](quickjsrealmadapter/struct.QuickJsRealmAdapter.html) 28 | //! This is done by adding a job to the [EventLoop](https://hirofa.github.io/utils/hirofa_utils/eventloop/struct.EventLoop.html) of the [QuickJsRuntimeFacade](facades/struct.QuickJsRuntimeFacade.html) 29 | //! 30 | //! ```dontrun 31 | //! // with the first Option you may specify which realm to use, None indicates the default or main realm 32 | //! let res = rt.loop_realm(None, |rt: QuickJsRuntimeAdapter, realm: QuickJsRealmAdapter| { 33 | //! // this will run in the Worker thread, here we can use the Adapters 34 | //! // since we passed None as realm the realm adapter will be the "main" realm 35 | //! return true; 36 | //! }).await; 37 | //! ``` 38 | //! All the non-sync functions return a Future so you can .await them from async functions. 39 | //! 40 | //! In order to do something and get the result synchronously you can use the sync variant 41 | //! ```dontrun 42 | //! use quickjs_runtime::quickjsruntime::QuickJsRuntime; 43 | //! let res = rt.loop_realm_sync(None, |rt, realm| { 44 | //! // this will run in the Worker thread, here we can use the quickjs API 45 | //! return 1; 46 | //! }); 47 | //! ``` 48 | //! 49 | //! One last thing you need to know is how to pass values from the js engine out of the worker thread 50 | //! 51 | //! This is where the JsValueFacade comes in 52 | //! 53 | //! ```dontrun 54 | //! 55 | //! // init a simple function 56 | //! rt.eval(Script::new("init_func.js", "globalThis.myObj = {someMember: {someFunction: function(input){return(input + " > hello rust!");}}};")).await; 57 | //! 58 | //! // create an input variable by using one of the constructor methods of the JsValueFacade 59 | //! let input_facade = JsValueFacade::new_str("hello js!"); 60 | //! // move it into a closure which will run in the worker thread 61 | //! let res = rt.loop_realm(None, move |rt: JsRuntimeAdapter, realm: JsRealmAdapter| { 62 | //! // convert the input JsValueFacade to JsValueAdapter 63 | //! let input_adapter = realm.from_js_value_facade(input_facade)?; 64 | //! // call myObj.someMember.someFunction(); 65 | //! let result_adapter = realm.invoke_function_by_name(&["myObj", "someMember"], "someFunction", &[input_adapter])?; 66 | //! // convert adapter to facade again so it may move out of the worker thread 67 | //! return realm.to_js_value_facade(&result_adapter); 68 | //! }).await; 69 | //! assert_eq!(res.get_str(), "hello_js! > hello rust!"); 70 | //! ``` 71 | //! 72 | //! For more details and examples please explore the packages below 73 | 74 | #[macro_use] 75 | extern crate lazy_static; 76 | extern crate core; 77 | 78 | pub mod builder; 79 | pub mod facades; 80 | #[cfg(any( 81 | feature = "settimeout", 82 | feature = "setinterval", 83 | feature = "console", 84 | feature = "setimmediate" 85 | ))] 86 | pub mod features; 87 | pub mod jsutils; 88 | pub mod quickjs_utils; 89 | pub mod quickjsrealmadapter; 90 | pub mod quickjsruntimeadapter; 91 | pub mod quickjsvalueadapter; 92 | pub mod reflection; 93 | #[cfg(feature = "typescript")] 94 | pub mod typescript; 95 | pub mod values; 96 | 97 | pub use libquickjs_sys; 98 | 99 | #[cfg(test)] 100 | pub mod tests { 101 | use crate::builder::QuickJsRuntimeBuilder; 102 | use crate::facades::tests::init_test_rt; 103 | use crate::facades::QuickJsRuntimeFacade; 104 | use crate::jsutils::jsproxies::JsProxy; 105 | use crate::jsutils::{JsError, Script}; 106 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 107 | use crate::values::{JsValueConvertable, JsValueFacade}; 108 | use futures::executor::block_on; 109 | use std::thread; 110 | use std::time::Duration; 111 | 112 | #[test] 113 | fn test_examples() { 114 | let rt = QuickJsRuntimeBuilder::new().build(); 115 | let outcome = block_on(run_examples(&rt)); 116 | if outcome.is_err() { 117 | log::error!("an error occured: {}", outcome.err().unwrap()); 118 | } 119 | log::info!("done"); 120 | } 121 | 122 | #[test] 123 | fn test_st() { 124 | let rt = init_test_rt(); 125 | 126 | let _res = rt 127 | .eval_sync( 128 | None, 129 | Script::new( 130 | "t.js", 131 | r#" 132 | 133 | async function a(){ 134 | await b(); 135 | } 136 | 137 | async function b(){ 138 | throw Error("poof"); 139 | } 140 | 141 | a().then(() => { 142 | console.log("a done"); 143 | }).catch(() => { 144 | console.log("a error"); 145 | }); 146 | 147 | 1 148 | "#, 149 | ), 150 | ) 151 | .expect("script failed"); 152 | thread::sleep(Duration::from_secs(1)); 153 | } 154 | 155 | async fn take_long() -> i32 { 156 | std::thread::sleep(Duration::from_millis(500)); 157 | 537 158 | } 159 | 160 | async fn run_examples(rt: &QuickJsRuntimeFacade) -> Result<(), JsError> { 161 | // ensure console.log calls get outputted 162 | //simple_logging::log_to_stderr(LevelFilter::Info); 163 | 164 | // do a simple eval on the main realm 165 | let eval_res = rt.eval(None, Script::new("simple_eval.js", "2*7;")).await?; 166 | log::info!("simple eval:{}", eval_res.get_i32()); 167 | 168 | // invoke a JS method from rust 169 | 170 | let meth_res = rt 171 | .invoke_function(None, &["Math"], "round", vec![12.321.to_js_value_facade()]) 172 | .await?; 173 | log::info!("Math.round(12.321) = {}", meth_res.get_i32()); 174 | 175 | // add a rust function to js as a callback 176 | 177 | let cb = JsValueFacade::new_callback(|args| { 178 | let a = args[0].get_i32(); 179 | let b = args[1].get_i32(); 180 | log::info!("rust cb was called with a:{} and b:{}", a, b); 181 | Ok(JsValueFacade::Null) 182 | }); 183 | rt.invoke_function( 184 | None, 185 | &[], 186 | "setTimeout", 187 | vec![ 188 | cb, 189 | 10.to_js_value_facade(), 190 | 12.to_js_value_facade(), 191 | 13.to_js_value_facade(), 192 | ], 193 | ) 194 | .await?; 195 | std::thread::sleep(Duration::from_millis(20)); 196 | log::info!("rust cb should have been called by now"); 197 | 198 | // create simple proxy class with an async function 199 | rt.loop_realm_sync(None, |_rt_adapter, realm_adapter| { 200 | let proxy = JsProxy::new() 201 | .namespace(&["com", "mystuff"]) 202 | .name("MyProxy") 203 | .static_method( 204 | "doSomething", 205 | |_rt_adapter, realm_adapter: &QuickJsRealmAdapter, _args| { 206 | realm_adapter.create_resolving_promise_async( 207 | async { Ok(take_long().await) }, 208 | |realm_adapter, producer_result| { 209 | realm_adapter.create_i32(producer_result) 210 | }, 211 | ) 212 | }, 213 | ); 214 | realm_adapter 215 | .install_proxy(proxy, true) 216 | .expect("could not install proxy"); 217 | }); 218 | 219 | rt.eval( 220 | None, 221 | Script::new( 222 | "testMyProxy.js", 223 | "async function a() {\ 224 | console.log('a called at %s ms', new Date().getTime());\ 225 | let res = await com.mystuff.MyProxy.doSomething();\ 226 | console.log('a got result %s at %s ms', res, new Date().getTime());\ 227 | }; a();", 228 | ), 229 | ) 230 | .await?; 231 | std::thread::sleep(Duration::from_millis(600)); 232 | log::info!("a should have been called by now"); 233 | 234 | Ok(()) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/quickjs_utils/arrays.rs: -------------------------------------------------------------------------------- 1 | use crate::jsutils::JsError; 2 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 3 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 4 | use libquickjs_sys as q; 5 | 6 | /// Check whether an object is an array 7 | /// # Example 8 | /// ```rust 9 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 10 | /// use quickjs_runtime::jsutils::Script; 11 | /// use quickjs_runtime::quickjs_utils::arrays; 12 | /// 13 | /// let rt = QuickJsRuntimeBuilder::new().build(); 14 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 15 | /// let q_ctx = q_js_rt.get_main_realm(); 16 | /// let obj_ref = q_ctx.eval(Script::new("is_array_test.es", "([1, 2, 3]);")).ok().expect("script failed"); 17 | /// let is_array = arrays::is_array_q(q_ctx, &obj_ref); 18 | /// assert!(is_array); 19 | /// }); 20 | /// ``` 21 | pub fn is_array_q(q_ctx: &QuickJsRealmAdapter, obj_ref: &QuickJsValueAdapter) -> bool { 22 | unsafe { is_array(q_ctx.context, obj_ref) } 23 | } 24 | 25 | /// # Safety 26 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 27 | #[allow(unused_variables)] 28 | pub unsafe fn is_array(context: *mut q::JSContext, obj_ref: &QuickJsValueAdapter) -> bool { 29 | let r = obj_ref.borrow_value(); 30 | 31 | #[cfg(feature = "bellard")] 32 | { 33 | let val = q::JS_IsArray(context, *r); 34 | val > 0 35 | } 36 | #[cfg(feature = "quickjs-ng")] 37 | { 38 | q::JS_IsArray(*r) 39 | } 40 | } 41 | 42 | /// Get the length of an Array 43 | /// # Example 44 | /// ```rust 45 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 46 | /// use quickjs_runtime::jsutils::Script; 47 | /// use quickjs_runtime::quickjs_utils::arrays; 48 | /// 49 | /// let rt = QuickJsRuntimeBuilder::new().build(); 50 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 51 | /// let q_ctx = q_js_rt.get_main_realm(); 52 | /// let obj_ref = q_ctx.eval(Script::new("get_length_test.es", "([1, 2, 3]);")).ok().expect("script failed"); 53 | /// let len = arrays::get_length_q(q_ctx, &obj_ref).ok().expect("could not get length"); 54 | /// assert_eq!(len, 3); 55 | /// }); 56 | /// ``` 57 | pub fn get_length_q( 58 | q_ctx: &QuickJsRealmAdapter, 59 | arr_ref: &QuickJsValueAdapter, 60 | ) -> Result { 61 | unsafe { get_length(q_ctx.context, arr_ref) } 62 | } 63 | 64 | /// Get the length of an Array 65 | /// # Safety 66 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 67 | pub unsafe fn get_length( 68 | context: *mut q::JSContext, 69 | arr_ref: &QuickJsValueAdapter, 70 | ) -> Result { 71 | let len_ref = crate::quickjs_utils::objects::get_property(context, arr_ref, "length")?; 72 | 73 | let len = crate::quickjs_utils::primitives::to_i32(&len_ref)?; 74 | 75 | Ok(len as u32) 76 | } 77 | 78 | /// Create a new Array 79 | /// # Example 80 | /// ```rust 81 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 82 | /// use quickjs_runtime::jsutils::Script; 83 | /// use quickjs_runtime::quickjs_utils::{arrays, primitives, functions}; 84 | /// use quickjs_runtime::quickjs_utils; 85 | /// 86 | /// let rt = QuickJsRuntimeBuilder::new().build(); 87 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 88 | /// let q_ctx = q_js_rt.get_main_realm(); 89 | /// // create a method to pass our new array to 90 | /// q_ctx.eval(Script::new("create_array_test.es", "this.create_array_func = function(arr){return arr.length;};")).ok().expect("script failed"); 91 | /// // create a new array 92 | /// let arr_ref = arrays::create_array_q(q_ctx).ok().expect("could not create array"); 93 | /// // add some values 94 | /// let val0 = primitives::from_i32(12); 95 | /// let val1 = primitives::from_i32(17); 96 | /// arrays::set_element_q(q_ctx, &arr_ref, 0, &val0).expect("could not set element"); 97 | /// arrays::set_element_q(q_ctx, &arr_ref, 1, &val1).expect("could not set element"); 98 | /// // call the function 99 | /// let result_ref = functions::invoke_member_function_q(q_ctx, &quickjs_utils::get_global_q(q_ctx), "create_array_func", &[arr_ref]).ok().expect("could not invoke function"); 100 | /// let len = primitives::to_i32(&result_ref).ok().unwrap(); 101 | /// assert_eq!(len, 2); 102 | /// }); 103 | /// ``` 104 | pub fn create_array_q(q_ctx: &QuickJsRealmAdapter) -> Result { 105 | unsafe { create_array(q_ctx.context) } 106 | } 107 | 108 | /// # Safety 109 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 110 | pub unsafe fn create_array(context: *mut q::JSContext) -> Result { 111 | let arr = q::JS_NewArray(context); 112 | let arr_ref = QuickJsValueAdapter::new(context, arr, false, true, "create_array"); 113 | if arr_ref.is_exception() { 114 | return Err(JsError::new_str("Could not create array in runtime")); 115 | } 116 | Ok(arr_ref) 117 | } 118 | 119 | /// Set a single element in an array 120 | /// # Example 121 | /// ```rust 122 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 123 | /// use quickjs_runtime::jsutils::Script; 124 | /// use quickjs_runtime::quickjs_utils::{arrays, primitives}; 125 | /// use quickjs_runtime::quickjs_utils; 126 | /// 127 | /// let rt = QuickJsRuntimeBuilder::new().build(); 128 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 129 | /// let q_ctx = q_js_rt.get_main_realm(); 130 | /// // get an Array from script 131 | /// let arr_ref = q_ctx.eval(Script::new("set_element_test.es", "([1, 2, 3]);")).ok().expect("script failed"); 132 | /// // add some values 133 | /// arrays::set_element_q(q_ctx, &arr_ref, 3, &primitives::from_i32(12)).expect("could not set element"); 134 | /// arrays::set_element_q(q_ctx, &arr_ref, 4, &primitives::from_i32(17)).expect("could not set element"); 135 | /// // get the length 136 | /// let len = arrays::get_length_q(q_ctx, &arr_ref).ok().unwrap(); 137 | /// assert_eq!(len, 5); 138 | /// }); 139 | /// ``` 140 | pub fn set_element_q( 141 | q_ctx: &QuickJsRealmAdapter, 142 | array_ref: &QuickJsValueAdapter, 143 | index: u32, 144 | entry_value_ref: &QuickJsValueAdapter, 145 | ) -> Result<(), JsError> { 146 | unsafe { set_element(q_ctx.context, array_ref, index, entry_value_ref) } 147 | } 148 | 149 | /// # Safety 150 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 151 | pub unsafe fn set_element( 152 | context: *mut q::JSContext, 153 | array_ref: &QuickJsValueAdapter, 154 | index: u32, 155 | entry_value_ref: &QuickJsValueAdapter, 156 | ) -> Result<(), JsError> { 157 | let ret = q::JS_DefinePropertyValueUint32( 158 | context, 159 | *array_ref.borrow_value(), 160 | index, 161 | entry_value_ref.clone_value_incr_rc(), 162 | q::JS_PROP_C_W_E as i32, 163 | ); 164 | if ret < 0 { 165 | return Err(JsError::new_str("Could not append element to array")); 166 | } 167 | Ok(()) 168 | } 169 | 170 | /// Get a single element from an array 171 | /// # Example 172 | /// ```rust 173 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 174 | /// use quickjs_runtime::jsutils::Script; 175 | /// use quickjs_runtime::quickjs_utils::{arrays, primitives}; 176 | /// use quickjs_runtime::quickjs_utils; 177 | /// 178 | /// let rt = QuickJsRuntimeBuilder::new().build(); 179 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 180 | /// let q_ctx = q_js_rt.get_main_realm(); 181 | /// // get an Array from script 182 | /// let arr_ref = q_ctx.eval(Script::new("get_element_test.es", "([1, 2, 3]);")).ok().expect("script failed"); 183 | /// // get a value, the 3 in this case 184 | /// let val_ref = arrays::get_element_q(q_ctx, &arr_ref, 2).ok().unwrap(); 185 | /// let val_i32 = primitives::to_i32(&val_ref).ok().unwrap(); 186 | /// // get the length 187 | /// assert_eq!(val_i32, 3); 188 | /// }); 189 | /// ``` 190 | pub fn get_element_q( 191 | q_ctx: &QuickJsRealmAdapter, 192 | array_ref: &QuickJsValueAdapter, 193 | index: u32, 194 | ) -> Result { 195 | unsafe { get_element(q_ctx.context, array_ref, index) } 196 | } 197 | 198 | /// # Safety 199 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 200 | pub unsafe fn get_element( 201 | context: *mut q::JSContext, 202 | array_ref: &QuickJsValueAdapter, 203 | index: u32, 204 | ) -> Result { 205 | let value_raw = q::JS_GetPropertyUint32(context, *array_ref.borrow_value(), index); 206 | let ret = QuickJsValueAdapter::new( 207 | context, 208 | value_raw, 209 | false, 210 | true, 211 | format!("get_element[{index}]").as_str(), 212 | ); 213 | if ret.is_exception() { 214 | return Err(JsError::new_str("Could not build array")); 215 | } 216 | Ok(ret) 217 | } 218 | 219 | #[cfg(test)] 220 | pub mod tests { 221 | use crate::facades::tests::init_test_rt; 222 | use crate::quickjs_utils::arrays::{create_array_q, get_element_q, set_element_q}; 223 | use crate::quickjs_utils::objects; 224 | 225 | #[test] 226 | fn test_array() { 227 | let rt = init_test_rt(); 228 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 229 | let q_ctx = q_js_rt.get_main_realm(); 230 | let arr = create_array_q(q_ctx).ok().unwrap(); 231 | 232 | #[cfg(feature = "bellard")] 233 | assert_eq!(arr.get_ref_count(), 1); 234 | 235 | let a = objects::create_object_q(q_ctx).ok().unwrap(); 236 | 237 | #[cfg(feature = "bellard")] 238 | assert_eq!(1, a.get_ref_count()); 239 | 240 | set_element_q(q_ctx, &arr, 0, &a).ok().unwrap(); 241 | 242 | #[cfg(feature = "bellard")] 243 | assert_eq!(2, a.get_ref_count()); 244 | 245 | let _a2 = get_element_q(q_ctx, &arr, 0).ok().unwrap(); 246 | 247 | #[cfg(feature = "bellard")] 248 | assert_eq!(3, a.get_ref_count()); 249 | #[cfg(feature = "bellard")] 250 | assert_eq!(3, _a2.get_ref_count()); 251 | }); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/quickjs_utils/json.rs: -------------------------------------------------------------------------------- 1 | //! serialize and stringify JavaScript objects 2 | 3 | use crate::jsutils::JsError; 4 | use crate::quickjs_utils; 5 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 6 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 7 | use libquickjs_sys as q; 8 | use std::ffi::CString; 9 | 10 | /// Parse a JSON string into an Object 11 | /// please note that JSON.parse requires member names to be enclosed in double quotes 12 | /// so {a: 1} and {'a': 1} will both fail 13 | /// {"a": 1} will parse ok 14 | /// # Example 15 | /// ```dontrun 16 | /// use quickjs_runtime::esruntimebuilder::EsRuntimeBuilder; 17 | /// use quickjs_runtime::quickjs_utils::{json, objects, primitives}; 18 | /// use quickjs_runtime::quickjs_utils::json::parse; 19 | /// let rt = EsRuntimeBuilder::new().build(); 20 | /// rt.add_to_event_queue_sync(|q_js_rt| { 21 | /// let q_ctx = q_js_rt.get_main_context(); 22 | /// let parse_res = json::parse_q(q_ctx, "{\"aaa\": 165}"); 23 | /// if parse_res.is_err() { 24 | /// panic!("could not parse: {}", parse_res.err().unwrap()); 25 | /// } 26 | /// let obj_ref = parse_res.ok().unwrap(); 27 | /// let a_ref = objects::get_property(q_ctx.context, &obj_ref, "aaa").ok().unwrap(); 28 | /// let i = primitives::to_i32(&a_ref).ok().unwrap(); 29 | /// assert_eq!(165, i); 30 | /// }); 31 | /// rt.gc_sync(); 32 | /// ``` 33 | pub fn parse_q(q_ctx: &QuickJsRealmAdapter, input: &str) -> Result { 34 | unsafe { parse(q_ctx.context, input) } 35 | } 36 | 37 | /// Parse a JSON string into an Object 38 | /// # Safety 39 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 40 | pub unsafe fn parse( 41 | context: *mut q::JSContext, 42 | input: &str, 43 | ) -> Result { 44 | let s = CString::new(input).ok().unwrap(); 45 | let f_n = CString::new("JSON.parse").ok().unwrap(); 46 | 47 | let len = input.len(); 48 | 49 | let val = q::JS_ParseJSON(context, s.as_ptr(), len as _, f_n.as_ptr()); 50 | 51 | let ret = QuickJsValueAdapter::new(context, val, false, true, "json::parse result"); 52 | 53 | if ret.is_exception() { 54 | if let Some(ex) = QuickJsRealmAdapter::get_exception(context) { 55 | Err(ex) 56 | } else { 57 | Err(JsError::new_str("unknown error while parsing json")) 58 | } 59 | } else { 60 | Ok(ret) 61 | } 62 | } 63 | /// Stringify an Object in script 64 | /// # Example 65 | /// ```rust 66 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 67 | /// use quickjs_runtime::quickjs_utils::{json, objects, primitives}; 68 | /// let rt = QuickJsRuntimeBuilder::new().build(); 69 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 70 | /// let q_ctx = q_js_rt.get_main_realm(); 71 | /// let obj_ref = objects::create_object_q(q_ctx).ok().unwrap(); 72 | /// objects::set_property_q(q_ctx, &obj_ref, "a", &primitives::from_i32(741)).ok().unwrap(); 73 | /// let str_ref = json::stringify_q(q_ctx, &obj_ref, None).ok().unwrap(); 74 | /// let str_str = primitives::to_string_q(q_ctx, &str_ref).ok().unwrap(); 75 | /// assert_eq!("{\"a\":741}", str_str); 76 | /// }); 77 | /// rt.gc_sync(); 78 | /// ``` 79 | pub fn stringify_q( 80 | q_ctx: &QuickJsRealmAdapter, 81 | input: &QuickJsValueAdapter, 82 | opt_space: Option, 83 | ) -> Result { 84 | unsafe { stringify(q_ctx.context, input, opt_space) } 85 | } 86 | 87 | /// # Safety 88 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 89 | pub unsafe fn stringify( 90 | context: *mut q::JSContext, 91 | input: &QuickJsValueAdapter, 92 | opt_space: Option, 93 | ) -> Result { 94 | //pub fn JS_JSONStringify( 95 | // ctx: *mut JSContext, 96 | // obj: JSValue, 97 | // replacer: JSValue, 98 | // space0: JSValue, 99 | // ) -> JSValue; 100 | 101 | let space_ref = match opt_space { 102 | None => quickjs_utils::new_null_ref(), 103 | Some(s) => s, 104 | }; 105 | 106 | let val = q::JS_JSONStringify( 107 | context, 108 | *input.borrow_value(), 109 | quickjs_utils::new_null(), 110 | *space_ref.borrow_value(), 111 | ); 112 | let ret = QuickJsValueAdapter::new(context, val, false, true, "json::stringify result"); 113 | 114 | if ret.is_exception() { 115 | if let Some(ex) = QuickJsRealmAdapter::get_exception(context) { 116 | Err(ex) 117 | } else { 118 | Err(JsError::new_str("unknown error in json::stringify")) 119 | } 120 | } else { 121 | Ok(ret) 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | pub mod tests { 127 | use crate::facades::tests::init_test_rt; 128 | use crate::jsutils::Script; 129 | use crate::quickjs_utils::json::parse_q; 130 | use crate::quickjs_utils::{get_global_q, json, objects, primitives}; 131 | use crate::values::JsValueFacade; 132 | use std::collections::HashMap; 133 | 134 | #[test] 135 | fn test_json() { 136 | let rt = init_test_rt(); 137 | 138 | log::info!("Starting json test"); 139 | 140 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 141 | let q_ctx = q_js_rt.get_main_realm(); 142 | 143 | let obj = objects::create_object_q(q_ctx).ok().unwrap(); 144 | objects::set_property_q(q_ctx, &obj, "a", &primitives::from_i32(532)) 145 | .ok() 146 | .unwrap(); 147 | objects::set_property_q(q_ctx, &obj, "b", &primitives::from_bool(true)) 148 | .ok() 149 | .unwrap(); 150 | objects::set_property_q( 151 | q_ctx, 152 | &obj, 153 | "c", 154 | &primitives::from_string_q(q_ctx, "abcdË").ok().unwrap(), 155 | ) 156 | .ok() 157 | .unwrap(); 158 | let str_res = json::stringify_q(q_ctx, &obj, None).ok().unwrap(); 159 | 160 | #[cfg(feature = "bellard")] 161 | assert_eq!(str_res.get_ref_count(), 1); 162 | let json = str_res.to_string().ok().unwrap(); 163 | assert_eq!(json, "{\"a\":532,\"b\":true,\"c\":\"abcdË\"}"); 164 | 165 | let obj2 = parse_q(q_ctx, json.as_str()).ok().unwrap(); 166 | 167 | let prop_c = objects::get_property_q(q_ctx, &obj2, "c").ok().unwrap(); 168 | assert_eq!("abcdË", prop_c.to_string().ok().unwrap()); 169 | }); 170 | } 171 | 172 | #[tokio::test] 173 | async fn test_json_arg() { 174 | let rt = init_test_rt(); 175 | 176 | // init my javascript function 177 | rt.eval( 178 | None, 179 | Script::new( 180 | "myFunc.js", 181 | r#" 182 | function myFunction(argObj) { 183 | console.log("I got an %s", typeof argObj); 184 | console.log("It looks like this %s", argObj); 185 | return "hello " + argObj["key"]; 186 | } 187 | "#, 188 | ), 189 | ) 190 | .await 191 | .ok() 192 | .expect("myFunc failed to parse"); 193 | 194 | // parse my obj to json 195 | let mut my_json_deserable_object = HashMap::new(); 196 | my_json_deserable_object.insert("key", "value"); 197 | let json = serde_json::to_string(&my_json_deserable_object) 198 | .ok() 199 | .expect("serializing failed"); 200 | 201 | let func_res = rt 202 | .loop_realm(None, move |_rt, realm| { 203 | // this runs in the worker thread for the EventLoop so json String needs to be moved here 204 | // now we parse the json to a JsValueRef 205 | let js_obj = parse_q(realm, json.as_str()) 206 | .ok() 207 | .expect("parsing json failed"); 208 | // then we can invoke the function with that js_obj as input 209 | // get the global obj as function container 210 | let global = get_global_q(realm); 211 | // invoke the function 212 | let func_res = crate::quickjs_utils::functions::invoke_member_function_q( 213 | realm, 214 | &global, 215 | "myFunction", 216 | &[js_obj], 217 | ); 218 | //return the value out of the worker thread as JsValueFacade 219 | realm.to_js_value_facade(&func_res.ok().expect("func failed")) 220 | }) 221 | .await; 222 | 223 | let jsv = func_res.ok().expect("got err"); 224 | assert_eq!(jsv.stringify(), "String: hello value"); 225 | } 226 | 227 | #[tokio::test] 228 | async fn test_json_arg2() { 229 | let rt = init_test_rt(); 230 | 231 | // init my javascript function 232 | rt.eval( 233 | None, 234 | Script::new( 235 | "myFunc.js", 236 | r#" 237 | function myFunction(argObj) { 238 | console.log("I got an %s", typeof argObj); 239 | console.log("It looks like this %s", argObj); 240 | return "hello " + argObj["key"]; 241 | } 242 | "#, 243 | ), 244 | ) 245 | .await 246 | .ok() 247 | .expect("myFunc failed to parse"); 248 | 249 | // parse my obj to json 250 | let mut my_json_deserable_object = HashMap::new(); 251 | my_json_deserable_object.insert("key", "value"); 252 | let json = serde_json::to_string(&my_json_deserable_object) 253 | .ok() 254 | .expect("serializing failed"); 255 | 256 | let json_js_value_facade = JsValueFacade::JsonStr { json }; 257 | 258 | let func_res = rt 259 | .invoke_function(None, &[], "myFunction", vec![json_js_value_facade]) 260 | .await; 261 | 262 | let jsv = func_res.ok().expect("got err"); 263 | assert_eq!(jsv.stringify(), "String: hello value"); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/quickjs_utils/sets.rs: -------------------------------------------------------------------------------- 1 | //! Set utils, these methods can be used to manage Set objects from rust 2 | //! see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Sap) for more on Sets 3 | 4 | use crate::jsutils::JsError; 5 | #[cfg(feature = "bellard")] 6 | use crate::quickjs_utils::class_ids::JS_CLASS_SET; 7 | use crate::quickjs_utils::objects::construct_object; 8 | use crate::quickjs_utils::{functions, get_constructor, iterators, objects, primitives}; 9 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 10 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 11 | use libquickjs_sys as q; 12 | #[cfg(feature = "bellard")] 13 | use libquickjs_sys::JS_GetClassID; 14 | #[cfg(feature = "quickjs-ng")] 15 | use libquickjs_sys::JS_IsSet; 16 | 17 | /// create new instance of Set 18 | /// # Example 19 | /// ```rust 20 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 21 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 22 | /// use quickjs_runtime::quickjs_utils::sets::new_set_q; 23 | /// 24 | /// let rt = QuickJsRuntimeBuilder::new().build(); 25 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 26 | /// let q_ctx = q_js_rt.get_main_realm(); 27 | /// let my_set: QuickJsValueAdapter = new_set_q(q_ctx).ok().unwrap(); 28 | /// }); 29 | /// ``` 30 | pub fn new_set_q(q_ctx: &QuickJsRealmAdapter) -> Result { 31 | unsafe { new_set(q_ctx.context) } 32 | } 33 | 34 | /// create new instance of Set 35 | /// # Safety 36 | /// please ensure the passed JSContext is still valid 37 | pub unsafe fn new_set(ctx: *mut q::JSContext) -> Result { 38 | let map_constructor = get_constructor(ctx, "Set")?; 39 | construct_object(ctx, &map_constructor, &[]) 40 | } 41 | 42 | /// see if a JSValueRef is an instance of Set 43 | pub fn is_set_q(obj: &QuickJsValueAdapter) -> bool { 44 | unsafe { is_set(obj) } 45 | } 46 | 47 | /// see if a JSValueRef is an instance of Set 48 | /// # Safety 49 | /// please ensure the passed JSContext is still valid 50 | pub unsafe fn is_set(obj: &QuickJsValueAdapter) -> bool { 51 | #[cfg(feature = "bellard")] 52 | return JS_GetClassID(*obj.borrow_value()) == JS_CLASS_SET; 53 | 54 | #[cfg(feature = "quickjs-ng")] 55 | return JS_IsSet(*obj.borrow_value()); 56 | } 57 | 58 | /// add a value to the Set 59 | /// # Example 60 | /// ```rust 61 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 62 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 63 | /// use quickjs_runtime::quickjs_utils::primitives; 64 | /// use quickjs_runtime::quickjs_utils::sets::{new_set_q, add_q}; 65 | /// 66 | /// let rt = QuickJsRuntimeBuilder::new().build(); 67 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 68 | /// let q_ctx = q_js_rt.get_main_realm(); 69 | /// let my_set: QuickJsValueAdapter = new_set_q(q_ctx).ok().unwrap(); 70 | /// let value = primitives::from_i32(23); 71 | /// add_q(q_ctx, &my_set, value).ok().unwrap(); 72 | /// }); 73 | /// ``` 74 | pub fn add_q( 75 | q_ctx: &QuickJsRealmAdapter, 76 | set: &QuickJsValueAdapter, 77 | val: QuickJsValueAdapter, 78 | ) -> Result { 79 | unsafe { add(q_ctx.context, set, val) } 80 | } 81 | 82 | /// add a value to a Set 83 | /// # Safety 84 | /// please ensure the passed JSContext is still valid 85 | pub unsafe fn add( 86 | ctx: *mut q::JSContext, 87 | set: &QuickJsValueAdapter, 88 | val: QuickJsValueAdapter, 89 | ) -> Result { 90 | functions::invoke_member_function(ctx, set, "add", &[val]) 91 | } 92 | 93 | /// delete a value from a set 94 | /// # Example 95 | /// ```rust 96 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 97 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 98 | /// use quickjs_runtime::quickjs_utils::primitives; 99 | /// use quickjs_runtime::quickjs_utils::sets::{add_q, new_set_q, delete_q}; 100 | /// 101 | /// let rt = QuickJsRuntimeBuilder::new().build(); 102 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 103 | /// let q_ctx = q_js_rt.get_main_realm(); 104 | /// let my_set: QuickJsValueAdapter = new_set_q(q_ctx).ok().unwrap(); 105 | /// let value = primitives::from_i32(23); 106 | /// add_q(q_ctx, &my_set, value.clone()).ok().unwrap(); 107 | /// delete_q(q_ctx, &my_set, value).ok().unwrap(); 108 | /// }); 109 | /// ``` 110 | pub fn delete_q( 111 | q_ctx: &QuickJsRealmAdapter, 112 | set: &QuickJsValueAdapter, 113 | value: QuickJsValueAdapter, 114 | ) -> Result { 115 | unsafe { delete(q_ctx.context, set, value) } 116 | } 117 | 118 | /// delete a value from a set 119 | /// # Safety 120 | /// please ensure the passed JSContext is still valid 121 | pub unsafe fn delete( 122 | ctx: *mut q::JSContext, 123 | set: &QuickJsValueAdapter, 124 | value: QuickJsValueAdapter, 125 | ) -> Result { 126 | let res = functions::invoke_member_function(ctx, set, "delete", &[value])?; 127 | primitives::to_bool(&res) 128 | } 129 | 130 | /// check whether a Set has a certain value 131 | /// # Example 132 | /// ```rust 133 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 134 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 135 | /// use quickjs_runtime::quickjs_utils::primitives; 136 | /// use quickjs_runtime::quickjs_utils::sets::{new_set_q, add_q, has_q}; 137 | /// 138 | /// let rt = QuickJsRuntimeBuilder::new().build(); 139 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 140 | /// let q_ctx = q_js_rt.get_main_realm(); 141 | /// let my_set: QuickJsValueAdapter = new_set_q(q_ctx).ok().unwrap(); 142 | /// let value = primitives::from_i32(23); 143 | /// add_q(q_ctx, &my_set, value.clone()).ok().unwrap(); 144 | /// let bln_has = has_q(q_ctx, &my_set, value).ok().unwrap(); 145 | /// assert!(bln_has); 146 | /// }); 147 | /// ``` 148 | pub fn has_q( 149 | q_ctx: &QuickJsRealmAdapter, 150 | set: &QuickJsValueAdapter, 151 | key: QuickJsValueAdapter, 152 | ) -> Result { 153 | unsafe { has(q_ctx.context, set, key) } 154 | } 155 | 156 | /// check whether a Set has a value 157 | /// # Safety 158 | /// please ensure the passed JSContext is still valid 159 | pub unsafe fn has( 160 | ctx: *mut q::JSContext, 161 | set: &QuickJsValueAdapter, 162 | key: QuickJsValueAdapter, 163 | ) -> Result { 164 | let res = functions::invoke_member_function(ctx, set, "has", &[key])?; 165 | primitives::to_bool(&res) 166 | } 167 | 168 | /// get the number of entries in a Set 169 | /// # Example 170 | /// ```rust 171 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 172 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 173 | /// use quickjs_runtime::quickjs_utils::primitives; 174 | /// use quickjs_runtime::quickjs_utils::sets::{add_q, new_set_q, size_q}; 175 | /// 176 | /// let rt = QuickJsRuntimeBuilder::new().build(); 177 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 178 | /// let q_ctx = q_js_rt.get_main_realm(); 179 | /// let my_set: QuickJsValueAdapter = new_set_q(q_ctx).ok().unwrap(); 180 | /// let value = primitives::from_i32(23); 181 | /// add_q(q_ctx, &my_set, value).ok().unwrap(); 182 | /// let i_size = size_q(q_ctx, &my_set).ok().unwrap(); 183 | /// assert_eq!(i_size, 1); 184 | /// }); 185 | /// ``` 186 | pub fn size_q(q_ctx: &QuickJsRealmAdapter, set: &QuickJsValueAdapter) -> Result { 187 | unsafe { size(q_ctx.context, set) } 188 | } 189 | 190 | /// get the number of entries in a Set 191 | /// # Safety 192 | /// please ensure the passed JSContext is still valid 193 | pub unsafe fn size(ctx: *mut q::JSContext, set: &QuickJsValueAdapter) -> Result { 194 | let res = objects::get_property(ctx, set, "size")?; 195 | primitives::to_i32(&res) 196 | } 197 | 198 | /// remove all entries from a Set 199 | /// # Example 200 | /// ```rust 201 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 202 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 203 | /// use quickjs_runtime::quickjs_utils::primitives; 204 | /// use quickjs_runtime::quickjs_utils::sets::{size_q, clear_q, add_q, new_set_q}; 205 | /// 206 | /// let rt = QuickJsRuntimeBuilder::new().build(); 207 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 208 | /// let q_ctx = q_js_rt.get_main_realm(); 209 | /// let my_set: QuickJsValueAdapter = new_set_q(q_ctx).ok().unwrap(); 210 | /// let value = primitives::from_i32(23); 211 | /// add_q(q_ctx, &my_set, value).ok().unwrap(); 212 | /// clear_q(q_ctx, &my_set).ok().unwrap(); 213 | /// let i_size = size_q(q_ctx, &my_set).ok().unwrap(); 214 | /// assert_eq!(i_size, 0); 215 | /// }); 216 | /// ``` 217 | pub fn clear_q(q_ctx: &QuickJsRealmAdapter, map: &QuickJsValueAdapter) -> Result<(), JsError> { 218 | unsafe { clear(q_ctx.context, map) } 219 | } 220 | 221 | /// remove all entries from a Set 222 | /// # Safety 223 | /// please ensure the passed JSContext is still valid 224 | pub unsafe fn clear(ctx: *mut q::JSContext, set: &QuickJsValueAdapter) -> Result<(), JsError> { 225 | let _ = functions::invoke_member_function(ctx, set, "clear", &[])?; 226 | Ok(()) 227 | } 228 | 229 | /// iterate over all values of a Set 230 | /// # Example 231 | /// ```rust 232 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 233 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 234 | /// use quickjs_runtime::quickjs_utils::primitives; 235 | /// use quickjs_runtime::quickjs_utils::sets::{new_set_q, add_q, values_q}; 236 | /// 237 | /// let rt = QuickJsRuntimeBuilder::new().build(); 238 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 239 | /// let q_ctx = q_js_rt.get_main_realm(); 240 | /// let my_set: QuickJsValueAdapter = new_set_q(q_ctx).ok().unwrap(); 241 | /// let value = primitives::from_i32(23); 242 | /// add_q(q_ctx, &my_set, value).ok().unwrap(); 243 | /// let mapped_values = values_q(q_ctx, &my_set, |value| {Ok(123)}).ok().unwrap(); 244 | /// assert_eq!(mapped_values.len(), 1); 245 | /// }); 246 | /// ``` 247 | pub fn values_q Result, R>( 248 | q_ctx: &QuickJsRealmAdapter, 249 | set: &QuickJsValueAdapter, 250 | consumer_producer: C, 251 | ) -> Result, JsError> { 252 | unsafe { values(q_ctx.context, set, consumer_producer) } 253 | } 254 | 255 | /// iterate over all values of a Set 256 | /// # Safety 257 | /// please ensure the passed JSContext is still valid 258 | pub unsafe fn values Result, R>( 259 | ctx: *mut q::JSContext, 260 | set: &QuickJsValueAdapter, 261 | consumer_producer: C, 262 | ) -> Result, JsError> { 263 | let iter_ref = functions::invoke_member_function(ctx, set, "values", &[])?; 264 | 265 | iterators::iterate(ctx, &iter_ref, consumer_producer) 266 | } 267 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | //! contains the QuickJsRuntimeBuilder which may be used to instantiate a new QuickjsRuntimeFacade 2 | 3 | use crate::facades::QuickJsRuntimeFacade; 4 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 5 | use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter; 6 | 7 | use crate::jsutils::modules::{CompiledModuleLoader, NativeModuleLoader, ScriptModuleLoader}; 8 | use crate::jsutils::{JsError, ScriptPreProcessor}; 9 | use std::time::Duration; 10 | 11 | pub type EsRuntimeInitHooks = 12 | Vec Result<(), JsError> + Send + 'static>>; 13 | 14 | /// the EsRuntimeBuilder is used to init an EsRuntime 15 | /// # Example 16 | /// ```rust 17 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 18 | /// // init a rt which may use 16MB of memory 19 | /// let rt = QuickJsRuntimeBuilder::new() 20 | /// .memory_limit(1024*1024*16) 21 | /// .build(); 22 | /// ``` 23 | pub struct QuickJsRuntimeBuilder { 24 | pub(crate) script_module_loaders: Vec>, 25 | pub(crate) native_module_loaders: Vec>, 26 | pub(crate) compiled_module_loaders: Vec>, 27 | pub(crate) opt_memory_limit_bytes: Option, 28 | pub(crate) opt_gc_threshold: Option, 29 | pub(crate) opt_max_stack_size: Option, 30 | pub(crate) opt_gc_interval: Option, 31 | pub(crate) runtime_init_hooks: EsRuntimeInitHooks, 32 | pub(crate) script_pre_processors: Vec>, 33 | #[allow(clippy::type_complexity)] 34 | pub(crate) interrupt_handler: Option bool + Send>>, 35 | } 36 | 37 | impl QuickJsRuntimeBuilder { 38 | /// build an EsRuntime 39 | pub fn build(self) -> QuickJsRuntimeFacade { 40 | log::debug!("QuickJsRuntimeBuilder.build"); 41 | QuickJsRuntimeFacade::new(self) 42 | } 43 | 44 | /// init a new EsRuntimeBuilder 45 | pub fn new() -> Self { 46 | Self { 47 | script_module_loaders: vec![], 48 | native_module_loaders: vec![], 49 | compiled_module_loaders: vec![], 50 | opt_memory_limit_bytes: None, 51 | opt_gc_threshold: None, 52 | opt_max_stack_size: None, 53 | opt_gc_interval: None, 54 | runtime_init_hooks: vec![], 55 | script_pre_processors: vec![], 56 | interrupt_handler: None, 57 | } 58 | } 59 | 60 | /// add a script loaders which will be used to load modules when they are imported from script 61 | /// # Example 62 | /// ```rust 63 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 64 | /// use quickjs_runtime::jsutils::modules::ScriptModuleLoader; 65 | /// use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 66 | /// use quickjs_runtime::jsutils::Script; 67 | /// struct MyModuleLoader {} 68 | /// impl ScriptModuleLoader for MyModuleLoader { 69 | /// fn normalize_path(&self, realm: &QuickJsRealmAdapter ,ref_path: &str,path: &str) -> Option { 70 | /// Some(path.to_string()) 71 | /// } 72 | /// 73 | /// fn load_module(&self, realm: &QuickJsRealmAdapter, absolute_path: &str) -> String { 74 | /// "export const foo = 12;".to_string() 75 | /// } 76 | /// } 77 | /// 78 | /// let rt = QuickJsRuntimeBuilder::new() 79 | /// .script_module_loader(MyModuleLoader{}) 80 | /// .build(); 81 | /// rt.eval_module_sync(None, Script::new("test_module.es", "import {foo} from 'some_module.mes';\nconsole.log('foo = %s', foo);")).ok().unwrap(); 82 | /// ``` 83 | pub fn script_module_loader( 84 | mut self, 85 | loader: M, 86 | ) -> Self { 87 | self.script_module_loaders.push(Box::new(loader)); 88 | self 89 | } 90 | 91 | /// add a ScriptPreProcessor which will be called for all scripts which are evaluated and compiled 92 | pub fn script_pre_processor( 93 | mut self, 94 | processor: S, 95 | ) -> Self { 96 | self.script_pre_processors.push(Box::new(processor)); 97 | self 98 | } 99 | 100 | /// add a module loader which can load native functions and proxy classes 101 | /// # Example 102 | /// ```rust 103 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 104 | /// use quickjs_runtime::jsutils::modules::NativeModuleLoader; 105 | /// use quickjs_runtime::jsutils::Script; 106 | /// use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 107 | /// use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 108 | /// use quickjs_runtime::quickjs_utils::functions; 109 | /// use quickjs_runtime::quickjs_utils::primitives::{from_bool, from_i32}; 110 | /// use quickjs_runtime::reflection::Proxy; 111 | /// 112 | /// struct MyModuleLoader{} 113 | /// impl NativeModuleLoader for MyModuleLoader { 114 | /// fn has_module(&self, _q_ctx: &QuickJsRealmAdapter,module_name: &str) -> bool { 115 | /// module_name.eq("my_module") 116 | /// } 117 | /// 118 | /// fn get_module_export_names(&self, _q_ctx: &QuickJsRealmAdapter, _module_name: &str) -> Vec<&str> { 119 | /// vec!["someVal", "someFunc", "SomeClass"] 120 | /// } 121 | /// 122 | /// fn get_module_exports(&self, q_ctx: &QuickJsRealmAdapter, _module_name: &str) -> Vec<(&str, QuickJsValueAdapter)> { 123 | /// 124 | /// let js_val = from_i32(1470); 125 | /// let js_func = functions::new_function_q( 126 | /// q_ctx, 127 | /// "someFunc", |_q_ctx, _this, _args| { 128 | /// return Ok(from_i32(432)); 129 | /// }, 0) 130 | /// .ok().unwrap(); 131 | /// let js_class = Proxy::new() 132 | /// .name("SomeClass") 133 | /// .static_method("doIt", |_rt, _q_ctx, _args|{ 134 | /// return Ok(from_i32(185)); 135 | /// }) 136 | /// .install(q_ctx, false) 137 | /// .ok().unwrap(); 138 | /// 139 | /// vec![("someVal", js_val), ("someFunc", js_func), ("SomeClass", js_class)] 140 | /// } 141 | /// } 142 | /// 143 | /// let rt = QuickJsRuntimeBuilder::new() 144 | /// .native_module_loader(MyModuleLoader{}) 145 | /// .build(); 146 | /// 147 | /// rt.eval_module_sync(None, Script::new("test_native_mod.es", "import {someVal, someFunc, SomeClass} from 'my_module';\nlet i = (someVal + someFunc() + SomeClass.doIt());\nif (i !== 2087){throw Error('i was not 2087');}")).ok().expect("script failed"); 148 | /// ``` 149 | pub fn native_module_loader( 150 | mut self, 151 | module_loader: S, 152 | ) -> Self 153 | where 154 | Self: Sized, 155 | { 156 | self.native_module_loaders.push(Box::new(module_loader)); 157 | self 158 | } 159 | 160 | /// set max memory the runtime may use 161 | pub fn memory_limit(mut self, bytes: u64) -> Self { 162 | self.opt_memory_limit_bytes = Some(bytes); 163 | self 164 | } 165 | 166 | /// number of allocations before gc is run 167 | pub fn gc_threshold(mut self, size: u64) -> Self { 168 | self.opt_gc_threshold = Some(size); 169 | self 170 | } 171 | 172 | /// set a max stack size 173 | pub fn max_stack_size(mut self, size: u64) -> Self { 174 | self.opt_max_stack_size = Some(size); 175 | self 176 | } 177 | 178 | /// set a Garbage Collection interval, this will start a timer thread which will trigger a full GC every set interval 179 | pub fn gc_interval(mut self, interval: Duration) -> Self { 180 | self.opt_gc_interval = Some(interval); 181 | self 182 | } 183 | 184 | /// add an interrupt handler, this will be called several times during script execution and may be used to cancel a running script 185 | pub fn set_interrupt_handler bool + Send + 'static>( 186 | mut self, 187 | interrupt_handler: I, 188 | ) -> Self { 189 | self.interrupt_handler = Some(Box::new(interrupt_handler)); 190 | self 191 | } 192 | } 193 | 194 | impl Default for QuickJsRuntimeBuilder { 195 | fn default() -> Self { 196 | QuickJsRuntimeBuilder::new() 197 | } 198 | } 199 | 200 | impl QuickJsRuntimeBuilder { 201 | pub fn runtime_facade_init_hook< 202 | H: FnOnce(&QuickJsRuntimeFacade) -> Result<(), JsError> + Send + 'static, 203 | >( 204 | mut self, 205 | hook: H, 206 | ) -> Self { 207 | self.runtime_init_hooks.push(Box::new(hook)); 208 | self 209 | } 210 | 211 | pub fn realm_adapter_init_hook< 212 | H: Fn(&QuickJsRuntimeAdapter, &QuickJsRealmAdapter) -> Result<(), JsError> + Send + 'static, 213 | >( 214 | self, 215 | hook: H, 216 | ) -> Self { 217 | self.runtime_adapter_init_hook(move |rt| { 218 | rt.add_context_init_hook(hook)?; 219 | Ok(()) 220 | }) 221 | } 222 | 223 | pub fn runtime_adapter_init_hook< 224 | H: FnOnce(&QuickJsRuntimeAdapter) -> Result<(), JsError> + Send + 'static, 225 | >( 226 | self, 227 | hook: H, 228 | ) -> Self { 229 | self.runtime_facade_init_hook(|rt| { 230 | rt.exe_rt_task_in_event_loop(|rt| { 231 | let _ = hook(rt); 232 | }); 233 | Ok(()) 234 | }) 235 | } 236 | 237 | pub fn compiled_module_loader( 238 | mut self, 239 | module_loader: S, 240 | ) -> Self { 241 | self.compiled_module_loaders.push(Box::new(module_loader)); 242 | self 243 | } 244 | } 245 | 246 | #[cfg(test)] 247 | pub mod tests { 248 | use crate::builder::QuickJsRuntimeBuilder; 249 | use crate::jsutils::modules::ScriptModuleLoader; 250 | use crate::jsutils::Script; 251 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 252 | 253 | #[test] 254 | fn test_module_loader() { 255 | crate::facades::tests::init_logging(); 256 | 257 | struct MyModuleLoader {} 258 | impl ScriptModuleLoader for MyModuleLoader { 259 | fn normalize_path( 260 | &self, 261 | _realm: &QuickJsRealmAdapter, 262 | _ref_path: &str, 263 | path: &str, 264 | ) -> Option { 265 | Some(path.to_string()) 266 | } 267 | 268 | fn load_module(&self, _realm: &QuickJsRealmAdapter, _absolute_path: &str) -> String { 269 | "export const foo = 12;".to_string() 270 | } 271 | } 272 | 273 | let rt = QuickJsRuntimeBuilder::new() 274 | .script_module_loader(MyModuleLoader {}) 275 | .build(); 276 | match rt.eval_module_sync( 277 | None, 278 | Script::new( 279 | "test_module.es", 280 | "import {foo} from 'some_module.mes';\nconsole.log('foo = %s', foo);", 281 | ), 282 | ) { 283 | Ok(_) => {} 284 | Err(e) => panic!("script failed {}", e), 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/jsutils/promises.rs: -------------------------------------------------------------------------------- 1 | use crate::jsutils::helper_tasks::{add_helper_task, add_helper_task_async}; 2 | use crate::jsutils::JsError; 3 | use crate::quickjs_utils::promises::QuickJsPromiseAdapter; 4 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 5 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 6 | use futures::Future; 7 | 8 | #[allow(clippy::type_complexity)] 9 | /// create a new promise with a producer and a mapper 10 | /// the producer will run in a helper thread(in the tokio thread pool) and thus get a result asynchronously 11 | /// the resulting value will then be mapped to a JSValueRef by the mapper in the EventQueue thread 12 | /// the promise which was returned is then resolved with the value which is returned by the mapper 13 | pub fn new_resolving_promise( 14 | realm: &QuickJsRealmAdapter, 15 | producer: P, 16 | mapper: M, 17 | ) -> Result 18 | where 19 | R: Send + 'static, 20 | P: FnOnce() -> Result + Send + 'static, 21 | M: FnOnce(&QuickJsRealmAdapter, R) -> Result + Send + 'static, 22 | { 23 | // create promise 24 | let promise_ref = realm.create_promise()?; 25 | let return_ref = promise_ref.js_promise_get_value(realm); 26 | 27 | // add to map and keep id 28 | let id = realm.cache_promise(promise_ref); 29 | 30 | let rti_ref = realm.get_runtime_facade_inner(); 31 | 32 | let realm_id = realm.get_realm_id().to_string(); 33 | // go async 34 | add_helper_task(move || { 35 | // in helper thread, produce result 36 | let produced_result = producer(); 37 | if let Some(rti) = rti_ref.upgrade() { 38 | rti.add_rt_task_to_event_loop_void(move |rt| { 39 | if let Some(realm) = rt.get_realm(realm_id.as_str()) { 40 | // in q_js_rt worker thread, resolve promise 41 | // retrieve promise 42 | let prom_ref_opt: Option = 43 | realm.consume_cached_promise(id); 44 | if let Some(prom_ref) = prom_ref_opt { 45 | //let prom_ref = realm.js_promise_cache_consume(id); 46 | match produced_result { 47 | Ok(ok_res) => { 48 | // map result to JSValueRef 49 | let raw_res = mapper(realm, ok_res); 50 | 51 | // resolve or reject promise 52 | match raw_res { 53 | Ok(val_ref) => { 54 | if let Err(e) = prom_ref.js_promise_resolve(realm, &val_ref) 55 | { 56 | log::error!( 57 | "[{}] could not resolve promise5: {}", 58 | realm.get_realm_id(), 59 | e 60 | ); 61 | } 62 | } 63 | Err(err) => { 64 | let err_ref = realm 65 | .create_error( 66 | err.get_name(), 67 | err.get_message(), 68 | err.get_stack(), 69 | ) 70 | .expect("could not create error"); 71 | if let Err(e) = prom_ref.js_promise_reject(realm, &err_ref) 72 | { 73 | log::error!( 74 | "[{}] could not reject promise4: {}", 75 | realm.get_realm_id(), 76 | e 77 | ); 78 | } 79 | } 80 | } 81 | } 82 | Err(err) => { 83 | // todo use error:new_error(err) 84 | let err_ref = realm 85 | .create_error( 86 | err.get_name(), 87 | err.get_message(), 88 | err.get_stack(), 89 | ) 90 | .expect("could not create error"); 91 | if let Err(e) = prom_ref.js_promise_reject(realm, &err_ref) { 92 | log::error!( 93 | "[{}] could not reject promise3: {}", 94 | realm.get_realm_id(), 95 | e 96 | ); 97 | } 98 | } 99 | } 100 | } else { 101 | log::error!( 102 | "async promise running for dropped realm: {} promise_id:{}", 103 | realm_id, 104 | id 105 | ); 106 | } 107 | } else { 108 | log::error!("async promise running for dropped realm: {}", realm_id); 109 | } 110 | }); 111 | } else { 112 | log::error!("async promise running for dropped runtime"); 113 | } 114 | }); 115 | 116 | Ok(return_ref) 117 | } 118 | 119 | #[allow(clippy::type_complexity)] 120 | /// create a new promise with an async producer and a mapper 121 | /// the producer will be awaited asynchronously and 122 | /// the resulting value will then be mapped to a JSValueRef by the mapper in the EventQueue thread 123 | /// the promise which was returned is then resolved with the value which is returned by the mapper 124 | pub(crate) fn new_resolving_promise_async( 125 | realm: &QuickJsRealmAdapter, 126 | producer: P, 127 | mapper: M, 128 | ) -> Result 129 | where 130 | R: Send + 'static, 131 | P: Future> + Send + 'static, 132 | M: FnOnce(&QuickJsRealmAdapter, R) -> Result + Send + 'static, 133 | { 134 | // create promise 135 | let promise_ref = realm.create_promise()?; 136 | let return_ref = promise_ref.js_promise_get_value(realm); 137 | 138 | // add to map and keep id 139 | let id = realm.cache_promise(promise_ref); 140 | 141 | let rti_ref = realm.get_runtime_facade_inner(); 142 | 143 | let realm_id = realm.get_realm_id().to_string(); 144 | // go async 145 | let _ignore_result = add_helper_task_async(async move { 146 | // in helper thread, produce result 147 | let produced_result = producer.await; 148 | if let Some(rti) = rti_ref.upgrade() { 149 | rti.add_rt_task_to_event_loop_void(move |rt| { 150 | if let Some(realm) = rt.get_realm(realm_id.as_str()) { 151 | // in q_js_rt worker thread, resolve promise 152 | // retrieve promise 153 | let prom_ref_opt: Option = 154 | realm.consume_cached_promise(id); 155 | if let Some(prom_ref) = prom_ref_opt { 156 | //let prom_ref = realm.js_promise_cache_consume(id); 157 | match produced_result { 158 | Ok(ok_res) => { 159 | // map result to JSValueRef 160 | let raw_res = mapper(realm, ok_res); 161 | 162 | // resolve or reject promise 163 | match raw_res { 164 | Ok(val_ref) => { 165 | if let Err(e) = prom_ref.js_promise_resolve(realm, &val_ref) 166 | { 167 | log::error!( 168 | "[{}] could not resolve promise: {}", 169 | realm.get_realm_id(), 170 | e 171 | ); 172 | } 173 | } 174 | Err(err) => { 175 | let err_ref = realm 176 | .create_error( 177 | err.get_name(), 178 | err.get_message(), 179 | err.get_stack(), 180 | ) 181 | .expect("could not create err"); 182 | if let Err(e) = prom_ref.js_promise_reject(realm, &err_ref) 183 | { 184 | log::error!( 185 | "[{}] could not reject promise: {}", 186 | realm.get_realm_id(), 187 | e 188 | ); 189 | } 190 | } 191 | } 192 | } 193 | Err(err) => { 194 | // todo use error:new_error(err) 195 | let err_ref = realm 196 | .create_error( 197 | err.get_name(), 198 | err.get_message(), 199 | err.get_stack(), 200 | ) 201 | .expect("could not create str"); 202 | if let Err(e) = prom_ref.js_promise_reject(realm, &err_ref) { 203 | log::error!( 204 | "[{}] could not reject promise2: {}", 205 | realm.get_realm_id(), 206 | e 207 | ); 208 | } 209 | } 210 | } 211 | } else { 212 | log::error!( 213 | "async promise running on dropped realm: {} promise_id:{}", 214 | realm_id, 215 | id 216 | ); 217 | } 218 | } else { 219 | log::error!("async promise running on dropped realm: {}", realm_id); 220 | } 221 | }); 222 | } else { 223 | log::error!("async promise running on dropped runtime"); 224 | } 225 | }); 226 | Ok(return_ref) 227 | } 228 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.17.0 2 | 3 | * upgrade to quickjs-ng 0.11.0 4 | 5 | # 0.16.1 6 | 7 | * added explicit + Send to futures returned by loop_realm functions and such 8 | 9 | # 0.16.0 10 | 11 | * update to bellard 2025-09-13 12 | * removed atom.to_str as it was badly implemented 13 | * removed string.to_str as it was badly implemented 14 | 15 | # 0.15.7 16 | 17 | * updated error handling / toString (include cause and such) 18 | 19 | # 0.15.6 20 | 21 | * minor textual update to unhandled promise rejection logging 22 | 23 | # 0.15.5 24 | 25 | * unhandled promise rejection stacktrace fixed (typescript transpiled) 26 | 27 | # 0.15.4 28 | 29 | * update swc so it compiles with latest serde again (https://github.com/HiRoFa/quickjs_es_runtime/issues/89) 30 | 31 | # 0.15.3 32 | 33 | * disable auto realm cleaning for now, will crash if timeouts running after realm cleaned 34 | 35 | # 0.15.2 36 | 37 | * put a max on number of realms auto created by calling runtimefacade::eval() with a realm id 38 | 39 | # 0.15.1 40 | 41 | * support string rope in bellard version 42 | 43 | # 0.15.0 44 | 45 | * quickjs-ng 0.10.0 46 | * quickjs 2025-04-26 47 | * some minor but api breaking changes 48 | 49 | # 0.14.9 50 | 51 | * TypeScript::TRANSPILER no longer uses external_helpers by default 52 | 53 | # 0.14.8 54 | 55 | * bumped some deps (especially swc) 56 | * fixed stack traces for transpiled scripts (line numbers now seem to match) 57 | 58 | # 0.14.6 59 | 60 | * quickjs-ng 0.9.0 61 | 62 | # 0.14.5 63 | 64 | * quickjs-ng 0.8.0 65 | 66 | # 0.14.4 67 | 68 | * don't panic on utf8 error in to_str 69 | 70 | # 0.14.3 71 | 72 | * update quickjs-ng to 0.6.0 73 | * bellard as default 74 | 75 | # 0.14.2 (yanked, accidentaly set quickjs-ng as default) 76 | 77 | * update quickjs-ng to 0.6.0 78 | 79 | # 0.14.1 80 | 81 | * update quickjs-ng to 0.5.0 82 | 83 | # 0.14.0 84 | 85 | * simplified tokio dep 86 | * removed tokio_full feature 87 | 88 | # 0.13.4 89 | 90 | * added rust backtrace to errors generated in rust 91 | 92 | # 0.13.3 93 | 94 | * added some debug info to async promise await code 95 | 96 | # 0.13.2 97 | 98 | * add realm id to log on unhandled promises 99 | * add stack to unhandled prom tracker 100 | 101 | # 0.13.1 102 | 103 | * update quickjs-ng to 4.0.1 104 | * fixed bigint support for quickjs-ng 105 | 106 | # 0.13.0 107 | 108 | * support quickjs-ng (v 0.3.0) as feature, it compiles, some test cases fail (bigint) but should be a nice first step 109 | 110 | # 0.12.1 111 | 112 | * bugfix: console.log("a:%s", undefined); would fail 113 | 114 | # 0.12.0 115 | 116 | * uses hirofa-quickjs-sys 0.2.0 and ['bellard'] feature and thus the 2024-01-13 version of the original quickjs by 117 | Fabrice Bellard 118 | * added get_proxy_instance_id for getting instance id without looking up the proxy 119 | * console functions output source filename 120 | 121 | # 0.11.5 122 | 123 | * more robust stacktrace parser, again 124 | 125 | # 0.11.4 126 | 127 | * fix %o pattern in console.log 128 | 129 | # 0.11.3 130 | 131 | * more robust stacktrace parser 132 | 133 | # 0.11.2 134 | 135 | * pin swc versions 136 | * stacktrace fixer/parser no longer fails on empty lines 137 | 138 | # 0.11.1 139 | 140 | * if stack parsing fails, log as error but just return original stack 141 | 142 | # 0.11.0 143 | 144 | * Script has code/transpiledcode/map for correct error reporting of transpiled code 145 | * built-in typescript support via feature in Cargo.toml 146 | 147 | # 0.10.2 148 | 149 | * removed error when dropping rt/ctx (finalizers of proxy classes causing an allreadyborrowed panic ) 150 | * Proxy instances now have a .constructor which is the constructor function.. 151 | 152 | # 0.10.1 153 | 154 | * replaced Mutex with DebugMutex and thus with parking_lot 155 | 156 | # 0.10.0 157 | 158 | * removed Js*Adapter/Facade traits 159 | * renamed JSValueRef to QuickjsValueAdapter 160 | * removed legacy EsValueFacade 161 | * removed Weak ref arg from JsValueFacade inners 162 | * renamed all js_ functions to more readable names (eg js_null_create()) -> create_null()) 163 | * added (static_)catch_all_getter_setter to Proxy for getting/setting all prop names 164 | * altered the way things are parsed in reflection, like propnames.. should lead to less string allocation 165 | 166 | # 0.9.0 167 | 168 | * when dropping a Realm dangling Promises will log an error instead of panicking 169 | 170 | # 0.8 171 | 172 | ## 0.8.7 173 | 174 | * removed win-api and once_cell from tokio features 175 | * serde support in utils 176 | 177 | ## 0.8.6 178 | 179 | * turned obj is proxy code around to prevent errors filling the log 180 | 181 | ## 0.8.5 182 | 183 | * better toString for errors 184 | 185 | ## 0.8.4 186 | 187 | * removed some logging 188 | 189 | ## 0.8.3 190 | 191 | * reference utils 0.5.4 (by reffing 0.5) (fixes #69) 192 | 193 | ## 0.8.2 194 | 195 | * reference utils 0.5.3 (fixes #68) 196 | 197 | ## 0.8.1 198 | 199 | * fixed memory usage report (#66) 200 | 201 | ## 0.8.0 202 | 203 | * impld realm init hook 204 | * proxy info functions (see if obj is an instance of a proxy class) 205 | 206 | # 0.7 207 | 208 | ## 0.7.2 209 | 210 | * fix for #62 (nested callback creation/drop fails) 211 | 212 | ## 0.7.1 213 | 214 | * fn to calc memory usage 215 | * implemented static event handlers for proxies 216 | 217 | ## 0.7.0 218 | 219 | * implemented js_proxy_new_instance_with_id from utils which allows you to create an instance of a proxy with a 220 | predefined id 221 | * implemented set_prop / has_prop functions in proxies 222 | * implemented CompiledModuleLoader 223 | * implemented jsValueAdapter.js_to_str() 224 | * changed some deps to minor version x.x instead of x.x.x 225 | * made console/setinterval/settimeout/setimmediate optional(but default) features 226 | * more complete stacktrace with errors 227 | * callback functions add name to stacktrace on error 228 | * implemented typedarrays (Uint8 only for now) 229 | 230 | # 0.6 231 | 232 | ## 0.6.0 233 | 234 | * updated quickjs to 2021-03-27 235 | * reverted back to EsRuntime having an Arc (helps me with my abstraction project) 236 | * renamed EsRuntime to QuickJsRuntimeFacade, and others to follow same conventions 237 | * removed fetch api (moved to greencopperruntime) 238 | * removed all panics when async promise resolution fails because of the realm being invalid 239 | * implemented a lot of js_utils abstractions, please note that js_utils::JsValueFacade will someday deprecate 240 | quickjs_runtime::EsValueFacade 241 | 242 | # 0.5 243 | 244 | ## 0.5.1 245 | 246 | * removed redundant prinltn (thanks SreeniIO!) 247 | * added testcase for abstractions, and fixed some typedefs 248 | 249 | ## 0.5.0 250 | 251 | * replaced EsScript with js_utils::Script 252 | * replaced ScriptPreProcessor with js_utils::ScriptPreProcessor 253 | * replaced EsError with js_utils::JsError 254 | * implemented utils::js_utils (generic adapters and facades, 255 | see [the green copper plan](https://github.com/HiRoFa/GreenCopperRuntime/blob/main/README.md#roadmap--the-plan) for 256 | what's this all about) 257 | * changes to function definitions 258 | * fixed interrupt handler 259 | 260 | # 0.4 261 | 262 | ## 0.4.2 263 | 264 | * moved reflection code to reflection/mod.rs (should not affect api) 265 | * toPrimitive for Proxy classes (do stuff like console.log('got: ' + MyProxyInstanceOrClass)) 266 | * removed droppablevalue, replaced with JSPropertyEnumRef 267 | * added is_enumerable(index) fn to JSPropertyEnumRef 268 | * added get_name(index) fn to JSPropertyEnumRef 269 | * added interrupt_handler 270 | 271 | ## 0.4.1 272 | 273 | * altered tokio dep, full is now optional (prevents valgrind errors) 274 | * altered utils dep to 0.1 275 | * added debug log for when eval/evalmodule fails 276 | * scriptpreproc returns Result instead of just script 277 | * q_js_rt.load_module_script_opt now returns Script instead of String 278 | 279 | ## 0.4.0 280 | 281 | * use EventLoop from hirofa_utils, cleaner code, much less Mutexes 282 | * Renamed a lot of public methods 283 | * e.g. rt.add_to_es_event_queue_sync -> rt.exe_rt_task_in_event_loop() 284 | * e.g. rt.add_to_es_event_queue -> rt.add_rt_task_to_event_loop() 285 | * Removed EsRuntime.inner Arc, was a duplicate solution to the same problem 286 | * mit lic 287 | * script preprocessors 288 | 289 | # 0.3.0 290 | 291 | * EsValueFacade now links to live object when object is passed out of runtime 292 | * added EsValueFacade.stringify() 293 | * added EsFunction struct to create functions as EsValueFacade 294 | * is/get_error for EsValueFacade used for when promise or async function is rejected with Error obj 295 | 296 | # 0.2 297 | 298 | ## 0.2.3 299 | 300 | * added a runtime_init_hook(hook) method to the EsRuntimeBuilder so we can add vars to the runtime when the builder is 301 | built 302 | * added EsPromise::new_async which can be used to instantiate a Promise with an async resolver 303 | * Big thanks to [SreeniIO](https://github.com/SreeniIO) for helping out! 304 | * refactored the module loaders in qjsrt so we can differentiate between script and native... I need that for CommonJS 305 | and probably later for transpiling and such 306 | * added quickjs_utils::get_script_or_module_name to get the current scripts filename or module name 307 | * Proxy supports multiple finalizers 308 | * Proxy as EventTarget first working code. needs to mature, but the goal for now is minimal support for dispatching 309 | events from rust to JavaScript 310 | 311 | ## 0.2.2 312 | 313 | * added _void variants for adding jobs to the event queue, this prevents Futures being dropped before being resolved 314 | resulting in errors in the logs 315 | * setTimeout / setInterval now correctly run pending jobs (fixes resolving promises with timeout and such) 316 | * removed logging from console.rs so we can set custom loglevel to that package 317 | * fixed deadlocks in esvalue promise resolution 318 | 319 | ## 0.2.1 320 | 321 | * altered esruntimebuilder to accept Box 322 | 323 | ## 0.2.0 324 | 325 | * rebuilt the module loading system 326 | * use Waker in Futures 327 | 328 | # 0.1 329 | 330 | ## 0.1.1 331 | 332 | * more precise timing for setTimeout and setInterval 333 | * quickjs_utils::maps utils for 334 | handling [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) instances from 335 | rust 336 | * quickjs_utils::sets utils for 337 | handling [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) instances from 338 | rust 339 | * Proxy.event_target and Proxy.static_event_target to allow a Proxy to be used 340 | as [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) 341 | * objects::construct_object util to create a new instance of a constructor 342 | * iterators util to handle iterators 343 | * EventQueue.async_task will be a starting point for being able to use async/await with javascript 344 | * made async functions in EsRuntime (eval, eval_module, call_function, gc) 345 | * made invoke_function in EsValueFacade async 346 | * added 347 | async [get_promise_result](https://hirofa.github.io/quickjs_es_runtime/quickjs_runtime/esvalue/struct.EsValueFacade.html#method.get_promise_result) 348 | to EsValueFacade so the result of a promise may be awaited async 349 | * added quickjs_utils::modules::detect_module() method to detect if a script source is a module 350 | * added ```es_args![]``` macro so you can use 351 | 352 | ```let args = es_args![1, 2, true, "sdf".to_string()]``` 353 | 354 | instead of 355 | 356 | ```let args = vec![1.to_es_value_facade(), 2.to_es_value_facade(), true.to_es_value_facade(), "sdf".to_string().to_es_value_facade()]``` 357 | * added quickjs_utils::modules::new_module/add_module_export/set_module_export 358 | * added NativeModuleLoader to QuickJSRuntime to enable implementors to load native modules on-demand 359 | * altered reflection to enable creation of JSValueRef without making it available in global scope (.install(ctx, false)) 360 | 361 | ## 0.1.0 362 | 363 | Initial release 364 | -------------------------------------------------------------------------------- /src/quickjs_utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! low level contains utils for calling the quickjs api 2 | 3 | use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter; 4 | 5 | #[cfg(feature = "bellard")] 6 | pub mod class_ids { 7 | pub const JS_CLASS_OBJECT: u32 = 1; 8 | pub const JS_CLASS_ARRAY: u32 = 2; 9 | pub const JS_CLASS_ERROR: u32 = 3; 10 | pub const JS_CLASS_NUMBER: u32 = 4; 11 | pub const JS_CLASS_STRING: u32 = 5; 12 | pub const JS_CLASS_BOOLEAN: u32 = 6; 13 | pub const JS_CLASS_SYMBOL: u32 = 7; 14 | pub const JS_CLASS_ARGUMENTS: u32 = 8; 15 | pub const JS_CLASS_MAPPED_ARGUMENTS: u32 = 9; 16 | pub const JS_CLASS_DATE: u32 = 10; 17 | pub const JS_CLASS_MODULE_NS: u32 = 11; 18 | pub const JS_CLASS_C_FUNCTION: u32 = 12; 19 | pub const JS_CLASS_BYTECODE_FUNCTION: u32 = 13; 20 | pub const JS_CLASS_BOUND_FUNCTION: u32 = 14; 21 | pub const JS_CLASS_C_FUNCTION_DATA: u32 = 15; 22 | pub const JS_CLASS_GENERATOR_FUNCTION: u32 = 16; 23 | pub const JS_CLASS_FOR_IN_ITERATOR: u32 = 17; 24 | pub const JS_CLASS_REGEXP: u32 = 18; 25 | pub const JS_CLASS_ARRAY_BUFFER: u32 = 19; 26 | pub const JS_CLASS_SHARED_ARRAY_BUFFER: u32 = 20; 27 | pub const JS_CLASS_UINT8C_ARRAY: u32 = 21; 28 | pub const JS_CLASS_INT8_ARRAY: u32 = 22; 29 | pub const JS_CLASS_UINT8_ARRAY: u32 = 23; 30 | pub const JS_CLASS_INT16_ARRAY: u32 = 24; 31 | pub const JS_CLASS_UINT16_ARRAY: u32 = 25; 32 | pub const JS_CLASS_INT32_ARRAY: u32 = 26; 33 | pub const JS_CLASS_UINT32_ARRAY: u32 = 27; 34 | pub const JS_CLASS_BIG_INT64_ARRAY: u32 = 28; 35 | pub const JS_CLASS_BIG_UINT64_ARRAY: u32 = 29; 36 | pub const JS_CLASS_FLOAT16_ARRAY: u32 = 30; 37 | pub const JS_CLASS_FLOAT32_ARRAY: u32 = 31; 38 | pub const JS_CLASS_FLOAT64_ARRAY: u32 = 32; 39 | pub const JS_CLASS_DATAVIEW: u32 = 33; 40 | pub const JS_CLASS_BIG_INT: u32 = 34; 41 | pub const JS_CLASS_MAP: u32 = 35; 42 | pub const JS_CLASS_SET: u32 = 36; 43 | pub const JS_CLASS_WEAKMAP: u32 = 37; 44 | pub const JS_CLASS_WEAKSET: u32 = 38; 45 | pub const JS_CLASS_MAP_ITERATOR: u32 = 39; 46 | pub const JS_CLASS_SET_ITERATOR: u32 = 40; 47 | pub const JS_CLASS_ARRAY_ITERATOR: u32 = 41; 48 | pub const JS_CLASS_STRING_ITERATOR: u32 = 42; 49 | pub const JS_CLASS_REGEXP_STRING_ITERATOR: u32 = 43; 50 | pub const JS_CLASS_GENERATOR: u32 = 44; 51 | pub const JS_CLASS_PROXY: u32 = 45; 52 | pub const JS_CLASS_PROMISE: u32 = 46; 53 | pub const JS_CLASS_PROMISE_RESOLVE_FUNCTION: u32 = 47; 54 | pub const JS_CLASS_PROMISE_REJECT_FUNCTION: u32 = 48; 55 | pub const JS_CLASS_ASYNC_FUNCTION: u32 = 49; 56 | pub const JS_CLASS_ASYNC_FUNCTION_RESOLVE: u32 = 50; 57 | pub const JS_CLASS_ASYNC_FUNCTION_REJECT: u32 = 51; 58 | pub const JS_CLASS_ASYNC_FROM_SYNC_ITERATOR: u32 = 52; 59 | pub const JS_CLASS_ASYNC_GENERATOR_FUNCTION: u32 = 53; 60 | pub const JS_CLASS_ASYNC_GENERATOR: u32 = 54; 61 | pub const JS_CLASS_WEAK_REF: u32 = 55; 62 | pub const JS_CLASS_FINALIZATION_REGISTRY: u32 = 56; 63 | pub const JS_CLASS_INIT_COUNT: u32 = 57; 64 | } 65 | #[cfg(feature = "quickjs-ng")] 66 | pub mod class_ids { 67 | pub const JS_CLASS_OBJECT: u32 = 1; 68 | pub const JS_CLASS_ARRAY: u32 = 2; 69 | pub const JS_CLASS_ERROR: u32 = 3; 70 | pub const JS_CLASS_NUMBER: u32 = 4; 71 | pub const JS_CLASS_STRING: u32 = 5; 72 | pub const JS_CLASS_BOOLEAN: u32 = 6; 73 | pub const JS_CLASS_SYMBOL: u32 = 7; 74 | pub const JS_CLASS_ARGUMENTS: u32 = 8; 75 | pub const JS_CLASS_MAPPED_ARGUMENTS: u32 = 9; 76 | pub const JS_CLASS_DATE: u32 = 10; 77 | pub const JS_CLASS_MODULE_NS: u32 = 11; 78 | pub const JS_CLASS_C_FUNCTION: u32 = 12; 79 | pub const JS_CLASS_BYTECODE_FUNCTION: u32 = 13; 80 | pub const JS_CLASS_BOUND_FUNCTION: u32 = 14; 81 | pub const JS_CLASS_C_FUNCTION_DATA: u32 = 15; 82 | pub const JS_CLASS_GENERATOR_FUNCTION: u32 = 16; 83 | pub const JS_CLASS_FOR_IN_ITERATOR: u32 = 17; 84 | pub const JS_CLASS_REGEXP: u32 = 18; 85 | pub const JS_CLASS_ARRAY_BUFFER: u32 = 19; 86 | pub const JS_CLASS_SHARED_ARRAY_BUFFER: u32 = 20; 87 | pub const JS_CLASS_UINT8C_ARRAY: u32 = 21; 88 | pub const JS_CLASS_INT8_ARRAY: u32 = 22; 89 | pub const JS_CLASS_UINT8_ARRAY: u32 = 23; 90 | pub const JS_CLASS_INT16_ARRAY: u32 = 24; 91 | pub const JS_CLASS_UINT16_ARRAY: u32 = 25; 92 | pub const JS_CLASS_INT32_ARRAY: u32 = 26; 93 | pub const JS_CLASS_UINT32_ARRAY: u32 = 27; 94 | pub const JS_CLASS_BIG_INT64_ARRAY: u32 = 28; 95 | pub const JS_CLASS_BIG_UINT64_ARRAY: u32 = 29; 96 | pub const JS_CLASS_FLOAT16_ARRAY: u32 = 30; 97 | pub const JS_CLASS_FLOAT32_ARRAY: u32 = 31; 98 | pub const JS_CLASS_FLOAT64_ARRAY: u32 = 32; 99 | pub const JS_CLASS_DATAVIEW: u32 = 33; 100 | pub const JS_CLASS_BIG_INT: u32 = 34; 101 | pub const JS_CLASS_MAP: u32 = 35; 102 | pub const JS_CLASS_SET: u32 = 36; 103 | pub const JS_CLASS_WEAKMAP: u32 = 37; 104 | pub const JS_CLASS_WEAKSET: u32 = 38; 105 | pub const JS_CLASS_ITERATOR: u32 = 39; 106 | pub const JS_CLASS_ITERATOR_HELPER: u32 = 40; 107 | pub const JS_CLASS_ITERATOR_WRAP: u32 = 41; 108 | pub const JS_CLASS_MAP_ITERATOR: u32 = 42; 109 | pub const JS_CLASS_SET_ITERATOR: u32 = 43; 110 | pub const JS_CLASS_ARRAY_ITERATOR: u32 = 44; 111 | pub const JS_CLASS_STRING_ITERATOR: u32 = 45; 112 | pub const JS_CLASS_REGEXP_STRING_ITERATOR: u32 = 46; 113 | pub const JS_CLASS_GENERATOR: u32 = 47; 114 | pub const JS_CLASS_PROXY: u32 = 48; 115 | pub const JS_CLASS_PROMISE: u32 = 49; 116 | pub const JS_CLASS_PROMISE_RESOLVE_FUNCTION: u32 = 50; 117 | pub const JS_CLASS_PROMISE_REJECT_FUNCTION: u32 = 51; 118 | pub const JS_CLASS_ASYNC_FUNCTION: u32 = 52; 119 | pub const JS_CLASS_ASYNC_FUNCTION_RESOLVE: u32 = 53; 120 | pub const JS_CLASS_ASYNC_FUNCTION_REJECT: u32 = 54; 121 | pub const JS_CLASS_ASYNC_FROM_SYNC_ITERATOR: u32 = 55; 122 | pub const JS_CLASS_ASYNC_GENERATOR_FUNCTION: u32 = 56; 123 | pub const JS_CLASS_ASYNC_GENERATOR: u32 = 57; 124 | pub const JS_CLASS_WEAK_REF: u32 = 58; 125 | pub const JS_CLASS_FINALIZATION_REGISTRY: u32 = 59; 126 | pub const JS_CLASS_CALL_SITE: u32 = 60; 127 | pub const JS_CLASS_INIT_COUNT: u32 = 61; 128 | } 129 | 130 | pub mod arrays; 131 | pub mod atoms; 132 | pub mod bigints; 133 | pub mod compile; 134 | pub mod dates; 135 | pub mod errors; 136 | pub mod functions; 137 | pub mod interrupthandler; 138 | pub mod iterators; 139 | pub mod json; 140 | pub mod maps; 141 | pub mod modules; 142 | pub mod objects; 143 | pub mod primitives; 144 | pub mod promises; 145 | pub mod properties; 146 | pub mod runtime; 147 | pub mod sets; 148 | pub mod typedarrays; 149 | 150 | use crate::jsutils::JsError; 151 | use crate::quickjs_utils::atoms::JSAtomRef; 152 | use crate::quickjs_utils::objects::get_property; 153 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 154 | use crate::quickjsvalueadapter::{QuickJsValueAdapter, TAG_NULL, TAG_UNDEFINED}; 155 | use libquickjs_sys as q; 156 | 157 | // todo 158 | // runtime and context in thread_local here 159 | // all function (where applicable) get an Option which if None will be gotten from the thread_local 160 | // every function which returns a q::JSValue will return a OwnedValueRef to ensure values are freed on drop 161 | 162 | pub fn gc(q_js_rt: &QuickJsRuntimeAdapter) { 163 | log::trace!("GC called"); 164 | unsafe { q::JS_RunGC(q_js_rt.runtime) } 165 | log::trace!("GC done"); 166 | } 167 | 168 | pub fn new_undefined_ref() -> QuickJsValueAdapter { 169 | QuickJsValueAdapter::new_no_context(new_undefined(), "new_undefined_ref") 170 | } 171 | 172 | pub fn new_null() -> q::JSValue { 173 | q::JSValue { 174 | u: q::JSValueUnion { int32: 0 }, 175 | tag: TAG_NULL, 176 | } 177 | } 178 | 179 | pub fn new_undefined() -> q::JSValue { 180 | q::JSValue { 181 | u: q::JSValueUnion { int32: 0 }, 182 | tag: TAG_UNDEFINED, 183 | } 184 | } 185 | 186 | pub fn new_null_ref() -> QuickJsValueAdapter { 187 | QuickJsValueAdapter::new_no_context(new_null(), "null_ref") 188 | } 189 | 190 | /// get the current filename 191 | pub fn get_script_or_module_name_q(ctx: &QuickJsRealmAdapter) -> Result { 192 | unsafe { get_script_or_module_name(ctx.context) } 193 | } 194 | 195 | /// get the current filename 196 | /// # Safety 197 | /// ensure the QuickJsContext has not been dropped 198 | pub unsafe fn get_script_or_module_name(context: *mut q::JSContext) -> Result { 199 | for x in 0..100 { 200 | let atom = q::JS_GetScriptOrModuleName(context, x); 201 | let atom_ref = JSAtomRef::new(context, atom); 202 | let r = atoms::to_string(context, &atom_ref)?; 203 | if !r.is_empty() { 204 | return Ok(r); 205 | } 206 | } 207 | Ok("".to_string()) 208 | } 209 | 210 | pub fn get_global_q(context: &QuickJsRealmAdapter) -> QuickJsValueAdapter { 211 | unsafe { get_global(context.context) } 212 | } 213 | /// # Safety 214 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 215 | pub unsafe fn get_global(context: *mut q::JSContext) -> QuickJsValueAdapter { 216 | let global = q::JS_GetGlobalObject(context); 217 | QuickJsValueAdapter::new(context, global, false, true, "global") 218 | } 219 | /// # Safety 220 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 221 | pub unsafe fn get_constructor( 222 | context: *mut q::JSContext, 223 | constructor_name: &str, 224 | ) -> Result { 225 | let global_ref = get_global(context); 226 | 227 | let constructor_ref = get_property(context, &global_ref, constructor_name)?; 228 | 229 | if constructor_ref.is_null_or_undefined() { 230 | Err(JsError::new_string(format!( 231 | "not found: {constructor_name}" 232 | ))) 233 | } else { 234 | Ok(constructor_ref) 235 | } 236 | } 237 | /// Calculate a runtimes memory usage 238 | /// # Safety 239 | /// runtime ref should be a valid existing runtime 240 | pub unsafe fn get_memory_usage(runtime: *mut q::JSRuntime) -> q::JSMemoryUsage { 241 | let mut mu = q::JSMemoryUsage { 242 | malloc_size: 0, 243 | malloc_limit: 0, 244 | memory_used_size: 0, 245 | malloc_count: 0, 246 | memory_used_count: 0, 247 | atom_count: 0, 248 | atom_size: 0, 249 | str_count: 0, 250 | str_size: 0, 251 | obj_count: 0, 252 | obj_size: 0, 253 | prop_count: 0, 254 | prop_size: 0, 255 | shape_count: 0, 256 | shape_size: 0, 257 | js_func_count: 0, 258 | js_func_size: 0, 259 | js_func_code_size: 0, 260 | js_func_pc2line_count: 0, 261 | js_func_pc2line_size: 0, 262 | c_func_count: 0, 263 | array_count: 0, 264 | fast_array_count: 0, 265 | fast_array_elements: 0, 266 | binary_object_count: 0, 267 | binary_object_size: 0, 268 | }; 269 | q::JS_ComputeMemoryUsage(runtime, &mut mu); 270 | 271 | mu 272 | } 273 | 274 | /// # Safety 275 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 276 | pub unsafe fn parse_args( 277 | context: *mut q::JSContext, 278 | argc: ::std::os::raw::c_int, 279 | argv: *mut q::JSValue, 280 | ) -> Vec { 281 | let arg_slice = std::slice::from_raw_parts(argv, argc as usize); 282 | arg_slice 283 | .iter() 284 | .map(|raw| QuickJsValueAdapter::new(context, *raw, true, true, "quickjs_utils::parse_args")) 285 | .collect::>() 286 | } 287 | 288 | #[cfg(test)] 289 | pub mod tests { 290 | use crate::facades::tests::init_test_rt; 291 | use crate::jsutils::Script; 292 | use crate::quickjs_utils::{get_global_q, get_script_or_module_name_q}; 293 | use crate::values::JsValueConvertable; 294 | 295 | #[test] 296 | fn test_global() { 297 | let rt = init_test_rt(); 298 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 299 | let q_ctx = q_js_rt.get_main_realm(); 300 | 301 | #[cfg(feature = "bellard")] 302 | let ct = get_global_q(q_ctx).get_ref_count(); 303 | for _ in 0..5 { 304 | let _global = get_global_q(q_ctx); 305 | #[cfg(feature = "bellard")] 306 | assert_eq!(_global.get_ref_count(), ct); 307 | } 308 | }); 309 | } 310 | 311 | #[test] 312 | fn test_script_name() { 313 | let rt = init_test_rt(); 314 | rt.set_function(&[], "testName", |q_ctx, _args| { 315 | let res = get_script_or_module_name_q(q_ctx)?.to_js_value_facade(); 316 | Ok(res) 317 | }) 318 | .ok() 319 | .expect("func set failed"); 320 | let name_esvf = rt 321 | .eval_sync( 322 | None, 323 | Script::new("the_name.es", "(function(){return(testName());}())"), 324 | ) 325 | .ok() 326 | .expect("script failed"); 327 | assert_eq!(name_esvf.get_str(), "the_name.es"); 328 | let name_esvf = rt 329 | .eval_sync( 330 | None, 331 | Script::new("https://githubstuff.org/tes.js", "(testName())"), 332 | ) 333 | .ok() 334 | .expect("script failed"); 335 | assert_eq!(name_esvf.get_str(), "https://githubstuff.org/tes.js"); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/quickjs_utils/modules.rs: -------------------------------------------------------------------------------- 1 | //! utils for working with ES6 Modules 2 | 3 | use crate::jsutils::{JsError, Script}; 4 | use crate::quickjs_utils::atoms; 5 | use crate::quickjs_utils::atoms::JSAtomRef; 6 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 7 | use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter; 8 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 9 | use core::ptr; 10 | 11 | use libquickjs_sys as q; 12 | use std::ffi::{CStr, CString}; 13 | 14 | /// compile a module, used for module loading 15 | /// # Safety 16 | /// please ensure the corresponding QuickJSContext is still valid 17 | pub unsafe fn compile_module( 18 | context: *mut q::JSContext, 19 | script: Script, 20 | ) -> Result { 21 | let code_str = script.get_runnable_code(); 22 | 23 | let code_c = CString::new(code_str).ok().unwrap(); 24 | let filename_c = CString::new(script.get_path()).ok().unwrap(); 25 | 26 | let value_raw = q::JS_Eval( 27 | context, 28 | code_c.as_ptr(), 29 | code_str.len() as _, 30 | filename_c.as_ptr(), 31 | (q::JS_EVAL_TYPE_MODULE | q::JS_EVAL_FLAG_COMPILE_ONLY) as i32, 32 | ); 33 | 34 | // check for error 35 | let ret = QuickJsValueAdapter::new( 36 | context, 37 | value_raw, 38 | false, 39 | true, 40 | format!("compile_module result of {}", script.get_path()).as_str(), 41 | ); 42 | 43 | log::trace!("compile module yielded a {}", ret.borrow_value().tag); 44 | 45 | if ret.is_exception() { 46 | let ex_opt = QuickJsRealmAdapter::get_exception(context); 47 | if let Some(ex) = ex_opt { 48 | Err(ex) 49 | } else { 50 | Err(JsError::new_str( 51 | "compile_module failed and could not get exception", 52 | )) 53 | } 54 | } else { 55 | Ok(ret) 56 | } 57 | } 58 | 59 | // get the ModuleDef obj from a JSValue, this is used for module loading 60 | pub fn get_module_def(value: &QuickJsValueAdapter) -> *mut q::JSModuleDef { 61 | log::trace!("get_module_def"); 62 | assert!(value.is_module()); 63 | log::trace!("get_module_def / 2"); 64 | unsafe { value.borrow_value().u.ptr as *mut q::JSModuleDef } 65 | } 66 | 67 | #[allow(dead_code)] 68 | pub fn set_module_loader(q_js_rt: &QuickJsRuntimeAdapter) { 69 | log::trace!("setting up module loader"); 70 | 71 | let module_normalize: q::JSModuleNormalizeFunc = Some(js_module_normalize); 72 | let module_loader: q::JSModuleLoaderFunc = Some(js_module_loader); 73 | 74 | let opaque = std::ptr::null_mut(); 75 | 76 | unsafe { q::JS_SetModuleLoaderFunc(q_js_rt.runtime, module_normalize, module_loader, opaque) } 77 | } 78 | 79 | /// detect if a script is module (contains import or export statements) 80 | pub fn detect_module(source: &str) -> bool { 81 | // own impl since detectmodule in quickjs-ng is different since 0.7.0 82 | // https://github.com/quickjs-ng/quickjs/issues/767 83 | 84 | // Check for static `import` statements 85 | 86 | #[cfg(feature = "quickjs-ng")] 87 | { 88 | for line in source.lines() { 89 | let trimmed = line.trim(); 90 | if trimmed.starts_with("import ") && !trimmed.contains("(") { 91 | return true; 92 | } 93 | if trimmed.starts_with("export ") { 94 | return true; 95 | } 96 | } 97 | false 98 | } 99 | 100 | #[cfg(feature = "bellard")] 101 | { 102 | let cstr = 103 | CString::new(source).expect("could not create CString due to null term in source"); 104 | let res = unsafe { q::JS_DetectModule(cstr.as_ptr(), source.len() as _) }; 105 | //println!("res for {} = {}", source, res); 106 | res != 0 107 | } 108 | } 109 | 110 | /// create new Module (JSModuleDef struct) which can be populated with exports after (and from) the init_func 111 | /// # Safety 112 | /// Please ensure the context passed is still valid 113 | pub unsafe fn new_module( 114 | ctx: *mut q::JSContext, 115 | name: &str, 116 | init_func: q::JSModuleInitFunc, 117 | ) -> Result<*mut q::JSModuleDef, JsError> { 118 | let name_cstr = CString::new(name).map_err(|_e| JsError::new_str("CString failed"))?; 119 | Ok(q::JS_NewCModule(ctx, name_cstr.as_ptr(), init_func)) 120 | } 121 | 122 | /// set an export in a JSModuleDef, this should be called AFTER the init_func(as passed to new_module()) is called 123 | /// please note that you always need to use this in combination with add_module_export() 124 | /// # Safety 125 | /// Please ensure the context passed is still valid 126 | pub unsafe fn set_module_export( 127 | ctx: *mut q::JSContext, 128 | module: *mut q::JSModuleDef, 129 | export_name: &str, 130 | js_val: QuickJsValueAdapter, 131 | ) -> Result<(), JsError> { 132 | let name_cstr = CString::new(export_name).map_err(|_e| JsError::new_str("CString failed"))?; 133 | let res = q::JS_SetModuleExport( 134 | ctx, 135 | module, 136 | name_cstr.as_ptr(), 137 | js_val.clone_value_incr_rc(), 138 | ); 139 | if res == 0 { 140 | Ok(()) 141 | } else { 142 | Err(JsError::new_str("JS_SetModuleExport failed")) 143 | } 144 | } 145 | 146 | /// set an export in a JSModuleDef, this should be called BEFORE this init_func(as passed to new_module()) is called 147 | /// # Safety 148 | /// Please ensure the context passed is still valid 149 | pub unsafe fn add_module_export( 150 | ctx: *mut q::JSContext, 151 | module: *mut q::JSModuleDef, 152 | export_name: &str, 153 | ) -> Result<(), JsError> { 154 | let name_cstr = CString::new(export_name).map_err(|_e| JsError::new_str("CString failed"))?; 155 | let res = q::JS_AddModuleExport(ctx, module, name_cstr.as_ptr()); 156 | if res == 0 { 157 | Ok(()) 158 | } else { 159 | Err(JsError::new_str("JS_SetModuleExport failed")) 160 | } 161 | } 162 | 163 | /// get the name of an JSModuleDef struct 164 | /// # Safety 165 | /// Please ensure the context passed is still valid 166 | pub unsafe fn get_module_name( 167 | ctx: *mut q::JSContext, 168 | module: *mut q::JSModuleDef, 169 | ) -> Result { 170 | let atom_raw = q::JS_GetModuleName(ctx, module); 171 | let atom_ref = JSAtomRef::new(ctx, atom_raw); 172 | atoms::to_string(ctx, &atom_ref) 173 | } 174 | 175 | unsafe extern "C" fn js_module_normalize( 176 | ctx: *mut q::JSContext, 177 | module_base_name: *const ::std::os::raw::c_char, 178 | module_name: *const ::std::os::raw::c_char, 179 | _opaque: *mut ::std::os::raw::c_void, 180 | ) -> *mut ::std::os::raw::c_char { 181 | log::trace!("js_module_normalize called."); 182 | 183 | let base_c = CStr::from_ptr(module_base_name); 184 | let base_str = base_c 185 | .to_str() 186 | .expect("could not convert module_base_name to str"); 187 | let name_c = CStr::from_ptr(module_name); 188 | let name_str = name_c 189 | .to_str() 190 | .expect("could not convert module_name to str"); 191 | 192 | log::trace!( 193 | "js_module_normalize called. base: {}. name: {}", 194 | base_str, 195 | name_str 196 | ); 197 | 198 | QuickJsRuntimeAdapter::do_with(|q_js_rt| { 199 | let q_ctx = q_js_rt.get_quickjs_context(ctx); 200 | 201 | if let Some(res) = q_js_rt.with_all_module_loaders(|loader| { 202 | if let Some(normalized_path) = loader.normalize_path(q_ctx, base_str, name_str) { 203 | let c_absolute_path = CString::new(normalized_path.as_str()).expect("fail"); 204 | Some(c_absolute_path.into_raw()) 205 | } else { 206 | None 207 | } 208 | }) { 209 | res 210 | } else { 211 | q_ctx.report_ex(format!("Module {name_str} was not found").as_str()); 212 | ptr::null_mut() 213 | } 214 | }) 215 | } 216 | 217 | unsafe extern "C" fn js_module_loader( 218 | ctx: *mut q::JSContext, 219 | module_name_raw: *const ::std::os::raw::c_char, 220 | _opaque: *mut ::std::os::raw::c_void, 221 | ) -> *mut q::JSModuleDef { 222 | log::trace!("js_module_loader called."); 223 | 224 | let module_name_c = CStr::from_ptr(module_name_raw); 225 | let module_name = module_name_c.to_str().expect("could not get module name"); 226 | 227 | log::trace!("js_module_loader called: {}", module_name); 228 | 229 | QuickJsRuntimeAdapter::do_with(|q_js_rt| { 230 | QuickJsRealmAdapter::with_context(ctx, |q_ctx| { 231 | if let Some(res) = q_js_rt.with_all_module_loaders(|module_loader| { 232 | if module_loader.has_module(q_ctx, module_name) { 233 | let mod_val_res = module_loader.load_module(q_ctx, module_name); 234 | return match mod_val_res { 235 | Ok(mod_val) => Some(mod_val), 236 | Err(e) => { 237 | let err = 238 | format!("Module load failed for {module_name} because of: {e}"); 239 | log::error!("{}", err); 240 | q_ctx.report_ex(err.as_str()); 241 | Some(std::ptr::null_mut()) 242 | } 243 | }; 244 | } 245 | None 246 | }) { 247 | res 248 | } else { 249 | std::ptr::null_mut() 250 | } 251 | }) 252 | }) 253 | } 254 | 255 | #[cfg(test)] 256 | pub mod tests { 257 | use crate::facades::tests::init_test_rt; 258 | use crate::jsutils::Script; 259 | use crate::quickjs_utils::modules::detect_module; 260 | use crate::values::JsValueFacade; 261 | use std::time::Duration; 262 | 263 | #[test] 264 | fn test_native_modules() { 265 | let rt = init_test_rt(); 266 | let mres = rt.eval_module_sync(None, Script::new( 267 | "test.mes", 268 | "import {a, b, c} from 'greco://testmodule1';\nconsole.log('testmodule1.a = %s, testmodule1.b = %s, testmodule1.c = %s', a, b, c);", 269 | )); 270 | match mres { 271 | Ok(_module_res) => {} 272 | Err(e) => panic!("test_native_modules failed: {}", e), 273 | } 274 | 275 | let res_prom = rt.eval_sync(None, Script::new("test_mod_nat_async.es", "(import('greco://someMod').then((module) => {return {a: module.a, b: module.b, c: module.c};}));")).ok().unwrap(); 276 | assert!(res_prom.is_js_promise()); 277 | 278 | match res_prom { 279 | JsValueFacade::JsPromise { cached_promise } => { 280 | let res = cached_promise 281 | .get_promise_result_sync() 282 | .expect("prom timed out"); 283 | let obj = res.expect("prom failed"); 284 | assert!(obj.is_js_object()); 285 | match obj { 286 | JsValueFacade::JsObject { cached_object } => { 287 | let map = cached_object.get_object_sync().expect("esvf to map failed"); 288 | let a = map.get("a").expect("obj did not have a"); 289 | assert_eq!(a.get_i32(), 1234); 290 | let b = map.get("b").expect("obj did not have b"); 291 | assert_eq!(b.get_i32(), 64834); 292 | } 293 | _ => {} 294 | } 295 | } 296 | _ => {} 297 | } 298 | } 299 | 300 | #[test] 301 | fn test_detect() { 302 | assert!(detect_module("import {} from 'foo.js';")); 303 | assert!(detect_module("export function a(){};")); 304 | assert!(!detect_module("//hi")); 305 | assert!(!detect_module("let a = 1;")); 306 | assert!(!detect_module("import('foo.js').then((a) = {});")); 307 | } 308 | 309 | #[test] 310 | fn test_module_sandbox() { 311 | log::info!("> test_module_sandbox"); 312 | 313 | let rt = init_test_rt(); 314 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 315 | let q_ctx = q_js_rt.get_main_realm(); 316 | let res = q_ctx.eval_module(Script::new( 317 | "test1.mes", 318 | "export const name = 'foobar';\nconsole.log('evalling module');", 319 | )); 320 | 321 | if res.is_err() { 322 | panic!("parse module failed: {}", res.err().unwrap()) 323 | } 324 | res.ok().expect("parse module failed"); 325 | }); 326 | 327 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 328 | let q_ctx = q_js_rt.get_main_realm(); 329 | let res = q_ctx.eval_module(Script::new( 330 | "test2.mes", 331 | "import {name} from 'test1.mes';\n\nconsole.log('imported name: ' + name);", 332 | )); 333 | 334 | if res.is_err() { 335 | panic!("parse module2 failed: {}", res.err().unwrap()) 336 | } 337 | 338 | res.ok().expect("parse module2 failed"); 339 | }); 340 | 341 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 342 | let q_ctx = q_js_rt.get_main_realm(); 343 | let res = q_ctx.eval_module(Script::new( 344 | "test3.mes", 345 | "import {name} from 'notfound.mes';\n\nconsole.log('imported name: ' + name);", 346 | )); 347 | 348 | assert!(res.is_err()); 349 | assert!(res 350 | .err() 351 | .unwrap() 352 | .get_message() 353 | .contains("Module notfound.mes was not found")); 354 | }); 355 | 356 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 357 | let q_ctx = q_js_rt.get_main_realm(); 358 | let res = q_ctx.eval_module(Script::new( 359 | "test4.mes", 360 | "import {name} from 'invalid.mes';\n\nconsole.log('imported name: ' + name);", 361 | )); 362 | 363 | assert!(res.is_err()); 364 | assert!(res 365 | .err() 366 | .unwrap() 367 | .get_message() 368 | .contains("Module load failed for invalid.mes")); 369 | }); 370 | 371 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 372 | let q_ctx = q_js_rt.get_main_realm(); 373 | let res = q_ctx.eval_module(Script::new( 374 | "test2.mes", 375 | "import {name} from 'test1.mes';\n\nconsole.log('imported name: ' + name);", 376 | )); 377 | 378 | if res.is_err() { 379 | panic!("parse module2 failed: {}", res.err().unwrap()) 380 | } 381 | 382 | res.ok().expect("parse module2 failed"); 383 | }); 384 | 385 | std::thread::sleep(Duration::from_secs(1)); 386 | 387 | log::info!("< test_module_sandbox"); 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/features/console.rs: -------------------------------------------------------------------------------- 1 | //! the console feature enables the script to use various cansole.log variants 2 | //! see also: [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console) 3 | //! the following methods are available 4 | //! * console.log() 5 | //! * console.info() 6 | //! * console.error() 7 | //! * console.warning() 8 | //! * console.trace() 9 | //! 10 | //! The methods use rust's log crate to output messages. e.g. console.info() uses the log::info!() macro 11 | //! so the console messages should appear in the log you initialized from rust 12 | //! 13 | //! All methods accept a single message string and optional substitution values 14 | //! 15 | //! e.g. 16 | //! ```javascript 17 | //! console.log('Oh dear %s totaly failed %i times because of a %.4f variance in the space time continuum', 'some guy', 12, 2.46) 18 | //! ``` 19 | //! will output 'Oh dear some guy totaly failed 12 times because of a 2.4600 variance in the space time continuum' 20 | //! 21 | //! The string substitution you can use are 22 | //! * %o or %O Outputs a JavaScript object (serialized) 23 | //! * %d or %i Outputs an integer. Number formatting is supported, for example console.log("Foo %.2d", 1.1) will output the number as two significant figures with a leading 0: Foo 01 24 | //! * %s Outputs a string (will attempt to call .toString() on objects, use %o to output a serialized JSON string) 25 | //! * %f Outputs a floating-point value. Formatting is supported, for example console.log("Foo %.2f", 1.1) will output the number to 2 decimal places: Foo 1.10 26 | //! # Example 27 | //! ```rust 28 | //! use quickjs_runtime::builder::QuickJsRuntimeBuilder; 29 | //! use log::LevelFilter; 30 | //! use quickjs_runtime::jsutils::Script; 31 | //! simple_logging::log_to_file("console_test.log", LevelFilter::max()) 32 | //! .ok() 33 | //! .expect("could not init logger"); 34 | //! let rt = QuickJsRuntimeBuilder::new().build(); 35 | //! rt.eval_sync(None, Script::new( 36 | //! "console.es", 37 | //! "console.log('the %s %s %s jumped over %i fences with a accuracy of %.2f', 'quick', 'brown', 'fox', 32, 0.512);" 38 | //! )).expect("script failed"); 39 | //! ``` 40 | //! 41 | //! which will result in a log entry like 42 | //! ```[00:00:00.012] (7f44e7d24700) INFO the quick brown fox jumped over 32 fences with a accuracy of 0.51``` 43 | 44 | use crate::jsutils::{JsError, JsValueType}; 45 | use crate::quickjs_utils; 46 | use crate::quickjs_utils::functions::call_to_string; 47 | use crate::quickjs_utils::json::stringify; 48 | use crate::quickjs_utils::{functions, json, parse_args, primitives}; 49 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 50 | use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter; 51 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 52 | use crate::reflection::Proxy; 53 | use libquickjs_sys as q; 54 | use log::LevelFilter; 55 | use std::str::FromStr; 56 | 57 | pub fn init(q_js_rt: &QuickJsRuntimeAdapter) -> Result<(), JsError> { 58 | q_js_rt.add_context_init_hook(|_q_js_rt, q_ctx| init_ctx(q_ctx)) 59 | } 60 | 61 | pub(crate) fn init_ctx(q_ctx: &QuickJsRealmAdapter) -> Result<(), JsError> { 62 | Proxy::new() 63 | .name("console") 64 | .static_native_method("log", Some(console_log)) 65 | .static_native_method("trace", Some(console_trace)) 66 | .static_native_method("info", Some(console_info)) 67 | .static_native_method("warn", Some(console_warn)) 68 | .static_native_method("error", Some(console_error)) 69 | //.static_native_method("assert", Some(console_assert)) // todo 70 | .static_native_method("debug", Some(console_debug)) 71 | .install(q_ctx, true) 72 | .map(|_| {}) 73 | } 74 | 75 | #[allow(clippy::or_fun_call)] 76 | unsafe fn parse_field_value( 77 | ctx: *mut q::JSContext, 78 | field: &str, 79 | value: &QuickJsValueAdapter, 80 | ) -> String { 81 | // format ints 82 | // only support ,2 / .3 to declare the number of digits to display, e.g. $.3i turns 3 to 003 83 | 84 | // format floats 85 | // only support ,2 / .3 to declare the number of decimals to display, e.g. $.3f turns 3.1 to 3.100 86 | 87 | if field.eq(&"%.0f".to_string()) { 88 | return parse_field_value(ctx, "%i", value); 89 | } 90 | 91 | if field.ends_with('d') || field.ends_with('i') { 92 | let mut i_val: String = call_to_string(ctx, value).unwrap_or_default(); 93 | 94 | // remove chars behind . 95 | if let Some(i) = i_val.find('.') { 96 | let _ = i_val.split_off(i); 97 | } 98 | 99 | if let Some(dot_in_field_idx) = field.find('.') { 100 | let mut m_field = field.to_string(); 101 | // get part behind dot 102 | let mut num_decimals_str = m_field.split_off(dot_in_field_idx + 1); 103 | // remove d or i at end 104 | let _ = num_decimals_str.split_off(num_decimals_str.len() - 1); 105 | // see if we have a number 106 | if !num_decimals_str.is_empty() { 107 | let ct_res = usize::from_str(num_decimals_str.as_str()); 108 | // check if we can parse the number to a usize 109 | if let Ok(ct) = ct_res { 110 | // and if so, make i_val longer 111 | while i_val.len() < ct { 112 | i_val = format!("0{i_val}"); 113 | } 114 | } 115 | } 116 | } 117 | 118 | return i_val; 119 | } else if field.ends_with('f') { 120 | let mut f_val: String = call_to_string(ctx, value).unwrap_or_default(); 121 | 122 | if let Some(dot_in_field_idx) = field.find('.') { 123 | let mut m_field = field.to_string(); 124 | // get part behind dot 125 | let mut num_decimals_str = m_field.split_off(dot_in_field_idx + 1); 126 | // remove d or i at end 127 | let _ = num_decimals_str.split_off(num_decimals_str.len() - 1); 128 | // see if we have a number 129 | if !num_decimals_str.is_empty() { 130 | let ct_res = usize::from_str(num_decimals_str.as_str()); 131 | // check if we can parse the number to a usize 132 | if let Ok(ct) = ct_res { 133 | // and if so, make i_val longer 134 | if ct > 0 { 135 | if !f_val.contains('.') { 136 | f_val.push('.'); 137 | } 138 | 139 | let dot_idx = f_val.find('.').unwrap(); 140 | 141 | while f_val.len() - dot_idx <= ct { 142 | f_val.push('0'); 143 | } 144 | if f_val.len() - dot_idx > ct { 145 | let _ = f_val.split_off(dot_idx + ct + 1); 146 | } 147 | } 148 | } 149 | } 150 | return f_val; 151 | } 152 | } else if field.ends_with('o') || field.ends_with('O') { 153 | let json_str_res = json::stringify(ctx, value, None); 154 | let json = match json_str_res { 155 | Ok(json_str) => { 156 | if json_str.is_undefined() { 157 | // if undefined is passed tp json.stringify it returns undefined, else always a string 158 | "undefined".to_string() 159 | } else { 160 | primitives::to_string(ctx, &json_str).unwrap_or_default() 161 | } 162 | } 163 | Err(_e) => "".to_string(), 164 | }; 165 | return json; 166 | } 167 | call_to_string(ctx, value).unwrap_or_default() 168 | } 169 | 170 | unsafe fn stringify_log_obj(ctx: *mut q::JSContext, arg: &QuickJsValueAdapter) -> String { 171 | match stringify(ctx, arg, None) { 172 | Ok(r) => match primitives::to_string(ctx, &r) { 173 | Ok(s) => s, 174 | Err(e) => format!("Error: {e}"), 175 | }, 176 | Err(e) => format!("Error: {e}"), 177 | } 178 | } 179 | 180 | #[allow(clippy::or_fun_call)] 181 | unsafe fn parse_line(ctx: *mut q::JSContext, args: Vec) -> String { 182 | let mut output = String::new(); 183 | 184 | output.push_str("JS_REALM:"); 185 | QuickJsRealmAdapter::with_context(ctx, |realm| { 186 | output.push('['); 187 | output.push_str(realm.id.as_str()); 188 | output.push_str("]["); 189 | if let Ok(script_or_module_name) = quickjs_utils::get_script_or_module_name_q(realm) { 190 | output.push_str(script_or_module_name.as_str()); 191 | } 192 | output.push_str("]: "); 193 | }); 194 | 195 | if args.is_empty() { 196 | return output; 197 | } 198 | 199 | let message = match &args[0].get_js_type() { 200 | JsValueType::Object => stringify_log_obj(ctx, &args[0]), 201 | JsValueType::Function => stringify_log_obj(ctx, &args[0]), 202 | JsValueType::Array => stringify_log_obj(ctx, &args[0]), 203 | _ => functions::call_to_string(ctx, &args[0]).unwrap_or_default(), 204 | }; 205 | 206 | let mut field_code = String::new(); 207 | let mut in_field = false; 208 | 209 | let mut x = 1; 210 | 211 | let mut filled = 1; 212 | 213 | if args[0].is_string() { 214 | for chr in message.chars() { 215 | if in_field { 216 | field_code.push(chr); 217 | if chr.eq(&'s') || chr.eq(&'d') || chr.eq(&'f') || chr.eq(&'o') || chr.eq(&'i') { 218 | // end field 219 | 220 | if x < args.len() { 221 | output.push_str( 222 | parse_field_value(ctx, field_code.as_str(), &args[x]).as_str(), 223 | ); 224 | x += 1; 225 | filled += 1; 226 | } 227 | 228 | in_field = false; 229 | field_code = String::new(); 230 | } 231 | } else if chr.eq(&'%') { 232 | in_field = true; 233 | } else { 234 | output.push(chr); 235 | } 236 | } 237 | } else { 238 | output.push_str(message.as_str()); 239 | } 240 | 241 | for arg in args.iter().skip(filled) { 242 | // add args which we're not filled in str 243 | output.push(' '); 244 | let tail_arg = match arg.get_js_type() { 245 | JsValueType::Object => stringify_log_obj(ctx, arg), 246 | JsValueType::Function => stringify_log_obj(ctx, arg), 247 | JsValueType::Array => stringify_log_obj(ctx, arg), 248 | _ => call_to_string(ctx, arg).unwrap_or_default(), 249 | }; 250 | output.push_str(tail_arg.as_str()); 251 | } 252 | 253 | output 254 | } 255 | 256 | unsafe extern "C" fn console_log( 257 | ctx: *mut q::JSContext, 258 | _this_val: q::JSValue, 259 | argc: ::std::os::raw::c_int, 260 | argv: *mut q::JSValue, 261 | ) -> q::JSValue { 262 | if log::max_level() >= LevelFilter::Info { 263 | let args = parse_args(ctx, argc, argv); 264 | log::info!("{}", parse_line(ctx, args)); 265 | } 266 | quickjs_utils::new_null() 267 | } 268 | 269 | unsafe extern "C" fn console_trace( 270 | ctx: *mut q::JSContext, 271 | _this_val: q::JSValue, 272 | argc: ::std::os::raw::c_int, 273 | argv: *mut q::JSValue, 274 | ) -> q::JSValue { 275 | if log::max_level() >= LevelFilter::Trace { 276 | let args = parse_args(ctx, argc, argv); 277 | log::trace!("{}", parse_line(ctx, args)); 278 | } 279 | quickjs_utils::new_null() 280 | } 281 | 282 | unsafe extern "C" fn console_debug( 283 | ctx: *mut q::JSContext, 284 | _this_val: q::JSValue, 285 | argc: ::std::os::raw::c_int, 286 | argv: *mut q::JSValue, 287 | ) -> q::JSValue { 288 | if log::max_level() >= LevelFilter::Debug { 289 | let args = parse_args(ctx, argc, argv); 290 | log::debug!("{}", parse_line(ctx, args)); 291 | } 292 | quickjs_utils::new_null() 293 | } 294 | 295 | unsafe extern "C" fn console_info( 296 | ctx: *mut q::JSContext, 297 | _this_val: q::JSValue, 298 | argc: ::std::os::raw::c_int, 299 | argv: *mut q::JSValue, 300 | ) -> q::JSValue { 301 | if log::max_level() >= LevelFilter::Info { 302 | let args = parse_args(ctx, argc, argv); 303 | log::info!("{}", parse_line(ctx, args)); 304 | } 305 | quickjs_utils::new_null() 306 | } 307 | 308 | unsafe extern "C" fn console_warn( 309 | ctx: *mut q::JSContext, 310 | _this_val: q::JSValue, 311 | argc: ::std::os::raw::c_int, 312 | argv: *mut q::JSValue, 313 | ) -> q::JSValue { 314 | if log::max_level() >= LevelFilter::Warn { 315 | let args = parse_args(ctx, argc, argv); 316 | log::warn!("{}", parse_line(ctx, args)); 317 | } 318 | quickjs_utils::new_null() 319 | } 320 | 321 | unsafe extern "C" fn console_error( 322 | ctx: *mut q::JSContext, 323 | _this_val: q::JSValue, 324 | argc: ::std::os::raw::c_int, 325 | argv: *mut q::JSValue, 326 | ) -> q::JSValue { 327 | if log::max_level() >= LevelFilter::Error { 328 | let args = parse_args(ctx, argc, argv); 329 | log::error!("{}", parse_line(ctx, args)); 330 | } 331 | quickjs_utils::new_null() 332 | } 333 | 334 | #[cfg(test)] 335 | pub mod tests { 336 | use crate::builder::QuickJsRuntimeBuilder; 337 | use crate::jsutils::Script; 338 | use std::thread; 339 | use std::time::Duration; 340 | 341 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 342 | pub async fn test_console() { 343 | eprintln!("> test_console"); 344 | /* 345 | let loglevel = log::LevelFilter::Info; 346 | 347 | tracing_log::LogTracer::builder() 348 | //.ignore_crate("swc_ecma_codegen") 349 | //.ignore_crate("swc_ecma_transforms_base") 350 | .with_max_level(loglevel) 351 | .init() 352 | .expect("could not init LogTracer"); 353 | 354 | // Graylog address 355 | let address = format!("{}:{}", "192.168.10.43", 12201); 356 | 357 | // Start tracing 358 | let mut conn_handle = tracing_gelf::Logger::builder() 359 | .init_udp(address) 360 | .expect("could not init udp con for logger"); 361 | 362 | // Spawn background task 363 | // Any futures executor can be used 364 | 365 | println!("> init"); 366 | tokio::runtime::Handle::current().spawn(async move { 367 | // 368 | conn_handle.connect().await 369 | // 370 | }); 371 | println!("< init"); 372 | log::error!("Logger initialized"); 373 | 374 | tracing::error!("via tracing"); 375 | */ 376 | // Send log using a macro defined in the create log 377 | 378 | log::info!("> test_console"); 379 | let rt = QuickJsRuntimeBuilder::new().build(); 380 | rt.eval_sync( 381 | None, 382 | Script::new( 383 | "test_console.es", 384 | "console.log('one %s', 'two', 3);\ 385 | console.error('two %s %s', 'two', 3);\ 386 | console.error('date:', new Date());\ 387 | console.error('err:', new Error('testpoof'));\ 388 | console.error('array:', [1, 2, true, {a: 1}]);\ 389 | console.error('obj: %o', {a: 1});\ 390 | console.error({obj: true}, {obj: false});", 391 | ), 392 | ) 393 | .expect("test_console.es failed"); 394 | log::info!("< test_console"); 395 | 396 | thread::sleep(Duration::from_secs(1)); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/quickjs_utils/compile.rs: -------------------------------------------------------------------------------- 1 | //! Utils to compile script to bytecode and run script from bytecode 2 | 3 | use crate::jsutils::JsError; 4 | use crate::jsutils::Script; 5 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 6 | use crate::quickjsruntimeadapter::make_cstring; 7 | use crate::quickjsvalueadapter::QuickJsValueAdapter; 8 | use libquickjs_sys as q; 9 | use std::os::raw::c_void; 10 | 11 | /// compile a script, will result in a JSValueRef with tag JS_TAG_FUNCTION_BYTECODE or JS_TAG_MODULE. 12 | /// It can be executed with run_compiled_function(). 13 | /// # Example 14 | /// ```rust 15 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 16 | /// use quickjs_runtime::jsutils::Script; 17 | /// use quickjs_runtime::quickjs_utils::primitives; 18 | /// use quickjs_runtime::quickjs_utils::compile::{compile, run_compiled_function}; 19 | /// let rt = QuickJsRuntimeBuilder::new().build(); 20 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 21 | /// unsafe { 22 | /// let q_ctx = q_js_rt.get_main_realm(); 23 | /// let func_res = compile(q_ctx.context, Script::new("test_func.es", "let a = 7; let b = 5; a * b;")); 24 | /// let func = func_res.ok().expect("func compile failed"); 25 | /// let run_res = run_compiled_function(q_ctx.context, &func); 26 | /// let res = run_res.ok().expect("run_compiled_function failed"); 27 | /// let i_res = primitives::to_i32(&res); 28 | /// let i = i_res.ok().expect("could not convert to i32"); 29 | /// assert_eq!(i, 7*5); 30 | /// } 31 | /// }); 32 | /// ``` 33 | /// # Safety 34 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 35 | pub unsafe fn compile( 36 | context: *mut q::JSContext, 37 | script: Script, 38 | ) -> Result { 39 | let filename_c = make_cstring(script.get_path())?; 40 | let code_str = script.get_runnable_code(); 41 | let code_c = make_cstring(code_str)?; 42 | 43 | log::debug!("q_js_rt.compile file {}", script.get_path()); 44 | 45 | let value_raw = q::JS_Eval( 46 | context, 47 | code_c.as_ptr(), 48 | code_str.len() as _, 49 | filename_c.as_ptr(), 50 | q::JS_EVAL_FLAG_COMPILE_ONLY as i32, 51 | ); 52 | 53 | log::trace!("after compile, checking error"); 54 | 55 | // check for error 56 | let ret = QuickJsValueAdapter::new( 57 | context, 58 | value_raw, 59 | false, 60 | true, 61 | format!("eval result of {}", script.get_path()).as_str(), 62 | ); 63 | if ret.is_exception() { 64 | let ex_opt = QuickJsRealmAdapter::get_exception(context); 65 | if let Some(ex) = ex_opt { 66 | Err(ex) 67 | } else { 68 | Err(JsError::new_str( 69 | "compile failed and could not get exception", 70 | )) 71 | } 72 | } else { 73 | Ok(ret) 74 | } 75 | } 76 | 77 | /// run a compiled function, see compile for an example 78 | /// # Safety 79 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 80 | pub unsafe fn run_compiled_function( 81 | context: *mut q::JSContext, 82 | compiled_func: &QuickJsValueAdapter, 83 | ) -> Result { 84 | assert!(compiled_func.is_compiled_function()); 85 | let val = q::JS_EvalFunction(context, compiled_func.clone_value_incr_rc()); 86 | let val_ref = 87 | QuickJsValueAdapter::new(context, val, false, true, "run_compiled_function result"); 88 | if val_ref.is_exception() { 89 | let ex_opt = QuickJsRealmAdapter::get_exception(context); 90 | if let Some(ex) = ex_opt { 91 | Err(ex) 92 | } else { 93 | Err(JsError::new_str( 94 | "run_compiled_function failed and could not get exception", 95 | )) 96 | } 97 | } else { 98 | Ok(val_ref) 99 | } 100 | } 101 | 102 | /// write a function to bytecode 103 | /// # Example 104 | /// ```rust 105 | /// use quickjs_runtime::builder::QuickJsRuntimeBuilder; 106 | /// use quickjs_runtime::jsutils::Script; 107 | /// use quickjs_runtime::quickjs_utils::primitives; 108 | /// use quickjs_runtime::quickjs_utils::compile::{compile, run_compiled_function, to_bytecode, from_bytecode}; 109 | /// let rt = QuickJsRuntimeBuilder::new().build(); 110 | /// rt.exe_rt_task_in_event_loop(|q_js_rt| { 111 | /// unsafe { 112 | /// let q_ctx = q_js_rt.get_main_realm(); 113 | /// let func_res = compile(q_ctx.context, Script::new("test_func.es", "let a = 7; let b = 5; a * b;")); 114 | /// let func = func_res.ok().expect("func compile failed"); 115 | /// let bytecode: Vec = to_bytecode(q_ctx.context, &func); 116 | /// drop(func); 117 | /// assert!(!bytecode.is_empty()); 118 | /// let func2_res = from_bytecode(q_ctx.context, &bytecode); 119 | /// let func2 = func2_res.ok().expect("could not read bytecode"); 120 | /// let run_res = run_compiled_function(q_ctx.context, &func2); 121 | /// let res = run_res.ok().expect("run_compiled_function failed"); 122 | /// let i_res = primitives::to_i32(&res); 123 | /// let i = i_res.ok().expect("could not convert to i32"); 124 | /// assert_eq!(i, 7*5); 125 | /// } 126 | /// }); 127 | /// ``` 128 | /// # Safety 129 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 130 | pub unsafe fn to_bytecode( 131 | context: *mut q::JSContext, 132 | compiled_func: &QuickJsValueAdapter, 133 | ) -> Vec { 134 | assert!(compiled_func.is_compiled_function() || compiled_func.is_module()); 135 | 136 | let mut len = 0; 137 | 138 | let slice_u8 = q::JS_WriteObject( 139 | context, 140 | &mut len, 141 | *compiled_func.borrow_value(), 142 | q::JS_WRITE_OBJ_BYTECODE as i32, 143 | ); 144 | 145 | let slice = std::slice::from_raw_parts(slice_u8, len as _); 146 | // it's a shame to copy the vec here but the alternative is to create a wrapping struct which free's the ptr on drop 147 | let ret = slice.to_vec(); 148 | q::js_free(context, slice_u8 as *mut c_void); 149 | ret 150 | } 151 | 152 | /// read a compiled function from bytecode, see to_bytecode for an example 153 | /// # Safety 154 | /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid 155 | pub unsafe fn from_bytecode( 156 | context: *mut q::JSContext, 157 | bytecode: &[u8], 158 | ) -> Result { 159 | assert!(!bytecode.is_empty()); 160 | { 161 | let len = bytecode.len(); 162 | 163 | let buf = bytecode.as_ptr(); 164 | let raw = q::JS_ReadObject(context, buf, len as _, q::JS_READ_OBJ_BYTECODE as i32); 165 | 166 | let func_ref = QuickJsValueAdapter::new(context, raw, false, true, "from_bytecode result"); 167 | if func_ref.is_exception() { 168 | let ex_opt = QuickJsRealmAdapter::get_exception(context); 169 | if let Some(ex) = ex_opt { 170 | Err(ex) 171 | } else { 172 | Err(JsError::new_str( 173 | "from_bytecode failed and could not get exception", 174 | )) 175 | } 176 | } else { 177 | Ok(func_ref) 178 | } 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | pub mod tests { 184 | use crate::builder::QuickJsRuntimeBuilder; 185 | use crate::facades::tests::init_test_rt; 186 | use crate::jsutils::modules::CompiledModuleLoader; 187 | use crate::jsutils::Script; 188 | use crate::quickjs_utils::compile::{ 189 | compile, from_bytecode, run_compiled_function, to_bytecode, 190 | }; 191 | use crate::quickjs_utils::modules::compile_module; 192 | use crate::quickjs_utils::primitives; 193 | use crate::quickjsrealmadapter::QuickJsRealmAdapter; 194 | use crate::values::JsValueFacade; 195 | //use backtrace::Backtrace; 196 | use futures::executor::block_on; 197 | use std::panic; 198 | use std::sync::Arc; 199 | 200 | #[test] 201 | fn test_compile() { 202 | let rt = init_test_rt(); 203 | 204 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 205 | let q_ctx = q_js_rt.get_main_realm(); 206 | let func_res = unsafe { 207 | compile( 208 | q_ctx.context, 209 | Script::new( 210 | "test_func.es", 211 | "let a_tb3 = 7; let b_tb3 = 5; a_tb3 * b_tb3;", 212 | ), 213 | ) 214 | }; 215 | let func = func_res.expect("func compile failed"); 216 | let bytecode: Vec = unsafe { to_bytecode(q_ctx.context, &func) }; 217 | drop(func); 218 | assert!(!bytecode.is_empty()); 219 | let func2_res = unsafe { from_bytecode(q_ctx.context, &bytecode) }; 220 | let func2 = func2_res.expect("could not read bytecode"); 221 | let run_res = unsafe { run_compiled_function(q_ctx.context, &func2) }; 222 | match run_res { 223 | Ok(res) => { 224 | let i_res = primitives::to_i32(&res); 225 | let i = i_res.expect("could not convert to i32"); 226 | assert_eq!(i, 7 * 5); 227 | } 228 | Err(e) => { 229 | panic!("run failed1: {}", e); 230 | } 231 | } 232 | }); 233 | } 234 | 235 | #[test] 236 | fn test_bytecode() { 237 | let rt = init_test_rt(); 238 | rt.exe_rt_task_in_event_loop(|q_js_rt| unsafe { 239 | let q_ctx = q_js_rt.get_main_realm(); 240 | let func_res = compile( 241 | q_ctx.context, 242 | Script::new( 243 | "test_func.es", 244 | "let a_tb4 = 7; let b_tb4 = 5; a_tb4 * b_tb4;", 245 | ), 246 | ); 247 | let func = func_res.expect("func compile failed"); 248 | let bytecode: Vec = to_bytecode(q_ctx.context, &func); 249 | drop(func); 250 | assert!(!bytecode.is_empty()); 251 | let func2_res = from_bytecode(q_ctx.context, &bytecode); 252 | let func2 = func2_res.expect("could not read bytecode"); 253 | let run_res = run_compiled_function(q_ctx.context, &func2); 254 | 255 | match run_res { 256 | Ok(res) => { 257 | let i_res = primitives::to_i32(&res); 258 | let i = i_res.expect("could not convert to i32"); 259 | assert_eq!(i, 7 * 5); 260 | } 261 | Err(e) => { 262 | panic!("run failed: {}", e); 263 | } 264 | } 265 | }); 266 | } 267 | 268 | #[test] 269 | fn test_bytecode_bad_compile() { 270 | let rt = QuickJsRuntimeBuilder::new().build(); 271 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 272 | let q_ctx = q_js_rt.get_main_realm(); 273 | 274 | let func_res = unsafe { 275 | compile( 276 | q_ctx.context, 277 | Script::new( 278 | "test_func_fail.es", 279 | "{the changes of me compil1ng a're slim to 0-0}", 280 | ), 281 | ) 282 | }; 283 | func_res.expect_err("func compiled unexpectedly"); 284 | }) 285 | } 286 | 287 | #[test] 288 | fn test_bytecode_bad_run() { 289 | let rt = QuickJsRuntimeBuilder::new().build(); 290 | rt.exe_rt_task_in_event_loop(|q_js_rt| unsafe { 291 | let q_ctx = q_js_rt.get_main_realm(); 292 | 293 | let func_res = compile( 294 | q_ctx.context, 295 | Script::new("test_func_runfail.es", "let abcdef = 1;"), 296 | ); 297 | let func = func_res.expect("func compile failed"); 298 | #[cfg(feature = "bellard")] 299 | assert_eq!(1, func.get_ref_count()); 300 | 301 | let bytecode: Vec = to_bytecode(q_ctx.context, &func); 302 | 303 | #[cfg(feature = "bellard")] 304 | assert_eq!(1, func.get_ref_count()); 305 | 306 | drop(func); 307 | 308 | #[cfg(feature = "bellard")] 309 | assert!(!bytecode.is_empty()); 310 | 311 | let func2_res = from_bytecode(q_ctx.context, &bytecode); 312 | let func2 = func2_res.expect("could not read bytecode"); 313 | //should fail the second time you run this because abcdef is already defined 314 | 315 | #[cfg(feature = "bellard")] 316 | assert_eq!(1, func2.get_ref_count()); 317 | 318 | let run_res1 = 319 | run_compiled_function(q_ctx.context, &func2).expect("run 1 failed unexpectedly"); 320 | drop(run_res1); 321 | 322 | #[cfg(feature = "bellard")] 323 | assert_eq!(1, func2.get_ref_count()); 324 | 325 | let _run_res2 = run_compiled_function(q_ctx.context, &func2) 326 | .expect_err("run 2 succeeded unexpectedly"); 327 | 328 | #[cfg(feature = "bellard")] 329 | assert_eq!(1, func2.get_ref_count()); 330 | }); 331 | } 332 | 333 | lazy_static! { 334 | static ref COMPILED_BYTES: Arc> = init_bytes(); 335 | } 336 | 337 | fn init_bytes() -> Arc> { 338 | // in order to init our bytes fgor our module we lazy init a rt 339 | let rt = QuickJsRuntimeBuilder::new().build(); 340 | rt.loop_realm_sync(None, |_rt, realm| unsafe { 341 | let script = Script::new( 342 | "test_module.js", 343 | "export function someFunction(a, b){return a*b;};", 344 | ); 345 | 346 | let module = compile_module(realm.context, script).expect("compile failed"); 347 | 348 | Arc::new(to_bytecode(realm.context, &module)) 349 | }) 350 | } 351 | 352 | struct Cml {} 353 | impl CompiledModuleLoader for Cml { 354 | fn normalize_path( 355 | &self, 356 | _q_ctx: &QuickJsRealmAdapter, 357 | _ref_path: &str, 358 | path: &str, 359 | ) -> Option { 360 | Some(path.to_string()) 361 | } 362 | 363 | fn load_module(&self, _q_ctx: &QuickJsRealmAdapter, _absolute_path: &str) -> Arc> { 364 | COMPILED_BYTES.clone() 365 | } 366 | } 367 | 368 | #[test] 369 | fn test_bytecode_module() { 370 | /*panic::set_hook(Box::new(|panic_info| { 371 | let backtrace = Backtrace::new(); 372 | println!("thread panic occurred: {panic_info}\nbacktrace: {backtrace:?}"); 373 | log::error!( 374 | "thread panic occurred: {}\nbacktrace: {:?}", 375 | panic_info, 376 | backtrace 377 | ); 378 | }));*/ 379 | 380 | //simple_logging::log_to_file("quickjs_runtime.log", LevelFilter::max()) 381 | // .expect("could not init logger"); 382 | 383 | let rt = QuickJsRuntimeBuilder::new() 384 | .compiled_module_loader(Cml {}) 385 | .build(); 386 | 387 | let test_script = Script::new( 388 | "test_bytecode_module.js", 389 | "import('testcompiledmodule').then((mod) => {return mod.someFunction(3, 5);})", 390 | ); 391 | let res_fut = rt.eval(None, test_script); 392 | let res_prom = block_on(res_fut).expect("script failed"); 393 | if let JsValueFacade::JsPromise { cached_promise } = res_prom { 394 | let prom_res_fut = cached_promise.get_promise_result(); 395 | let prom_res = block_on(prom_res_fut) 396 | .expect("prom failed") 397 | .expect("prom was rejected"); 398 | assert!(prom_res.is_i32()); 399 | assert_eq!(prom_res.get_i32(), 15); 400 | } else { 401 | panic!("did not get a prom"); 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/quickjsvalueadapter.rs: -------------------------------------------------------------------------------- 1 | //! JSValueRef is a wrapper for quickjs's JSValue. it provides automatic reference counting making it safer to use 2 | 3 | use crate::jsutils::{JsError, JsValueType}; 4 | use crate::quickjs_utils::typedarrays::is_typed_array; 5 | use crate::quickjs_utils::{arrays, errors, functions, primitives, promises}; 6 | use crate::reflection::is_proxy_instance; 7 | use libquickjs_sys as q; 8 | use std::hash::{Hash, Hasher}; 9 | use std::ptr::null_mut; 10 | 11 | #[allow(clippy::upper_case_acronyms)] 12 | pub struct QuickJsValueAdapter { 13 | pub(crate) context: *mut q::JSContext, 14 | value: q::JSValue, 15 | ref_ct_decr_on_drop: bool, 16 | label: String, 17 | } 18 | 19 | impl Hash for QuickJsValueAdapter { 20 | fn hash(&self, state: &mut H) { 21 | let self_u = self.value.u; 22 | if self.is_i32() || self.is_bool() { 23 | unsafe { self_u.int32.hash(state) }; 24 | } else if self.is_f64() { 25 | unsafe { (self_u.float64 as i32).hash(state) }; 26 | } else { 27 | unsafe { self_u.ptr.hash(state) }; 28 | } 29 | } 30 | } 31 | 32 | impl PartialEq for QuickJsValueAdapter { 33 | fn eq(&self, other: &Self) -> bool { 34 | if self.get_tag() != other.get_tag() { 35 | false 36 | } else { 37 | let self_u = self.value.u; 38 | let other_u = other.value.u; 39 | unsafe { 40 | self_u.int32 == other_u.int32 41 | && self_u.float64 == other_u.float64 42 | && self_u.ptr == other_u.ptr 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl Eq for QuickJsValueAdapter {} 49 | 50 | impl QuickJsValueAdapter { 51 | #[allow(dead_code)] 52 | pub(crate) fn label(&mut self, label: &str) { 53 | self.label = label.to_string() 54 | } 55 | } 56 | 57 | impl Clone for QuickJsValueAdapter { 58 | fn clone(&self) -> Self { 59 | Self::new( 60 | self.context, 61 | self.value, 62 | true, 63 | true, 64 | format!("clone of {}", self.label).as_str(), 65 | ) 66 | } 67 | } 68 | 69 | impl Drop for QuickJsValueAdapter { 70 | fn drop(&mut self) { 71 | //log::debug!( 72 | // "dropping OwnedValueRef, before free: {}, ref_ct: {}, tag: {}", 73 | // self.label, 74 | // self.get_ref_count(), 75 | // self.value.tag 76 | //); 77 | 78 | // All tags < 0 are garbage collected and need to be freed. 79 | if self.value.tag < 0 { 80 | // This transmute is OK since if tag < 0, the union will be a refcount 81 | // pointer. 82 | 83 | if self.ref_ct_decr_on_drop { 84 | #[cfg(feature = "bellard")] 85 | if self.get_ref_count() <= 0 { 86 | log::error!( 87 | "dropping ref while refcount already 0, which is bad mmkay.. {}", 88 | self.label 89 | ); 90 | panic!( 91 | "dropping ref while refcount already 0, which is bad mmkay.. {}", 92 | self.label 93 | ); 94 | } 95 | self.decrement_ref_count(); 96 | } 97 | } 98 | //log::trace!("dropping OwnedValueRef, after free",); 99 | } 100 | } 101 | 102 | impl std::fmt::Debug for QuickJsValueAdapter { 103 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 104 | match self.value.tag { 105 | TAG_EXCEPTION => write!(f, "Exception(?)"), 106 | TAG_NULL => write!(f, "NULL"), 107 | TAG_UNDEFINED => write!(f, "UNDEFINED"), 108 | TAG_BOOL => write!(f, "Bool(?)",), 109 | TAG_INT => write!(f, "Int(?)"), 110 | TAG_FLOAT64 => write!(f, "Float(?)"), 111 | TAG_STRING => write!(f, "String(?)"), 112 | #[cfg(feature = "bellard")] 113 | TAG_STRING_ROPE => write!(f, "String(?)"), 114 | TAG_OBJECT => write!(f, "Object(?)"), 115 | TAG_MODULE => write!(f, "Module(?)"), 116 | TAG_BIG_INT => write!(f, "BigInt(?)"), 117 | _ => write!(f, "Unknown Tag(?)"), 118 | } 119 | } 120 | } 121 | 122 | impl QuickJsValueAdapter { 123 | pub(crate) fn increment_ref_count(&self) { 124 | if self.get_tag() < 0 { 125 | unsafe { 126 | #[allow(clippy::let_unit_value)] 127 | let _ = libquickjs_sys::JS_DupValue(self.context, *self.borrow_value()); 128 | } 129 | } 130 | } 131 | 132 | pub(crate) fn decrement_ref_count(&self) { 133 | if self.get_tag() < 0 { 134 | unsafe { libquickjs_sys::JS_FreeValue(self.context, *self.borrow_value()) } 135 | } 136 | } 137 | 138 | pub fn get_tag(&self) -> i64 { 139 | self.value.tag 140 | } 141 | 142 | pub fn new_no_context(value: q::JSValue, label: &str) -> Self { 143 | Self { 144 | context: null_mut(), 145 | value, 146 | ref_ct_decr_on_drop: false, 147 | label: label.to_string(), 148 | } 149 | } 150 | 151 | pub fn new( 152 | context: *mut q::JSContext, 153 | value: q::JSValue, 154 | ref_ct_incr: bool, 155 | ref_ct_decr_on_drop: bool, 156 | label: &str, 157 | ) -> Self { 158 | debug_assert!(!label.is_empty()); 159 | 160 | let s = Self { 161 | context, 162 | value, 163 | ref_ct_decr_on_drop, 164 | label: label.to_string(), 165 | }; 166 | if ref_ct_incr { 167 | s.increment_ref_count(); 168 | } 169 | s 170 | } 171 | 172 | #[cfg(feature = "bellard")] 173 | pub fn get_ref_count(&self) -> i32 { 174 | if self.get_tag() < 0 { 175 | // This transmute is OK since if tag < 0, the union will be a refcount 176 | // pointer. 177 | let ptr = unsafe { self.value.u.ptr as *mut q::JSRefCountHeader }; 178 | let pref: &mut q::JSRefCountHeader = &mut unsafe { *ptr }; 179 | pref.ref_count 180 | } else { 181 | -1 182 | } 183 | } 184 | 185 | /// borrow the value but first increment the refcount, this is useful for when the value is returned or passed to functions 186 | pub fn clone_value_incr_rc(&self) -> q::JSValue { 187 | self.increment_ref_count(); 188 | self.value 189 | } 190 | 191 | pub fn borrow_value(&self) -> &q::JSValue { 192 | &self.value 193 | } 194 | 195 | pub fn borrow_value_mut(&mut self) -> &mut q::JSValue { 196 | &mut self.value 197 | } 198 | 199 | pub fn is_null_or_undefined(&self) -> bool { 200 | self.is_null() || self.is_undefined() 201 | } 202 | 203 | /// return true if the wrapped value represents a JS null value 204 | pub fn is_undefined(&self) -> bool { 205 | unsafe { q::JS_IsUndefined(self.value) } 206 | } 207 | 208 | /// return true if the wrapped value represents a JS null value 209 | pub fn is_null(&self) -> bool { 210 | unsafe { q::JS_IsNull(self.value) } 211 | } 212 | 213 | /// return true if the wrapped value represents a JS boolean value 214 | pub fn is_bool(&self) -> bool { 215 | unsafe { q::JS_IsBool(self.value) } 216 | } 217 | 218 | /// return true if the wrapped value represents a JS INT value 219 | pub fn is_i32(&self) -> bool { 220 | // todo figure out diff between i32/f64/Number 221 | // unsafe { q::JS_IsNumber(self.borrow_value()) } 222 | self.borrow_value().tag == TAG_INT 223 | } 224 | 225 | /// return true if the wrapped value represents a Module 226 | pub fn is_module(&self) -> bool { 227 | self.borrow_value().tag == TAG_MODULE 228 | } 229 | 230 | /// return true if the wrapped value represents a compiled function 231 | pub fn is_compiled_function(&self) -> bool { 232 | self.borrow_value().tag == TAG_FUNCTION_BYTECODE 233 | } 234 | 235 | /// return true if the wrapped value represents a JS F64 value 236 | pub fn is_f64(&self) -> bool { 237 | self.borrow_value().tag == TAG_FLOAT64 238 | } 239 | 240 | pub fn is_big_int(&self) -> bool { 241 | // unsafe { q::JS_IsBigInt(ctx, self.borrow_value()) } 242 | self.borrow_value().tag == TAG_BIG_INT || self.borrow_value().tag == TAG_SHORT_BIG_INT 243 | } 244 | 245 | /// return true if the wrapped value represents a JS Exception value 246 | pub fn is_exception(&self) -> bool { 247 | unsafe { q::JS_IsException(self.value) } 248 | } 249 | 250 | /// return true if the wrapped value represents a JS Object value 251 | pub fn is_object(&self) -> bool { 252 | unsafe { q::JS_IsObject(self.value) } 253 | } 254 | 255 | /// return true if the wrapped value represents a JS String value 256 | pub fn is_string(&self) -> bool { 257 | unsafe { q::JS_IsString(self.value) } 258 | } 259 | } 260 | 261 | pub(crate) const TAG_BIG_INT: i64 = libquickjs_sys::JS_TAG_BIG_INT as i64; 262 | pub(crate) const TAG_SHORT_BIG_INT: i64 = libquickjs_sys::JS_TAG_SHORT_BIG_INT as i64; 263 | 264 | pub(crate) const TAG_STRING: i64 = libquickjs_sys::JS_TAG_STRING as i64; 265 | 266 | #[cfg(feature = "bellard")] 267 | pub(crate) const TAG_STRING_ROPE: i64 = libquickjs_sys::JS_TAG_STRING_ROPE as i64; 268 | 269 | pub(crate) const TAG_MODULE: i64 = libquickjs_sys::JS_TAG_MODULE as i64; 270 | pub(crate) const TAG_FUNCTION_BYTECODE: i64 = libquickjs_sys::JS_TAG_FUNCTION_BYTECODE as i64; 271 | pub(crate) const TAG_OBJECT: i64 = libquickjs_sys::JS_TAG_OBJECT as i64; 272 | pub(crate) const TAG_INT: i64 = libquickjs_sys::JS_TAG_INT as i64; 273 | pub(crate) const TAG_BOOL: i64 = libquickjs_sys::JS_TAG_BOOL as i64; 274 | pub(crate) const TAG_NULL: i64 = libquickjs_sys::JS_TAG_NULL as i64; 275 | pub(crate) const TAG_UNDEFINED: i64 = libquickjs_sys::JS_TAG_UNDEFINED as i64; 276 | pub(crate) const TAG_EXCEPTION: i64 = libquickjs_sys::JS_TAG_EXCEPTION as i64; 277 | pub(crate) const TAG_FLOAT64: i64 = libquickjs_sys::JS_TAG_FLOAT64 as i64; 278 | 279 | impl QuickJsValueAdapter { 280 | pub fn is_function(&self) -> bool { 281 | self.is_object() && self.get_js_type() == JsValueType::Function 282 | } 283 | pub fn is_array(&self) -> bool { 284 | self.is_object() && self.get_js_type() == JsValueType::Array 285 | } 286 | pub fn is_error(&self) -> bool { 287 | self.is_object() && self.get_js_type() == JsValueType::Error 288 | } 289 | pub fn is_promise(&self) -> bool { 290 | self.is_object() && self.get_js_type() == JsValueType::Promise 291 | } 292 | 293 | pub fn get_js_type(&self) -> JsValueType { 294 | match self.get_tag() { 295 | TAG_EXCEPTION => JsValueType::Error, 296 | TAG_NULL => JsValueType::Null, 297 | TAG_UNDEFINED => JsValueType::Undefined, 298 | TAG_BOOL => JsValueType::Boolean, 299 | TAG_INT => JsValueType::I32, 300 | TAG_FLOAT64 => JsValueType::F64, 301 | TAG_STRING => JsValueType::String, 302 | #[cfg(feature = "bellard")] 303 | TAG_STRING_ROPE => JsValueType::String, 304 | TAG_OBJECT => { 305 | // todo get classProto.name and match 306 | // if bellard, match on classid 307 | if unsafe { functions::is_function(self.context, self) } { 308 | JsValueType::Function 309 | } else if unsafe { errors::is_error(self.context, self) } { 310 | JsValueType::Error 311 | } else if unsafe { arrays::is_array(self.context, self) } { 312 | JsValueType::Array 313 | } else if unsafe { promises::is_promise(self.context, self) } { 314 | JsValueType::Promise 315 | } else { 316 | JsValueType::Object 317 | } 318 | } 319 | TAG_BIG_INT | TAG_SHORT_BIG_INT => JsValueType::BigInt, 320 | TAG_MODULE => todo!(), 321 | _ => JsValueType::Undefined, 322 | } 323 | } 324 | 325 | pub fn is_typed_array(&self) -> bool { 326 | self.is_object() && unsafe { is_typed_array(self.context, self) } 327 | } 328 | 329 | pub fn is_proxy_instance(&self) -> bool { 330 | self.is_object() && unsafe { is_proxy_instance(self.context, self) } 331 | } 332 | 333 | pub fn type_of(&self) -> &'static str { 334 | match self.get_tag() { 335 | TAG_BIG_INT => "bigint", 336 | TAG_STRING => "string", 337 | #[cfg(feature = "bellard")] 338 | TAG_STRING_ROPE => "string", 339 | TAG_MODULE => "module", 340 | TAG_FUNCTION_BYTECODE => "function", 341 | TAG_OBJECT => { 342 | if self.get_js_type() == JsValueType::Function { 343 | "function" 344 | } else { 345 | "object" 346 | } 347 | } 348 | TAG_INT => "number", 349 | TAG_BOOL => "boolean", 350 | TAG_NULL => "object", 351 | TAG_UNDEFINED => "undefined", 352 | TAG_EXCEPTION => "object", 353 | TAG_FLOAT64 => "number", 354 | _ => "unknown", 355 | } 356 | } 357 | 358 | pub fn to_bool(&self) -> bool { 359 | if self.get_js_type() == JsValueType::Boolean { 360 | primitives::to_bool(self).expect("could not convert bool to bool") 361 | } else { 362 | panic!("not a boolean"); 363 | } 364 | } 365 | 366 | pub fn to_i32(&self) -> i32 { 367 | if self.get_js_type() == JsValueType::I32 { 368 | primitives::to_i32(self).expect("could not convert to i32") 369 | } else { 370 | panic!("not an i32"); 371 | } 372 | } 373 | 374 | pub fn to_f64(&self) -> f64 { 375 | if self.get_js_type() == JsValueType::F64 { 376 | primitives::to_f64(self).expect("could not convert to f64") 377 | } else { 378 | panic!("not a f64"); 379 | } 380 | } 381 | 382 | pub fn to_string(&self) -> Result { 383 | match self.get_js_type() { 384 | JsValueType::I32 => Ok(self.to_i32().to_string()), 385 | JsValueType::F64 => Ok(self.to_f64().to_string()), 386 | JsValueType::String => unsafe { primitives::to_string(self.context, self) }, 387 | JsValueType::Boolean => { 388 | if self.to_bool() { 389 | Ok("true".to_string()) 390 | } else { 391 | Ok("false".to_string()) 392 | } 393 | } 394 | JsValueType::Error => { 395 | let js_error = unsafe { errors::error_to_js_error(self.context, self) }; 396 | Ok(format!("{js_error}")) 397 | } 398 | _ => unsafe { functions::call_to_string(self.context, self) }, 399 | } 400 | } 401 | } 402 | 403 | #[cfg(test)] 404 | pub mod tests { 405 | use crate::facades::tests::init_test_rt; 406 | use crate::jsutils::{JsValueType, Script}; 407 | 408 | #[test] 409 | fn test_to_str() { 410 | let rt = init_test_rt(); 411 | rt.exe_rt_task_in_event_loop(|q_js_rt| { 412 | let q_ctx = q_js_rt.get_main_realm(); 413 | let res = q_ctx.eval(Script::new("test_to_str.es", "('hello ' + 'world');")); 414 | 415 | match res { 416 | Ok(res) => { 417 | log::info!("script ran ok: {:?}", res); 418 | assert!(res.get_js_type() == JsValueType::String); 419 | assert_eq!(res.to_string().expect("str conv failed"), "hello world"); 420 | } 421 | Err(e) => { 422 | log::error!("script failed: {}", e); 423 | panic!("script failed"); 424 | } 425 | } 426 | }); 427 | } 428 | } 429 | --------------------------------------------------------------------------------