├── .gitignore ├── rust-toolchain ├── examples ├── js │ ├── test_runner │ │ ├── bar.test.ts │ │ ├── tsconfig.json │ │ └── foo.test.ts │ ├── modules │ │ ├── c.js │ │ ├── a.js │ │ ├── b.js │ │ └── index.js │ ├── console-log.js │ ├── hello-world.js │ ├── console-log-module.js │ ├── set-timeout.js │ ├── set-interval.js │ ├── set-timeout-multiple.js │ ├── set-timeout-nested.js │ └── set-timeout-cancel.js ├── src │ ├── testing │ │ ├── transformers │ │ │ ├── js │ │ │ │ ├── object.json │ │ │ │ └── main.js │ │ │ └── mod.rs │ │ ├── typescript │ │ │ ├── js │ │ │ │ ├── main.ts │ │ │ │ ├── bar.ts │ │ │ │ └── foo.ts │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── multiple_workers │ │ │ └── mod.rs │ │ ├── memory_usage_worker │ │ │ └── mod.rs │ │ ├── memory_usage_value │ │ │ └── mod.rs │ │ ├── memory_usage_module │ │ │ └── mod.rs │ │ ├── memory_usage_context │ │ │ └── mod.rs │ │ ├── memory_usage_tsfn │ │ │ └── mod.rs │ │ ├── memory_usage │ │ │ └── mod.rs │ │ ├── background_tasks │ │ │ └── mod.rs │ │ ├── wait │ │ │ └── mod.rs │ │ └── multiple_contexts │ │ │ └── mod.rs │ ├── http_server │ │ ├── http1 │ │ │ ├── mod.rs │ │ │ ├── bytes.rs │ │ │ ├── res_ext.rs │ │ │ └── http1_server.rs │ │ ├── handlers │ │ │ ├── root.js │ │ │ ├── tsconfig.json │ │ │ ├── async.js │ │ │ ├── headers.js │ │ │ ├── todo.js │ │ │ ├── mod.rs │ │ │ └── binding.d.ts │ │ └── worker_pool.rs │ ├── eval │ │ └── mod.rs │ ├── basic │ │ └── mod.rs │ ├── custom_resolver │ │ └── mod.rs │ ├── deferred │ │ └── mod.rs │ ├── set_interval │ │ └── mod.rs │ ├── run │ │ └── mod.rs │ ├── set_timeout │ │ └── mod.rs │ ├── custom_extension │ │ └── mod.rs │ ├── promise │ │ └── mod.rs │ ├── thread_safe_promise │ │ └── mod.rs │ ├── main.rs │ └── thread_safe_function │ │ └── mod.rs ├── tsconfig.json └── Cargo.toml ├── .docs └── arch.jpg ├── .editorconfig ├── crates ├── ion │ ├── src │ │ ├── resolvers │ │ │ ├── mod.rs │ │ │ └── relative.rs │ │ ├── values │ │ │ ├── root.rs │ │ │ ├── common │ │ │ │ ├── mod.rs │ │ │ │ ├── js_values.rs │ │ │ │ └── js_values_vec.rs │ │ │ ├── mod.rs │ │ │ ├── js_object.rs │ │ │ ├── js_exception.rs │ │ │ ├── js_null.rs │ │ │ ├── js_unknown.rs │ │ │ ├── js_undefined.rs │ │ │ ├── js_boolean.rs │ │ │ ├── js_string.rs │ │ │ ├── js_number.rs │ │ │ ├── thread_safe_promise.rs │ │ │ ├── js_promise.rs │ │ │ ├── js_deferred.rs │ │ │ ├── thread_safe_function.rs │ │ │ ├── js_external.rs │ │ │ └── js_array.rs │ │ ├── transformers │ │ │ ├── mod.rs │ │ │ └── json │ │ │ │ └── mod.rs │ │ ├── extensions │ │ │ ├── performance │ │ │ │ ├── binding.d.ts │ │ │ │ ├── binding.js │ │ │ │ ├── tsconfig.json │ │ │ │ └── mod.rs │ │ │ ├── test │ │ │ │ ├── tsconfig.json │ │ │ │ ├── binding.d.ts │ │ │ │ ├── binding.ts │ │ │ │ └── mod.rs │ │ │ ├── global_this │ │ │ │ ├── mod.rs │ │ │ │ ├── tsconfig.json │ │ │ │ ├── binding.ts │ │ │ │ └── binding.d.ts │ │ │ ├── console │ │ │ │ ├── tsconfig.json │ │ │ │ ├── binding.d.ts │ │ │ │ ├── binding.ts │ │ │ │ └── mod.rs │ │ │ ├── set_interval │ │ │ │ ├── tsconfig.json │ │ │ │ ├── binding.d.ts │ │ │ │ ├── binding.ts │ │ │ │ └── mod.rs │ │ │ ├── set_timeout │ │ │ │ ├── tsconfig.json │ │ │ │ ├── binding.d.ts │ │ │ │ ├── binding.ts │ │ │ │ └── mod.rs │ │ │ ├── event_target │ │ │ │ ├── tsconfig.json │ │ │ │ ├── mod.rs │ │ │ │ ├── binding.d.ts │ │ │ │ └── binding.ts │ │ │ └── mod.rs │ │ ├── utils │ │ │ ├── hash.rs │ │ │ ├── channel.rs │ │ │ ├── random_string.rs │ │ │ ├── mod.rs │ │ │ ├── v8.rs │ │ │ ├── tokio_ext.rs │ │ │ ├── ref_counter.rs │ │ │ ├── ref_counter_atomic.rs │ │ │ ├── debug.rs │ │ │ ├── os_string_ext.rs │ │ │ ├── hash_map_ext.rs │ │ │ └── path_ext.rs │ │ ├── platform │ │ │ ├── sys │ │ │ │ ├── mod.rs │ │ │ │ ├── value.rs │ │ │ │ ├── isolate_scope.rs │ │ │ │ ├── context_scope.rs │ │ │ │ ├── context.rs │ │ │ │ ├── global.rs │ │ │ │ └── error.rs │ │ │ ├── mod.rs │ │ │ ├── resolve.rs │ │ │ ├── module_map.rs │ │ │ ├── active_context.rs │ │ │ ├── background_worker.rs │ │ │ ├── finalizer_registry.rs │ │ │ └── platform.rs │ │ ├── testing.rs │ │ ├── js_resolver.rs │ │ ├── lib.rs │ │ ├── js_transformer.rs │ │ ├── async_env.rs │ │ ├── error.rs │ │ ├── js_extension.rs │ │ ├── js_context.rs │ │ └── js_worker.rs │ └── Cargo.toml └── ion_cli │ ├── src │ ├── cmd │ │ ├── mod.rs │ │ ├── eval.rs │ │ ├── run.rs │ │ └── test.rs │ └── main.rs │ └── Cargo.toml ├── rustfmt.toml ├── .vscode └── settings.json ├── nodemon.json ├── watch.bash ├── LICENSE ├── Cargo.toml └── justfile /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.90.0 -------------------------------------------------------------------------------- /examples/js/test_runner/bar.test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/js/modules/c.js: -------------------------------------------------------------------------------- 1 | export const c = 'c' -------------------------------------------------------------------------------- /examples/js/console-log.js: -------------------------------------------------------------------------------- 1 | console.log("hello world") -------------------------------------------------------------------------------- /examples/js/hello-world.js: -------------------------------------------------------------------------------- 1 | console.log("Hello World") -------------------------------------------------------------------------------- /.docs/arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alshdavid/ion/HEAD/.docs/arch.jpg -------------------------------------------------------------------------------- /examples/js/modules/a.js: -------------------------------------------------------------------------------- 1 | export * from './c.js' 2 | 3 | export const a = 'a' -------------------------------------------------------------------------------- /examples/js/modules/b.js: -------------------------------------------------------------------------------- 1 | export * from './c.js' 2 | 3 | export const b = 'b' -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | indent_size=4 5 | indent_style=space 6 | -------------------------------------------------------------------------------- /crates/ion/src/resolvers/mod.rs: -------------------------------------------------------------------------------- 1 | mod relative; 2 | 3 | pub use relative::*; 4 | -------------------------------------------------------------------------------- /crates/ion_cli/src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod eval; 2 | pub mod run; 3 | pub mod test; 4 | -------------------------------------------------------------------------------- /examples/src/testing/transformers/js/object.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /crates/ion/src/values/root.rs: -------------------------------------------------------------------------------- 1 | /// TODO v8::Global a.k.a. "root" type 2 | pub struct Root {} 3 | -------------------------------------------------------------------------------- /examples/src/testing/transformers/js/main.js: -------------------------------------------------------------------------------- 1 | import data from "./object.json"; 2 | 3 | console.log(data); 4 | -------------------------------------------------------------------------------- /crates/ion/src/transformers/mod.rs: -------------------------------------------------------------------------------- 1 | mod json; 2 | mod typescript; 3 | 4 | pub use json::*; 5 | pub use typescript::*; 6 | -------------------------------------------------------------------------------- /examples/js/console-log-module.js: -------------------------------------------------------------------------------- 1 | import console from "ion:console" 2 | 3 | console.log("hello world", import.meta, { foo: 42 }) 4 | -------------------------------------------------------------------------------- /examples/src/testing/typescript/js/main.ts: -------------------------------------------------------------------------------- 1 | import { foo, bar } from "./foo.ts"; 2 | 3 | console.log(foo); 4 | console.log(bar); 5 | -------------------------------------------------------------------------------- /examples/src/testing/typescript/js/bar.ts: -------------------------------------------------------------------------------- 1 | export type Bar = string; 2 | export type Foo = string; 3 | export const bar: Bar = "bar" 4 | -------------------------------------------------------------------------------- /examples/src/testing/typescript/js/foo.ts: -------------------------------------------------------------------------------- 1 | import type { Foo } from "./bar.ts"; 2 | import { bar, Bar } from "./bar.ts"; 3 | 4 | export const foo: Bar & Foo = "foo"; 5 | export { bar }; 6 | -------------------------------------------------------------------------------- /crates/ion/src/values/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod js_object_value; 2 | mod js_values; 3 | mod js_values_vec; 4 | 5 | pub use js_object_value::*; 6 | pub use js_values::*; 7 | pub use js_values_vec::*; 8 | -------------------------------------------------------------------------------- /examples/src/http_server/http1/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | mod bytes; 3 | mod http1_server; 4 | mod res_ext; 5 | 6 | pub use self::bytes::*; 7 | pub use self::http1_server::*; 8 | pub use self::res_ext::*; 9 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Item" 2 | indent_style = "Block" 3 | hard_tabs = false 4 | unstable_features = false 5 | tab_spaces = 4 6 | fn_params_layout = "Vertical" 7 | group_imports = "StdExternalCrate" -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emeraldwalk.runonsave": { 3 | "commands": [ 4 | { 5 | "match": ".rs", 6 | "cmd": "cargo xfmt --file ${file}" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/js/set-timeout.js: -------------------------------------------------------------------------------- 1 | import console from 'ion:console' 2 | import { setTimeout } from 'ion:timers/timeout' 3 | 4 | console.log("Sync start") 5 | 6 | setTimeout(() => console.log("Async done"), 1000) 7 | 8 | console.log("Sync end") 9 | -------------------------------------------------------------------------------- /examples/src/http_server/handlers/root.js: -------------------------------------------------------------------------------- 1 | /** @type {import("./binding").HandlerFunc} */ 2 | export function handler(req, res) { 3 | res.writeHead(200); 4 | 5 | res.write("hello"); 6 | res.write(" world"); 7 | res.end() 8 | }; 9 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/performance/binding.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | var performance: any; 5 | 6 | interface ImportMeta { 7 | extension: { 8 | now(): number; 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "lib": ["ESNext"], 5 | "noEmit": true, 6 | "moduleResolution": "classic" 7 | }, 8 | "include": [ 9 | "js/**/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /crates/ion/src/utils/hash.rs: -------------------------------------------------------------------------------- 1 | use base64::Engine; 2 | use sha2::Digest; 3 | use sha2::Sha256; 4 | 5 | pub fn hash_sha256(bytes: &[u8]) -> String { 6 | let result = Sha256::digest(bytes); 7 | base64::prelude::BASE64_STANDARD.encode(result).to_string() 8 | } 9 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/performance/binding.js: -------------------------------------------------------------------------------- 1 | export default class Performance { 2 | static now() { 3 | import.meta.extension.now(); 4 | } 5 | } 6 | 7 | export const performance = Performance; 8 | 9 | globalThis.performance = performance; 10 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "module": "ESNext", 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "lib": ["ESNext"] 9 | } 10 | } -------------------------------------------------------------------------------- /crates/ion/src/utils/channel.rs: -------------------------------------------------------------------------------- 1 | pub use flume::*; 2 | 3 | pub fn channel() -> (flume::Sender, flume::Receiver) { 4 | flume::unbounded() 5 | } 6 | 7 | pub fn oneshot() -> (flume::Sender, flume::Receiver) { 8 | flume::bounded(1) 9 | } 10 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/global_this/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::JsExtension; 2 | 3 | static BINDING: &str = include_str!("./binding.ts"); 4 | 5 | pub fn global_this() -> JsExtension { 6 | JsExtension::GlobalBinding { 7 | binding: BINDING.to_string(), 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/console/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["./binding.d.ts"], 4 | "module": "ESNext", 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "lib": ["ESNext"] 9 | } 10 | } -------------------------------------------------------------------------------- /examples/src/http_server/handlers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["./binding.d.ts"], 4 | "module": "ESNext", 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "lib": ["ESNext"] 9 | } 10 | } -------------------------------------------------------------------------------- /crates/ion/src/extensions/performance/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["./binding.d.ts"], 4 | "module": "ESNext", 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "lib": ["ESNext"] 9 | } 10 | } -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_interval/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["./binding.d.ts"], 4 | "module": "ESNext", 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "lib": ["ESNext"] 9 | } 10 | } -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_timeout/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["./binding.d.ts"], 4 | "module": "ESNext", 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "lib": ["ESNext"] 9 | } 10 | } -------------------------------------------------------------------------------- /examples/js/set-interval.js: -------------------------------------------------------------------------------- 1 | console.log("Sync start") 2 | 3 | let i = 0 4 | let interval = setInterval(() => { 5 | console.log(`Interval ${i}`) 6 | 7 | if (i === 5) { 8 | clearInterval(interval) 9 | } 10 | 11 | i += 1 12 | }, 500) 13 | 14 | console.log("Sync end") 15 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/event_target/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "module": "ESNext", 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "target": "esnext", 9 | "lib": ["ESNext"] 10 | } 11 | } -------------------------------------------------------------------------------- /examples/src/http_server/handlers/async.js: -------------------------------------------------------------------------------- 1 | /** @type {import("./binding").HandlerFunc} */ 2 | export function handler(req, res) { 3 | res.writeHead(200); 4 | 5 | setTimeout(() => { 6 | res.write("hello"); 7 | res.write(" world"); 8 | res.end() 9 | }, 1000) 10 | }; 11 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/console/binding.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ion:console" { 2 | export default class Console { 3 | static log(...args: any[]): void 4 | static error(...args: any[]): void 5 | static warn(...args: any[]): void 6 | } 7 | 8 | export const console: typeof Console; 9 | } 10 | -------------------------------------------------------------------------------- /examples/src/http_server/handlers/headers.js: -------------------------------------------------------------------------------- 1 | /** @type {import("./binding").HandlerFunc} */ 2 | export function handler(req, res) { 3 | res.headers().set("Content-Type", "text/html; charset=utf-8"); 4 | 5 | res.writeHead(200); 6 | 7 | res.write("hello"); 8 | res.write(" world"); 9 | res.end() 10 | }; 11 | -------------------------------------------------------------------------------- /crates/ion/src/platform/sys/mod.rs: -------------------------------------------------------------------------------- 1 | mod context; 2 | mod context_scope; 3 | mod error; 4 | mod global; 5 | mod isolate_scope; 6 | mod value; 7 | 8 | pub use self::context::*; 9 | pub use self::context_scope::*; 10 | pub use self::error::*; 11 | pub use self::global::*; 12 | pub use self::isolate_scope::*; 13 | pub use self::value::*; 14 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_timeout/binding.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ion:timers/timeout" { 2 | export function setTimeout( 3 | callback: (...args: Array) => any | Promise, 4 | duration?: number, 5 | ...args: Array 6 | ): number; 7 | 8 | export function clearTimeout(ref: number): void; 9 | } 10 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_interval/binding.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ion:timers/interval" { 2 | export function setInterval( 3 | callback: (...args: Array) => any | Promise, 4 | duration?: number, 5 | ...args: Array 6 | ): number; 7 | 8 | export function clearInterval(ref: string): void; 9 | } 10 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | mod console; 2 | mod event_target; 3 | mod global_this; 4 | mod performance; 5 | mod set_interval; 6 | mod set_timeout; 7 | mod test; 8 | 9 | pub use console::*; 10 | pub use event_target::*; 11 | pub use global_this::*; 12 | pub use performance::*; 13 | pub use set_interval::*; 14 | pub use set_timeout::*; 15 | pub use test::*; 16 | -------------------------------------------------------------------------------- /examples/js/modules/index.js: -------------------------------------------------------------------------------- 1 | import { a as _a } from './a.js' 2 | import { b as _b } from './b.js' 3 | import { c as _c1 } from './a.js' 4 | import { c as _c2 } from './b.js' 5 | 6 | export const a = _a 7 | export const b = _b 8 | export const c1 = _c1 9 | export const c2 = _c2 10 | 11 | // console.log({ 12 | // a, 13 | // b, 14 | // c1, 15 | // c2, 16 | // }) -------------------------------------------------------------------------------- /crates/ion/src/extensions/event_target/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::JsExtension; 2 | 3 | static MODULE_NAME: &str = "ion:event_target"; 4 | static BINDING: &str = include_str!("./binding.ts"); 5 | 6 | pub fn event_target() -> JsExtension { 7 | JsExtension::BindingModule { 8 | module_name: MODULE_NAME.to_string(), 9 | binding: BINDING.to_string(), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/ion/src/utils/random_string.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | use rand::Rng; 4 | 5 | pub fn generate_random_string(len: usize) -> String { 6 | const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 7 | let mut rng = rand::rng(); 8 | let one_char = || CHARSET[rng.random_range(0..CHARSET.len())] as char; 9 | iter::repeat_with(one_char).take(len).collect() 10 | } 11 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["**/node_modules/**", "**/.git/**", "**/target/**"], 3 | "watch": [ 4 | "_scratch/**/*", 5 | "crates/**/*", 6 | "examples/**/*" 7 | ], 8 | "ext": "*", 9 | "delay": 250, 10 | "signal": "SIGTERM", 11 | "exec": "clear && cargo build --release --package ion_scratch && .\\target\\release\\ion_scratch.exe" 12 | } 13 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/test/binding.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ion:test" { 2 | export type TestFunc = () => any | Promise; 3 | 4 | export function test(message: string, callback: TestFunc): void; 5 | export function it(message: string, callback: TestFunc): void; 6 | 7 | export function before(callback: TestFunc): void; 8 | export function after(callback: TestFunc): void; 9 | } 10 | -------------------------------------------------------------------------------- /crates/ion/src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] 2 | pub(crate) mod active_context; 3 | pub mod background_worker; 4 | pub(crate) mod extension; 5 | pub(crate) mod finalizer_registry; 6 | pub mod module; 7 | pub mod module_map; 8 | pub(crate) mod platform; 9 | mod realm; 10 | pub mod resolve; 11 | pub(crate) mod sys; 12 | pub(crate) mod worker; 13 | 14 | pub(crate) use realm::*; 15 | -------------------------------------------------------------------------------- /crates/ion_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ion_cli" 3 | version = "0.1.0" 4 | edition = "2024" 5 | description = "Entrypoint for CLI" 6 | 7 | [lints] 8 | workspace = true 9 | 10 | [dependencies] 11 | ion.path = "../ion" 12 | clap.workspace = true 13 | anyhow.workspace = true 14 | normalize-path.workspace = true 15 | flume.workspace = true 16 | tokio.workspace = true 17 | num_cpus.workspace = true -------------------------------------------------------------------------------- /examples/src/http_server/handlers/todo.js: -------------------------------------------------------------------------------- 1 | /** @type {import("./binding").HandlerFunc} */ 2 | export function handler(req, res) { 3 | res.writeHead(201); 4 | 5 | const message = new Uint8Array([ 6 | // "hello world" 7 | 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 8 | ]); 9 | 10 | res.write("hello"); 11 | res.write(" world"); 12 | res.end() 13 | }; 14 | -------------------------------------------------------------------------------- /examples/src/testing/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod background_tasks; 2 | pub mod memory_usage; 3 | pub mod memory_usage_context; 4 | pub mod memory_usage_module; 5 | pub mod memory_usage_tsfn; 6 | pub mod memory_usage_value; 7 | pub mod memory_usage_worker; 8 | pub mod multiple_contexts; 9 | pub mod multiple_workers; 10 | pub mod transformers; 11 | pub mod typescript; 12 | pub mod wait; 13 | 14 | pub use memory_usage::*; 15 | -------------------------------------------------------------------------------- /examples/js/set-timeout-multiple.js: -------------------------------------------------------------------------------- 1 | // import console from 'ion:console' 2 | // import { setTimeout } from 'ion:timers/timeout' 3 | 4 | // console.log("Sync start") 5 | 6 | // setTimeout(() => console.log("Async done 1000"), 1) 7 | // setTimeout(() => console.log("Async done 2000"), 2) 8 | // setTimeout(() => console.log("Async done 3000"), 3) 9 | // setTimeout(() => console.log("Async done 4000"), 4) 10 | 11 | // console.log("Sync end") 12 | -------------------------------------------------------------------------------- /crates/ion/src/testing.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::sync::LazyLock; 3 | 4 | use crate::*; 5 | 6 | pub static JS_RUNTIME: LazyLock> = LazyLock::new(|| { 7 | JsRuntime::initialize_once(JsRuntimeOptions::debug(JsRuntimeOptions { 8 | v8_args: vec![], 9 | resolvers: vec![], 10 | transformers: vec![], 11 | extensions: vec![], 12 | })) 13 | .expect("Unable to start runtime") 14 | }); 15 | -------------------------------------------------------------------------------- /examples/src/testing/multiple_workers/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 5 | 6 | let wrk1 = runtime.spawn_worker(JsWorkerOptions::default())?; 7 | let wrk2 = runtime.spawn_worker(JsWorkerOptions::default())?; 8 | let wrk3 = runtime.spawn_worker(JsWorkerOptions::default())?; 9 | 10 | drop(wrk1); 11 | drop(wrk2); 12 | drop(wrk3); 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /crates/ion/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod channel; 2 | pub mod debug; 3 | pub mod hash; 4 | pub mod hash_map_ext; 5 | mod os_string_ext; 6 | mod path_ext; 7 | pub mod random_string; 8 | pub mod ref_counter; 9 | pub mod ref_counter_atomic; 10 | pub mod tokio_ext; 11 | pub mod v8; 12 | 13 | pub use debug::*; 14 | pub use hash::*; 15 | pub use hash_map_ext::*; 16 | pub use random_string::*; 17 | pub use ref_counter::*; 18 | pub use ref_counter_atomic::*; 19 | 20 | pub use self::os_string_ext::*; 21 | pub use self::path_ext::*; 22 | -------------------------------------------------------------------------------- /examples/js/set-timeout-nested.js: -------------------------------------------------------------------------------- 1 | import console from 'ion:console' 2 | import { setTimeout } from 'ion:timers/timeout' 3 | 4 | console.log("Sync start") 5 | 6 | setTimeout(() => { 7 | console.log("Async starting") 8 | 9 | setTimeout(() => console.log("Async done 1000"), 1000) 10 | setTimeout(() => console.log("Async done 2000"), 2000) 11 | setTimeout(() => console.log("Async done 3000"), 3000) 12 | setTimeout(() => console.log("Async done 4000"), 4000) 13 | }, 1000) 14 | 15 | console.log("Sync end") 16 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/global_this/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "./binding.d.ts", 5 | "../console/binding.d.ts", 6 | "../event_target/binding.d.ts", 7 | "../set_interval/binding.d.ts", 8 | "../set_timeout/binding.d.ts", 9 | ], 10 | "module": "ESNext", 11 | "checkJs": true, 12 | "noEmit": true, 13 | "strict": true, 14 | "target": "esnext", 15 | "lib": ["ESNext"] 16 | } 17 | } -------------------------------------------------------------------------------- /crates/ion/src/utils/v8.rs: -------------------------------------------------------------------------------- 1 | pub fn v8_create_string<'a>( 2 | scope: &mut v8::HandleScope<'a, v8::Context>, 3 | s: impl AsRef, 4 | ) -> crate::Result> { 5 | let Some(value) = v8::String::new(scope, s.as_ref()) else { 6 | return Err(crate::Error::ValueCreateError); 7 | }; 8 | 9 | Ok(value) 10 | } 11 | 12 | pub fn v8_create_undefined<'a>( 13 | scope: &mut v8::HandleScope<'a, v8::Context> 14 | ) -> crate::Result> { 15 | Ok(v8::undefined(scope).into()) 16 | } 17 | -------------------------------------------------------------------------------- /crates/ion/src/platform/sys/value.rs: -------------------------------------------------------------------------------- 1 | #[allow(non_camel_case_types)] 2 | pub type Value = v8::Local<'static, v8::Value>; 3 | 4 | pub fn v8_from_value<'a>(value: impl Into>) -> Value { 5 | unsafe { std::mem::transmute(value.into()) } 6 | } 7 | 8 | pub fn v8_into_static_value<'a, V, T>(value: v8::Local<'a, T>) -> v8::Local<'static, V> { 9 | unsafe { std::mem::transmute(value) } 10 | } 11 | 12 | pub fn v8_value_cast<'a, V, T>(value: v8::Local<'a, T>) -> v8::Local<'static, V> { 13 | unsafe { std::mem::transmute(value) } 14 | } 15 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ion_examples" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [dependencies] 10 | ion = { path = "../crates/ion" } 11 | anyhow.workspace = true 12 | tokio.workspace = true 13 | http.workspace = true 14 | http-body-util.workspace = true 15 | hyper.workspace = true 16 | hyper-util.workspace = true 17 | futures.workspace = true 18 | tokio-util.workspace = true 19 | num_cpus.workspace = true 20 | memory-stats.workspace = true 21 | normalize-path.workspace = true 22 | parking_lot.workspace = true -------------------------------------------------------------------------------- /crates/ion/src/transformers/json/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::JsTransformer; 2 | use crate::TransformerContext; 3 | use crate::TransformerResult; 4 | 5 | pub fn json() -> JsTransformer { 6 | JsTransformer { 7 | kind: "json".to_string(), 8 | transformer: Box::new(transformer), 9 | } 10 | } 11 | 12 | fn transformer(ctx: TransformerContext) -> crate::Result { 13 | let mut code = String::from_utf8(ctx.content)?; 14 | // TODO escapes 15 | code = format!("export default JSON.parse(`{}`)", code); 16 | 17 | Ok(TransformerResult { code }) 18 | } 19 | -------------------------------------------------------------------------------- /crates/ion/src/platform/sys/isolate_scope.rs: -------------------------------------------------------------------------------- 1 | #[allow(non_camel_case_types)] 2 | pub type __v8_root_scope = *mut v8::HandleScope<'static, ()>; 3 | 4 | pub fn v8_new_root_scope(root_scope: v8::HandleScope<'_, ()>) -> __v8_root_scope { 5 | Box::into_raw(Box::new(root_scope)) as _ 6 | } 7 | 8 | pub fn v8_get_root_scope(root_scope: __v8_root_scope) -> &'static mut v8::HandleScope<'static, ()> { 9 | unsafe { &mut *root_scope } 10 | } 11 | 12 | pub fn v8_drop_root_scope(root_scope: __v8_root_scope) -> v8::HandleScope<'static, ()> { 13 | unsafe { *Box::from_raw(root_scope) } 14 | } 15 | -------------------------------------------------------------------------------- /crates/ion/src/js_resolver.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::pin::Pin; 3 | use std::sync::Arc; 4 | 5 | use crate::fs::FileSystem; 6 | 7 | #[derive(Debug)] 8 | pub struct ResolverContext { 9 | pub fs: FileSystem, 10 | pub specifier: String, 11 | pub from: PathBuf, 12 | } 13 | 14 | #[derive(Debug)] 15 | pub struct ResolverResult { 16 | pub code: Vec, 17 | pub path: PathBuf, 18 | pub kind: String, 19 | } 20 | 21 | pub type JsResolver = Arc JsResolverFut>; 22 | 23 | pub type JsResolverFut = 24 | Pin>>>>; 25 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/global_this/binding.ts: -------------------------------------------------------------------------------- 1 | import { EventTarget, Event, CustomEvent } from 'ion:event_target' 2 | import { console } from 'ion:console' 3 | import { setInterval, clearInterval } from 'ion:timers/interval' 4 | import { setTimeout, clearTimeout } from 'ion:timers/timeout' 5 | 6 | globalThis.self = globalThis 7 | 8 | Object.setPrototypeOf(globalThis, new EventTarget); 9 | globalThis.Event = Event 10 | globalThis.CustomEvent = CustomEvent 11 | 12 | globalThis.setInterval = setInterval 13 | globalThis.clearInterval = clearInterval 14 | globalThis.setTimeout = setTimeout 15 | globalThis.clearTimeout = clearTimeout 16 | 17 | globalThis.console = console -------------------------------------------------------------------------------- /crates/ion/src/extensions/test/binding.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | extension: { 4 | test(message: string, callback: () => (any | Promise)): void; 5 | }; 6 | } 7 | } 8 | 9 | export type TestFunc = () => (any | Promise) 10 | 11 | let tests: Array<[string, TestFunc]> = [] 12 | 13 | export const test = (message: string, callback: TestFunc) => { 14 | tests.push([message, callback]) 15 | } 16 | 17 | export const it = test 18 | 19 | export const before = () => {} 20 | 21 | export const after = () => {} 22 | 23 | export const getTests = (): Array<[string, TestFunc]> => { 24 | return [ 25 | ...tests 26 | ] 27 | } -------------------------------------------------------------------------------- /crates/ion/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ion" 3 | version = "0.2.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [dependencies] 12 | flume.workspace = true 13 | v8.workspace = true 14 | tokio = { workspace = true, features = ["full"] } 15 | normalize-path.workspace = true 16 | num_cpus.workspace = true 17 | parking_lot.workspace = true 18 | rand.workspace = true 19 | oxc = { workspace = true, features = [ 20 | "transformer", 21 | "codegen", 22 | "semantic", 23 | ]} 24 | sha2.workspace = true 25 | base64.workspace = true 26 | tracing.workspace = true 27 | oxc-miette.workspace = true 28 | 29 | [dev-dependencies] 30 | anyhow.workspace = true 31 | -------------------------------------------------------------------------------- /crates/ion/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_crate_dependencies)] 2 | mod async_env; 3 | mod env; 4 | mod error; 5 | pub mod extensions; 6 | pub mod fs; 7 | mod js_context; 8 | mod js_extension; 9 | mod js_resolver; 10 | mod js_runtime; 11 | mod js_transformer; 12 | mod js_worker; 13 | pub mod platform; 14 | pub mod resolvers; 15 | #[cfg(test)] 16 | pub mod testing; 17 | pub mod transformers; 18 | pub mod utils; 19 | pub mod values; 20 | 21 | pub use async_env::*; 22 | pub use env::*; 23 | pub use error::*; 24 | pub use js_context::*; 25 | pub use js_extension::*; 26 | pub use js_resolver::*; 27 | pub use js_runtime::*; 28 | pub use js_transformer::*; 29 | pub use js_worker::*; 30 | pub use values::*; 31 | -------------------------------------------------------------------------------- /examples/src/http_server/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use tokio::fs; 4 | 5 | static CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); 6 | 7 | pub async fn get_handler(path: &str) -> anyhow::Result { 8 | let handler_root = PathBuf::from(CARGO_MANIFEST_DIR) 9 | .join("src") 10 | .join("http_server") 11 | .join("handlers"); 12 | 13 | let mut path = path; 14 | if path == "/" { 15 | path = "/root" 16 | } 17 | 18 | let mut target = handler_root; 19 | for segment in path.split("/") { 20 | target = target.join(segment) 21 | } 22 | target.set_extension("js"); 23 | 24 | Ok(fs::read_to_string(target).await?) 25 | } 26 | -------------------------------------------------------------------------------- /examples/src/eval/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | let code = std::env::args() 5 | .collect::>() 6 | .get(2) 7 | .cloned() 8 | .expect("No code provided"); 9 | 10 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 11 | extensions: vec![ 12 | ion::extensions::console(), 13 | ion::extensions::set_interval(), 14 | ion::extensions::set_timeout(), 15 | ], 16 | ..Default::default() 17 | })?; 18 | 19 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 20 | let ctx = worker.create_context()?; 21 | 22 | ctx.eval(code)?; 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/js/test_runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "../../../crates/ion/src/extensions/console/binding.d.ts", 5 | "../../../crates/ion/src/extensions/event_target/binding.d.ts", 6 | "../../../crates/ion/src/extensions/set_interval/binding.d.ts", 7 | "../../../crates/ion/src/extensions/set_timeout/binding.d.ts", 8 | "../../../crates/ion/src/extensions/global_this/binding.d.ts", 9 | "../../../crates/ion/src/extensions/test/binding.d.ts", 10 | ], 11 | "module": "ESNext", 12 | "checkJs": true, 13 | "noEmit": true, 14 | "strict": true, 15 | "lib": ["ESNext"] 16 | } 17 | } -------------------------------------------------------------------------------- /crates/ion/src/utils/tokio_ext.rs: -------------------------------------------------------------------------------- 1 | use tokio::task::LocalSet; 2 | 3 | // Convenience methods for starting a local set 4 | pub trait LocalRuntimeExt { 5 | fn local_block_on( 6 | &self, 7 | future: F, 8 | ) -> F::Output; 9 | } 10 | 11 | impl LocalRuntimeExt for tokio::runtime::Runtime { 12 | fn local_block_on( 13 | &self, 14 | future: F, 15 | ) -> F::Output { 16 | LocalSet::default().block_on(self, future) 17 | } 18 | } 19 | 20 | pub fn local_thread_runtime(fut: F) -> std::io::Result { 21 | Ok(tokio::runtime::Builder::new_current_thread() 22 | .enable_all() 23 | .build()? 24 | .local_block_on(fut)) 25 | } 26 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_timeout/binding.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | extension: { 4 | setTimeout( 5 | callback: () => any | Promise, 6 | duration: number 7 | ): number; 8 | clearTimeout(timerRef: number): void; 9 | }; 10 | } 11 | } 12 | 13 | export function setTimeout( 14 | callback: (...args: Array) => any | Promise, 15 | duration: number = 0, 16 | ...args: Array 17 | ): number { 18 | return import.meta.extension.setTimeout(() => callback(...args), duration); 19 | } 20 | 21 | export function clearTimeout(ref: number) { 22 | return import.meta.extension.clearTimeout(ref); 23 | } 24 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_interval/binding.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | extension: { 4 | setInterval( 5 | callback: () => any | Promise, 6 | duration: number 7 | ): number; 8 | clearInterval(timerRef: number): void; 9 | }; 10 | } 11 | } 12 | 13 | export function setInterval( 14 | callback: (...args: Array) => any | Promise, 15 | duration: number = 0, 16 | ...args: Array 17 | ): number { 18 | return import.meta.extension.setInterval(() => callback(...args), duration); 19 | } 20 | 21 | export function clearInterval(ref: number) { 22 | return import.meta.extension.clearInterval(ref); 23 | } 24 | -------------------------------------------------------------------------------- /crates/ion/src/platform/sys/context_scope.rs: -------------------------------------------------------------------------------- 1 | #[allow(non_camel_case_types)] 2 | pub type __v8_context_scope = *mut v8::ContextScope<'static, v8::HandleScope<'static>>; 3 | 4 | pub fn v8_new_context_scope( 5 | scope: v8::ContextScope<'static, v8::HandleScope<'static>> 6 | ) -> __v8_context_scope { 7 | Box::into_raw(Box::new(scope)) as _ 8 | } 9 | 10 | pub fn v8_get_context_scope( 11 | context_scope: __v8_context_scope 12 | ) -> &'static mut v8::ContextScope<'static, v8::HandleScope<'static>> { 13 | unsafe { &mut *context_scope } 14 | } 15 | 16 | pub fn v8_drop_context_scope( 17 | context_scope: __v8_context_scope 18 | ) -> v8::ContextScope<'static, v8::HandleScope<'static>> { 19 | unsafe { *Box::from_raw(context_scope) } 20 | } 21 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/console/binding.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | extension: { 4 | log(...args: Array): void; 5 | warn(...args: Array): void; 6 | error(...args: Array): void; 7 | }; 8 | } 9 | } 10 | 11 | export default class Console { 12 | static log(/** @type {Array} */ ...args: any[]) { 13 | import.meta.extension.log(...args); 14 | } 15 | 16 | static error(/** @type {Array} */ ...args: any[]) { 17 | import.meta.extension.error(...args); 18 | } 19 | 20 | static warn(/** @type {Array} */ ...args: any[]) { 21 | import.meta.extension.warn(...args); 22 | } 23 | } 24 | 25 | export const console = Console; 26 | -------------------------------------------------------------------------------- /examples/js/test_runner/foo.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'ion:test' 2 | 3 | test("Should do something 1", () => { 4 | // console.log(1) 5 | }) 6 | 7 | test("Should do something 2", () => { 8 | // console.log(2) 9 | }) 10 | 11 | test("Should do something 3", () => { 12 | // console.log(3) 13 | }) 14 | 15 | test("Should do something 4", () => { 16 | // console.log(4) 17 | }) 18 | 19 | test("Should do something 5", () => { 20 | // console.log(5) 21 | }) 22 | 23 | test("Should do something 6", () => { 24 | // console.log(6) 25 | }) 26 | 27 | test("Should do something 7", () => { 28 | // console.log(7) 29 | }) 30 | 31 | test("Should do something 8", () => { 32 | // console.log(8) 33 | }) 34 | 35 | test("Should do something 9", () => { 36 | // console.log(9) 37 | }) 38 | -------------------------------------------------------------------------------- /examples/src/testing/memory_usage_worker/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use ion::*; 5 | 6 | use crate::testing::MemoryUsageCounter; 7 | 8 | pub fn main() -> anyhow::Result<()> { 9 | let memu = MemoryUsageCounter::default(); 10 | println!("[0] {:?}", memu); 11 | 12 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 13 | println!("[1] {:?}", memu); 14 | 15 | for i in 2..50 { 16 | { 17 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 18 | worker.run_garbage_collection_for_testing()?; 19 | drop(worker); 20 | }; 21 | 22 | println!("[{}] {:?}", i, memu); 23 | thread::sleep(Duration::from_millis(100)); 24 | } 25 | 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /crates/ion/src/platform/resolve.rs: -------------------------------------------------------------------------------- 1 | use crate::JsResolver; 2 | use crate::ResolverContext; 3 | use crate::ResolverResult; 4 | 5 | pub async fn run_resolvers( 6 | resolvers: &Vec, 7 | ctx: ResolverContext, 8 | ) -> crate::Result> { 9 | for resolver in resolvers { 10 | match resolver(ResolverContext { 11 | fs: ctx.fs.clone(), 12 | specifier: ctx.specifier.clone(), 13 | from: ctx.from.clone(), 14 | }) 15 | .await? 16 | { 17 | Some(result) => { 18 | return Ok(Some(result)); 19 | } 20 | None => continue, 21 | } 22 | } 23 | 24 | // Always fall back to resolving relative paths 25 | crate::resolvers::relative()(ctx).await 26 | } 27 | -------------------------------------------------------------------------------- /examples/js/set-timeout-cancel.js: -------------------------------------------------------------------------------- 1 | import console from 'ion:console' 2 | import { setTimeout, clearTimeout } from 'ion:timers/timeout' 3 | 4 | async function main() { 5 | for (let i = 0; i < 2; i++) { 6 | await waitForSetTimeout(i) 7 | await new Promise((res) => setTimeout(res, 2000)); 8 | } 9 | } 10 | 11 | main() 12 | 13 | async function waitForSetTimeout(run) { 14 | console.log(`[${run}] Sync start`); 15 | 16 | let int = setTimeout(() => { 17 | console.log(`[${run}] Should not run`); 18 | }, 1000); 19 | 20 | console.log(`[${run}] setTimeout started: ${int}`); 21 | 22 | setTimeout(() => { 23 | console.log(`[${run}] setTimeout cancelled: ${int}`); 24 | clearTimeout(int); 25 | }, 500); 26 | 27 | console.log(`[${run}] Sync end`); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /crates/ion/src/js_transformer.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub struct JsTransformer { 4 | /// The file extension of the input file handled by this transformer. 5 | /// Example: "ts" 6 | pub kind: String, 7 | /// The callback run to transform the input file into JavaScript 8 | pub transformer: 9 | Box crate::Result>, 10 | } 11 | 12 | pub struct TransformerContext { 13 | /// The bytes of the input file 14 | pub content: Vec, 15 | /// Path to the source file 16 | pub path: PathBuf, 17 | /// The extension of the file being processed 18 | pub kind: String, 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct TransformerResult { 23 | /// The transformed JavaScript to run 24 | pub code: String, 25 | } 26 | -------------------------------------------------------------------------------- /crates/ion_cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod cmd; 2 | 3 | use clap::Parser; 4 | use clap::Subcommand; 5 | 6 | #[derive(Debug, Parser)] 7 | struct Command { 8 | #[clap(subcommand)] 9 | command: Commands, 10 | } 11 | 12 | #[derive(Debug, Subcommand)] 13 | enum Commands { 14 | /// Execute a file 15 | Run(cmd::run::RunCommand), 16 | /// Evaluate code from commandline 17 | Eval(cmd::eval::EvalCommand), 18 | /// Run tests 19 | Test(cmd::test::TestCommand), 20 | } 21 | 22 | pub fn main() -> anyhow::Result<()> { 23 | let command = Command::parse(); 24 | 25 | // dbg!(&command); 26 | 27 | match command.command { 28 | Commands::Run(command) => cmd::run::main(command), 29 | Commands::Eval(command) => cmd::eval::main(command), 30 | Commands::Test(command) => cmd::test::main(command), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /watch.bash: -------------------------------------------------------------------------------- 1 | # exec cargo watch \ 2 | # -w crates \ 3 | # -w examples \ 4 | # -w _scratch \ 5 | # -- bash -c "\ 6 | # clear && \ 7 | # cargo build --package ion_scratch && \ 8 | # ./target/debug/ion_scratch && \ 9 | # echo -------- \ 10 | # " 11 | 12 | exec cargo watch \ 13 | -w crates \ 14 | -w examples \ 15 | -w _scratch \ 16 | -- bash -c "\ 17 | clear && \ 18 | cargo build --package ion_cli && \ 19 | ./target/debug/ion_cli run ./examples/js/modules/index.js && \ 20 | echo -------- \ 21 | " 22 | 23 | # cargo watch \ 24 | # -w crates \ 25 | # -w examples \ 26 | # -w _scratch \ 27 | # -- bash -c "\ 28 | # clear && \ 29 | # cargo build --package ion_examples && \ 30 | # ./target/debug/ion_examples basic && \ 31 | # echo -------- \ 32 | # " -------------------------------------------------------------------------------- /crates/ion/src/values/mod.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod js_array; 3 | mod js_array_buffer; 4 | mod js_boolean; 5 | mod js_deferred; 6 | mod js_exception; 7 | mod js_external; 8 | mod js_function; 9 | mod js_null; 10 | mod js_number; 11 | mod js_object; 12 | mod js_promise; 13 | mod js_string; 14 | mod js_undefined; 15 | mod js_unknown; 16 | mod root; 17 | pub mod thread_safe_function; 18 | pub mod thread_safe_promise; 19 | 20 | pub use common::*; 21 | pub use js_array::*; 22 | pub use js_array_buffer::*; 23 | pub use js_boolean::*; 24 | pub use js_deferred::*; 25 | pub use js_exception::*; 26 | pub use js_external::*; 27 | pub use js_function::*; 28 | pub use js_null::*; 29 | pub use js_number::*; 30 | pub use js_object::*; 31 | pub use js_promise::*; 32 | pub use js_string::*; 33 | pub use js_undefined::*; 34 | pub use js_unknown::*; 35 | pub use root::*; 36 | pub use thread_safe_function::*; 37 | pub use thread_safe_promise::*; 38 | -------------------------------------------------------------------------------- /examples/src/basic/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 5 | 6 | // Create an isolate running on a dedicated thread 7 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 8 | 9 | // // Open a JavaScript context on the isolate thread to execute JavaScript on 10 | // // You can open multiple contexts, sharing the same thread 11 | let ctx = worker.create_context()?; 12 | 13 | // Execute some JavaScript in the context 14 | ctx.exec_blocking(|env| { 15 | // Evaluate arbitrary JavaScript, the result of the last line is returned 16 | let value = env.eval_script::("1 + 1")?; 17 | 18 | // Cast to Rust type 19 | let result = value.get_u32()?; 20 | 21 | println!("Returned: {}", result); 22 | Ok(()) 23 | })?; 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /crates/ion/src/utils/ref_counter.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | /// Simple single threaded reference counter 5 | #[derive(Debug, Clone)] 6 | pub struct RefCounter(Rc>); 7 | 8 | impl Default for RefCounter { 9 | fn default() -> Self { 10 | Self::new(1) 11 | } 12 | } 13 | 14 | impl RefCounter { 15 | pub fn new(start: usize) -> Self { 16 | Self(Rc::new(RefCell::new(start))) 17 | } 18 | 19 | pub fn inc(&self) -> usize { 20 | let mut count = self.0.borrow_mut(); 21 | (*count) += 1; 22 | *count 23 | } 24 | 25 | pub fn dec(&self) -> bool { 26 | let mut count = self.0.borrow_mut(); 27 | if *count == 0 { 28 | panic!("Cannot decrement below 0") 29 | } 30 | (*count) -= 1; 31 | *count == 0 32 | } 33 | 34 | pub fn count(&self) -> usize { 35 | *self.0.borrow() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/src/testing/memory_usage_value/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use ion::*; 5 | 6 | use crate::testing::MemoryUsageCounter; 7 | 8 | pub fn main() -> anyhow::Result<()> { 9 | let memu = MemoryUsageCounter::default(); 10 | println!("[0] {:?}", memu); 11 | 12 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 13 | println!("[1] {:?}", memu); 14 | 15 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 16 | let ctx = worker.create_context()?; 17 | 18 | for i in 2..50 { 19 | for _ in 2..100 { 20 | let _value = ctx.exec_blocking(|env| { 21 | let value = env.eval_script::("1 + 1")?; 22 | value.get_u32() 23 | })?; 24 | } 25 | 26 | println!("[{}] {:?}", i, memu); 27 | thread::sleep(Duration::from_millis(100)); 28 | } 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /crates/ion/src/platform/sys/context.rs: -------------------------------------------------------------------------------- 1 | #[allow(non_camel_case_types)] 2 | pub type __v8_context = *mut v8::Global; 3 | 4 | pub fn v8_new_context( 5 | isolate: *mut v8::Isolate, 6 | scope: &mut v8::HandleScope<'_, ()>, 7 | ) -> __v8_context { 8 | // Note: [`v8::Global::into_raw`] appears to have a memory leak 9 | let context_local = v8::Context::new(scope, Default::default()); 10 | let context_global = v8::Global::new(unsafe { &mut *isolate }, context_local); 11 | Box::into_raw(Box::new(context_global)) 12 | } 13 | 14 | pub fn v8_get_context(context: __v8_context) -> v8::Local<'static, v8::Context> { 15 | unsafe { *(context as *mut v8::Local<'static, v8::Context>) } 16 | } 17 | 18 | pub fn v8_get_context_address(context: __v8_context) -> usize { 19 | context as usize 20 | } 21 | 22 | pub fn v8_drop_context(context: __v8_context) -> v8::Global { 23 | unsafe { *Box::from_raw(context) } 24 | } 25 | -------------------------------------------------------------------------------- /examples/src/testing/memory_usage_module/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use ion::*; 5 | 6 | use crate::testing::MemoryUsageCounter; 7 | 8 | pub fn main() -> anyhow::Result<()> { 9 | let memu = MemoryUsageCounter::default(); 10 | println!("[0] {:?}", memu); 11 | 12 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 13 | println!("[1] {:?}", memu); 14 | 15 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 16 | 17 | for i in 2..50 { 18 | let ctx = worker.create_context()?; 19 | 20 | for _ in 2..1000 { 21 | ctx.exec_blocking(|env| { 22 | env.eval_module("export {}")?; 23 | Ok(()) 24 | })?; 25 | } 26 | 27 | worker.run_garbage_collection_for_testing()?; 28 | println!("[{}] {:?}", i, memu); 29 | thread::sleep(Duration::from_millis(100)); 30 | } 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/test/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::JsExtension; 3 | use crate::JsFunction; 4 | use crate::JsObject; 5 | use crate::JsObjectValue; 6 | use crate::JsString; 7 | 8 | static MODULE_NAME: &str = "ion:test"; 9 | static BINDING: &str = include_str!("./binding.ts"); 10 | 11 | pub fn test() -> JsExtension { 12 | JsExtension::NativeModuleWithBinding { 13 | module_name: MODULE_NAME.to_string(), 14 | binding: BINDING.to_string(), 15 | extension: Box::new(extension_hook), 16 | } 17 | } 18 | 19 | fn extension_hook( 20 | env: &Env, 21 | exports: &mut JsObject, 22 | ) -> crate::Result<()> { 23 | exports.set_named_property( 24 | "test", 25 | JsFunction::new(env, |_env, ctx| { 26 | let message = ctx.arg::(0)?; 27 | let _callback = ctx.arg::(1)?; 28 | 29 | println!("{}", message.get_string()?); 30 | Ok(()) 31 | })?, 32 | )?; 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/src/testing/memory_usage_context/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use ion::*; 5 | 6 | use crate::testing::MemoryUsageCounter; 7 | 8 | pub fn main() -> anyhow::Result<()> { 9 | let memu = MemoryUsageCounter::default(); 10 | println!("[0] {:?}", memu); 11 | 12 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 13 | println!("[1] {:?}", memu); 14 | 15 | for i in 0..50 { 16 | { 17 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 18 | 19 | { 20 | let ctx0 = worker.create_context()?; 21 | let ctx1 = worker.create_context()?; 22 | 23 | drop(ctx0); 24 | drop(ctx1); 25 | }; 26 | 27 | worker.run_garbage_collection_for_testing()?; 28 | drop(worker); 29 | }; 30 | 31 | println!("[{}] {:?}", i, memu); 32 | thread::sleep(Duration::from_millis(100)); 33 | } 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /crates/ion/src/platform/sys/global.rs: -------------------------------------------------------------------------------- 1 | use super::__v8_context; 2 | use super::__v8_context_scope; 3 | use super::v8_get_context; 4 | use super::v8_get_context_scope; 5 | 6 | #[allow(non_camel_case_types)] 7 | pub type __v8_global_this = *mut v8::Global; 8 | 9 | pub fn v8_new_global_this( 10 | context: __v8_context, 11 | context_scope: __v8_context_scope, 12 | ) -> __v8_global_this { 13 | let scope = v8_get_context_scope(context_scope); 14 | 15 | // Note: [`v8::Global::into_raw`] appears to have a memory leak 16 | let global_local = v8_get_context(context).global(scope); 17 | let global_global = v8::Global::new(scope, global_local); 18 | Box::into_raw(Box::new(global_global)) 19 | } 20 | 21 | pub fn v8_get_global_this(v8_global_this: __v8_global_this) -> v8::Local<'static, v8::Object> { 22 | unsafe { *(v8_global_this as *mut v8::Local<'static, v8::Object>) } 23 | } 24 | 25 | pub fn v8_drop_global_this(v8_global_this: __v8_global_this) -> v8::Global { 26 | unsafe { *Box::from_raw(v8_global_this) } 27 | } 28 | -------------------------------------------------------------------------------- /examples/src/custom_resolver/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | use ion::utils::PathExt; 5 | use ion::*; 6 | 7 | static CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); 8 | 9 | pub fn main() -> anyhow::Result<()> { 10 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 11 | resolvers: vec![custom_resolver()], 12 | ..Default::default() 13 | })?; 14 | 15 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 16 | let ctx = worker.create_context()?; 17 | 18 | let entry_point = PathBuf::from(CARGO_MANIFEST_DIR) 19 | .join("js") 20 | .join("modules") 21 | .join("index.js") 22 | .try_to_string()?; 23 | 24 | ctx.import(&entry_point)?; 25 | 26 | Ok(()) 27 | } 28 | 29 | pub fn custom_resolver() -> JsResolver { 30 | Arc::new(|ctx: ResolverContext| -> JsResolverFut { 31 | Box::pin(async move { 32 | println!("Custom Resolver Has Run For Path {:?}", ctx.from); 33 | Ok(None) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/performance/mod.rs: -------------------------------------------------------------------------------- 1 | use tokio::time::Instant; 2 | 3 | use crate::Env; 4 | use crate::JsExtension; 5 | use crate::JsFunction; 6 | use crate::JsNumber; 7 | use crate::JsObject; 8 | use crate::JsObjectValue; 9 | 10 | static MODULE_NAME: &str = "ion:performance"; 11 | static BINDING: &str = include_str!("./binding.js"); 12 | 13 | fn extension_hook( 14 | env: &Env, 15 | exports: &mut JsObject, 16 | ) -> crate::Result<()> { 17 | exports.set_named_property( 18 | "now", 19 | JsFunction::new(env, |env, _| { 20 | let now = Instant::now(); 21 | // Convert the internal nanoseconds to milliseconds 22 | let elapsed = now.elapsed().as_nanos() as f64 / 1_000_000.0; 23 | 24 | JsNumber::from_f64(env, elapsed) 25 | })?, 26 | )?; 27 | 28 | Ok(()) 29 | } 30 | 31 | pub fn performance() -> JsExtension { 32 | JsExtension::NativeModuleWithBinding { 33 | module_name: MODULE_NAME.to_string(), 34 | binding: BINDING.to_string(), 35 | extension: Box::new(extension_hook), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/global_this/binding.d.ts: -------------------------------------------------------------------------------- 1 | import * as stdEventTarget from 'ion:event_target' 2 | import * as stdConsole from 'ion:console' 3 | import * as stdTimersInterval from 'ion:timers/interval' 4 | import * as stdTimersTimeout from 'ion:timers/timeout' 5 | 6 | declare global { 7 | var self: typeof globalThis 8 | 9 | var addEventListener: InstanceType['addEventListener'] 10 | var removeEventListener: InstanceType['removeEventListener'] 11 | var dispatchEvent: InstanceType['dispatchEvent'] 12 | type Event = stdEventTarget.Event; 13 | var Event: typeof stdEventTarget.Event 14 | type CustomEvent = stdEventTarget.CustomEvent; 15 | var CustomEvent: typeof stdEventTarget.CustomEvent 16 | 17 | var console: typeof stdConsole.default 18 | 19 | var setInterval: typeof stdTimersInterval.setInterval 20 | var clearInterval: typeof stdTimersInterval.clearInterval 21 | var setTimeout: typeof stdTimersTimeout.setTimeout 22 | var clearTimeout: typeof stdTimersTimeout.clearTimeout 23 | } 24 | 25 | export {} 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 David Alsh 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 | -------------------------------------------------------------------------------- /crates/ion/src/utils/ref_counter_atomic.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::sync::atomic::AtomicUsize; 3 | use std::sync::atomic::Ordering; 4 | 5 | /// Simple single threaded reference counter 6 | #[derive(Debug)] 7 | pub struct AtomicRefCounter(Arc); 8 | 9 | impl Clone for AtomicRefCounter { 10 | fn clone(&self) -> Self { 11 | self.inc(); 12 | Self(self.0.clone()) 13 | } 14 | } 15 | 16 | impl Drop for AtomicRefCounter { 17 | fn drop(&mut self) { 18 | self.dec(); 19 | } 20 | } 21 | 22 | impl Default for AtomicRefCounter { 23 | fn default() -> Self { 24 | Self::new(1) 25 | } 26 | } 27 | 28 | impl AtomicRefCounter { 29 | pub fn new(start: usize) -> Self { 30 | Self(Arc::new(AtomicUsize::new(start))) 31 | } 32 | 33 | pub fn inc(&self) -> usize { 34 | self.0.fetch_add(1, Ordering::Relaxed) 35 | } 36 | 37 | pub fn dec(&self) -> bool { 38 | let previous = self.0.fetch_sub(1, Ordering::Relaxed); 39 | previous == 1 40 | } 41 | 42 | pub fn count(&self) -> usize { 43 | self.0.load(Ordering::Relaxed) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/testing/typescript/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use ion::utils::PathExt; 4 | use ion::*; 5 | 6 | static CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); 7 | 8 | pub fn main() -> anyhow::Result<()> { 9 | let entry_point = PathBuf::from(CARGO_MANIFEST_DIR) 10 | .join("src") 11 | .join("testing") 12 | .join("typescript") 13 | .join("js") 14 | .join("main.ts"); 15 | 16 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 17 | extensions: vec![ 18 | ion::extensions::console(), 19 | ion::extensions::set_interval(), 20 | ion::extensions::set_timeout(), 21 | ], 22 | transformers: vec![ 23 | ion::transformers::json(), 24 | ion::transformers::ts(), 25 | ion::transformers::tsx(), 26 | ], 27 | ..Default::default() 28 | })?; 29 | 30 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 31 | let ctx = worker.create_context()?; 32 | 33 | ctx.exec_blocking(move |env| env.import(entry_point.try_to_string()?))?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/src/testing/transformers/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use ion::utils::PathExt; 4 | use ion::*; 5 | 6 | static CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); 7 | 8 | pub fn main() -> anyhow::Result<()> { 9 | let entry_point = PathBuf::from(CARGO_MANIFEST_DIR) 10 | .join("src") 11 | .join("testing") 12 | .join("transformers") 13 | .join("js") 14 | .join("main.js"); 15 | 16 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 17 | extensions: vec![ 18 | ion::extensions::console(), 19 | ion::extensions::set_interval(), 20 | ion::extensions::set_timeout(), 21 | ], 22 | transformers: vec![ 23 | ion::transformers::json(), 24 | ion::transformers::ts(), 25 | ion::transformers::tsx(), 26 | ], 27 | ..Default::default() 28 | })?; 29 | 30 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 31 | let ctx = worker.create_context()?; 32 | 33 | ctx.exec_blocking(move |env| env.import(entry_point.try_to_string()?))?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/src/deferred/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use ion::*; 5 | 6 | pub fn main() -> anyhow::Result<()> { 7 | let rt = JsRuntime::initialize_once(JsRuntimeOptions { 8 | extensions: vec![ion::extensions::console()], 9 | ..Default::default() 10 | })?; 11 | 12 | let wrk = rt.spawn_worker(JsWorkerOptions::default())?; 13 | let ctx = wrk.create_context()?; 14 | 15 | ctx.exec_blocking(|env| { 16 | let (promise, deferred) = JsDeferred::new(env)?; 17 | 18 | thread::spawn({ 19 | move || { 20 | thread::sleep(Duration::from_secs(1)); 21 | deferred.resolve(|_env| { 22 | // 23 | Ok(42) 24 | }) 25 | } 26 | }); 27 | 28 | let mut global_this = env.global_this()?; 29 | global_this.set_named_property("foo", promise)?; 30 | 31 | Ok(()) 32 | })?; 33 | 34 | ctx.eval( 35 | r#" 36 | globalThis.foo 37 | .then(() => console.log("Done")) 38 | .catch(() => console.log("threw")) 39 | "#, 40 | )?; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /crates/ion_cli/src/cmd/eval.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ion::*; 3 | 4 | #[derive(Debug, Parser)] 5 | pub struct EvalCommand { 6 | /// Code to evaluate 7 | pub code: String, 8 | } 9 | 10 | pub fn main(command: EvalCommand) -> anyhow::Result<()> { 11 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 12 | v8_args: vec![], 13 | resolvers: vec![ion::resolvers::relative()], 14 | transformers: vec![ 15 | ion::transformers::json(), 16 | ion::transformers::ts(), 17 | ion::transformers::tsx(), 18 | ], 19 | extensions: vec![ 20 | ion::extensions::event_target(), 21 | ion::extensions::console(), 22 | ion::extensions::set_timeout(), 23 | ion::extensions::set_interval(), 24 | ion::extensions::test(), 25 | ion::extensions::global_this(), 26 | ], 27 | })?; 28 | 29 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 30 | let ctx = worker.create_context()?; 31 | 32 | ctx.exec_blocking(|env| { 33 | env.eval_script::(command.code)?; 34 | Ok(()) 35 | })?; 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /crates/ion/src/utils/debug.rs: -------------------------------------------------------------------------------- 1 | // Helpful for debugging, not needed in the application 2 | use std::ops::Deref; 3 | use std::ops::DerefMut; 4 | use std::sync::LazyLock; 5 | use std::sync::atomic::AtomicU64; 6 | 7 | static ID: LazyLock = LazyLock::new(Default::default); //Default::default(); 8 | 9 | pub fn new_id() -> u64 { 10 | ID.fetch_add(1, std::sync::atomic::Ordering::AcqRel) 11 | } 12 | 13 | pub struct DropDetector(u64, String, T); 14 | 15 | impl DropDetector { 16 | pub fn new( 17 | name: impl AsRef, 18 | v: T, 19 | ) -> Self { 20 | let id = new_id(); 21 | println!("-> [{}] [{}] Created", id, name.as_ref()); 22 | Self(id, name.as_ref().to_string(), v) 23 | } 24 | } 25 | 26 | impl Drop for DropDetector { 27 | fn drop(&mut self) { 28 | println!("<- [{}] [{}] Dropped", self.0, self.1); 29 | } 30 | } 31 | 32 | impl Deref for DropDetector { 33 | type Target = T; 34 | 35 | fn deref(&self) -> &Self::Target { 36 | &self.2 37 | } 38 | } 39 | 40 | impl DerefMut for DropDetector { 41 | fn deref_mut(&mut self) -> &mut Self::Target { 42 | &mut self.2 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/ion/src/utils/os_string_ext.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::ffi::OsString; 3 | 4 | pub trait OsStringExt { 5 | fn try_to_string(self) -> std::io::Result; 6 | } 7 | 8 | impl OsStringExt for OsString { 9 | fn try_to_string(self) -> std::io::Result { 10 | match self.into_string() { 11 | Ok(name) => Ok(name), 12 | Err(_) => Err(std::io::Error::other( 13 | "Unable to convert OsString to String", 14 | )), 15 | } 16 | } 17 | } 18 | 19 | impl OsStringExt for &OsStr { 20 | fn try_to_string(self) -> std::io::Result { 21 | match self.to_str() { 22 | Some(name) => Ok(name.to_string()), 23 | None => Err(std::io::Error::other( 24 | "Unable to convert OsString to String", 25 | )), 26 | } 27 | } 28 | } 29 | 30 | impl OsStringExt for Option<&OsStr> { 31 | fn try_to_string(self) -> std::io::Result { 32 | match self { 33 | Some(name) => Ok(name.try_to_string()?), 34 | None => Err(std::io::Error::other( 35 | "Unable to convert OsString to String", 36 | )), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/ion/src/values/common/js_values.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::JsUnknown; 3 | use crate::platform::sys::Value; 4 | 5 | pub trait FromJsValue: Sized { 6 | /// this function called to convert JavaScript values to native rust values 7 | fn from_js_value( 8 | env: &Env, 9 | value: Value, 10 | ) -> crate::Result; 11 | } 12 | 13 | pub trait JsValue: Sized + FromJsValue { 14 | fn value(&self) -> &Value; 15 | fn env(&self) -> &Env; 16 | 17 | fn type_of(&self) -> String { 18 | let scope = &mut self.env().scope(); 19 | let type_of = self.value().type_of(scope); 20 | type_of.to_rust_string_lossy(scope) 21 | } 22 | } 23 | 24 | pub trait ToJsValue: Sized { 25 | /// this function called to convert rust values to JavaScript values 26 | fn to_js_value( 27 | env: &Env, 28 | val: Self, 29 | ) -> crate::Result; 30 | } 31 | 32 | pub trait ToJsUnknown: Sized + JsValue { 33 | /// this function called to convert JavaScript values into unknown JavaScript values 34 | fn into_unknown(self) -> JsUnknown { 35 | JsUnknown { 36 | env: self.env().clone(), 37 | value: *self.value(), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/src/http_server/http1/bytes.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::convert::Infallible; 3 | 4 | use http_body_util::Full; 5 | use http_body_util::combinators::BoxBody; 6 | use hyper::body::Bytes as HyperBytes; 7 | 8 | pub struct Bytes(Vec); 9 | 10 | impl From> for Bytes { 11 | fn from(value: Vec) -> Self { 12 | Self(value) 13 | } 14 | } 15 | 16 | impl From<&[u8]> for Bytes { 17 | fn from(value: &[u8]) -> Self { 18 | Self(value.to_vec()) 19 | } 20 | } 21 | 22 | impl<'a> From> for Bytes { 23 | fn from(value: Cow<'a, [u8]>) -> Self { 24 | Self(value.to_vec()) 25 | } 26 | } 27 | 28 | impl From<&str> for Bytes { 29 | fn from(value: &str) -> Self { 30 | Self(value.as_bytes().to_vec()) 31 | } 32 | } 33 | 34 | impl From for Bytes { 35 | fn from(value: String) -> Self { 36 | Self(value.as_bytes().to_vec()) 37 | } 38 | } 39 | 40 | impl From for Full { 41 | fn from(val: Bytes) -> Self { 42 | Full::new(HyperBytes::from(val.0)) 43 | } 44 | } 45 | 46 | impl From for BoxBody { 47 | fn from(val: Bytes) -> Self { 48 | BoxBody::new(Full::new(HyperBytes::from(val.0))) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/src/set_interval/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 5 | extensions: vec![ 6 | ion::extensions::console(), 7 | ion::extensions::set_interval(), 8 | ion::extensions::set_timeout(), 9 | ], 10 | transformers: vec![ 11 | ion::transformers::json(), 12 | ion::transformers::ts(), 13 | ion::transformers::tsx(), 14 | ], 15 | ..Default::default() 16 | })?; 17 | 18 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 19 | let ctx = worker.create_context()?; 20 | 21 | ctx.exec_blocking(|env| { 22 | env.eval_script::( 23 | r#" 24 | let i = 0; 25 | 26 | let timerRef = setInterval(() => { 27 | console.log(`${i} Interval Ran`); 28 | i += 1; 29 | }, 100); 30 | 31 | setTimeout(() => { 32 | console.log(`setInterval cleared`); 33 | clearInterval(timerRef); 34 | }, 500); 35 | "#, 36 | )?; 37 | 38 | Ok(()) 39 | })?; 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/event_target/binding.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ion:event_target" { 2 | export type EventListener = (event: Ev) => any | Promise; 3 | export type EventListenerOptions = { once?: boolean }; 4 | 5 | export class EventTarget { 6 | addEventListener( 7 | type: string, 8 | listener: EventListener, 9 | options?: EventListenerOptions 10 | ): void; 11 | removeEventListener( 12 | type: string, 13 | listener: EventListener 14 | ): void; 15 | dispatchEvent(event: Event): void; 16 | } 17 | 18 | export class Event { 19 | isTrusted: boolean; 20 | bubbles: boolean; 21 | cancelBubble: boolean; 22 | cancelable: boolean; 23 | composed: boolean; 24 | currentTarget: null | EventTarget; 25 | defaultPrevented: boolean; 26 | eventPhase: number; 27 | returnValue: boolean; 28 | srcElement: null; 29 | target: null | EventTarget; 30 | timeStamp: number; 31 | type: string; 32 | } 33 | 34 | export class CustomEvent extends Event { 35 | detail: any; 36 | constructor(type: string, detail?: any); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/ion/src/platform/module_map.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::rc::Rc; 4 | 5 | use crate::platform::module::Module; 6 | 7 | #[derive(Debug, Default, Clone)] 8 | pub struct ModuleMap { 9 | inner: Rc>>>, 10 | names: Rc>>, 11 | } 12 | 13 | impl ModuleMap { 14 | pub fn insert( 15 | &self, 16 | module: Module, 17 | ) { 18 | let id = module.id; 19 | let name = module.name.clone(); 20 | 21 | let mut inner = self.inner.borrow_mut(); 22 | let mut names = self.names.borrow_mut(); 23 | 24 | inner.insert(id, Rc::new(module)); 25 | names.insert(name, id); 26 | } 27 | 28 | pub fn get_module_by_id( 29 | &self, 30 | id: &i32, 31 | ) -> Option> { 32 | let inner = self.inner.borrow(); 33 | if let Some(module) = inner.get(id) { 34 | return Some(Rc::clone(module)); 35 | } 36 | None 37 | } 38 | 39 | pub fn get_module( 40 | &self, 41 | name: impl AsRef, 42 | ) -> Option> { 43 | let names = self.names.borrow(); 44 | let id = names.get(name.as_ref())?; 45 | self.get_module_by_id(id) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["./crates/*", "./examples"] 4 | 5 | [workspace.lints.rust] 6 | elided_lifetimes_in_paths = "allow" 7 | rust_2018_idioms = { priority = -1, level = "deny" } 8 | 9 | [workspace.lints.clippy] 10 | module_inception = "allow" 11 | uninlined-format-args = "allow" 12 | type-complexity = "allow" 13 | new-without-default = "allow" 14 | 15 | [profile.release] 16 | opt-level = 3 17 | debug = false 18 | lto = true 19 | strip = "debuginfo" 20 | panic = 'unwind' 21 | incremental = false 22 | codegen-units = 1 23 | rpath = false 24 | 25 | [workspace.dependencies] 26 | anyhow = "1.0.99" 27 | async-io = "2.5.0" 28 | base64 = "0.22.1" 29 | clap = { version = "4.5.47", features = ["derive"] } 30 | flume = "0.11.1" 31 | futures = "0.3.31" 32 | futures-time = "3.0.0" 33 | http = "1.3.1" 34 | http-body-util = "0.1.3" 35 | hyper = { version = "1.7.0", features = ["full"] } 36 | hyper-util = { version = "0.1.17", features = ["tokio"] } 37 | memory-stats = "1.0.0" 38 | normalize-path = "0.2.1" 39 | num_cpus = "1.17.0" 40 | oxc = "0.90.0" 41 | parking_lot = "0.12.4" 42 | rand = "0.9.2" 43 | sha2 = "0.10.9" 44 | tokio = { version = "1.47.1", features = ["full"] } 45 | tokio-util = "0.7.16" 46 | v8 = "140.0.0" 47 | tracing = { version = "0.1.41" } 48 | oxc-miette = { version = "2.4.0", features = ["fancy"] } 49 | -------------------------------------------------------------------------------- /examples/src/run/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use ion::utils::PathExt; 4 | use ion::*; 5 | use normalize_path::NormalizePath; 6 | 7 | pub fn main() -> anyhow::Result<()> { 8 | let file_path = std::env::args() 9 | .collect::>() 10 | .get(2) 11 | .cloned() 12 | .expect("No filepath provided"); 13 | 14 | let file_path = PathBuf::from(file_path); 15 | let file_path = if file_path.is_absolute() { 16 | file_path 17 | } else { 18 | let Ok(cwd) = std::env::current_dir() else { 19 | return Err(anyhow::anyhow!("Unable to get cwd")); 20 | }; 21 | cwd.join(&file_path) 22 | } 23 | .normalize(); 24 | 25 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 26 | extensions: vec![ 27 | ion::extensions::console(), 28 | ion::extensions::set_interval(), 29 | ion::extensions::set_timeout(), 30 | ], 31 | transformers: vec![ 32 | ion::transformers::json(), 33 | ion::transformers::ts(), 34 | ion::transformers::tsx(), 35 | ], 36 | ..Default::default() 37 | })?; 38 | 39 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 40 | let ctx = worker.create_context()?; 41 | 42 | ctx.import(file_path.try_to_string()?)?; 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/testing/memory_usage_tsfn/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use ion::*; 5 | 6 | use crate::testing::MemoryUsageCounter; 7 | 8 | pub fn main() -> anyhow::Result<()> { 9 | let memu = MemoryUsageCounter::default(); 10 | println!("[0] {:?}", memu); 11 | 12 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 13 | println!("[1] {:?}", memu); 14 | 15 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 16 | 17 | for i in 2..50 { 18 | let ctx = worker.create_context()?; 19 | let mut v = vec![]; 20 | 21 | for _ in 2..1000 { 22 | let tsfn = ctx.exec_blocking(|env| { 23 | let func = JsFunction::new(env, |_env, ctx| ctx.arg::(0))?; 24 | ThreadSafeFunction::new(&func) 25 | })?; 26 | 27 | tsfn.call_blocking( 28 | // Map Args 29 | |_env| Ok(42), 30 | // Map Ret 31 | move |_env, ret| ret.cast::()?.get_u32(), 32 | ) 33 | .unwrap(); 34 | 35 | v.push(tsfn); 36 | } 37 | 38 | v.clear(); 39 | worker.run_garbage_collection_for_testing()?; 40 | println!("[{}] {:?}", i, memu); 41 | thread::sleep(Duration::from_millis(100)); 42 | } 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/set_timeout/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 5 | extensions: vec![ 6 | ion::extensions::console(), 7 | ion::extensions::set_interval(), 8 | ion::extensions::set_timeout(), 9 | ], 10 | transformers: vec![ 11 | ion::transformers::json(), 12 | ion::transformers::ts(), 13 | ion::transformers::tsx(), 14 | ], 15 | ..Default::default() 16 | })?; 17 | 18 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 19 | let ctx = worker.create_context()?; 20 | 21 | ctx.exec_blocking(|env| { 22 | env.eval_script::( 23 | r#" 24 | const sleep = d => new Promise(r => setTimeout(r, d)) 25 | 26 | void async function main() { 27 | console.log(`1`) 28 | await sleep(1000) 29 | console.log(`2`) 30 | await sleep(1000) 31 | console.log(`3`) 32 | await sleep(1000) 33 | console.log(`4`) 34 | await sleep(1000) 35 | console.log(`5`) 36 | }() 37 | "#, 38 | )?; 39 | 40 | Ok(()) 41 | })?; 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/src/testing/memory_usage/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use memory_stats::memory_stats; 4 | use parking_lot::Mutex; 5 | 6 | #[derive(Default)] 7 | pub struct MemoryUsageCounter(Arc>); 8 | 9 | impl std::fmt::Debug for MemoryUsageCounter { 10 | fn fmt( 11 | &self, 12 | f: &mut std::fmt::Formatter<'_>, 13 | ) -> std::fmt::Result { 14 | let mut previous = self.0.lock(); 15 | let current = Self::get_memory_usage_mb(); 16 | 17 | let result = if current > *previous { 18 | write!( 19 | f, 20 | "Memory Usage: {}mb (+{}mb)", 21 | current, 22 | current - *previous 23 | ) 24 | } else if current == *previous { 25 | write!(f, "Memory Usage: {}mb", current,) 26 | } else { 27 | write!(f, "Memory Usage: {}mb ({}mb)", current, current - *previous) 28 | }; 29 | 30 | (*previous) = current; 31 | result 32 | } 33 | } 34 | 35 | impl MemoryUsageCounter { 36 | fn get_memory_usage_mb() -> isize { 37 | if let Some(usage) = memory_stats() { 38 | let b = usage.physical_mem; 39 | let kb = b / 1000; 40 | let mb = kb / 1000; 41 | mb as isize 42 | } else { 43 | panic!("Couldn't get the current memory usage :("); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/src/custom_extension/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | // Start the runtime 5 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 6 | extensions: vec![custom_extension()], 7 | ..Default::default() 8 | })?; 9 | 10 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 11 | let ctx = worker.create_context()?; 12 | 13 | ctx.exec_blocking(|env| { 14 | let module = env.eval_module( 15 | r#" 16 | import { foo } from "ion:foo"; 17 | 18 | export default foo() 19 | "#, 20 | )?; 21 | 22 | let default_export = module.get_named_property_unchecked::("default")?; 23 | println!("Got: {}", default_export.get_string()?); 24 | Ok(()) 25 | })?; 26 | 27 | Ok(()) 28 | } 29 | 30 | fn custom_extension() -> JsExtension { 31 | JsExtension::NativeModuleWithBinding { 32 | module_name: "ion:foo".to_string(), 33 | binding: r#" 34 | export function foo() { 35 | return import.meta.extension.foo 36 | } 37 | "# 38 | .to_string(), 39 | extension: Box::new(|env, exports| { 40 | let key = env.create_string("foo")?; 41 | let value = env.create_string("bar")?; 42 | exports.set_property(key, value)?; 43 | Ok(()) 44 | }), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_object.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::ToJsUnknown; 3 | use crate::platform::sys; 4 | use crate::platform::sys::Value; 5 | use crate::values::FromJsValue; 6 | use crate::values::JsObjectValue; 7 | use crate::values::JsValue; 8 | use crate::values::ToJsValue; 9 | 10 | #[derive(Clone)] 11 | pub struct JsObject { 12 | pub(crate) value: Value, 13 | pub(crate) env: Env, 14 | } 15 | 16 | impl JsObject { 17 | pub fn new(env: &Env) -> crate::Result { 18 | let scope = &mut env.scope(); 19 | let object = v8::Object::new(scope); 20 | Ok(Self { 21 | value: sys::v8_from_value(object), 22 | env: env.clone(), 23 | }) 24 | } 25 | } 26 | 27 | impl JsValue for JsObject { 28 | fn value(&self) -> &Value { 29 | &self.value 30 | } 31 | 32 | fn env(&self) -> &Env { 33 | &self.env 34 | } 35 | } 36 | 37 | impl ToJsUnknown for JsObject {} 38 | impl JsObjectValue for JsObject {} 39 | 40 | impl FromJsValue for JsObject { 41 | fn from_js_value( 42 | env: &Env, 43 | value: Value, 44 | ) -> crate::Result { 45 | Ok(Self { 46 | value, 47 | env: env.clone(), 48 | }) 49 | } 50 | } 51 | 52 | impl ToJsValue for JsObject { 53 | fn to_js_value( 54 | _env: &Env, 55 | val: Self, 56 | ) -> crate::Result { 57 | Ok(val.value) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_exception.rs: -------------------------------------------------------------------------------- 1 | // TODO 2 | use crate::Env; 3 | use crate::ToJsUnknown; 4 | use crate::platform::sys::Value; 5 | use crate::values::FromJsValue; 6 | use crate::values::JsValue; 7 | use crate::values::ToJsValue; 8 | 9 | #[derive(Clone)] 10 | pub struct JsException { 11 | pub(crate) value: Value, 12 | pub(crate) env: Env, 13 | } 14 | 15 | impl JsException { 16 | /// # SAFETY 17 | /// 18 | /// Skips checks for type conversion (TODO) 19 | pub unsafe fn cast_unchecked(self) -> T { 20 | T::from_js_value(&self.env, self.value).expect("Failed to cast JsException") 21 | } 22 | 23 | pub fn cast(self) -> crate::Result { 24 | T::from_js_value(&self.env, self.value) 25 | } 26 | } 27 | 28 | impl JsValue for JsException { 29 | fn value(&self) -> &Value { 30 | &self.value 31 | } 32 | 33 | fn env(&self) -> &Env { 34 | &self.env 35 | } 36 | } 37 | 38 | impl ToJsUnknown for JsException {} 39 | 40 | impl FromJsValue for JsException { 41 | fn from_js_value( 42 | env: &Env, 43 | value: Value, 44 | ) -> crate::Result { 45 | Ok(Self { 46 | value, 47 | env: env.clone(), 48 | }) 49 | } 50 | } 51 | 52 | impl ToJsValue for JsException { 53 | fn to_js_value( 54 | _env: &Env, 55 | val: Self, 56 | ) -> crate::Result { 57 | Ok(val.value) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/ion_cli/src/cmd/run.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | use ion::utils::PathExt; 5 | use ion::*; 6 | use normalize_path::NormalizePath; 7 | 8 | #[derive(Debug, Parser)] 9 | pub struct RunCommand { 10 | /// Target get file to run 11 | pub path: PathBuf, 12 | } 13 | 14 | pub fn main(command: RunCommand) -> anyhow::Result<()> { 15 | let entry = if command.path.is_absolute() { 16 | command.path 17 | } else { 18 | let Ok(cwd) = std::env::current_dir() else { 19 | return Err(anyhow::anyhow!("Unable to get cwd")); 20 | }; 21 | cwd.join(&command.path).normalize() 22 | } 23 | .normalize(); 24 | 25 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 26 | v8_args: vec![], 27 | resolvers: vec![ion::resolvers::relative()], 28 | transformers: vec![ 29 | ion::transformers::json(), 30 | ion::transformers::ts(), 31 | ion::transformers::tsx(), 32 | ], 33 | extensions: vec![ 34 | ion::extensions::event_target(), 35 | ion::extensions::console(), 36 | ion::extensions::set_timeout(), 37 | ion::extensions::set_interval(), 38 | ion::extensions::test(), 39 | ion::extensions::global_this(), 40 | ], 41 | })?; 42 | 43 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 44 | let ctx = worker.create_context()?; 45 | 46 | ctx.import(entry.try_to_string()?)?; 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /examples/src/promise/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 5 | extensions: vec![ 6 | ion::extensions::console(), 7 | ion::extensions::set_interval(), 8 | ion::extensions::set_timeout(), 9 | ], 10 | ..Default::default() 11 | })?; 12 | 13 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 14 | let ctx = worker.create_context()?; 15 | 16 | // Execute some JavaScript in the context 17 | ctx.exec_blocking(|env| { 18 | // Evaluate arbitrary JavaScript, the result of the last line is returned 19 | let value = env.eval_script::( 20 | r#" 21 | console.log("[JS] Promise Started"); 22 | 23 | new Promise((resolve) => setTimeout(() => { 24 | console.log("[JS] Promise Resolved"); 25 | resolve(42); 26 | }, 3_000)); 27 | "#, 28 | )?; 29 | 30 | // Cast to Rust type 31 | value.settled::(|_env, result| { 32 | match result { 33 | JsPromiseResult::Resolved(resolved) => { 34 | println!("[Rust] Got {}", resolved.get_u32()?) 35 | } 36 | JsPromiseResult::Rejected(_) => unreachable!(), 37 | }; 38 | Ok(()) 39 | })?; 40 | 41 | println!("Exec Complete (Not Blocked)"); 42 | 43 | Ok(()) 44 | })?; 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/src/thread_safe_promise/mod.rs: -------------------------------------------------------------------------------- 1 | use ion::*; 2 | 3 | pub fn main() -> anyhow::Result<()> { 4 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 5 | extensions: vec![ 6 | ion::extensions::console(), 7 | ion::extensions::set_interval(), 8 | ion::extensions::set_timeout(), 9 | ], 10 | transformers: vec![ 11 | ion::transformers::json(), 12 | ion::transformers::ts(), 13 | ion::transformers::tsx(), 14 | ], 15 | ..Default::default() 16 | })?; 17 | 18 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 19 | let ctx = worker.create_context()?; 20 | 21 | // Execute some JavaScript in the context 22 | let promise = ctx.exec_blocking(|env| { 23 | // Evaluate arbitrary JavaScript, the result of the last line is returned 24 | let value = env.eval_script::( 25 | r#" 26 | console.log("[JS] Promise Started"); 27 | 28 | new Promise((resolve) => setTimeout(() => { 29 | console.log("[JS] Promise Resolved"); 30 | resolve(42); 31 | }, 3_000)); 32 | "#, 33 | )?; 34 | 35 | println!("Exec Complete (Not Blocked)"); 36 | ThreadSafePromise::new(&value) 37 | })?; 38 | 39 | // Cast to Rust type 40 | let result = promise.settled_blocking::(|_env, result| match result { 41 | JsPromiseResult::Resolved(resolved) => resolved.get_u32(), 42 | JsPromiseResult::Rejected(_) => unreachable!(), 43 | })?; 44 | 45 | println!("[Rust] Got {}", result); 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /crates/ion/src/async_env.rs: -------------------------------------------------------------------------------- 1 | use flume::Sender; 2 | use flume::bounded; 3 | 4 | use crate::Env; 5 | use crate::platform::worker::JsWorkerEvent; 6 | 7 | pub struct AsyncEnv { 8 | pub(crate) tx: Sender, 9 | pub(crate) realm_id: usize, 10 | } 11 | 12 | impl AsyncEnv { 13 | pub fn exec( 14 | &self, 15 | callback: impl 'static + Send + FnOnce(&Env) -> crate::Result<()>, 16 | ) -> crate::Result<()> { 17 | let span = tracing::Span::current(); 18 | if self 19 | .tx 20 | .try_send(JsWorkerEvent::Exec { 21 | id: self.realm_id, 22 | callback: Box::new(callback), 23 | span, 24 | }) 25 | .is_err() 26 | { 27 | return Err(crate::Error::ExecError); 28 | }; 29 | Ok(()) 30 | } 31 | 32 | pub async fn exec_async( 33 | &self, 34 | callback: impl 'static + Send + FnOnce(&Env) -> crate::Result, 35 | ) -> crate::Result { 36 | let (tx, rx) = bounded(1); 37 | 38 | self.exec(move |env| Ok(tx.try_send(callback(env)?)?))?; 39 | 40 | let Ok(ret) = rx.recv_async().await else { 41 | return Err(crate::Error::ExecError); 42 | }; 43 | Ok(ret) 44 | } 45 | 46 | pub fn exec_blocking( 47 | &self, 48 | callback: impl 'static + Send + FnOnce(&Env) -> crate::Result, 49 | ) -> crate::Result { 50 | let (tx, rx) = bounded::(1); 51 | 52 | self.exec(move |env| Ok(tx.try_send(callback(env)?)?))?; 53 | 54 | let Ok(ret) = rx.recv() else { 55 | return Err(crate::Error::ExecError); 56 | }; 57 | Ok(ret) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/src/http_server/http1/res_ext.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use futures::TryStreamExt; 4 | use http::Response as HttpResponse; 5 | use http::Result as HttpResult; 6 | use http_body_util::StreamBody; 7 | use http_body_util::combinators::BoxBody; 8 | use hyper::body::Bytes as HyperBytes; 9 | use hyper::http::response::Builder as ResponseBuilder; 10 | use tokio::io::DuplexStream; 11 | 12 | use super::Bytes; 13 | 14 | pub trait ResponseBuilderExt { 15 | fn body_stream( 16 | self, 17 | stream_buffer_size: usize, 18 | ) -> HttpResult<(HttpResponse>, DuplexStream)>; 19 | fn body_from( 20 | self, 21 | bytes: impl Into, 22 | ) -> HttpResult>>; 23 | } 24 | 25 | impl ResponseBuilderExt for ResponseBuilder { 26 | fn body_stream( 27 | self, 28 | stream_buffer_size: usize, 29 | ) -> HttpResult<(HttpResponse>, DuplexStream)> { 30 | let (writer, reader) = tokio::io::duplex(stream_buffer_size); 31 | 32 | let reader_stream = tokio_util::io::ReaderStream::new(reader) 33 | .map_ok(hyper::body::Frame::data) 34 | .map_err(|_item| panic!()); 35 | 36 | let stream_body = StreamBody::new(reader_stream); 37 | let boxed_body: BoxBody = 38 | BoxBody::::new(stream_body); 39 | 40 | let res: http::Response> = self.body(boxed_body)?; 41 | 42 | Ok((res, writer)) 43 | } 44 | 45 | fn body_from( 46 | self, 47 | bytes: impl Into, 48 | ) -> HttpResult>> { 49 | self.body(bytes.into().into()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/src/testing/background_tasks/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use ion::*; 4 | 5 | pub fn main() -> anyhow::Result<()> { 6 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions { 7 | extensions: vec![ 8 | ion::extensions::console(), 9 | ion::extensions::set_interval(), 10 | ion::extensions::set_timeout(), 11 | ], 12 | transformers: vec![ 13 | ion::transformers::json(), 14 | ion::transformers::ts(), 15 | ion::transformers::tsx(), 16 | ], 17 | ..Default::default() 18 | })?; 19 | 20 | // Create an isolate running on a dedicated thread 21 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 22 | 23 | // // Open a JavaScript context on the isolate thread to execute JavaScript on 24 | // // You can open multiple contexts, sharing the same thread 25 | let ctx = worker.create_context()?; 26 | 27 | // Execute some JavaScript in the context 28 | ctx.exec(|env| { 29 | env.inc_ref(); 30 | 31 | env.spawn_background({ 32 | let env = env.as_async(); 33 | async move { 34 | println!("Background Task Started"); 35 | tokio::time::sleep(Duration::from_secs(1)).await; 36 | println!("Background Task Ended"); 37 | 38 | env.exec_async(|env| { 39 | println!("hi"); 40 | env.dec_ref(); 41 | Ok(()) 42 | }) 43 | .await?; 44 | Ok(()) 45 | } 46 | })?; 47 | 48 | Ok(()) 49 | })?; 50 | 51 | println!("Context Dropping"); 52 | drop(ctx); 53 | drop(worker); 54 | 55 | println!("Context Dropped"); 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /crates/ion/src/utils/hash_map_ext.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::collections::HashMap; 3 | use std::hash::BuildHasher; 4 | use std::hash::Hash; 5 | 6 | pub trait HashMapExt { 7 | fn try_get_mut( 8 | &mut self, 9 | k: &Q, 10 | ) -> crate::Result<&mut V> 11 | where 12 | K: Borrow, 13 | Q: Hash + Eq + ?Sized; 14 | 15 | fn try_get( 16 | &self, 17 | k: &Q, 18 | ) -> crate::Result<&V> 19 | where 20 | K: Borrow, 21 | Q: Hash + Eq + ?Sized; 22 | 23 | fn try_remove( 24 | &mut self, 25 | k: &Q, 26 | ) -> crate::Result 27 | where 28 | K: Borrow, 29 | Q: Hash + Eq + ?Sized; 30 | } 31 | 32 | impl HashMapExt for HashMap 33 | where 34 | K: Eq + Hash, 35 | S: BuildHasher, 36 | { 37 | fn try_get_mut( 38 | &mut self, 39 | k: &Q, 40 | ) -> crate::Result<&mut V> 41 | where 42 | K: Borrow, 43 | Q: Hash + Eq + ?Sized, 44 | { 45 | let Some(value) = self.get_mut(k) else { 46 | return Err(crate::Error::ValueGetError); 47 | }; 48 | Ok(value) 49 | } 50 | 51 | fn try_get( 52 | &self, 53 | k: &Q, 54 | ) -> crate::Result<&V> 55 | where 56 | K: Borrow, 57 | Q: Hash + Eq + ?Sized, 58 | { 59 | let Some(value) = self.get(k) else { 60 | return Err(crate::Error::ValueGetError); 61 | }; 62 | Ok(value) 63 | } 64 | 65 | fn try_remove( 66 | &mut self, 67 | k: &Q, 68 | ) -> crate::Result 69 | where 70 | K: Borrow, 71 | Q: Hash + Eq + ?Sized, 72 | { 73 | let Some(value) = self.remove(k) else { 74 | return Err(crate::Error::ValueGetError); 75 | }; 76 | Ok(value) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/src/http_server/handlers/binding.d.ts: -------------------------------------------------------------------------------- 1 | export type HandlerFunc = (req: Request, res: Response) => any | Promise 2 | 3 | export interface Request { 4 | body: Reader; 5 | } 6 | 7 | export interface Response extends Writer, Ender { 8 | headers(): Headers 9 | writeHead(status: number): Promise 10 | } 11 | 12 | export interface Headers { 13 | set(header: string, value: string): void 14 | } 15 | 16 | export interface Reader { 17 | // Read populates the given byte slice with data and returns the number of bytes populated and an error value. It returns null when the stream ends. 18 | read(recv: Array): Promise; 19 | // Read populates the given byte slice with data and returns the number of bytes populated and an error value. It returns null when the stream ends. 20 | read(recv: ArrayBuffer): Promise; 21 | // Read populates the given byte slice with data and returns the number of bytes populated and an error value. It returns null when the stream ends. 22 | read(recv: Uint8Array): Promise; 23 | } 24 | 25 | export interface Writer extends Ender { 26 | // Write writes bytes from the buffer to the underlying data stream. 27 | write(bytes: Array): Promise; 28 | // Write writes bytes from the buffer to the underlying data stream. 29 | write(bytes: ArrayBuffer): Promise; 30 | // Write writes bytes from the buffer to the underlying data stream. 31 | write(bytes: Uint8Array): Promise; 32 | // Write writes bytes from the buffer to the underlying data stream. 33 | write(string: string): Promise; 34 | // Flush flushes buffered data to the client 35 | flush(): Promise; 36 | } 37 | 38 | export interface Ender { 39 | end(): Promise 40 | } 41 | 42 | declare global { 43 | var setTimeout: (callback: () => any | Promise, duration?: number) => number; 44 | } -------------------------------------------------------------------------------- /crates/ion/src/values/js_null.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::ToJsUnknown; 3 | use crate::platform::sys; 4 | use crate::platform::sys::Value; 5 | use crate::values::FromJsValue; 6 | use crate::values::JsValue; 7 | use crate::values::ToJsValue; 8 | 9 | #[derive(Clone)] 10 | pub struct JsNull { 11 | pub(crate) value: Value, 12 | pub(crate) env: Env, 13 | } 14 | 15 | impl JsNull { 16 | pub fn new(env: &Env) -> crate::Result { 17 | let scope = &mut env.scope(); 18 | JsNull::from_js_value(env, sys::v8_from_value(v8::null(scope))) 19 | } 20 | 21 | /// # SAFETY 22 | /// 23 | /// Skips checks for type conversion (TODO) 24 | pub unsafe fn cast_unchecked(self) -> T { 25 | T::from_js_value(&self.env, self.value).expect("Failed to cast JsUnknown") 26 | } 27 | 28 | pub fn cast(self) -> crate::Result { 29 | T::from_js_value(&self.env, self.value) 30 | } 31 | 32 | pub fn type_of(&self) -> String { 33 | let scope = &mut self.env.scope(); 34 | self.value.type_of(scope).to_rust_string_lossy(scope) 35 | } 36 | } 37 | 38 | impl JsValue for JsNull { 39 | fn value(&self) -> &Value { 40 | &self.value 41 | } 42 | 43 | fn env(&self) -> &Env { 44 | &self.env 45 | } 46 | } 47 | 48 | impl ToJsUnknown for JsNull {} 49 | 50 | impl FromJsValue for JsNull { 51 | fn from_js_value( 52 | env: &Env, 53 | value: Value, 54 | ) -> crate::Result { 55 | Ok(Self { 56 | value, 57 | env: env.clone(), 58 | }) 59 | } 60 | } 61 | 62 | impl ToJsValue for JsNull { 63 | fn to_js_value( 64 | _env: &Env, 65 | val: Self, 66 | ) -> crate::Result { 67 | Ok(val.value) 68 | } 69 | } 70 | 71 | impl Env { 72 | pub fn get_null(&self) -> crate::Result { 73 | JsNull::new(self) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_unknown.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::ToJsUnknown; 3 | use crate::platform::sys::Value; 4 | use crate::utils::v8::v8_create_undefined; 5 | use crate::values::FromJsValue; 6 | use crate::values::JsValue; 7 | use crate::values::ToJsValue; 8 | 9 | #[derive(Clone)] 10 | pub struct JsUnknown { 11 | pub(crate) value: Value, 12 | pub(crate) env: Env, 13 | } 14 | 15 | impl JsUnknown { 16 | /// # SAFETY 17 | /// 18 | /// Skips checks for type conversion (TODO) 19 | pub unsafe fn cast_unchecked(self) -> T { 20 | T::from_js_value(&self.env, self.value).expect("Failed to cast JsUnknown") 21 | } 22 | 23 | pub fn cast(self) -> crate::Result { 24 | T::from_js_value(&self.env, self.value) 25 | } 26 | 27 | pub fn type_of(&self) -> String { 28 | let scope = &mut self.env.scope(); 29 | self.value.type_of(scope).to_rust_string_lossy(scope) 30 | } 31 | } 32 | 33 | impl JsValue for JsUnknown { 34 | fn value(&self) -> &Value { 35 | &self.value 36 | } 37 | 38 | fn env(&self) -> &Env { 39 | &self.env 40 | } 41 | } 42 | 43 | impl ToJsUnknown for JsUnknown {} 44 | 45 | impl FromJsValue for JsUnknown { 46 | fn from_js_value( 47 | env: &Env, 48 | value: Value, 49 | ) -> crate::Result { 50 | Ok(Self { 51 | value, 52 | env: env.clone(), 53 | }) 54 | } 55 | } 56 | 57 | impl ToJsValue for JsUnknown { 58 | fn to_js_value( 59 | _env: &Env, 60 | val: Self, 61 | ) -> crate::Result { 62 | Ok(val.value) 63 | } 64 | } 65 | 66 | impl ToJsValue for () { 67 | fn to_js_value( 68 | env: &Env, 69 | _val: Self, 70 | ) -> crate::Result { 71 | let scope = &mut env.scope(); 72 | let local = v8_create_undefined(scope)?; 73 | Ok(local) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_undefined.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::ToJsUnknown; 3 | use crate::platform::sys; 4 | use crate::platform::sys::Value; 5 | use crate::values::FromJsValue; 6 | use crate::values::JsValue; 7 | use crate::values::ToJsValue; 8 | 9 | #[derive(Clone)] 10 | pub struct JsUndefined { 11 | pub(crate) value: Value, 12 | pub(crate) env: Env, 13 | } 14 | 15 | impl JsUndefined { 16 | pub fn new(env: &Env) -> crate::Result { 17 | let scope = &mut env.scope(); 18 | JsUndefined::from_js_value(env, sys::v8_from_value(v8::undefined(scope))) 19 | } 20 | 21 | /// # SAFETY 22 | /// 23 | /// Skips checks for type conversion (TODO) 24 | pub unsafe fn cast_unchecked(self) -> T { 25 | T::from_js_value(&self.env, self.value).expect("Failed to cast JsUnknown") 26 | } 27 | 28 | pub fn cast(self) -> crate::Result { 29 | T::from_js_value(&self.env, self.value) 30 | } 31 | 32 | pub fn type_of(&self) -> String { 33 | let scope = &mut self.env.scope(); 34 | self.value.type_of(scope).to_rust_string_lossy(scope) 35 | } 36 | } 37 | 38 | impl JsValue for JsUndefined { 39 | fn value(&self) -> &Value { 40 | &self.value 41 | } 42 | 43 | fn env(&self) -> &Env { 44 | &self.env 45 | } 46 | } 47 | 48 | impl ToJsUnknown for JsUndefined {} 49 | 50 | impl FromJsValue for JsUndefined { 51 | fn from_js_value( 52 | env: &Env, 53 | value: Value, 54 | ) -> crate::Result { 55 | Ok(Self { 56 | value, 57 | env: env.clone(), 58 | }) 59 | } 60 | } 61 | 62 | impl ToJsValue for JsUndefined { 63 | fn to_js_value( 64 | _env: &Env, 65 | val: Self, 66 | ) -> crate::Result { 67 | Ok(val.value) 68 | } 69 | } 70 | 71 | impl Env { 72 | pub fn get_undefined(&self) -> crate::Result { 73 | JsUndefined::new(self) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_crate_dependencies)] 2 | mod basic; 3 | mod custom_extension; 4 | mod custom_resolver; 5 | mod deferred; 6 | mod eval; 7 | mod http_server; 8 | mod promise; 9 | mod run; 10 | mod set_interval; 11 | mod set_timeout; 12 | mod testing; 13 | mod thread_safe_function; 14 | mod thread_safe_promise; 15 | 16 | fn main() -> anyhow::Result<()> { 17 | let example = std::env::args() 18 | .collect::>() 19 | .get(1) 20 | .cloned() 21 | .unwrap_or("basic".to_string()); 22 | 23 | match example.as_str() { 24 | "basic" => basic::main(), 25 | "custom_extension" => custom_extension::main(), 26 | "custom_resolver" => custom_resolver::main(), 27 | "deferred" => deferred::main(), 28 | "eval" => eval::main(), 29 | "http_server" => http_server::main(), 30 | "promise" => promise::main(), 31 | "run" => run::main(), 32 | "set_interval" => set_interval::main(), 33 | "set_timeout" => set_timeout::main(), 34 | "testing_background_tasks" => testing::background_tasks::main(), 35 | "testing_memory_usage_context" => testing::memory_usage_context::main(), 36 | "testing_memory_usage_module" => testing::memory_usage_module::main(), 37 | "testing_memory_usage_tsfn" => testing::memory_usage_tsfn::main(), 38 | "testing_memory_usage_value" => testing::memory_usage_value::main(), 39 | "testing_memory_usage_worker" => testing::memory_usage_worker::main(), 40 | "testing_multiple_contexts" => testing::multiple_contexts::main(), 41 | "testing_multiple_workers" => testing::multiple_workers::main(), 42 | "testing_transformers" => testing::transformers::main(), 43 | "testing_typescript" => testing::typescript::main(), 44 | "testing_wait" => testing::wait::main(), 45 | "thread_safe_function" => thread_safe_function::main(), 46 | "thread_safe_promise" => thread_safe_promise::main(), 47 | _ => Err(anyhow::anyhow!("No example for: \"{}\"", example)), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_boolean.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::ToJsUnknown; 3 | use crate::platform::sys; 4 | use crate::platform::sys::Value; 5 | use crate::values::FromJsValue; 6 | use crate::values::JsValue; 7 | use crate::values::ToJsValue; 8 | 9 | #[derive(Clone)] 10 | pub struct JsBoolean { 11 | pub(crate) value: Value, 12 | pub(crate) env: Env, 13 | } 14 | 15 | impl JsBoolean { 16 | pub fn new( 17 | env: &Env, 18 | val: bool, 19 | ) -> crate::Result { 20 | let scope = &mut env.scope(); 21 | let boolean = v8::Boolean::new(scope, val); 22 | Ok(Self { 23 | value: sys::v8_from_value(boolean), 24 | env: env.clone(), 25 | }) 26 | } 27 | 28 | pub fn get_value(&self) -> crate::Result { 29 | let Ok(local) = self.value.try_cast::() else { 30 | return Err(crate::Error::ValueCastError); 31 | }; 32 | Ok(local.is_true()) 33 | } 34 | } 35 | 36 | impl JsValue for JsBoolean { 37 | fn value(&self) -> &Value { 38 | &self.value 39 | } 40 | 41 | fn env(&self) -> &Env { 42 | &self.env 43 | } 44 | } 45 | 46 | impl ToJsUnknown for JsBoolean {} 47 | 48 | impl FromJsValue for JsBoolean { 49 | fn from_js_value( 50 | env: &Env, 51 | value: Value, 52 | ) -> crate::Result { 53 | Ok(Self { 54 | value, 55 | env: env.clone(), 56 | }) 57 | } 58 | } 59 | 60 | impl ToJsValue for JsBoolean { 61 | fn to_js_value( 62 | _env: &Env, 63 | val: Self, 64 | ) -> crate::Result { 65 | Ok(val.value) 66 | } 67 | } 68 | 69 | impl ToJsValue for bool { 70 | fn to_js_value( 71 | env: &Env, 72 | val: Self, 73 | ) -> crate::Result { 74 | Ok(*JsBoolean::new(env, val)?.value()) 75 | } 76 | } 77 | 78 | impl Env { 79 | pub fn create_boolean( 80 | &self, 81 | value: bool, 82 | ) -> crate::Result { 83 | JsBoolean::new(self, value) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/ion/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::string::FromUtf8Error; 2 | use std::sync::Arc; 3 | 4 | use flume::RecvError; 5 | use flume::TrySendError; 6 | 7 | pub type Result = std::result::Result; 8 | 9 | #[derive(Debug, Clone)] 10 | pub enum Error { 11 | IO(Arc), 12 | Generic(String), 13 | PlatformCommunicationError, 14 | PlatformInitializeError, 15 | IsolateNotInitializedError, 16 | EventLoopNotInitializedError, 17 | WorkerInitializeError, 18 | ValueCreateError, 19 | ValueGetError, 20 | ValueCastError, 21 | FunctionCallError, 22 | ScriptCompileError, 23 | ScriptRunError, 24 | ExecError, 25 | TaskSpawnError, 26 | OutOfBounds, 27 | ResolveError, 28 | NewInstanceError, 29 | PromiseResolveError, 30 | BackgroundThreadError, 31 | FileNotFound(String), 32 | NoTransformerError(String), 33 | TransformerError(String), 34 | ArrayExpected, 35 | } 36 | 37 | impl Error { 38 | pub fn generic(message: impl AsRef) -> Self { 39 | Self::Generic(message.as_ref().to_string()) 40 | } 41 | 42 | pub fn generic_err(message: impl AsRef) -> Result<()> { 43 | Err(Self::Generic(message.as_ref().to_string())) 44 | } 45 | } 46 | 47 | impl std::fmt::Display for Error { 48 | fn fmt( 49 | &self, 50 | f: &mut std::fmt::Formatter<'_>, 51 | ) -> std::fmt::Result { 52 | write!(f, "{:?}", self) 53 | } 54 | } 55 | 56 | impl std::error::Error for Error {} 57 | 58 | impl From for Error { 59 | fn from(value: std::io::Error) -> Self { 60 | Error::IO(Arc::new(value)) 61 | } 62 | } 63 | 64 | impl From> for Error { 65 | fn from(_value: TrySendError) -> Self { 66 | Error::PlatformCommunicationError 67 | } 68 | } 69 | 70 | impl From for Error { 71 | fn from(_value: RecvError) -> Self { 72 | Error::PlatformCommunicationError 73 | } 74 | } 75 | 76 | impl From for Error { 77 | fn from(_value: FromUtf8Error) -> Self { 78 | Self::generic("Unable to create UTF8 string from buffer") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/ion/src/js_extension.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::JsObject; 3 | 4 | pub type ExtensionHook = 5 | Box crate::Result<()>>; 6 | 7 | pub enum JsExtension { 8 | /// Extension available as a module that has both native code and an associated JavaScript glue code binding 9 | NativeModuleWithBinding { 10 | module_name: String, 11 | binding: String, 12 | extension: ExtensionHook, 13 | }, 14 | /// Extension available as a module that is written in native code 15 | NativeModule { 16 | module_name: String, 17 | extension: ExtensionHook, 18 | }, 19 | /// Extension that runs JavaScript code to generate a module 20 | BindingModule { 21 | module_name: String, 22 | binding: String, 23 | }, 24 | /// Extension that runs native code when a JsContext is started, used to mutate globalThis 25 | NativeGlobal { extension: ExtensionHook }, 26 | /// Extension that runs JavaScript code when a JsContext is started, used to mutate globalThis 27 | GlobalBinding { binding: String }, 28 | } 29 | 30 | impl std::fmt::Debug for JsExtension { 31 | fn fmt( 32 | &self, 33 | f: &mut std::fmt::Formatter<'_>, 34 | ) -> std::fmt::Result { 35 | match self { 36 | JsExtension::NativeModuleWithBinding { 37 | module_name, 38 | binding: _, 39 | extension: _, 40 | } => write!(f, "NativeModuleWithBinding({})", module_name), 41 | JsExtension::NativeModule { 42 | module_name, 43 | extension: _, 44 | } => { 45 | write!(f, "NativeModule({})", module_name) 46 | } 47 | JsExtension::NativeGlobal { extension: _ } => { 48 | write!(f, "NativeGlobal(unnamed)") 49 | } 50 | JsExtension::GlobalBinding { binding: _ } => { 51 | write!(f, "GlobalBinding(unnamed)") 52 | } 53 | JsExtension::BindingModule { 54 | module_name, 55 | binding: _, 56 | } => write!(f, "NativeModuleWithBinding({})", module_name), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/ion/src/resolvers/relative.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | use normalize_path::NormalizePath; 5 | 6 | use crate::JsResolver; 7 | use crate::JsResolverFut; 8 | use crate::ResolverContext; 9 | use crate::ResolverResult; 10 | use crate::utils::OsStringExt; 11 | 12 | pub fn relative() -> JsResolver { 13 | Arc::new(|ctx: ResolverContext| -> JsResolverFut { 14 | Box::pin(async move { 15 | let mut specifier = PathBuf::from(ctx.specifier); 16 | if specifier.is_relative() { 17 | specifier = ctx.from.parent().unwrap().join(&specifier).normalize(); 18 | } else { 19 | specifier = specifier.normalize() 20 | } 21 | 22 | if !ctx.fs.try_exists(&specifier).await? { 23 | return Ok(None); 24 | } 25 | 26 | let Some(kind) = specifier.extension().map(|v| v.try_to_string().unwrap()) else { 27 | return Err(crate::Error::ResolveError); 28 | }; 29 | 30 | Ok(Some(ResolverResult { 31 | code: ctx.fs.read(&specifier).await?, 32 | path: specifier, 33 | kind, 34 | })) 35 | }) 36 | }) 37 | } 38 | 39 | // TODO: virtual filesystem 40 | // 41 | // #[cfg(test)] 42 | // mod test { 43 | // use std::path::PathBuf; 44 | 45 | // use crate::{self as ion}; 46 | 47 | // #[tokio::test] 48 | // async fn should_resolve_relative_path() -> ion::Result<()> { 49 | // let ctx = ion::ResolverContext { 50 | // fs: ion::fs::FileSystem::Virtual, 51 | // specifier: "./foo.js".into(), 52 | // from: "/index.js".into(), 53 | // }; 54 | 55 | // ctx.fs.write(&PathBuf::from("/index.js"), b"").await?; 56 | // ctx.fs.write(&PathBuf::from("/foo.js"), b"").await?; 57 | 58 | // let Some(result) = ion::resolvers::relative(ctx).await? else { 59 | // return ion::Error::generic_err("Unable to resolve relative path") 60 | // }; 61 | 62 | // let expect = PathBuf::from("/").join("foo.js"); 63 | // if result.path != expect { 64 | // return ion::Error::generic_err("Invalid path resolved") 65 | // } 66 | 67 | // Ok(()) 68 | // } 69 | // } 70 | -------------------------------------------------------------------------------- /crates/ion/src/platform/active_context.rs: -------------------------------------------------------------------------------- 1 | use crate::platform::sys; 2 | 3 | pub struct ActiveContext { 4 | current: Option<(usize, sys::__v8_context_scope, sys::__v8_root_scope)>, 5 | isolate: *mut v8::Isolate, 6 | } 7 | 8 | impl std::fmt::Debug for ActiveContext { 9 | fn fmt( 10 | &self, 11 | f: &mut std::fmt::Formatter<'_>, 12 | ) -> std::fmt::Result { 13 | if let Some((id, _, _)) = &self.current { 14 | write!(f, "ActiveContext {:?}", id) 15 | } else { 16 | write!(f, "ActiveContext (none)",) 17 | } 18 | } 19 | } 20 | 21 | impl ActiveContext { 22 | pub fn new(isolate: *mut v8::Isolate) -> Self { 23 | Self { 24 | current: None, 25 | isolate, 26 | } 27 | } 28 | 29 | pub fn set( 30 | &mut self, 31 | context: sys::__v8_context, 32 | ) -> bool { 33 | if let Some((id, _, _)) = self.current 34 | && id == sys::v8_get_context_address(context) 35 | { 36 | return false; 37 | } 38 | 39 | // Drop the current context first so v8 can do clean up 40 | self.unset(); 41 | 42 | // Create new context and put it on the stack 43 | let handle_scope = 44 | sys::v8_new_root_scope(v8::HandleScope::new(unsafe { &mut *self.isolate })); 45 | let context_scope = sys::v8_new_context_scope(v8::ContextScope::new( 46 | sys::v8_get_root_scope(handle_scope), 47 | sys::v8_get_context(context), 48 | )); 49 | 50 | self.current.replace(( 51 | sys::v8_get_context_address(context), 52 | context_scope, 53 | handle_scope, 54 | )); 55 | 56 | true 57 | } 58 | 59 | pub fn take( 60 | &mut self 61 | ) -> Option<( 62 | v8::ContextScope<'static, v8::HandleScope<'static>>, 63 | v8::HandleScope<'static, ()>, 64 | )> { 65 | let (_id, context_scope, handle_scope) = self.current.take()?; 66 | Some(( 67 | sys::v8_drop_context_scope(context_scope), 68 | sys::v8_drop_root_scope(handle_scope), 69 | )) 70 | } 71 | 72 | pub fn unset(&mut self) { 73 | if let Some((_id, context_scope, handle_scope)) = self.current.take() { 74 | sys::v8_drop_context_scope(context_scope); 75 | sys::v8_drop_root_scope(handle_scope); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/ion/src/utils/path_ext.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | 4 | use super::os_string_ext::OsStringExt; 5 | 6 | pub trait PathExt { 7 | fn try_parent(&self) -> std::io::Result<&Path>; 8 | fn try_file_name(&self) -> std::io::Result; 9 | fn try_file_stem(&self) -> std::io::Result; 10 | fn try_to_string(&self) -> std::io::Result; 11 | } 12 | 13 | impl PathExt for PathBuf { 14 | fn try_parent(&self) -> std::io::Result<&Path> { 15 | match self.parent() { 16 | Some(path) => Ok(path), 17 | None => Err(std::io::Error::other("Unable to find parent")), 18 | } 19 | } 20 | 21 | fn try_file_name(&self) -> std::io::Result { 22 | match self.file_name() { 23 | Some(v) => Ok(v.try_to_string()?), 24 | None => Err(std::io::Error::other("Cannot get file name")), 25 | } 26 | } 27 | 28 | fn try_file_stem(&self) -> std::io::Result { 29 | match self.file_stem() { 30 | Some(v) => Ok(v.try_to_string()?), 31 | None => Err(std::io::Error::other("Cannot get file stem")), 32 | } 33 | } 34 | 35 | fn try_to_string(&self) -> std::io::Result { 36 | match self.to_str() { 37 | Some(v) => Ok(v.to_string()), 38 | None => Err(std::io::Error::other("Cannot convert Path to string")), 39 | } 40 | } 41 | } 42 | 43 | impl PathExt for Path { 44 | fn try_parent(&self) -> std::io::Result<&Path> { 45 | match self.parent() { 46 | Some(path) => Ok(path), 47 | None => Err(std::io::Error::other("Unable to find parent")), 48 | } 49 | } 50 | 51 | fn try_file_name(&self) -> std::io::Result { 52 | match self.file_name() { 53 | Some(v) => Ok(v.try_to_string()?), 54 | None => Err(std::io::Error::other("Cannot get file name")), 55 | } 56 | } 57 | 58 | fn try_file_stem(&self) -> std::io::Result { 59 | match self.file_stem() { 60 | Some(v) => Ok(v.try_to_string()?), 61 | None => Err(std::io::Error::other("Cannot get file stem")), 62 | } 63 | } 64 | 65 | fn try_to_string(&self) -> std::io::Result { 66 | match self.to_str() { 67 | Some(v) => Ok(v.to_string()), 68 | None => Err(std::io::Error::other("Cannot convert Path to string")), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/src/http_server/http1/http1_server.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::future::Future; 3 | use std::sync::Arc; 4 | 5 | use http_body_util::Full; 6 | use http_body_util::combinators::BoxBody; 7 | use hyper::Request; 8 | use hyper::Response; 9 | use hyper::body::Bytes as HyperBytes; 10 | use hyper::body::Incoming; 11 | use hyper::http::response::Builder as ResponseBuilder; 12 | use hyper::server::conn::http1; 13 | use hyper::service::service_fn; 14 | use hyper_util::rt::TokioIo; 15 | use tokio::net::TcpListener; 16 | use tokio::net::ToSocketAddrs; 17 | 18 | /// Simple wrapper around hyper to make it a little nicer to use 19 | pub async fn http1_server( 20 | addr: A, 21 | handle_func: F, 22 | ) -> anyhow::Result<()> 23 | where 24 | A: ToSocketAddrs, 25 | F: 'static + Send + Sync + Fn(Request, ResponseBuilder) -> Fut, 26 | Fut: Send + Future>>>, 27 | { 28 | let listener = TcpListener::bind(&addr).await?; 29 | let handler_func_ref = Arc::new(handle_func); 30 | 31 | loop { 32 | let Ok((stream, _)) = listener.accept().await else { 33 | continue; 34 | }; 35 | 36 | let io = TokioIo::new(stream); 37 | let handler_func_ref = handler_func_ref.clone(); 38 | 39 | tokio::task::spawn(async move { 40 | let service_builder = http1::Builder::new(); 41 | let service_handler = service_fn(move |req| { 42 | let fut = handler_func_ref(req, Response::builder()); 43 | 44 | async move { 45 | let handler_response = match fut.await { 46 | Ok(handler_response) => handler_response, 47 | Err(handler_error) => handle_error(handler_error), 48 | }; 49 | 50 | Ok::>, anyhow::Error>(handler_response) 51 | } 52 | }); 53 | 54 | service_builder 55 | .serve_connection(io, service_handler) 56 | .await 57 | .ok(); 58 | }); 59 | } 60 | } 61 | 62 | fn handle_error(error: anyhow::Error) -> Response> { 63 | let content = HyperBytes::from(format!("{}", error)); 64 | let body = BoxBody::new(Full::new(content)); 65 | let response = Response::builder().status(500).body(body); 66 | let Ok(response) = response else { todo!() }; 67 | response 68 | } 69 | -------------------------------------------------------------------------------- /examples/src/thread_safe_function/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use ion::*; 5 | 6 | pub fn main() -> anyhow::Result<()> { 7 | let rt = JsRuntime::initialize_once(JsRuntimeOptions { 8 | extensions: vec![ 9 | ion::extensions::console(), 10 | ion::extensions::set_interval(), 11 | ion::extensions::set_timeout(), 12 | ], 13 | transformers: vec![ 14 | ion::transformers::json(), 15 | ion::transformers::ts(), 16 | ion::transformers::tsx(), 17 | ], 18 | ..Default::default() 19 | })?; 20 | 21 | let wrk = rt.spawn_worker(JsWorkerOptions::default())?; 22 | let ctx = wrk.create_context()?; 23 | 24 | ctx.exec_blocking(|env| { 25 | let func = JsFunction::new(env, |_env, ctx| { 26 | let arg0 = ctx.arg::(0)?; 27 | let arg1 = ctx.arg::(1)?; 28 | 29 | let result = arg0.get_u32()? + arg1.get_u32()?; 30 | Ok(result) 31 | })?; 32 | 33 | let tsfn = ThreadSafeFunction::new(&func)?; 34 | 35 | thread::spawn({ 36 | let tsfn = tsfn.clone(); 37 | move || { 38 | let a = 1; 39 | let b = 1; 40 | 41 | let ret = tsfn 42 | .call_blocking( 43 | // Rust values to pass into JavaScript 44 | move |_env| Ok((a, b)), 45 | // JavaScript values to pass back into Rust 46 | |_env, ret| ret.cast::()?.get_u32(), 47 | ) 48 | .unwrap(); 49 | 50 | println!("Ret: {}", ret); 51 | thread::sleep(Duration::from_secs(1)); 52 | } 53 | }); 54 | 55 | thread::spawn({ 56 | let tsfn = tsfn.clone(); 57 | move || { 58 | let a = 1; 59 | let b = 1; 60 | 61 | let ret = tsfn 62 | .call_blocking( 63 | // Rust values to pass into JavaScript 64 | move |_env| Ok((a, b)), 65 | // JavaScript values to pass back into Rust 66 | |_env, ret| ret.cast::()?.get_u32(), 67 | ) 68 | .unwrap(); 69 | 70 | println!("Ret: {}", ret); 71 | thread::sleep(Duration::from_secs(1)); 72 | } 73 | }); 74 | 75 | Ok(()) 76 | })?; 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /examples/src/http_server/worker_pool.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ion::utils::channel::Sender; 4 | use ion::utils::channel::channel; 5 | use ion::utils::channel::oneshot; 6 | use ion::*; 7 | use tokio::task::JoinHandle; 8 | 9 | pub struct WorkerPoolOptions { 10 | pub runtime: Arc, 11 | pub worker_count: usize, 12 | pub contexts_per_worker: usize, 13 | } 14 | 15 | // Basic load balancer, round robin 16 | pub struct WorkerPool { 17 | #[allow(clippy::type_complexity)] 18 | queue: Sender ion::Result<()>>>, 19 | } 20 | 21 | impl WorkerPool { 22 | pub fn new( 23 | WorkerPoolOptions { 24 | runtime, 25 | worker_count, 26 | contexts_per_worker, 27 | }: WorkerPoolOptions 28 | ) -> anyhow::Result { 29 | let (tx, rx) = channel(); 30 | 31 | for i in 0..worker_count { 32 | println!("[{}] Worker Started ({} Contexts)", i, contexts_per_worker); 33 | 34 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 35 | 36 | for _ in 0..contexts_per_worker { 37 | let rx = rx.clone(); 38 | let worker = worker.clone(); 39 | 40 | let _handle: JoinHandle> = tokio::task::spawn(async move { 41 | let ctx = worker.create_context()?; 42 | 43 | while let Ok(callback) = rx.recv_async().await { 44 | if ctx.exec(callback).is_err() { 45 | eprintln!("Error communicating with JavaScript") 46 | } 47 | 48 | worker.run_garbage_collection_for_testing()?; 49 | } 50 | 51 | Ok(()) 52 | }); 53 | } 54 | } 55 | 56 | Ok(Self { queue: tx }) 57 | } 58 | 59 | pub fn exec( 60 | &self, 61 | callback: impl 'static + Send + FnOnce(&Env) -> ion::Result<()>, 62 | ) -> anyhow::Result<()> { 63 | self.queue.try_send(Box::new(callback)).unwrap(); 64 | Ok(()) 65 | } 66 | 67 | pub async fn exec_async( 68 | &self, 69 | callback: impl 'static + Send + FnOnce(&Env) -> ion::Result, 70 | ) -> anyhow::Result { 71 | let (tx, rx) = oneshot(); 72 | 73 | self.exec(move |env| Ok(tx.try_send(callback(env)?)?))?; 74 | 75 | let Ok(ret) = rx.recv_async().await else { 76 | return Err(anyhow::anyhow!("Failed to send")); 77 | }; 78 | Ok(ret) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/event_target/binding.ts: -------------------------------------------------------------------------------- 1 | type EventListener = (event: Event) => any | Promise; 2 | type EventListenerOptions = { once?: boolean }; 3 | 4 | const listenersGlobal: Map> = new Map() 5 | 6 | export class EventTarget { 7 | addEventListener( 8 | type: string, 9 | listener: EventListener, 10 | options: EventListenerOptions = {} 11 | ) { 12 | let listeners = listenersGlobal.get(type); 13 | if (!listeners) { 14 | listeners = []; 15 | listenersGlobal.set(type, listeners); 16 | } 17 | listeners.push([listener, options]); 18 | } 19 | 20 | removeEventListener(type: string, listener: EventListener) { 21 | let listeners = listenersGlobal.get(type); 22 | if (!listeners) { 23 | return; 24 | } 25 | let index = listeners.findIndex((v) => v[0] === listener); 26 | listeners.splice(index, 1); 27 | } 28 | 29 | dispatchEvent(event: Event) { 30 | const listeners = listenersGlobal.get(event.type)?.values() || []; 31 | for (const [listener, options] of listeners) { 32 | event.target = this; 33 | event.currentTarget = this; 34 | listener(event); 35 | event.target = null; 36 | event.currentTarget = null; 37 | if (options.once) { 38 | this.removeEventListener(event.type, listener) 39 | } 40 | } 41 | } 42 | } 43 | 44 | export class Event { 45 | isTrusted: boolean; 46 | bubbles: boolean; 47 | cancelBubble: boolean; 48 | cancelable: boolean; 49 | composed: boolean; 50 | currentTarget: null | EventTarget; 51 | defaultPrevented: boolean; 52 | eventPhase: number; 53 | returnValue: boolean; 54 | srcElement: null; 55 | target: null | EventTarget; 56 | timeStamp: number; 57 | type: string; 58 | 59 | constructor(type: string) { 60 | this.isTrusted = false; 61 | this.bubbles = false; 62 | this.cancelBubble = false; 63 | this.cancelable = false; 64 | this.composed = false; 65 | this.currentTarget = null; 66 | this.defaultPrevented = false; 67 | this.eventPhase = 0; 68 | this.returnValue = false; 69 | this.srcElement = null; 70 | this.target = null; 71 | this.timeStamp = Date.now(); 72 | this.type = type; 73 | } 74 | } 75 | 76 | export class CustomEvent extends Event { 77 | detail: any; 78 | 79 | constructor(type: string, detail?: any) { 80 | super(type) 81 | this.detail = detail 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/ion/src/platform/sys/error.rs: -------------------------------------------------------------------------------- 1 | /// Throw a JavaScript exception with the given value 2 | pub fn v8_throw_exception( 3 | scope: &mut v8::HandleScope, 4 | value: v8::Local, 5 | ) { 6 | scope.throw_exception(value); 7 | } 8 | 9 | /// Create and throw a JavaScript Error with the given message 10 | pub fn v8_throw_error( 11 | scope: &mut v8::HandleScope, 12 | message: &str, 13 | ) -> crate::Result<()> { 14 | let Some(msg) = v8::String::new(scope, message) else { 15 | return Err(crate::Error::ValueCreateError); 16 | }; 17 | 18 | let error = v8::Exception::error(scope, msg); 19 | scope.throw_exception(error); 20 | Ok(()) 21 | } 22 | 23 | // /// Create and throw a JavaScript TypeError with the given message 24 | // pub fn v8_throw_type_error( 25 | // scope: &mut v8::HandleScope, 26 | // message: &str, 27 | // ) -> crate::Result<()> { 28 | // let Some(msg) = v8::String::new(scope, message) else { 29 | // return Err(crate::Error::ValueCreateError); 30 | // }; 31 | 32 | // let error = v8::Exception::type_error(scope, msg); 33 | // scope.throw_exception(error); 34 | // Ok(()) 35 | // } 36 | 37 | // /// Create and throw a JavaScript RangeError with the given message 38 | // pub fn v8_throw_range_error( 39 | // scope: &mut v8::HandleScope, 40 | // message: &str, 41 | // ) -> crate::Result<()> { 42 | // let Some(msg) = v8::String::new(scope, message) else { 43 | // return Err(crate::Error::ValueCreateError); 44 | // }; 45 | 46 | // let error = v8::Exception::range_error(scope, msg); 47 | // scope.throw_exception(error); 48 | // Ok(()) 49 | // } 50 | 51 | // /// Create and throw a JavaScript ReferenceError with the given message 52 | // pub fn v8_throw_reference_error( 53 | // scope: &mut v8::HandleScope, 54 | // message: &str, 55 | // ) -> crate::Result<()> { 56 | // let Some(msg) = v8::String::new(scope, message) else { 57 | // return Err(crate::Error::ValueCreateError); 58 | // }; 59 | 60 | // let error = v8::Exception::reference_error(scope, msg); 61 | // scope.throw_exception(error); 62 | // Ok(()) 63 | // } 64 | 65 | // /// Create and throw a JavaScript SyntaxError with the given message 66 | // pub fn v8_throw_syntax_error( 67 | // scope: &mut v8::HandleScope, 68 | // message: &str, 69 | // ) -> crate::Result<()> { 70 | // let Some(msg) = v8::String::new(scope, message) else { 71 | // return Err(crate::Error::ValueCreateError); 72 | // }; 73 | 74 | // let error = v8::Exception::syntax_error(scope, msg); 75 | // scope.throw_exception(error); 76 | // Ok(()) 77 | // } 78 | -------------------------------------------------------------------------------- /crates/ion/src/platform/background_worker.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::thread::JoinHandle; 4 | use std::thread::{self}; 5 | 6 | use flume::Sender; 7 | use flume::unbounded; 8 | use tracing::Instrument; 9 | 10 | use crate::utils::channel::oneshot; 11 | 12 | pub(crate) enum BackgroundTaskManagerEvent { 13 | ExecFut { 14 | fut: Pin>>>, 15 | span: tracing::Span, 16 | }, 17 | } 18 | 19 | pub struct BackgroundTaskManager { 20 | tx: Sender, 21 | #[allow(unused)] 22 | handle: JoinHandle>, 23 | } 24 | 25 | impl BackgroundTaskManager { 26 | pub fn new() -> crate::Result { 27 | let (tx, rx) = unbounded::(); 28 | 29 | let (bg_tx, bg_rx) = oneshot::>(); 30 | let handle: JoinHandle> = thread::spawn({ 31 | move || { 32 | let Ok(runtime) = tokio::runtime::Builder::new_multi_thread() 33 | .worker_threads(num_cpus::get_physical()) 34 | .enable_all() 35 | .build() 36 | else { 37 | bg_tx 38 | .send(Err(crate::Error::BackgroundThreadError)) 39 | .expect("Unable to start background thread"); 40 | 41 | return Err(crate::Error::BackgroundThreadError); 42 | }; 43 | 44 | bg_tx 45 | .send(Ok(())) 46 | .expect("Unable to start background thread"); 47 | 48 | runtime.block_on(async move { 49 | while let Ok(event) = rx.recv_async().await { 50 | match event { 51 | BackgroundTaskManagerEvent::ExecFut { fut, span } => { 52 | tokio::task::spawn(fut.instrument(span)); 53 | } 54 | } 55 | } 56 | Ok(()) 57 | }) 58 | } 59 | }); 60 | bg_rx.recv()??; 61 | 62 | Ok(Self { tx, handle }) 63 | } 64 | 65 | pub fn spawn( 66 | &self, 67 | fut: impl 'static + Send + Sync + Future>, 68 | ) -> crate::Result<()> { 69 | let span = tracing::Span::current(); 70 | Ok(self.tx.try_send(BackgroundTaskManagerEvent::ExecFut { 71 | fut: Box::pin(fut), 72 | span, 73 | })?) 74 | } 75 | 76 | pub fn spawn_then( 77 | &self, 78 | fut: impl 'static + Send + Sync + Future>, 79 | ) -> crate::Result<()> { 80 | let span = tracing::Span::current(); 81 | Ok(self.tx.try_send(BackgroundTaskManagerEvent::ExecFut { 82 | fut: Box::pin(fut), 83 | span, 84 | })?) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_string.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::sync::Arc; 3 | 4 | use crate::Env; 5 | use crate::ToJsUnknown; 6 | use crate::platform::sys; 7 | use crate::platform::sys::Value; 8 | use crate::values::FromJsValue; 9 | use crate::values::JsObjectValue; 10 | use crate::values::JsValue; 11 | use crate::values::ToJsValue; 12 | 13 | #[derive(Clone)] 14 | pub struct JsString { 15 | pub(crate) value: Value, 16 | pub(crate) env: Env, 17 | } 18 | 19 | impl JsString { 20 | pub fn new( 21 | env: &Env, 22 | text: impl AsRef, 23 | ) -> crate::Result { 24 | let scope = &mut env.scope(); 25 | let string = crate::utils::v8::v8_create_string(scope, text.as_ref())?; 26 | Ok(Self { 27 | value: sys::v8_from_value(string), 28 | env: env.clone(), 29 | }) 30 | } 31 | 32 | pub fn get_string(&self) -> crate::Result { 33 | let scope = &mut self.env.scope(); 34 | let Ok(local) = self.value.try_cast::() else { 35 | return Err(crate::Error::ValueCastError); 36 | }; 37 | Ok(local.to_rust_string_lossy(scope)) 38 | } 39 | } 40 | 41 | impl JsValue for JsString { 42 | fn value(&self) -> &Value { 43 | &self.value 44 | } 45 | 46 | fn env(&self) -> &Env { 47 | &self.env 48 | } 49 | } 50 | 51 | impl ToJsUnknown for JsString {} 52 | impl JsObjectValue for JsString {} 53 | 54 | impl FromJsValue for JsString { 55 | fn from_js_value( 56 | env: &Env, 57 | value: Value, 58 | ) -> crate::Result { 59 | Ok(Self { 60 | value, 61 | env: env.clone(), 62 | }) 63 | } 64 | } 65 | 66 | impl ToJsValue for JsString { 67 | fn to_js_value( 68 | _env: &Env, 69 | val: Self, 70 | ) -> crate::Result { 71 | Ok(val.value) 72 | } 73 | } 74 | 75 | impl ToJsValue for String { 76 | fn to_js_value( 77 | env: &Env, 78 | val: Self, 79 | ) -> crate::Result { 80 | Ok(*JsString::new(env, val)?.value()) 81 | } 82 | } 83 | 84 | impl ToJsValue for &str { 85 | fn to_js_value( 86 | env: &Env, 87 | val: Self, 88 | ) -> crate::Result { 89 | Ok(*JsString::new(env, val)?.value()) 90 | } 91 | } 92 | 93 | impl ToJsValue for Rc { 94 | fn to_js_value( 95 | env: &Env, 96 | val: Self, 97 | ) -> crate::Result { 98 | Ok(*JsString::new(env, val)?.value()) 99 | } 100 | } 101 | 102 | impl ToJsValue for Arc { 103 | fn to_js_value( 104 | env: &Env, 105 | val: Self, 106 | ) -> crate::Result { 107 | Ok(*JsString::new(env, val)?.value()) 108 | } 109 | } 110 | 111 | impl Env { 112 | pub fn create_string( 113 | &self, 114 | value: impl AsRef, 115 | ) -> crate::Result { 116 | JsString::new(self, value) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /crates/ion/src/platform/finalizer_registry.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::rc::Rc; 4 | 5 | #[derive(Clone)] 6 | pub struct FinalizerRegistery { 7 | #[allow(clippy::type_complexity)] 8 | callbacks: Rc, Box)>>>, 9 | isolate: *mut v8::Isolate, 10 | } 11 | 12 | impl FinalizerRegistery { 13 | pub fn new(isolate: *mut v8::Isolate) -> Self { 14 | Self { 15 | callbacks: Default::default(), 16 | isolate, 17 | } 18 | } 19 | 20 | pub fn register( 21 | &self, 22 | value: &v8::Local<'_, v8::Value>, 23 | callback: impl 'static + FnOnce(), 24 | ) -> usize { 25 | let mut callback = Box::new(callback); 26 | let id = callback.as_mut() as *mut _ as usize; 27 | 28 | let weak = v8::Weak::with_guaranteed_finalizer( 29 | unsafe { &mut *self.isolate }, 30 | value, 31 | Box::new({ 32 | let callbacks = self.callbacks.clone(); 33 | move || { 34 | let mut callbacks = callbacks.borrow_mut(); 35 | if let Some((_, callback)) = callbacks.remove(&id) { 36 | callback(); 37 | }; 38 | } 39 | }), 40 | ); 41 | 42 | let mut callbacks = self.callbacks.borrow_mut(); 43 | callbacks.insert(id, (weak, callback)); 44 | id 45 | } 46 | 47 | pub fn clear(&self) { 48 | let mut callbacks = self.callbacks.borrow_mut(); 49 | for (_, (_, callback)) in callbacks.drain() { 50 | callback(); 51 | } 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use crate::*; 58 | 59 | #[test] 60 | fn should_run_drop() -> anyhow::Result<()> { 61 | let worker = testing::JS_RUNTIME.spawn_worker(JsWorkerOptions { 62 | resolvers: vec![], 63 | transformers: vec![], 64 | extensions: vec![], 65 | })?; 66 | 67 | let context = worker.create_context()?; 68 | let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); 69 | 70 | context.exec_blocking(move |env| { 71 | let value = env.create_int32(42)?; 72 | 73 | env.finalizer_registry.register(value.value(), move || { 74 | tx.send(()).ok(); 75 | }); 76 | 77 | env.global_this()?.set_named_property("__global", value)?; 78 | 79 | Ok(()) 80 | })?; 81 | 82 | assert_eq!(rx.len(), 0, "Unexpect GC Notification"); 83 | 84 | context.exec_blocking(|env| { 85 | env.global_this()?.delete_named_property("__global")?; 86 | Ok(()) 87 | })?; 88 | 89 | // TODO: It appears that the value will only be dropped if the context is dropped 90 | // Ideally I want it to be dropped when the value is actually GC'd 91 | // 92 | // worker.run_garbage_collection_for_testing()?; 93 | // assert_eq!(rx.len(), 1, "GC notification not sent"); 94 | 95 | drop(context); 96 | assert_eq!(rx.len(), 1, "GC notification not sent"); 97 | 98 | Ok(()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/ion/src/js_context.rs: -------------------------------------------------------------------------------- 1 | use flume::Sender; 2 | use flume::bounded; 3 | 4 | use crate::Env; 5 | use crate::Error; 6 | use crate::JsUnknown; 7 | use crate::platform::worker::JsWorkerEvent; 8 | use crate::utils::channel::oneshot; 9 | 10 | /// This is a handle to a v8::Context 11 | #[derive(Debug, Clone)] 12 | pub struct JsContext { 13 | pub(crate) id: usize, 14 | pub(crate) tx: Sender, 15 | } 16 | 17 | impl JsContext { 18 | pub fn exec( 19 | &self, 20 | callback: impl 'static + Send + FnOnce(&Env) -> crate::Result<()>, 21 | ) -> crate::Result<()> { 22 | let span = tracing::Span::current(); 23 | if self 24 | .tx 25 | .try_send(JsWorkerEvent::Exec { 26 | id: self.id, 27 | callback: Box::new(callback), 28 | span, 29 | }) 30 | .is_err() 31 | { 32 | return Err(Error::ExecError); 33 | }; 34 | Ok(()) 35 | } 36 | 37 | pub async fn exec_async( 38 | &self, 39 | callback: impl 'static + Send + FnOnce(&Env) -> crate::Result, 40 | ) -> crate::Result { 41 | let (tx, rx) = bounded(1); 42 | 43 | self.exec(move |env| Ok(tx.try_send(callback(env)?)?))?; 44 | 45 | let Ok(ret) = rx.recv_async().await else { 46 | return Err(Error::ExecError); 47 | }; 48 | Ok(ret) 49 | } 50 | 51 | pub fn exec_blocking( 52 | &self, 53 | callback: impl 'static + Send + FnOnce(&Env) -> crate::Result, 54 | ) -> crate::Result { 55 | let (tx, rx) = bounded::(1); 56 | 57 | self.exec(move |env| Ok(tx.try_send(callback(env)?)?))?; 58 | 59 | let Ok(ret) = rx.recv() else { 60 | return Err(Error::ExecError); 61 | }; 62 | Ok(ret) 63 | } 64 | 65 | /// Evaluate script, ignoring return value. If you need the return value 66 | /// use a variant of [`JsContext::exec`] then run [`Env::eval_script`] 67 | pub fn eval( 68 | &self, 69 | code: impl AsRef, 70 | ) -> crate::Result<()> { 71 | let code = code.as_ref().to_string(); 72 | self.exec_blocking(move |env| { 73 | env.eval_script::(code)?; 74 | Ok(()) 75 | }) 76 | } 77 | 78 | /// Load a file and evaluate it 79 | pub fn import( 80 | &self, 81 | specifier: impl AsRef, 82 | ) -> crate::Result<()> { 83 | let specifier = specifier.as_ref().to_string(); 84 | self.exec_blocking(move |env| env.import(specifier)) 85 | } 86 | } 87 | 88 | impl Drop for JsContext { 89 | fn drop(&mut self) { 90 | let (tx, rx) = oneshot(); 91 | 92 | if self 93 | .tx 94 | .send(JsWorkerEvent::RequestContextShutdown { 95 | id: self.id, 96 | resolve: Some(tx), 97 | }) 98 | .is_err() 99 | { 100 | panic!("Cannot drop JsContext 1") 101 | }; 102 | 103 | if rx.recv().is_err() { 104 | panic!("Cannot drop JsContext 2") 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/ion/src/js_worker.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::sync::Mutex; 3 | use std::thread::JoinHandle; 4 | 5 | use flume::Sender; 6 | use flume::bounded; 7 | 8 | use super::JsContext; 9 | use crate::Error; 10 | use crate::JsExtension; 11 | use crate::JsResolver; 12 | use crate::JsTransformer; 13 | use crate::platform::worker::JsWorkerEvent; 14 | use crate::utils::channel::oneshot; 15 | 16 | #[derive(Default)] 17 | pub struct JsWorkerOptions { 18 | /// Hook that runs before code is imported. This can be used to 19 | /// customize the behavior of "import" statements 20 | pub resolvers: Vec, 21 | /// Hook that runs before code is loaded. This can be used to 22 | /// convert TypeScript into JavaScript or JSON into JavaScript 23 | pub transformers: Vec, 24 | /// Extensions that will be available to all [`crate::JsWorker`] and [`crate::JsContext`] instances 25 | pub extensions: Vec, 26 | } 27 | 28 | /// This is a handle to a v8::Isolate running on a dedicated thread. 29 | /// A worker thread can spawn multiple v8::Contexts within that thread 30 | /// to be used to execute JavaScript 31 | #[derive(Debug)] 32 | pub struct JsWorker { 33 | tx: Sender, 34 | handle: Mutex>>>, 35 | } 36 | 37 | impl JsWorker { 38 | pub(crate) fn new( 39 | tx: Sender, 40 | handle: Mutex>>>, 41 | ) -> Self { 42 | JsWorker { tx, handle } 43 | } 44 | 45 | /// Create a handle to a v8::Context associated with this v8::Isolate 46 | pub fn create_context(&self) -> crate::Result> { 47 | let (tx, rx) = bounded(1); 48 | 49 | if self 50 | .tx 51 | .send(JsWorkerEvent::CreateContext { resolve: tx }) 52 | .is_err() 53 | { 54 | return Err(Error::WorkerInitializeError); 55 | }; 56 | 57 | let Ok((id, tx)) = rx.recv() else { 58 | return Err(Error::WorkerInitializeError); 59 | }; 60 | 61 | Ok(Arc::new(JsContext { id, tx })) 62 | } 63 | 64 | pub fn run_garbage_collection_for_testing(&self) -> crate::Result<()> { 65 | let (tx, rx) = bounded(1); 66 | 67 | if self 68 | .tx 69 | .send(JsWorkerEvent::RunGarbageCollectionForTesting { resolve: tx }) 70 | .is_err() 71 | { 72 | return Err(Error::WorkerInitializeError); 73 | }; 74 | 75 | Ok(rx.recv()?) 76 | } 77 | } 78 | 79 | impl Drop for JsWorker { 80 | fn drop(&mut self) { 81 | let (tx, rx) = oneshot(); 82 | 83 | if self 84 | .tx 85 | .send(JsWorkerEvent::RequestShutdown { resolve: tx }) 86 | .is_err() 87 | { 88 | panic!("Cannot drop JsWorker 1"); 89 | }; 90 | 91 | if rx.recv().is_err() { 92 | panic!("Cannot drop JsWorker 2"); 93 | } 94 | 95 | let Ok(mut handle) = self.handle.lock() else { 96 | panic!("Cannot drop JsWorker 3"); 97 | }; 98 | 99 | if let Some(handle) = handle.take() { 100 | drop(handle.join().unwrap()); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_timeout/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::sync::Arc; 3 | 4 | use parking_lot::Mutex; 5 | 6 | use crate::Env; 7 | use crate::JsExtension; 8 | use crate::JsFunction; 9 | use crate::JsNumber; 10 | use crate::JsObject; 11 | use crate::JsObjectValue; 12 | use crate::JsString; 13 | use crate::ThreadSafeFunction; 14 | use crate::thread_safe_function; 15 | use crate::utils::AtomicRefCounter; 16 | 17 | static MODULE_NAME: &str = "ion:timers/timeout"; 18 | static BINDING: &str = include_str!("./binding.ts"); 19 | 20 | pub fn set_timeout() -> JsExtension { 21 | JsExtension::NativeModuleWithBinding { 22 | module_name: MODULE_NAME.to_string(), 23 | binding: BINDING.to_string(), 24 | extension: Box::new(extension_hook), 25 | } 26 | } 27 | 28 | fn extension_hook( 29 | env: &Env, 30 | exports: &mut JsObject, 31 | ) -> crate::Result<()> { 32 | let timer_refs = Arc::new(Mutex::new(HashSet::::new())); 33 | let ref_count = AtomicRefCounter::new(0); 34 | 35 | exports.set_named_property( 36 | "setTimeout", 37 | JsFunction::new(env, { 38 | let timer_refs = Arc::clone(&timer_refs); 39 | let ref_count = ref_count.clone(); 40 | 41 | move |env, ctx| { 42 | let arg0 = ctx.arg::(0)?; 43 | let arg1 = ctx.arg::(1)?; 44 | 45 | let callback = ThreadSafeFunction::new(&arg0)?; 46 | let duration = arg1.get_u32()?; 47 | let timer_ref = format!("{}", ref_count.inc()); 48 | 49 | { 50 | let mut timer_refs = timer_refs.lock(); 51 | timer_refs.insert(timer_ref.clone()); 52 | } 53 | 54 | env.spawn_background({ 55 | let timer_ref = timer_ref.clone(); 56 | let timer_refs = Arc::clone(&timer_refs); 57 | async move { 58 | tokio::time::sleep(tokio::time::Duration::from_millis(duration as u64)) 59 | .await; 60 | 61 | { 62 | let mut timer_refs = timer_refs.lock(); 63 | if !timer_refs.remove(&timer_ref) { 64 | return Ok(()); 65 | } 66 | } 67 | 68 | callback 69 | .call_async( 70 | thread_safe_function::map_arguments::noop, 71 | thread_safe_function::map_return::noop, 72 | ) 73 | .await?; 74 | 75 | Ok(()) 76 | } 77 | })?; 78 | 79 | Ok(timer_ref) 80 | } 81 | })?, 82 | )?; 83 | 84 | exports.set_named_property( 85 | "clearTimeout", 86 | JsFunction::new(env, { 87 | move |_env, ctx| { 88 | let arg0 = ctx.arg::(0)?; 89 | 90 | { 91 | let mut timer_refs = timer_refs.lock(); 92 | timer_refs.remove(&arg0.get_string()?); 93 | } 94 | 95 | Ok(()) 96 | } 97 | })?, 98 | )?; 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/set_interval/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::sync::Arc; 3 | 4 | use parking_lot::RwLock; 5 | 6 | use crate::Env; 7 | use crate::JsExtension; 8 | use crate::JsFunction; 9 | use crate::JsNumber; 10 | use crate::JsObject; 11 | use crate::JsObjectValue; 12 | use crate::JsString; 13 | use crate::ThreadSafeFunction; 14 | use crate::thread_safe_function; 15 | use crate::utils::AtomicRefCounter; 16 | 17 | static MODULE_NAME: &str = "ion:timers/interval"; 18 | static BINDING: &str = include_str!("./binding.ts"); 19 | 20 | pub fn set_interval() -> JsExtension { 21 | JsExtension::NativeModuleWithBinding { 22 | module_name: MODULE_NAME.to_string(), 23 | binding: BINDING.to_string(), 24 | extension: Box::new(extension_hook), 25 | } 26 | } 27 | 28 | fn extension_hook( 29 | env: &Env, 30 | exports: &mut JsObject, 31 | ) -> crate::Result<()> { 32 | let timer_refs = Arc::new(RwLock::new(HashSet::::new())); 33 | let ref_count = AtomicRefCounter::new(0); 34 | 35 | exports.set_named_property( 36 | "setInterval", 37 | JsFunction::new(env, { 38 | let timer_refs = Arc::clone(&timer_refs); 39 | let ref_count = ref_count.clone(); 40 | 41 | move |env, ctx| { 42 | let arg0 = ctx.arg::(0)?; 43 | let arg1 = ctx.arg::(1)?; 44 | 45 | let callback = ThreadSafeFunction::new(&arg0)?; 46 | let duration = arg1.get_u32()?; 47 | let timer_ref = format!("{}", ref_count.inc()); 48 | 49 | { 50 | let mut timer_refs = timer_refs.write(); 51 | timer_refs.insert(timer_ref.clone()); 52 | } 53 | 54 | env.spawn_background({ 55 | let timer_ref = timer_ref.clone(); 56 | let timer_refs = Arc::clone(&timer_refs); 57 | async move { 58 | loop { 59 | tokio::time::sleep(tokio::time::Duration::from_millis(duration as u64)) 60 | .await; 61 | 62 | { 63 | let timer_refs = timer_refs.read(); 64 | if !timer_refs.contains(&timer_ref) { 65 | return Ok(()); 66 | } 67 | } 68 | 69 | callback 70 | .call_async( 71 | thread_safe_function::map_arguments::noop, 72 | thread_safe_function::map_return::noop, 73 | ) 74 | .await?; 75 | } 76 | } 77 | })?; 78 | 79 | Ok(timer_ref) 80 | } 81 | })?, 82 | )?; 83 | 84 | exports.set_named_property( 85 | "clearInterval", 86 | JsFunction::new(env, { 87 | move |_env, ctx| { 88 | let arg0 = ctx.arg::(0)?; 89 | 90 | { 91 | let mut timer_refs = timer_refs.write(); 92 | timer_refs.remove(&arg0.get_string()?); 93 | } 94 | 95 | Ok(()) 96 | } 97 | })?, 98 | )?; 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /crates/ion/src/extensions/console/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::JsExtension; 3 | use crate::JsFunction; 4 | use crate::JsObject; 5 | use crate::JsObjectValue; 6 | use crate::JsString; 7 | use crate::JsUnknown; 8 | 9 | static MODULE_NAME: &str = "ion:console"; 10 | static BINDING: &str = include_str!("./binding.ts"); 11 | 12 | pub fn console() -> JsExtension { 13 | JsExtension::NativeModuleWithBinding { 14 | module_name: MODULE_NAME.to_string(), 15 | binding: BINDING.to_string(), 16 | extension: Box::new(extension_hook), 17 | } 18 | } 19 | 20 | fn extension_hook( 21 | env: &Env, 22 | exports: &mut JsObject, 23 | ) -> crate::Result<()> { 24 | exports.set_named_property( 25 | "log", 26 | JsFunction::new(env, |env, ctx| { 27 | let global_this = env.global_this()?; 28 | 29 | let json = global_this.get_named_property_unchecked::("JSON")?; 30 | let json_stringify = json.get_named_property_unchecked::("stringify")?; 31 | 32 | let mut args_string = vec![]; 33 | 34 | for i in 0..ctx.len() { 35 | let arg = ctx.arg::(i)?; 36 | let replacer = env.get_null()?; 37 | let spaces = env.create_int32(2)?; 38 | let result: JsString = json_stringify.call_with_args((arg, replacer, spaces))?; 39 | args_string.push((result.get_string()?).to_string()); 40 | } 41 | 42 | let output = args_string.join(", "); 43 | println!("{}", output); 44 | 45 | Ok(()) 46 | })?, 47 | )?; 48 | 49 | exports.set_named_property( 50 | "warn", 51 | JsFunction::new(env, |env, ctx| { 52 | let global_this = env.global_this()?; 53 | 54 | let json = global_this.get_named_property_unchecked::("JSON")?; 55 | let json_stringify = json.get_named_property_unchecked::("stringify")?; 56 | 57 | let mut args_string = vec![]; 58 | 59 | for i in 0..ctx.len() { 60 | let arg = ctx.arg::(i)?; 61 | let replacer = env.get_null()?; 62 | let spaces = env.create_int32(2)?; 63 | let result: JsString = json_stringify.call_with_args((arg, replacer, spaces))?; 64 | args_string.push((result.get_string()?).to_string()); 65 | } 66 | 67 | let output = args_string.join(", "); 68 | println!("{}", output); 69 | 70 | Ok(()) 71 | })?, 72 | )?; 73 | 74 | exports.set_named_property( 75 | "error", 76 | JsFunction::new(env, |env, ctx| { 77 | let global_this = env.global_this()?; 78 | 79 | let json = global_this.get_named_property_unchecked::("JSON")?; 80 | let json_stringify = json.get_named_property_unchecked::("stringify")?; 81 | 82 | let mut args_string = vec![]; 83 | 84 | for i in 0..ctx.len() { 85 | let arg = ctx.arg::(i)?; 86 | let replacer = env.get_null()?; 87 | let spaces = env.create_int32(2)?; 88 | let result: JsString = json_stringify.call_with_args((arg, replacer, spaces))?; 89 | args_string.push((result.get_string()?).to_string()); 90 | } 91 | 92 | let output = args_string.join(", "); 93 | eprintln!("{}", output); 94 | 95 | Ok(()) 96 | })?, 97 | )?; 98 | 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_number.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::ToJsUnknown; 3 | use crate::platform::sys; 4 | use crate::platform::sys::Value; 5 | use crate::values::FromJsValue; 6 | use crate::values::JsValue; 7 | use crate::values::ToJsValue; 8 | 9 | #[derive(Clone)] 10 | pub struct JsNumber { 11 | pub(crate) value: Value, 12 | pub(crate) env: Env, 13 | } 14 | 15 | impl JsNumber { 16 | pub fn from_u32( 17 | env: &Env, 18 | val: u32, 19 | ) -> crate::Result { 20 | let scope = &mut env.scope(); 21 | 22 | let local = v8::Integer::new_from_unsigned(scope, val); 23 | let value = sys::v8_from_value(local); 24 | Ok(Self { 25 | value, 26 | env: env.clone(), 27 | }) 28 | } 29 | 30 | pub fn from_i32( 31 | env: &Env, 32 | val: i32, 33 | ) -> crate::Result { 34 | let scope = &mut env.scope(); 35 | 36 | let local = v8::Integer::new(scope, val); 37 | let value = sys::v8_from_value(local); 38 | Ok(Self { 39 | value, 40 | env: env.clone(), 41 | }) 42 | } 43 | 44 | pub fn from_f64( 45 | env: &Env, 46 | val: f64, 47 | ) -> crate::Result { 48 | let scope = &mut env.scope(); 49 | let local = v8::Number::new(scope, val); 50 | let value = sys::v8_from_value(local); 51 | Ok(Self { 52 | value, 53 | env: env.clone(), 54 | }) 55 | } 56 | 57 | pub fn get_u32(&self) -> crate::Result { 58 | let scope = &mut self.env.scope(); 59 | let local = self.value.cast::(); 60 | let Some(value) = local.uint32_value(scope) else { 61 | return Err(crate::Error::ValueGetError); 62 | }; 63 | Ok(value) 64 | } 65 | 66 | pub fn get_i32(&self) -> crate::Result { 67 | let scope = &mut self.env.scope(); 68 | let local = self.value.cast::(); 69 | let Some(value) = local.int32_value(scope) else { 70 | return Err(crate::Error::ValueGetError); 71 | }; 72 | Ok(value) 73 | } 74 | 75 | pub fn get_f64(&self) -> crate::Result { 76 | let local = self.value.cast::(); 77 | Ok(local.value()) 78 | } 79 | } 80 | 81 | impl JsValue for JsNumber { 82 | fn value(&self) -> &Value { 83 | &self.value 84 | } 85 | 86 | fn env(&self) -> &Env { 87 | &self.env 88 | } 89 | } 90 | 91 | impl ToJsUnknown for JsNumber {} 92 | 93 | impl FromJsValue for JsNumber { 94 | fn from_js_value( 95 | env: &Env, 96 | value: Value, 97 | ) -> crate::Result { 98 | Ok(Self { 99 | value, 100 | env: env.clone(), 101 | }) 102 | } 103 | } 104 | 105 | impl ToJsValue for JsNumber { 106 | fn to_js_value( 107 | _env: &Env, 108 | val: Self, 109 | ) -> crate::Result { 110 | Ok(val.value) 111 | } 112 | } 113 | 114 | impl ToJsValue for i32 { 115 | fn to_js_value( 116 | env: &Env, 117 | val: Self, 118 | ) -> crate::Result { 119 | Ok(*JsNumber::from_i32(env, val)?.value()) 120 | } 121 | } 122 | 123 | impl ToJsValue for u32 { 124 | fn to_js_value( 125 | env: &Env, 126 | val: Self, 127 | ) -> crate::Result { 128 | Ok(*JsNumber::from_u32(env, val)?.value()) 129 | } 130 | } 131 | 132 | impl Env { 133 | pub fn create_int32( 134 | &self, 135 | value: i32, 136 | ) -> crate::Result { 137 | JsNumber::from_i32(self, value) 138 | } 139 | 140 | pub fn create_uint32( 141 | &self, 142 | value: u32, 143 | ) -> crate::Result { 144 | JsNumber::from_u32(self, value) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /examples/src/testing/wait/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This is testing Ion's context switching behavior. 3 | 4 | v8 uses "scope" handles to track values for GC. A consumer 5 | must build "scope"s up in a stack starting from an root scope up 6 | to the scope used to manage the values themselves 7 | 8 | Isolate 9 | └─ IsolateScope 10 | └─ ContextScope 11 | └─ HandleScope 12 | ├─ JsObject 13 | └─ JsNumber 14 | 15 | When a "scope" is dropped, v8 looks at the values associated 16 | with that scope and determines if those values should be 17 | garbage collected. 18 | 19 | You can only have one scope on the stack at any given time and 20 | must manually drop the scopes on the stack in order of creation. 21 | 22 | If you create a scope while another scope is active, v8 will throw 23 | an error. This can happen when asynchronous tasks are spawned. 24 | 25 | Isolate 26 | └─ IsolateScope 27 | └─ ContextScope 28 | ├─ HandleScope // Scope0 29 | │ ├─ JsObject 30 | │ ├─ JsNumber 31 | └─ HandleScope // Error: "Scope0 already exists" 32 | └─ JsNumber 33 | 34 | Additionally, if there are multiple v8::Contexts sharing an Isolate 35 | and you want to switch between them (as we do in Ion), their scopes 36 | must be unwound/dropped correctly and new root scopes must be created 37 | 38 | Isolate 39 | ├─ IsolateScope (ctx0) // Scope0 40 | │ └─ ContextScope 41 | └─ IsolateScope (ctx1) // Error: "Scope0 already exists" 42 | └─ ContextScope 43 | 44 | To switch between multiple v8 Contexts correctly you must: 45 | 46 | Step 1: Create a root scope for that context 47 | 48 | Isolate 49 | └─ IsolateScope (ctx0) 50 | └─ ContextScope 51 | 52 | Step 2: Create children scopes to handle interacting with values 53 | 54 | Isolate 55 | └─ IsolateScope (ctx0) 56 | └─ ContextScope 57 | └─ HandleScope 58 | └─ JsObject 59 | 60 | Step 3: To switch to a new context, drop all scopes 61 | 62 | Isolate 63 | └─ None 64 | 65 | Step 4: Create a root scope for the second context 66 | 67 | Isolate 68 | └─ IsolateScope (ctx1) 69 | └─ ContextScope 70 | 71 | Step 5: Create children scopes to handle interacting with values 72 | 73 | Isolate 74 | └─ IsolateScope (ctx1) 75 | └─ ContextScope 76 | └─ HandleScope 77 | └─ JsObject 78 | 79 | Step 6: Rinse and repeat every time you need to switch between 80 | contexts to interact with the values held in that context 81 | */ 82 | use ion::*; 83 | 84 | pub fn main() -> anyhow::Result<()> { 85 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 86 | 87 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 88 | 89 | println!("[ctx0] Started"); 90 | let ctx0 = worker.create_context()?; 91 | 92 | println!("[ctx1] Started"); 93 | let ctx1 = worker.create_context()?; 94 | 95 | println!("[ctx2] Started"); 96 | let ctx2 = worker.create_context()?; 97 | 98 | ctx0.exec_blocking(|env| { 99 | let value = env.eval_script::("1 + 1")?; 100 | let result = value.get_u32()?; 101 | println!("[ctx0]: {}", result); 102 | Ok(()) 103 | })?; 104 | 105 | ctx1.exec_blocking(|env| { 106 | let value = env.eval_script::("1 + 1")?; 107 | let result = value.get_u32()?; 108 | println!("[ctx1]: {}", result); 109 | Ok(()) 110 | })?; 111 | 112 | // Make sure contexts dropped clean up properly 113 | drop(ctx0); 114 | 115 | ctx2.exec_blocking(|env| { 116 | let value = env.eval_script::("1 + 1")?; 117 | let result = value.get_u32()?; 118 | println!("[ctx1]: {}", result); 119 | Ok(()) 120 | })?; 121 | 122 | // For illustrative purposes 123 | drop(ctx1); 124 | drop(ctx2); 125 | 126 | Ok(()) 127 | } 128 | -------------------------------------------------------------------------------- /examples/src/testing/multiple_contexts/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This is testing Ion's context switching behavior. 3 | 4 | v8 uses "scope" handles to track values for GC. A consumer 5 | must build "scope"s up in a stack starting from an root scope up 6 | to the scope used to manage the values themselves 7 | 8 | Isolate 9 | └─ IsolateScope 10 | └─ ContextScope 11 | └─ HandleScope 12 | ├─ JsObject 13 | └─ JsNumber 14 | 15 | When a "scope" is dropped, v8 looks at the values associated 16 | with that scope and determines if those values should be 17 | garbage collected. 18 | 19 | You can only have one scope on the stack at any given time and 20 | must manually drop the scopes on the stack in order of creation. 21 | 22 | If you create a scope while another scope is active, v8 will throw 23 | an error. This can happen when asynchronous tasks are spawned. 24 | 25 | Isolate 26 | └─ IsolateScope 27 | └─ ContextScope 28 | ├─ HandleScope // Scope0 29 | │ ├─ JsObject 30 | │ ├─ JsNumber 31 | └─ HandleScope // Error: "Scope0 already exists" 32 | └─ JsNumber 33 | 34 | Additionally, if there are multiple v8::Contexts sharing an Isolate 35 | and you want to switch between them (as we do in Ion), their scopes 36 | must be unwound/dropped correctly and new root scopes must be created 37 | 38 | Isolate 39 | ├─ IsolateScope (ctx0) // Scope0 40 | │ └─ ContextScope 41 | └─ IsolateScope (ctx1) // Error: "Scope0 already exists" 42 | └─ ContextScope 43 | 44 | To switch between multiple v8 Contexts correctly you must: 45 | 46 | Step 1: Create a root scope for that context 47 | 48 | Isolate 49 | └─ IsolateScope (ctx0) 50 | └─ ContextScope 51 | 52 | Step 2: Create children scopes to handle interacting with values 53 | 54 | Isolate 55 | └─ IsolateScope (ctx0) 56 | └─ ContextScope 57 | └─ HandleScope 58 | └─ JsObject 59 | 60 | Step 3: To switch to a new context, drop all scopes 61 | 62 | Isolate 63 | └─ None 64 | 65 | Step 4: Create a root scope for the second context 66 | 67 | Isolate 68 | └─ IsolateScope (ctx1) 69 | └─ ContextScope 70 | 71 | Step 5: Create children scopes to handle interacting with values 72 | 73 | Isolate 74 | └─ IsolateScope (ctx1) 75 | └─ ContextScope 76 | └─ HandleScope 77 | └─ JsObject 78 | 79 | Step 6: Rinse and repeat every time you need to switch between 80 | contexts to interact with the values held in that context 81 | */ 82 | use ion::*; 83 | 84 | pub fn main() -> anyhow::Result<()> { 85 | let runtime = JsRuntime::initialize_once(JsRuntimeOptions::default())?; 86 | 87 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 88 | 89 | println!("[ctx0] Started"); 90 | let ctx0 = worker.create_context()?; 91 | 92 | println!("[ctx1] Started"); 93 | let ctx1 = worker.create_context()?; 94 | 95 | println!("[ctx2] Started"); 96 | let ctx2 = worker.create_context()?; 97 | 98 | ctx0.exec_blocking(|env| { 99 | let value = env.eval_script::("1 + 1")?; 100 | let result = value.get_u32()?; 101 | println!("[ctx0]: {}", result); 102 | Ok(()) 103 | })?; 104 | 105 | ctx1.exec_blocking(|env| { 106 | let value = env.eval_script::("1 + 1")?; 107 | let result = value.get_u32()?; 108 | println!("[ctx1]: {}", result); 109 | Ok(()) 110 | })?; 111 | 112 | // Make sure contexts dropped clean up properly 113 | drop(ctx0); 114 | 115 | ctx2.exec_blocking(|env| { 116 | let value = env.eval_script::("1 + 1")?; 117 | let result = value.get_u32()?; 118 | println!("[ctx1]: {}", result); 119 | Ok(()) 120 | })?; 121 | 122 | // For illustrative purposes 123 | drop(ctx1); 124 | drop(ctx2); 125 | 126 | Ok(()) 127 | } 128 | -------------------------------------------------------------------------------- /crates/ion/src/values/common/js_values_vec.rs: -------------------------------------------------------------------------------- 1 | use crate::Env; 2 | use crate::ToJsValue; 3 | use crate::platform::sys::Value; 4 | 5 | pub trait JsValuesTupleIntoVec { 6 | fn into_vec( 7 | self, 8 | env: &Env, 9 | ) -> crate::Result>; 10 | } 11 | 12 | impl JsValuesTupleIntoVec for T 13 | where 14 | T: ToJsValue, 15 | { 16 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 17 | fn into_vec( 18 | self, 19 | env: &Env, 20 | ) -> crate::Result> { 21 | // allow call function with `()` and function's arguments should be empty array 22 | if std::mem::size_of::() == 0 { 23 | Ok(vec![]) 24 | } else { 25 | Ok(vec![{ ::to_js_value(env, self)? }]) 26 | } 27 | } 28 | } 29 | 30 | pub trait TupleFromSliceValues { 31 | #[allow(clippy::missing_safety_doc)] 32 | unsafe fn from_slice_values( 33 | env: &Env, 34 | values: &[Value], 35 | ) -> crate::Result 36 | where 37 | Self: Sized; 38 | } 39 | 40 | pub struct FnArgs { 41 | pub data: T, 42 | } 43 | 44 | impl From for FnArgs { 45 | fn from(value: T) -> Self { 46 | FnArgs { data: value } 47 | } 48 | } 49 | 50 | // TODO Use a macro to generate these 51 | // impl JsValuesTupleIntoVec for () { 52 | // fn into_vec(self, env: &Env) -> crate::Result> { 53 | // Ok(vec![ 54 | // ]) 55 | // } 56 | // } 57 | 58 | // impl JsValuesTupleIntoVec for A { 59 | // fn into_vec(self, env: &Env) -> crate::Result> { 60 | // Ok(vec![ 61 | // A::to_js_value(env, self)?, 62 | // ]) 63 | // } 64 | // } 65 | 66 | impl JsValuesTupleIntoVec for (A, B) { 67 | fn into_vec( 68 | self, 69 | env: &Env, 70 | ) -> crate::Result> { 71 | Ok(vec![ 72 | A::to_js_value(env, self.0)?, 73 | B::to_js_value(env, self.1)?, 74 | ]) 75 | } 76 | } 77 | 78 | impl JsValuesTupleIntoVec for (A, B, C) { 79 | fn into_vec( 80 | self, 81 | env: &Env, 82 | ) -> crate::Result> { 83 | Ok(vec![ 84 | A::to_js_value(env, self.0)?, 85 | B::to_js_value(env, self.1)?, 86 | C::to_js_value(env, self.2)?, 87 | ]) 88 | } 89 | } 90 | 91 | impl JsValuesTupleIntoVec for (A, B, C, D) { 92 | fn into_vec( 93 | self, 94 | env: &Env, 95 | ) -> crate::Result> { 96 | Ok(vec![ 97 | A::to_js_value(env, self.0)?, 98 | B::to_js_value(env, self.1)?, 99 | C::to_js_value(env, self.2)?, 100 | D::to_js_value(env, self.3)?, 101 | ]) 102 | } 103 | } 104 | 105 | impl JsValuesTupleIntoVec 106 | for (A, B, C, D, E) 107 | { 108 | fn into_vec( 109 | self, 110 | env: &Env, 111 | ) -> crate::Result> { 112 | Ok(vec![ 113 | A::to_js_value(env, self.0)?, 114 | B::to_js_value(env, self.1)?, 115 | C::to_js_value(env, self.2)?, 116 | D::to_js_value(env, self.3)?, 117 | E::to_js_value(env, self.4)?, 118 | ]) 119 | } 120 | } 121 | 122 | impl 123 | JsValuesTupleIntoVec for (A, B, C, D, E, F) 124 | { 125 | fn into_vec( 126 | self, 127 | env: &Env, 128 | ) -> crate::Result> { 129 | Ok(vec![ 130 | A::to_js_value(env, self.0)?, 131 | B::to_js_value(env, self.1)?, 132 | C::to_js_value(env, self.2)?, 133 | D::to_js_value(env, self.3)?, 134 | E::to_js_value(env, self.4)?, 135 | F::to_js_value(env, self.5)?, 136 | ]) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /crates/ion_cli/src/cmd/test.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | use clap::Parser; 5 | use flume::unbounded; 6 | use ion::utils::PathExt; 7 | use ion::*; 8 | use normalize_path::NormalizePath; 9 | use tokio::task::JoinSet; 10 | 11 | #[derive(Debug, Parser)] 12 | pub struct TestCommand { 13 | /// Target get file to run 14 | pub files: Vec, 15 | } 16 | 17 | pub fn main(command: TestCommand) -> anyhow::Result<()> { 18 | tokio::runtime::Builder::new_multi_thread() 19 | .worker_threads(num_cpus::get_physical()) 20 | .enable_all() 21 | .build()? 22 | .block_on(main_async(command)) 23 | } 24 | 25 | async fn main_async(command: TestCommand) -> anyhow::Result<()> { 26 | let mut entries = vec![]; 27 | 28 | let Ok(cwd) = std::env::current_dir() else { 29 | return Err(anyhow::anyhow!("Unable to get cwd")); 30 | }; 31 | 32 | // Convert paths from relative to absolute 33 | for file in command.files { 34 | if file.is_absolute() { 35 | entries.push(file.normalize()); 36 | } else { 37 | entries.push(cwd.join(&file).normalize()); 38 | } 39 | } 40 | 41 | let runtime = ion::JsRuntime::initialize_once(JsRuntimeOptions { 42 | v8_args: vec![], 43 | resolvers: vec![ion::resolvers::relative()], 44 | transformers: vec![ 45 | ion::transformers::json(), 46 | ion::transformers::ts(), 47 | ion::transformers::tsx(), 48 | ], 49 | extensions: vec![ 50 | ion::extensions::event_target(), 51 | ion::extensions::console(), 52 | ion::extensions::set_timeout(), 53 | ion::extensions::set_interval(), 54 | ion::extensions::test(), 55 | ion::extensions::global_this(), 56 | ], 57 | })?; 58 | 59 | let (tx, rx) = unbounded::<(PathBuf, String, u32)>(); 60 | let mut set = JoinSet::>::new(); 61 | 62 | for _ in 0..1 { 63 | set.spawn({ 64 | let runtime = Arc::clone(&runtime); 65 | let rx = rx.clone(); 66 | async move { 67 | while let Ok((file, message, test_id)) = rx.recv() { 68 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 69 | let ctx = worker.create_context()?; 70 | 71 | println!("- {}", message); 72 | ctx.import(file.try_to_string()?)?; 73 | ctx.exec_async(move |env| { 74 | env.eval_module(format!( 75 | r#" 76 | import {{ getTests }} from "ion:test" 77 | const tests = getTests() 78 | tests[{}][1]() 79 | "#, 80 | test_id 81 | ))?; 82 | Ok(()) 83 | }) 84 | .await?; 85 | } 86 | Ok(()) 87 | } 88 | }); 89 | } 90 | 91 | for file in entries { 92 | let worker = runtime.spawn_worker(JsWorkerOptions::default())?; 93 | let ctx = worker.create_context()?; 94 | 95 | ctx.import(file.try_to_string()?)?; 96 | ctx.exec({ 97 | let tx = tx.clone(); 98 | move |env| { 99 | let module = env.eval_module( 100 | r#" 101 | import { getTests } from "ion:test" 102 | export default getTests() 103 | "#, 104 | )?; 105 | 106 | let tests = module.get_named_property_unchecked::("default")?; 107 | let length = tests.get_array_length()?; 108 | for i in 0..length { 109 | let Some(value) = tests.get_element::(i)? else { 110 | panic!(); 111 | }; 112 | let message = value.get_element::(0)?.unwrap(); 113 | tx.try_send((file.clone(), message.get_string()?, i))?; 114 | } 115 | 116 | Ok(()) 117 | } 118 | })?; 119 | } 120 | 121 | drop(tx); 122 | 123 | while let Some(res) = set.join_next().await { 124 | res?? 125 | } 126 | Ok(()) 127 | } 128 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # Windows requires GNU coreuitls 2 | set windows-shell := ["pwsh", "-NoLogo", "-NoProfileLoadTime", "-Command"] 3 | 4 | project_name := "ion_cli" 5 | profile := env_var_or_default("profile", "debug") 6 | 7 | os := \ 8 | if \ 9 | env_var_or_default("os", "") == "Windows_NT" { "windows" } \ 10 | else if \ 11 | env_var_or_default("os", "") != "" { env_var("os") } \ 12 | else \ 13 | { os() } 14 | 15 | arch := \ 16 | if \ 17 | env_var_or_default("arch", "") != "" { env_var("arch") } \ 18 | else if \ 19 | arch() == "x86_64" { "amd64" } \ 20 | else if \ 21 | arch() == "aarch64" { "arm64" } \ 22 | else \ 23 | { arch() } 24 | 25 | target := \ 26 | if \ 27 | os + arch == "linuxamd64" { "x86_64-unknown-linux-gnu" } \ 28 | else if \ 29 | os + arch == "linuxarm64" { "aarch64-unknown-linux-gnu" } \ 30 | else if \ 31 | os + arch == "macosamd64" { "x86_64-apple-darwin" } \ 32 | else if\ 33 | os + arch == "macosarm64" { "aarch64-apple-darwin" } \ 34 | else if \ 35 | os + arch == "windowsamd64" { "x86_64-pc-windows-msvc" } \ 36 | else if \ 37 | os + arch == "windowsarm64" { "aarch64-pc-windows-msvc" } \ 38 | else \ 39 | { env_var_or_default("target", "debug") } 40 | 41 | profile_cargo := \ 42 | if \ 43 | profile != "debug" { "--profile " + profile } \ 44 | else \ 45 | { "" } 46 | 47 | target_cargo := \ 48 | if \ 49 | target == "debug" { "" } \ 50 | else if \ 51 | target == "" { "" } \ 52 | else \ 53 | { "--target " + target } 54 | 55 | bin_name := \ 56 | if \ 57 | os == "windows" { project_name + ".exe" } \ 58 | else \ 59 | { project_name } 60 | 61 | out_dir := join(justfile_directory(), "target", target, profile) 62 | out_dir_dist := join(justfile_directory(), "target", os + "-" + arch, profile) 63 | fmt_file := join(justfile_directory(), "rust-fmt.toml") 64 | 65 | [linux] 66 | build: 67 | rm -rf {{out_dir_dist}} 68 | cargo build {{profile_cargo}} {{target_cargo}} 69 | mkdir -p {{out_dir_dist}} 70 | cp {{join(out_dir, "ion_cli")}} {{join(out_dir_dist, "ion")}} 71 | # cp {{join(out_dir, "libion_c.so")}} {{join(out_dir_dist, "libion.so")}} 72 | # cp {{join(out_dir, "libion_c.a")}} {{join(out_dir_dist, "libion.a")}} 73 | 74 | [macos] 75 | build: 76 | rm -rf {{out_dir_dist}} 77 | cargo build {{profile_cargo}} {{target_cargo}} 78 | mkdir -p {{out_dir_dist}} 79 | cp {{join(out_dir, "ion_cli")}} {{join(out_dir_dist, "ion")}} 80 | # cp {{join(out_dir, "libion_c.dylib")}} {{join(out_dir_dist, "libion.dylib")}} 81 | # cp {{join(out_dir, "libion_c.a")}} {{join(out_dir_dist, "libion.a")}} 82 | 83 | [windows] 84 | build: 85 | if (Test-Path {{out_dir_dist}}){ Remove-Item -recurse -force {{out_dir_dist}} } 86 | cargo build {{profile_cargo}} {{target_cargo}} 87 | New-Item -force -type directory {{out_dir_dist}} 88 | Copy-Item {{join(out_dir, "ion_cli.exe")}} {{join(out_dir_dist, "ion.exe")}} 89 | # Copy-Item {{join(out_dir, "ion_c.dll")}} {{join(out_dir_dist, "ion.dll")}} 90 | # Copy-Item {{join(out_dir, "ion_c.lib")}} {{join(out_dir_dist, "ion.lib")}} 91 | 92 | run *ARGS: 93 | just build 94 | {{join(out_dir, bin_name)}} {{ARGS}} 95 | 96 | example target: 97 | cargo run --package ion_examples {{target}} 98 | 99 | test: 100 | cargo test 101 | 102 | format arg="--check": 103 | just fmt {{arg}} 104 | just lint {{arg}} 105 | 106 | _fmt arg="--check": 107 | #!/usr/bin/env bash 108 | args="" 109 | while read -r line; do 110 | line=$(echo "$line" | tr -d "[:space:]") 111 | args="$args --config $line" 112 | done < "rust-fmt.toml" 113 | args=$(echo "$args" | xargs) 114 | if [ "{{arg}}" = "--fix" ]; then 115 | cargo fmt -- $args 116 | else 117 | cargo fmt --check -- $args 118 | fi 119 | 120 | [unix] 121 | fmt arg="--check": 122 | just _fmt {{arg}} 123 | 124 | [windows] 125 | fmt arg="--check": 126 | bash -c "just _fmt {{arg}}" 127 | 128 | _lint arg="--check": 129 | #!/usr/bin/env bash 130 | if [ "{{arg}}" = "--fix" ]; then 131 | cargo clippy --fix --allow-dirty -- --deny "warnings" 132 | else 133 | cargo clippy -- --deny "warnings" 134 | fi 135 | 136 | [unix] 137 | lint arg="--check": 138 | just _lint {{arg}} 139 | 140 | [windows] 141 | lint arg="--check": 142 | bash -c "just _lint {{arg}}" 143 | 144 | watch *ARGS: 145 | cargo watch --watch src -- just run {{ARGS}} 146 | 147 | watch-silent *ARGS: 148 | cargo watch -- bash -c "just build && clear; {{out_dir}}/http-server {{ARGS}}" 149 | -------------------------------------------------------------------------------- /crates/ion/src/values/thread_safe_promise.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::sync::atomic::AtomicUsize; 3 | use std::sync::atomic::Ordering; 4 | 5 | use crate::AsyncEnv; 6 | use crate::Env; 7 | use crate::FromJsValue; 8 | use crate::JsPromise; 9 | use crate::JsPromiseResult; 10 | use crate::JsValue; 11 | use crate::utils::channel::oneshot; 12 | 13 | pub struct ThreadSafePromise { 14 | ref_count: Arc, 15 | env: Arc, 16 | /// Box> 17 | inner: usize, 18 | } 19 | 20 | impl ThreadSafePromise { 21 | pub fn new(target: &JsPromise) -> crate::Result { 22 | let env = target.env(); 23 | let scope = &mut env.scope(); 24 | 25 | // Create threadsafe function with an initial refcount of 1 26 | let ref_count = Arc::new(AtomicUsize::new(1)); 27 | // Indicate that the current environment cannot exit until the ref_count is 0 28 | env.inc_ref(); 29 | 30 | // SAFETY: Force function to be Send + Sync 31 | let inner = *target.value(); 32 | let inner = v8::Global::new(scope, inner); 33 | let inner = Box::new(inner); 34 | let inner = Box::into_raw(inner); 35 | let inner = inner as usize; 36 | 37 | Ok(Self { 38 | ref_count, 39 | env: env.as_async(), 40 | inner, 41 | }) 42 | } 43 | 44 | /// Non blocking call to then/catch 45 | pub fn settled( 46 | self, 47 | settled_callback: impl 'static 48 | + Send 49 | + Sync 50 | + FnOnce(&Env, JsPromiseResult) -> crate::Result<()>, 51 | ) -> crate::Result<()> { 52 | let inner = self.inner; 53 | 54 | self.env.exec(move |env| { 55 | let scope = &mut env.scope(); 56 | 57 | let inner = inner as *const v8::Local<'static, v8::Value>; 58 | let inner = unsafe { *inner }; 59 | let inner = v8::Local::new(scope, inner); 60 | 61 | let promise = JsPromise::from_js_value(env, inner)?; 62 | 63 | promise.settled(settled_callback) 64 | }) 65 | } 66 | 67 | /// Blocking call to then/catch 68 | pub fn settled_blocking( 69 | self, 70 | settled_callback: impl 'static 71 | + Send 72 | + Sync 73 | + FnOnce(&Env, JsPromiseResult) -> crate::Result, 74 | ) -> crate::Result { 75 | let (tx, rx) = oneshot(); 76 | self.settled(move |env, result| { 77 | tx.try_send(settled_callback(env, result)?)?; 78 | Ok(()) 79 | })?; 80 | Ok(rx.recv()?) 81 | } 82 | 83 | /// Async blocking call to then/catch 84 | pub async fn settled_async( 85 | self, 86 | settled_callback: impl 'static 87 | + Send 88 | + Sync 89 | + FnOnce(&Env, JsPromiseResult) -> crate::Result, 90 | ) -> crate::Result { 91 | let (tx, rx) = oneshot(); 92 | self.settled(move |env, result| { 93 | tx.try_send(settled_callback(env, result)?)?; 94 | Ok(()) 95 | })?; 96 | Ok(rx.recv_async().await?) 97 | } 98 | 99 | pub fn inc_ref(&self) -> crate::Result<()> { 100 | self.ref_count.fetch_add(1, Ordering::Relaxed); 101 | Ok(()) 102 | } 103 | 104 | pub fn dec_ref(&self) -> crate::Result<()> { 105 | let previous = self.ref_count.fetch_sub(1, Ordering::Relaxed); 106 | if previous == 1 { 107 | let inner = self.inner; 108 | self.env.exec(move |env| { 109 | let inner = inner as *mut v8::Global; 110 | drop(unsafe { Box::from_raw(inner) }); 111 | env.dec_ref(); 112 | Ok(()) 113 | })?; 114 | } 115 | Ok(()) 116 | } 117 | } 118 | 119 | unsafe impl Send for ThreadSafePromise {} 120 | unsafe impl Sync for ThreadSafePromise {} 121 | 122 | impl Clone for ThreadSafePromise { 123 | fn clone(&self) -> Self { 124 | drop(self.inc_ref()); 125 | Self { 126 | ref_count: Arc::clone(&self.ref_count), 127 | env: Arc::clone(&self.env), 128 | inner: self.inner, 129 | } 130 | } 131 | } 132 | 133 | impl Drop for ThreadSafePromise { 134 | fn drop(&mut self) { 135 | drop(self.dec_ref()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /crates/ion/src/values/js_promise.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use crate::Env; 5 | use crate::JsFunction; 6 | use crate::JsUnknown; 7 | use crate::ToJsUnknown; 8 | use crate::platform::sys; 9 | use crate::platform::sys::Value; 10 | use crate::utils::v8::v8_create_string; 11 | use crate::values::FromJsValue; 12 | use crate::values::JsValue; 13 | use crate::values::ToJsValue; 14 | 15 | #[derive(Clone)] 16 | pub struct JsPromise { 17 | pub(crate) value: Value, 18 | pub(crate) env: Env, 19 | } 20 | 21 | impl JsPromise { 22 | pub fn new(env: &Env) -> crate::Result { 23 | let scope = &mut env.scope(); 24 | let object = v8::Object::new(scope); 25 | Ok(Self { 26 | value: sys::v8_from_value(object), 27 | env: env.clone(), 28 | }) 29 | } 30 | 31 | /// Non blocking call to then/catch 32 | pub fn settled( 33 | self, 34 | settled_callback: impl 'static 35 | + Send 36 | + Sync 37 | + FnOnce(&Env, JsPromiseResult) -> crate::Result<()>, 38 | ) -> crate::Result<()> { 39 | // RefCell