├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── crabzilla-tests ├── Cargo.toml ├── js │ └── module.js └── src │ └── main.rs ├── crabzilla ├── Cargo.toml ├── LICENSE.md ├── README.md └── src │ └── lib.rs └── import_fn ├── Cargo.toml ├── LICENSE.md ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crabzilla", 4 | "import_fn", 5 | "crabzilla-tests", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrew Herbert 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crabzilla 2 | 3 | Crabzilla provides a _simple_ interface for running JavaScript modules alongside Rust code. 4 | 5 | ## Example 6 | ```rust 7 | use crabzilla::*; 8 | use std::io::stdin; 9 | 10 | #[import_fn(name = "read", scope = "Stdin")] 11 | fn read_from_stdin() -> Value { 12 | let mut buffer = String::new(); 13 | println!("Type your name: "); 14 | stdin().read_line(&mut buffer)?; 15 | buffer.pop(); // Remove newline 16 | if buffer.is_empty() { 17 | throw!("Expected name!"); 18 | } 19 | json!(buffer) 20 | } 21 | 22 | #[import_fn(name = "sayHello", scope = "Stdout")] 23 | fn say_hello(args: Vec) { 24 | if let Some(Value::String(string)) = args.get(0) { 25 | println!("Hello, {}", string); 26 | } 27 | } 28 | 29 | #[tokio::main] 30 | async fn main() { 31 | let mut runtime = runtime! { 32 | read_from_stdin, 33 | say_hello, 34 | }; 35 | if let Err(error) = runtime.load_module("./module.js").await { 36 | eprintln!("{}", error); 37 | } 38 | } 39 | ``` 40 | 41 | In `module.js`: 42 | 43 | ```js 44 | const user = Stdin.read(); 45 | Stdout.sayHello(user); 46 | ``` 47 | -------------------------------------------------------------------------------- /crabzilla-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crabzilla-tests" 3 | version = "0.1.1" 4 | authors = ["Andy Herbert "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | crabzilla = {path = "../crabzilla", version = "0.1"} 9 | 10 | [dependencies.tokio] 11 | version = "1.2" 12 | features = [ 13 | "macros", 14 | "time", 15 | "rt-multi-thread", 16 | ] 17 | 18 | -------------------------------------------------------------------------------- /crabzilla-tests/js/module.js: -------------------------------------------------------------------------------- 1 | const user = Stdin.read(); 2 | Stdout.sayHello(user); 3 | -------------------------------------------------------------------------------- /crabzilla-tests/src/main.rs: -------------------------------------------------------------------------------- 1 | use crabzilla::*; 2 | use std::io::stdin; 3 | 4 | #[import_fn(name = "read", scope = "Stdin")] 5 | fn read_from_stdin() -> Value { 6 | let mut buffer = String::new(); 7 | println!("Type your name: "); 8 | stdin().read_line(&mut buffer)?; 9 | buffer.pop(); // Remove newline 10 | if buffer.is_empty() { 11 | throw!("Expected name!"); 12 | } 13 | json!(buffer) 14 | } 15 | 16 | #[import_fn(name = "sayHello", scope = "Stdout")] 17 | fn say_hello(args: Vec) { 18 | if let Some(Value::String(string)) = args.get(0) { 19 | println!("Hello, {}", string); 20 | } 21 | } 22 | 23 | #[tokio::main] 24 | async fn main() { 25 | let mut runtime = runtime! { 26 | read_from_stdin, 27 | say_hello, 28 | }; 29 | if let Err(error) = runtime.load_module("crabzilla-tests/js/module.js").await { 30 | eprintln!("{}", error); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crabzilla/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crabzilla" 3 | version = "0.1.2" 4 | authors = [ 5 | "Andy Herbert ", 6 | ] 7 | edition = "2021" 8 | license = "MIT" 9 | description = "Provides a JavaScript runtime" 10 | repository = "https://github.com/andyherbert/crabzilla" 11 | keywords = [ 12 | "javascript", 13 | "ecmascript", 14 | "scripting", 15 | "runtime", 16 | ] 17 | categories = [ 18 | "compilers", 19 | "game-development", 20 | "parser-implementations", 21 | ] 22 | 23 | [dependencies] 24 | deno_core = "0.112" 25 | futures = "0.3" 26 | import_fn = {version = "0.1", path = "../import_fn"} 27 | -------------------------------------------------------------------------------- /crabzilla/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /crabzilla/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /crabzilla/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! Crabzilla provides a _simple_ interface for running JavaScript modules alongside Rust code. 2 | # Example 3 | ``` 4 | use crabzilla::*; 5 | use std::io::stdin; 6 | 7 | #[import_fn(name="read", scope="Stdin")] 8 | fn read_from_stdin() -> Value { 9 | let mut buffer = String::new(); 10 | println!("Type your name: "); 11 | stdin().read_line(&mut buffer)?; 12 | buffer.pop(); // Remove newline 13 | if buffer.is_empty() { 14 | throw!("Expected name!"); 15 | } 16 | json!(buffer) 17 | } 18 | 19 | #[import_fn(name="sayHello", scope="Stdout")] 20 | fn say_hello(args: Vec) { 21 | if let Some(string) = args.get(0) { 22 | if let Value::String(string) = string { 23 | println!("Hello, {}", string); 24 | } 25 | } 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() { 30 | let mut runtime = runtime! { 31 | read_from_stdin, 32 | say_hello, 33 | }; 34 | if let Err(error) = runtime.load_module("./module.js").await { 35 | eprintln!("{}", error); 36 | } 37 | } 38 | ``` 39 | In `module.js`: 40 | ``` 41 | const user = Stdin.read(); 42 | Stdout.sayHello(user); 43 | ``` 44 | */ 45 | pub use deno_core::error::custom_error; 46 | pub use deno_core::error::AnyError; 47 | pub use deno_core::serde_json::{json, value::Value}; 48 | use deno_core::{op_sync, resolve_path, FsModuleLoader, JsRuntime, OpFn, OpState}; 49 | pub use import_fn::import_fn; 50 | use std::rc::Rc; 51 | 52 | fn get_args(value: &Value) -> Vec { 53 | if let Value::Object(map) = &value { 54 | if let Some(Value::Array(args)) = &map.get("args") { 55 | return args.to_owned(); 56 | } 57 | } 58 | unreachable!(); 59 | } 60 | 61 | /// Represents an imported Rust function. 62 | pub struct ImportedFn { 63 | op_fn: Box, 64 | name: String, 65 | scope: Option, 66 | } 67 | 68 | /// Receives a Rust function and returns a structure that can be imported in to a runtime. 69 | pub fn create_sync_fn(imported_fn: F, name: &str, scope: Option) -> ImportedFn 70 | where 71 | F: Fn(Vec) -> Result + 'static, 72 | { 73 | let op_fn = op_sync( 74 | move |_state: &mut OpState, value: Value, _: ()| -> Result { 75 | imported_fn(get_args(&value)) 76 | }, 77 | ); 78 | ImportedFn { 79 | op_fn, 80 | name: name.to_string(), 81 | scope, 82 | } 83 | } 84 | 85 | struct ImportedName { 86 | name: String, 87 | scope: Option, 88 | } 89 | 90 | /// Represents a JavaScript runtime instance. 91 | pub struct Runtime { 92 | runtime: JsRuntime, 93 | imported_names: Vec, 94 | scopes: Vec, 95 | } 96 | 97 | impl Default for Runtime { 98 | fn default() -> Self { 99 | let runtime = JsRuntime::new(deno_core::RuntimeOptions { 100 | module_loader: Some(Rc::new(FsModuleLoader)), 101 | ..Default::default() 102 | }); 103 | let imported_names = vec![]; 104 | let scopes = vec![]; 105 | Runtime { 106 | runtime, 107 | imported_names, 108 | scopes, 109 | } 110 | } 111 | } 112 | 113 | impl Runtime { 114 | /// Creates a new Runtime 115 | pub fn new() -> Self { 116 | Default::default() 117 | } 118 | 119 | /// Imports a new ImportedFn 120 | pub fn import(&mut self, imported_fn: F) 121 | where 122 | F: Fn() -> ImportedFn, 123 | { 124 | let import_fn = imported_fn(); 125 | if let Some(scope) = &import_fn.scope { 126 | self.scopes.push(scope.clone()); 127 | } 128 | self.runtime.register_op(&import_fn.name, import_fn.op_fn); 129 | self.imported_names.push(ImportedName { 130 | name: import_fn.name, 131 | scope: import_fn.scope, 132 | }); 133 | } 134 | 135 | /// Generates JavaScript hooks for the Runtime when all functions have been imported 136 | pub fn importing_finished(&mut self) { 137 | let mut scope_definitions = String::new(); 138 | for scope in self.scopes.iter() { 139 | scope_definitions.push_str(&format!(" window[{:?}] = {{}};\n", scope)); 140 | } 141 | let mut name_definitions = String::new(); 142 | for import in &self.imported_names { 143 | let scope = match &import.scope { 144 | Some(scope) => format!("window[{:?}][{:?}]", scope, import.name), 145 | None => format!("window[{:?}]", import.name), 146 | }; 147 | name_definitions.push_str(&format!( 148 | " {} = (...args) => Deno.core.opSync({:?}, {{args}});\n", 149 | scope, import.name 150 | )); 151 | } 152 | let js_source = format!( 153 | "\"use strict\";\n((window) => {{\n{}{}}})(this);", 154 | scope_definitions, name_definitions, 155 | ); 156 | self.runtime.sync_ops_cache(); 157 | self.runtime 158 | .execute_script("rust:core.js", &js_source) 159 | .expect("runtime exporting"); 160 | } 161 | 162 | /// Loads a JavaScript module and evaluates it 163 | pub async fn load_module(&mut self, path_str: &str) -> Result<(), AnyError> { 164 | let specifier = resolve_path(path_str)?; 165 | let id = self.runtime.load_main_module(&specifier, None).await?; 166 | let result = self.runtime.mod_evaluate(id); 167 | self.runtime.run_event_loop(false).await?; 168 | result.await? 169 | } 170 | } 171 | 172 | /// Creates a runtime object and imports a list of functions. 173 | /// 174 | /// # Example 175 | /// ``` 176 | /// #[import_fn] 177 | /// fn foo() { 178 | /// // Do something 179 | /// } 180 | /// 181 | /// #[import_fn] 182 | /// fn bar() { 183 | /// // Do something else 184 | /// } 185 | /// 186 | /// let mut runtime = runtime! { 187 | /// foo, 188 | /// bar, 189 | /// }; 190 | /// ``` 191 | #[macro_export] 192 | macro_rules! runtime { 193 | ($($fn:ident),* $(,)?) => { 194 | { 195 | let mut runtime = crabzilla::Runtime::new(); 196 | $( 197 | runtime.import($fn); 198 | )* 199 | runtime.importing_finished(); 200 | runtime 201 | } 202 | } 203 | } 204 | 205 | /// Throws an error with a custom message in an imported Rust function. 206 | #[macro_export] 207 | macro_rules! throw { 208 | ($message:expr) => { 209 | return Err(crabzilla::custom_error("Error", $message)); 210 | }; 211 | } 212 | -------------------------------------------------------------------------------- /import_fn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "import_fn" 3 | version = "0.1.2" 4 | authors = [ 5 | "Andy Herbert ", 6 | ] 7 | edition = "2021" 8 | license = "MIT" 9 | description = "Implementation of #[import_fn] macro for Crabzilla" 10 | repository = "https://github.com/andyherbert/crabzilla" 11 | keywords = [ 12 | "javascript", 13 | "ecmascript", 14 | "scripting", 15 | "runtime", 16 | ] 17 | categories = [ 18 | "compilers", 19 | "game-development", 20 | "parser-implementations", 21 | ] 22 | 23 | [lib] 24 | proc-macro = true 25 | 26 | [dependencies] 27 | quote = "1.0.9" 28 | syn = {version = "1.0.60", features = ["full"]} 29 | -------------------------------------------------------------------------------- /import_fn/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /import_fn/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /import_fn/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{quote, quote_spanned, ToTokens}; 3 | use syn::{ 4 | parse_macro_input, spanned::Spanned, AttributeArgs, Block, Ident, ItemFn, Lit, Meta, NestedMeta, 5 | }; 6 | 7 | macro_rules! option_string_to_token_stream { 8 | ($opt_string:expr) => { 9 | match $opt_string { 10 | Some(string) => quote! { 11 | Some(String::from(#string)) 12 | }, 13 | None => quote! { 14 | None 15 | }, 16 | } 17 | }; 18 | } 19 | 20 | fn quote_without_return(ident: &Ident, block: &Block, crab_meta: ImportOptions) -> TokenStream { 21 | let name = crab_meta.name.unwrap_or_else(|| ident.to_string()); 22 | let scope = option_string_to_token_stream!(crab_meta.scope); 23 | quote! { 24 | fn #ident() -> crabzilla::ImportedFn { 25 | crabzilla::create_sync_fn( 26 | |args: Vec| -> std::result::Result { 27 | Ok(#block) 28 | }, 29 | #name, 30 | #scope, 31 | ) 32 | } 33 | }.into() 34 | } 35 | 36 | fn quote_with_return(ident: &Ident, block: &Block, crab_meta: ImportOptions) -> TokenStream { 37 | let name = crab_meta.name.unwrap_or_else(|| ident.to_string()); 38 | let scope = option_string_to_token_stream!(crab_meta.scope); 39 | quote! { 40 | fn #ident() -> crabzilla::ImportedFn { 41 | crabzilla::create_sync_fn( 42 | |args: Vec| -> std::result::Result { 43 | #block 44 | Ok(crabzilla::Value::Null) 45 | }, 46 | #name, 47 | #scope, 48 | ) 49 | } 50 | }.into() 51 | } 52 | 53 | fn error(item: T, msg: &str) -> TokenStream { 54 | let span = item.span(); 55 | let error = quote_spanned! { 56 | span => compile_error!(#msg); 57 | }; 58 | error.into() 59 | } 60 | 61 | #[derive(Default)] 62 | struct ImportOptions { 63 | scope: Option, 64 | name: Option, 65 | } 66 | 67 | fn validate_literal(name: &str) -> bool { 68 | if name.is_empty() { 69 | return false; 70 | } 71 | for char in name.chars() { 72 | if !char.is_ascii() { 73 | return false; 74 | } 75 | if char.is_whitespace() { 76 | return false; 77 | } 78 | } 79 | true 80 | } 81 | 82 | fn parse_meta(metas: Vec) -> Result { 83 | let mut options = ImportOptions::default(); 84 | for meta in metas { 85 | match meta { 86 | NestedMeta::Meta(meta) => match meta { 87 | Meta::NameValue(meta_name_value) => { 88 | let string = meta_name_value.path.to_token_stream().to_string(); 89 | match string.as_str() { 90 | "scope" => match meta_name_value.lit { 91 | Lit::Str(lit_str) => { 92 | let scope = lit_str.value(); 93 | if !validate_literal(&scope) { 94 | return Err(error(lit_str, "Invalid scope")); 95 | } 96 | options.scope = Some(scope); 97 | } 98 | _ => return Err(error(meta_name_value.lit, "Unsupported value")), 99 | }, 100 | "name" => match meta_name_value.lit { 101 | Lit::Str(lit_str) => { 102 | let name = lit_str.value(); 103 | if !validate_literal(&name) { 104 | return Err(error(lit_str, "Invalid name")); 105 | } 106 | options.name = Some(lit_str.value()); 107 | } 108 | _ => return Err(error(meta_name_value.lit, "Unsupported value")), 109 | }, 110 | _ => return Err(error(meta_name_value, "Unsupported meta")), 111 | } 112 | } 113 | _ => return Err(error(meta, "Unsupported meta")), 114 | }, 115 | _ => return Err(error(meta, "Unsupported meta")), 116 | } 117 | } 118 | Ok(options) 119 | } 120 | 121 | /// An attribute macro to convert Rust functions so they can be imported into a runtime. 122 | /// The meta attributes `name` and `scope` can be used to define the scoping of a particular 123 | /// when calling from javascript, for example `scope = "Foo", name = "bar"` would assign 124 | /// the function as Foo.bar. Without a scope the function will be attached to the global 125 | /// object, and without a name it will be assigned with the Rust function name. 126 | #[proc_macro_attribute] 127 | pub fn import_fn(attr: TokenStream, item: TokenStream) -> TokenStream { 128 | let input = parse_macro_input!(item as ItemFn); 129 | let attr = parse_macro_input!(attr as AttributeArgs); 130 | let crab_meta = match parse_meta(attr) { 131 | Ok(crab_meta) => crab_meta, 132 | Err(error) => return error, 133 | }; 134 | match input.sig.inputs.to_token_stream().to_string().as_str() { 135 | "" | "args : Vec < Value >" | "args : Vec < crabzilla :: Value >" => {} 136 | "args : std :: vec :: Vec < Value >" => {} 137 | "args : std :: vec :: Vec < crabzilla :: Value >" => {} 138 | "args : :: vec :: Vec < Value >" => {} 139 | "args : :: vec :: Vec < crabzilla :: Value >" => {} 140 | _ => { 141 | return error( 142 | input.sig.inputs, 143 | "Illegal arguments, should be empty or \"args: Vec\"", 144 | ) 145 | } 146 | } 147 | match input.sig.output.to_token_stream().to_string().as_str() { 148 | "-> crabzilla :: Value" | "-> Value" => { 149 | quote_without_return(&input.sig.ident, &input.block, crab_meta) 150 | } 151 | "-> ()" | "" => quote_with_return(&input.sig.ident, &input.block, crab_meta), 152 | _ => error( 153 | input.sig.output, 154 | "Illegal return type, should be empty or \"Value\"", 155 | ), 156 | } 157 | } 158 | --------------------------------------------------------------------------------