├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── test ├── fib32.wasm ├── fib32.wat ├── fib64.wasm ├── mal │ ├── README.md │ ├── core.mal │ ├── env.mal │ ├── fib.mal │ ├── mal.mal │ └── mal.wasm ├── swap.wasm ├── swap.wat └── wasi │ ├── coremark.metered.wasm │ ├── coremark.wasm │ ├── test-wasi-snapshot-preview1.wasm │ └── test-wasi-unstable.wasm └── wasm-run.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Dependency directories 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wasm3 Labs 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 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine) 2 | 3 | # wasm-run 4 | Run arbitrary WASM/WASI files 5 | 6 | [![NPM version](https://img.shields.io/npm/v/wasm-run.svg)](https://www.npmjs.com/package/wasm-run) 7 | [![GitHub stars](https://img.shields.io/github/stars/wasm3/node-wasm-run.svg)](https://github.com/wasm3/node-wasm-run) 8 | [![GitHub issues](https://img.shields.io/github/issues/wasm3/node-wasm-run.svg)](https://github.com/wasm3/node-wasm-run/issues) 9 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/wasm3/node-wasm-run) 10 | 11 | ## Installation 12 | 13 | ```sh 14 | $ npm install wasm-run -g 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```sh 20 | $ wasm-run --help 21 | wasm-run [options] [--] [args..] 22 | 23 | Options: 24 | -i, --invoke Function to execute [string] 25 | -t, --timeout Execution timeout (ms) 26 | --trace Trace imported function calls [boolean] 27 | --gas-limit Gas limit [default: 100000] 28 | --version Show version number [boolean] 29 | --help Show help [boolean] 30 | ``` 31 | 32 | #### Run a single exported function 33 | ```sh 34 | $ wasm-run ./test/fib32.wasm 32 35 | [runtime] Running fib(32)... 36 | [runtime] Result: 2178309 37 | ``` 38 | 39 | #### WAT file with multivalue support 40 | ```sh 41 | $ wasm-run --invoke=swap_i64 ./test/swap.wat 10 12 42 | [runtime] Converted to binary (256 bytes) 43 | [runtime] Running swap_i64(10,12)... 44 | [runtime] Result: 12,10 45 | ``` 46 | 47 | #### WASI support 48 | ```sh 49 | $ wasm-run wasi-hello-world.wasm 50 | Hello world! 51 | ``` 52 | 53 | #### Imported function tracing 54 | ```sh 55 | $ wasm-run --trace wasi-hello-world.wasm 56 | [runtime] wasi_snapshot_preview1!fd_prestat_get 3,65528 => 0 57 | [runtime] wasi_snapshot_preview1!fd_prestat_dir_name 3,70064,2 => 0 58 | [runtime] wasi_snapshot_preview1!fd_prestat_get 4,65528 => 0 59 | [runtime] wasi_snapshot_preview1!fd_prestat_dir_name 4,70064,2 => 0 60 | ... 61 | ``` 62 | 63 | #### Gas metering/limiting 64 | `wasm-meter` can be installed via `npm install wasm-metering -g` 65 | ```sh 66 | $ wasm-meter fib64.wasm fib64.metered.wasm 67 | $ wasm-run fib64.metered.wasm 8 68 | [runtime] Running fib(8)... 69 | [runtime] Result: 21 70 | [runtime] Gas used: 5.1874 71 | $ wasm-run fib64.metered.wasm 30 72 | [runtime] Running fib(30)... 73 | [runtime] Error: Run out of gas (gas used: 100000.0177) 74 | ``` 75 | 76 | ## Features 77 | 78 | ☑ Load `wasm` and `wat` files (using `Binaryen`) 79 | ☑ Run specific exported function 80 | ☑ Run `wasi-snapshot-preview1` apps via `--experimental-wasi-unstable-preview1` flag 81 | ☑ Run `wasi-unstable` apps (compatibility layer) 82 | ☑ `i64` args, `multi-value`, `bulk-memory`, `tail-calls` support via experimental flags 83 | ☑ Generic imports tracing 84 | ☑ Gas metering/limiting 85 | ☐ Compiled wasm caching (blocked by [#1](https://github.com/wasm3/node-wasm-run/issues/1)) 86 | ☐ WASI API and structures decoding (generate from witx?) 87 | ☐ REPL mode 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-run", 3 | "version": "0.1.2", 4 | "description": "Run arbitrary WASM/WASI files", 5 | "author": "Volodymyr Shymanskyy", 6 | "license": "MIT", 7 | "main": "wasm-run.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "bin": { 12 | "wasm-run": "wasm-run.js" 13 | }, 14 | "dependencies": { 15 | "binaryen": "^101.0.0", 16 | "chalk": "^4.1.1", 17 | "restructure": "^2.0.0", 18 | "yargs": "^16.2.0" 19 | }, 20 | "keywords": [ 21 | "WebAssembly", 22 | "wasm", 23 | "WASI", 24 | "tracing" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/wasm3/node-wasm-run" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/wasm3/node-wasm-run/issues" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/fib32.wasm: -------------------------------------------------------------------------------- 1 | asm`fib 2 |  AI@  Ak Akj -------------------------------------------------------------------------------- /test/fib32.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (export "fib" (func $fib)) ;; fib exported function 3 | (func $fib (param $n i32) (result i32) 4 | (if 5 | (i32.lt_u 6 | (get_local $n) 7 | (i32.const 2) 8 | ) 9 | (return 10 | (get_local $n) 11 | ) 12 | ) 13 | (return 14 | (i32.add 15 | (call $fib 16 | (i32.sub 17 | (get_local $n) 18 | (i32.const 2) 19 | ) 20 | ) 21 | (call $fib 22 | (i32.sub 23 | (get_local $n) 24 | (i32.const 1) 25 | ) 26 | ) 27 | ) 28 | ) 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /test/fib64.wasm: -------------------------------------------------------------------------------- 1 | asm`~~fib 2 |  BT@  B} B}| -------------------------------------------------------------------------------- /test/mal/README.md: -------------------------------------------------------------------------------- 1 | # MAL (Make A Lisp) 2 | 3 | Run Lisp REPL: 4 | ``` 5 | $ wasm-run mal.wasm 6 | Mal [WebAssembly] 7 | user> (+ 1 2) 8 | 3 9 | ``` 10 | 11 | Run fib function: 12 | ``` 13 | $ wasm-run mal.wasm ./fib.mal 11 14 | 89 15 | ``` 16 | 17 | Run Lisp REPL (self-hosting): 18 | 19 | ``` 20 | $ wasm-run mal.wasm ./mal.mal 21 | Mal [WebAssembly-mal] 22 | nil 23 | mal-user> (+ 1 2) 24 | 3 25 | ``` 26 | 27 | Run fib function (self-hosting): 28 | ``` 29 | $ wasm-run mal.wasm ./mal.mal ./fib.mal 10 30 | 55 31 | ``` 32 | -------------------------------------------------------------------------------- /test/mal/core.mal: -------------------------------------------------------------------------------- 1 | (def! _macro? (fn* [x] 2 | (if (map? x) 3 | (contains? x :__MAL_MACRO__) 4 | false))) 5 | 6 | (def! core_ns 7 | [['= =] 8 | ['throw throw] 9 | ['nil? nil?] 10 | ['true? true?] 11 | ['false? false?] 12 | ['number? number?] 13 | ['string? string?] 14 | ['symbol symbol] 15 | ['symbol? symbol?] 16 | ['keyword keyword] 17 | ['keyword? keyword?] 18 | ['fn? fn?] 19 | ['macro? _macro?] 20 | 21 | ['pr-str pr-str] 22 | ['str str] 23 | ['prn prn] 24 | ['println println] 25 | ['readline readline] 26 | ['read-string read-string] 27 | ['slurp slurp] 28 | ['< <] 29 | ['<= <=] 30 | ['> >] 31 | ['>= >=] 32 | ['+ +] 33 | ['- -] 34 | ['* *] 35 | ['/ /] 36 | ['time-ms time-ms] 37 | 38 | ['list list] 39 | ['list? list?] 40 | ['vector vector] 41 | ['vector? vector?] 42 | ['hash-map hash-map] 43 | ['map? map?] 44 | ['assoc assoc] 45 | ['dissoc dissoc] 46 | ['get get] 47 | ['contains? contains?] 48 | ['keys keys] 49 | ['vals vals] 50 | 51 | ['sequential? sequential?] 52 | ['cons cons] 53 | ['concat concat] 54 | ['vec vec] 55 | ['nth nth] 56 | ['first first] 57 | ['rest rest] 58 | ['empty? empty?] 59 | ['count count] 60 | ['apply apply] 61 | ['map map] 62 | 63 | ['conj conj] 64 | ['seq seq] 65 | 66 | ['with-meta with-meta] 67 | ['meta meta] 68 | ['atom atom] 69 | ['atom? atom?] 70 | ['deref deref] 71 | ['reset! reset!] 72 | ['swap! swap!]]) 73 | -------------------------------------------------------------------------------- /test/mal/env.mal: -------------------------------------------------------------------------------- 1 | (def! bind-env (fn* [env b e] 2 | (if (empty? b) 3 | env 4 | (let* [b0 (first b)] 5 | (if (= '& b0) 6 | (assoc env (str (nth b 1)) e) 7 | (bind-env (assoc env (str b0) (first e)) (rest b) (rest e))))))) 8 | 9 | (def! new-env (fn* [& args] 10 | (if (<= (count args) 1) 11 | (atom {:outer (first args)}) 12 | (atom (apply bind-env {:outer (first args)} (rest args)))))) 13 | 14 | (def! env-find (fn* [env k] 15 | (env-find-str env (str k)))) 16 | 17 | (def! env-find-str (fn* [env ks] 18 | (if env 19 | (let* [data @env] 20 | (if (contains? data ks) 21 | env 22 | (env-find-str (get data :outer) ks)))))) 23 | 24 | (def! env-get (fn* [env k] 25 | (let* [ks (str k) 26 | e (env-find-str env ks)] 27 | (if e 28 | (get @e ks) 29 | (throw (str "'" ks "' not found")))))) 30 | 31 | (def! env-set (fn* [env k v] 32 | (do 33 | (swap! env assoc (str k) v) 34 | v))) 35 | -------------------------------------------------------------------------------- /test/mal/fib.mal: -------------------------------------------------------------------------------- 1 | ;; Compute a Fibonacci number with two recursions. 2 | (def! fib 3 | (fn* [n] ; non-negative number 4 | (if (<= n 1) 5 | n 6 | (+ (fib (- n 1)) (fib (- n 2)))))) 7 | 8 | (println (fib (read-string (first *ARGV*)))) 9 | -------------------------------------------------------------------------------- /test/mal/mal.mal: -------------------------------------------------------------------------------- 1 | (load-file "./env.mal") 2 | (load-file "./core.mal") 3 | 4 | ;; read 5 | (def! READ read-string) 6 | 7 | 8 | ;; eval 9 | 10 | (def! qq-loop (fn* [elt acc] 11 | (if (if (list? elt) (= (first elt) 'splice-unquote)) ; 2nd 'if' means 'and' 12 | (list 'concat (nth elt 1) acc) 13 | (list 'cons (QUASIQUOTE elt) acc)))) 14 | (def! qq-foldr (fn* [xs] 15 | (if (empty? xs) 16 | (list) 17 | (qq-loop (first xs) (qq-foldr (rest xs)))))) 18 | (def! QUASIQUOTE (fn* [ast] 19 | (cond 20 | (vector? ast) (list 'vec (qq-foldr ast)) 21 | (map? ast) (list 'quote ast) 22 | (symbol? ast) (list 'quote ast) 23 | (not (list? ast)) ast 24 | (= (first ast) 'unquote) (nth ast 1) 25 | "else" (qq-foldr ast)))) 26 | 27 | (def! MACROEXPAND (fn* [ast env] 28 | (let* [a0 (if (list? ast) (first ast)) 29 | e (if (symbol? a0) (env-find env a0)) 30 | m (if e (env-get e a0))] 31 | (if (_macro? m) 32 | (MACROEXPAND (apply (get m :__MAL_MACRO__) (rest ast)) env) 33 | ast)))) 34 | 35 | (def! eval-ast (fn* [ast env] 36 | ;; (do (prn "eval-ast" ast "/" (keys env)) ) 37 | (cond 38 | (symbol? ast) (env-get env ast) 39 | 40 | (list? ast) (map (fn* [exp] (EVAL exp env)) ast) 41 | 42 | (vector? ast) (vec (map (fn* [exp] (EVAL exp env)) ast)) 43 | 44 | (map? ast) (apply hash-map 45 | (apply concat 46 | (map (fn* [k] [k (EVAL (get ast k) env)]) 47 | (keys ast)))) 48 | 49 | "else" ast))) 50 | 51 | (def! LET (fn* [env binds form] 52 | (if (empty? binds) 53 | (EVAL form env) 54 | (do 55 | (env-set env (first binds) (EVAL (nth binds 1) env)) 56 | (LET env (rest (rest binds)) form))))) 57 | 58 | (def! EVAL (fn* [ast env] 59 | ;; (do (prn "EVAL" ast "/" (keys @env)) ) 60 | (let* [ast (MACROEXPAND ast env)] 61 | (if (not (list? ast)) 62 | (eval-ast ast env) 63 | 64 | ;; apply list 65 | (let* [a0 (first ast)] 66 | (cond 67 | (empty? ast) 68 | ast 69 | 70 | (= 'def! a0) 71 | (env-set env (nth ast 1) (EVAL (nth ast 2) env)) 72 | 73 | (= 'let* a0) 74 | (LET (new-env env) (nth ast 1) (nth ast 2)) 75 | 76 | (= 'quote a0) 77 | (nth ast 1) 78 | 79 | (= 'quasiquoteexpand a0) 80 | (QUASIQUOTE (nth ast 1)) 81 | 82 | (= 'quasiquote a0) 83 | (EVAL (QUASIQUOTE (nth ast 1)) env) 84 | 85 | (= 'defmacro! a0) 86 | (env-set env (nth ast 1) (hash-map :__MAL_MACRO__ 87 | (EVAL (nth ast 2) env))) 88 | 89 | (= 'macroexpand a0) 90 | (MACROEXPAND (nth ast 1) env) 91 | 92 | (= 'try* a0) 93 | (if (< (count ast) 3) 94 | (EVAL (nth ast 1) env) 95 | (try* 96 | (EVAL (nth ast 1) env) 97 | (catch* exc 98 | (let* [a2 (nth ast 2)] 99 | (EVAL (nth a2 2) (new-env env [(nth a2 1)] [exc])))))) 100 | 101 | (= 'do a0) 102 | (let* [el (eval-ast (rest ast) env)] 103 | (nth el (- (count el) 1))) 104 | 105 | (= 'if a0) 106 | (if (EVAL (nth ast 1) env) 107 | (EVAL (nth ast 2) env) 108 | (if (> (count ast) 3) 109 | (EVAL (nth ast 3) env))) 110 | 111 | (= 'fn* a0) 112 | (fn* [& args] (EVAL (nth ast 2) (new-env env (nth ast 1) args))) 113 | 114 | "else" 115 | (let* [el (eval-ast ast env)] 116 | (apply (first el) (rest el))))))))) 117 | 118 | 119 | ;; print 120 | (def! PRINT pr-str) 121 | 122 | ;; repl 123 | (def! repl-env (new-env)) 124 | (def! rep (fn* [strng] 125 | (PRINT (EVAL (READ strng) repl-env)))) 126 | 127 | ;; core.mal: defined directly using mal 128 | (map (fn* [data] (apply env-set repl-env data)) core_ns) 129 | (env-set repl-env 'eval (fn* [ast] (EVAL ast repl-env))) 130 | (env-set repl-env '*ARGV* (rest *ARGV*)) 131 | 132 | ;; core.mal: defined using the new language itself 133 | (rep (str "(def! *host-language* \"" *host-language* "-mal\")")) 134 | (rep "(def! not (fn* [a] (if a false true)))") 135 | (rep "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))") 136 | (rep "(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))") 137 | 138 | ;; repl loop 139 | (def! repl-loop (fn* [line] 140 | (if line 141 | (do 142 | (if (not (= "" line)) 143 | (try* 144 | (println (rep line)) 145 | (catch* exc 146 | (println "Uncaught exception:" exc)))) 147 | (repl-loop (readline "mal-user> ")))))) 148 | 149 | ;; main 150 | (if (empty? *ARGV*) 151 | (repl-loop "(println (str \"Mal [\" *host-language* \"]\"))") 152 | (rep (str "(load-file \"" (first *ARGV*) "\")"))) 153 | -------------------------------------------------------------------------------- /test/mal/mal.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm3/node-wasm-run/3981de558ac54bf99b0f7303599446687c60487f/test/mal/mal.wasm -------------------------------------------------------------------------------- /test/swap.wasm: -------------------------------------------------------------------------------- 1 | asm``~~~~`}}}}`||||-swap_i32swap_i64swap_f32swap_f64 2 |         nname)swap_i32swap_i64swap_f32swap_f64)p0p1p0p1p0p1p0p1t0t1t2t3 -------------------------------------------------------------------------------- /test/swap.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (type $t0 (func (param i32 i32) (result i32 i32))) 3 | (type $t1 (func (param i64 i64) (result i64 i64))) 4 | (type $t2 (func (param f32 f32) (result f32 f32))) 5 | (type $t3 (func (param f64 f64) (result f64 f64))) 6 | (func $swap_i32 (export "swap_i32") (type $t0) (param $p0 i32) (param $p1 i32) (result i32 i32) 7 | (local.get $p1) 8 | (local.get $p0)) 9 | (func $swap_i64 (export "swap_i64") (type $t1) (param $p0 i64) (param $p1 i64) (result i64 i64) 10 | (local.get $p1) 11 | (local.get $p0)) 12 | (func $swap_f32 (export "swap_f32") (type $t2) (param $p0 f32) (param $p1 f32) (result f32 f32) 13 | (local.get $p1) 14 | (local.get $p0)) 15 | (func $swap_f64 (export "swap_f64") (type $t3) (param $p0 f64) (param $p1 f64) (result f64 f64) 16 | (local.get $p1) 17 | (local.get $p0)) 18 | ) 19 | -------------------------------------------------------------------------------- /test/wasi/coremark.metered.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm3/node-wasm-run/3981de558ac54bf99b0f7303599446687c60487f/test/wasi/coremark.metered.wasm -------------------------------------------------------------------------------- /test/wasi/coremark.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm3/node-wasm-run/3981de558ac54bf99b0f7303599446687c60487f/test/wasi/coremark.wasm -------------------------------------------------------------------------------- /test/wasi/test-wasi-snapshot-preview1.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm3/node-wasm-run/3981de558ac54bf99b0f7303599446687c60487f/test/wasi/test-wasi-snapshot-preview1.wasm -------------------------------------------------------------------------------- /test/wasi/test-wasi-unstable.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm3/node-wasm-run/3981de558ac54bf99b0f7303599446687c60487f/test/wasi/test-wasi-unstable.wasm -------------------------------------------------------------------------------- /wasm-run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * TODO: 5 | * [ ] --inspect 6 | * [ ] check if imports are satisfied, print missing parts 7 | * [ ] auto-import memory 8 | * [ ] --validate flag 9 | * [ ] --imports flag 10 | */ 11 | 12 | "use strict"; 13 | 14 | /* 15 | * Author: Volodymyr Shymanskyy 16 | */ 17 | 18 | const fs = require("fs"); 19 | const assert = require("assert"); 20 | const chalk = require("chalk"); 21 | const r = require("restructure"); 22 | 23 | /* 24 | * Arguments 25 | */ 26 | 27 | const argv = require("yargs") 28 | .usage("$0 [options] [--] [args..]") 29 | .example('$0 fib32.wasm 32', 'run a single exported function') 30 | .example('$0 --invoke=swap_i64 swap.wat 10 12', 'wat file with multivalue support') 31 | .example('$0 test-wasi-snapshot-preview1.wasm', 'wasi support') 32 | .example('$0 test-wasi-unstable.wasm', 'wasi-unstable compatibility layer') 33 | .example('$0 --trace wasi-hello-world.wasm', 'imported function tracing') 34 | .example('$0 --gas-limit=500000000 coremark.metered.wasm', 'gas metering') 35 | .option({ 36 | "respawn": { type: "boolean", hidden: true }, 37 | "invoke": { 38 | alias: "i", 39 | type: "string", 40 | describe: "Function to execute", 41 | nargs: 1 42 | }, 43 | "timeout": { 44 | alias: "t", 45 | type: "int", 46 | describe: "Execution timeout (ms)", 47 | nargs: 1 48 | }, 49 | "trace": { 50 | type: "boolean", 51 | describe: "Trace imported function calls", 52 | }, 53 | "validate": { 54 | type: "boolean", 55 | describe: "Validate module and exit", 56 | }, 57 | "gas-limit": { 58 | type: "float", 59 | describe: "Gas limit", 60 | default: 100000, 61 | nargs: 1 62 | }, 63 | }) 64 | .string('_') 65 | .strict() 66 | .version() 67 | .help() 68 | .wrap(null) 69 | .argv; 70 | 71 | /* 72 | * Respawn with experimental flags 73 | */ 74 | 75 | if (!argv.respawn) 76 | { 77 | const { execFileSync, spawnSync } = require('child_process'); 78 | const node = process.argv[0]; 79 | const script = process.argv[1]; 80 | const script_args = process.argv.slice(2); 81 | 82 | let allFlags = execFileSync(node, ["--v8-options"]).toString(); 83 | allFlags += execFileSync(node, ["--help"]).toString(); 84 | 85 | const nodeFlags = [ "--experimental-wasm-bigint", 86 | "--experimental-wasm-mv", 87 | "--experimental-wasm-return-call", 88 | "--experimental-wasm-bulk-memory", 89 | "--experimental-wasi-unstable-preview1", 90 | "--wasm-opt"].filter(x => allFlags.includes(x)); 91 | 92 | let res = spawnSync(node, 93 | [...nodeFlags, script, "--respawn", ...script_args], 94 | { stdio: ['inherit', 'inherit', 'inherit'], 95 | timeout: argv.timeout }); 96 | 97 | if (res.error) { 98 | fatal(res.error); 99 | } 100 | process.exit(res.status); 101 | } 102 | 103 | /* 104 | * Helpers 105 | */ 106 | 107 | function fatal(msg) { 108 | console.error(chalk.grey('[runtime] ') + chalk.red.bold("Error: ") + msg); 109 | process.exit(1); 110 | } 111 | 112 | function warn(msg) { 113 | console.error(chalk.grey('[runtime] ') + chalk.yellow.bold("Warning: ") + msg); 114 | } 115 | 116 | function log(msg) { 117 | console.error(chalk.grey('[runtime] ') + msg); 118 | } 119 | 120 | class EncodeBuffer { 121 | constructor(bufferSize) { 122 | bufferSize = bufferSize || 65536; 123 | 124 | this.pos = 0; 125 | this.buff = Buffer.alloc(bufferSize); 126 | 127 | for (let key in Buffer.prototype) { 128 | if (key.slice(0, 5) === 'write') { 129 | (function(key) { 130 | const bytes = r.DecodeStream.TYPES[key.replace(/write|[BL]E/g, '')]; 131 | return EncodeBuffer.prototype[key] = function(value) { 132 | this.buff[key](value, this.pos); 133 | return this.pos += bytes; 134 | }; 135 | })(key); 136 | } 137 | } 138 | } 139 | 140 | get buffer() { 141 | return this.buff.slice(0, this.pos); 142 | } 143 | 144 | writeString(string, encoding) { 145 | encoding = encoding || 'ascii'; 146 | 147 | switch (encoding) { 148 | case 'utf16le': 149 | case 'ucs2': 150 | case 'utf8': 151 | case 'ascii': 152 | return this.writeBuffer(Buffer.from(string, encoding)); 153 | default: 154 | throw new Error('String encoding need to be implemented with iconv-lite.'); 155 | } 156 | } 157 | 158 | writeBuffer(buffer) { 159 | buffer.copy(this.buff, this.pos); 160 | return this.pos += buffer.length; 161 | } 162 | 163 | fill(val, length) { 164 | this.buff.fill(val, this.pos, this.pos + length); 165 | return this.pos += length; 166 | } 167 | } 168 | 169 | function encodeStruct(T, data) { 170 | let encoder = new EncodeBuffer(T.size()); 171 | T.encode(encoder, data); 172 | assert.equal(encoder.buffer.length, T.size()); 173 | return encoder.buffer; 174 | }; 175 | 176 | function decodeStruct(T, buff) { 177 | buff = new Buffer(buff); 178 | assert.equal(buff.length, T.size()); 179 | return T.decode(new r.DecodeStream(buff)); 180 | }; 181 | 182 | /* 183 | async function wat2wasm(binary) 184 | { 185 | const wat = binary.toString() 186 | .replace(/\(\;.*?\;\)/, '') 187 | .replace(/local\.get/g, 'get_local') 188 | .replace(/global\.get/g, 'get_global') 189 | .replace(/local\.set/g, 'set_local') 190 | .replace(/global\.set/g, 'set_global'); 191 | 192 | const wabt = await (require("wabt")()); 193 | 194 | const module = wabt.parseWat("mod.wasm", wat); 195 | module.resolveNames(); 196 | module.validate(); 197 | 198 | let result = module.toBinary({ 199 | log: false, 200 | write_debug_names: true, 201 | canonicalize_lebs: true, 202 | relocatable: true, 203 | }).buffer; 204 | 205 | return result; 206 | } 207 | */ 208 | 209 | async function wat2wasm(binary) 210 | { 211 | const Binaryen = require("binaryen"); 212 | Binaryen.setDebugInfo(true); 213 | 214 | const wat = binary.toString() 215 | .replace(/get_local/g, 'local.get') 216 | .replace(/get_global/g, 'global.get') 217 | .replace(/set_local/g, 'local.set') 218 | .replace(/set_global/g, 'global.set'); 219 | 220 | let module = Binaryen.parseText(wat); 221 | let result = module.emitBinary(); 222 | module.dispose(); 223 | 224 | return result; 225 | } 226 | 227 | async function parseWasmInfo(binary) 228 | { 229 | const Binaryen = require("binaryen"); 230 | Binaryen.setDebugInfo(true); 231 | 232 | let module = Binaryen.readBinary(binary); 233 | 234 | function decodeType(t) { 235 | switch (t) { 236 | case Binaryen.none: return "none"; 237 | case Binaryen.i32: return "i32"; 238 | case Binaryen.i64: return "i64"; 239 | case Binaryen.f32: return "f32"; 240 | case Binaryen.f64: return "f64"; 241 | case Binaryen.v128: return "v128"; 242 | case Binaryen.funcref: return "funcref"; 243 | case Binaryen.anyref: return "anyref"; 244 | case Binaryen.nullref: return "nullref"; 245 | case Binaryen.exnref: return "externref"; 246 | default: return "unknown"; 247 | } 248 | } 249 | 250 | let result = { 251 | funcsByIndex: [], 252 | funcsByName: {} 253 | }; 254 | 255 | for (let i = 0; i < module.getNumFunctions(); i++) { 256 | let info = Binaryen.getFunctionInfo(module.getFunctionByIndex(i)); 257 | 258 | result.funcsByIndex[i] = result.funcsByName[info.name] = { 259 | index: i, 260 | params: Binaryen.expandType(info.params).map(x => decodeType(x)), 261 | results: Binaryen.expandType(info.results).map(x => decodeType(x)), 262 | } 263 | } 264 | 265 | for (let i = 0; i < module.getNumExports(); i++) { 266 | let exp = Binaryen.getExportInfo(module.getExportByIndex(i)); 267 | 268 | if (exp.kind == Binaryen.ExternalFunction) { 269 | let item = result.funcsByName[exp.value]; 270 | result.funcsByName[exp.name] = item; 271 | } 272 | } 273 | 274 | module.dispose(); 275 | 276 | return result; 277 | } 278 | 279 | 280 | /******************************************************************* 281 | * Main 282 | *******************************************************************/ 283 | 284 | (async () => { 285 | const inputFile = argv._[0] 286 | 287 | if (!inputFile) { 288 | fatal(`Please specify input file. See ${chalk.white.bold('--help')} for details.`); 289 | } 290 | 291 | let binary; 292 | try { 293 | binary = fs.readFileSync(inputFile); 294 | } catch (e) { 295 | fatal(`File ${chalk.white.bold(inputFile)} not found`); 296 | } 297 | 298 | if (inputFile.endsWith('.wat')) { 299 | binary = await wat2wasm(binary); 300 | log(`Converted to binary (${binary.length} bytes)`); 301 | } 302 | 303 | if (argv.validate) 304 | { 305 | if (WebAssembly.validate(binary)) { 306 | log(`Validation ${chalk.green.bold("PASSED")}`); 307 | process.exit(0); 308 | } else { 309 | log(`Validation ${chalk.red.bold("FAILED")}`); 310 | process.exit(1); 311 | } 312 | } 313 | 314 | /* 315 | * Compile 316 | */ 317 | 318 | /* TODO: caching 319 | const v8 = require('v8'); 320 | const compiled = await WebAssembly.compile(binary); 321 | const cached = v8.serialize(compiled); 322 | binary = v8.deserialize(cached); 323 | */ 324 | 325 | let module = await WebAssembly.compile(binary); 326 | 327 | /* 328 | * Analyze 329 | */ 330 | 331 | let wasmInfo = {} 332 | 333 | let expectedImports = WebAssembly.Module.imports(module); 334 | for (const i of expectedImports) { 335 | if (i.module.startsWith("wasi_")) { 336 | wasmInfo.wasiVersion = i.module; 337 | } 338 | } 339 | 340 | wasmInfo.exportedFuncs = WebAssembly.Module.exports(module).filter(x => x.kind == 'function').map(x => x.name); 341 | 342 | /* 343 | * Prepare imports 344 | */ 345 | 346 | let imports = { } 347 | 348 | /* 349 | * Gas Metering 350 | */ 351 | 352 | const GAS_FACTOR = 10000; 353 | 354 | let ctx = { 355 | gasCurrent: argv.gasLimit * GAS_FACTOR, 356 | gasLimit: argv.gasLimit * GAS_FACTOR, 357 | }; 358 | 359 | function getGasUsed() { 360 | return (ctx.gasLimit - ctx.gasCurrent) / GAS_FACTOR; 361 | } 362 | 363 | function printGasUsed() { 364 | const gasUsed = getGasUsed(); 365 | if (gasUsed) { 366 | log(`Gas used: ${gasUsed.toFixed(4)}`); 367 | } 368 | } 369 | 370 | imports.metering = { 371 | usegas: function (gas) { 372 | if ((ctx.gasCurrent -= gas) < 0) { 373 | throw `Run out of gas (gas used: ${getGasUsed().toFixed(4)})`; 374 | } 375 | } 376 | } 377 | 378 | /* 379 | * WASI 380 | */ 381 | 382 | if (wasmInfo.wasiVersion) 383 | { 384 | const { WASI } = require('wasi'); 385 | ctx.wasi = new WASI({ 386 | returnOnExit: true, 387 | args: argv._, 388 | env: { 389 | "NODEJS": 1 390 | }, 391 | 392 | // TODO: --mapdir flag 393 | preopens: { 394 | "/": ".", 395 | ".": ".", 396 | } 397 | }); 398 | 399 | const wasiImport = ctx.wasi.wasiImport; 400 | 401 | if (wasmInfo.wasiVersion == "wasi_snapshot_preview1") { 402 | imports.wasi_snapshot_preview1 = wasiImport; 403 | } else if (wasmInfo.wasiVersion == "wasi_unstable") { 404 | imports.wasi_unstable = Object.assign({}, wasiImport); 405 | 406 | const uint8 = r.uint8; 407 | const uint16 = r.uint16le; 408 | const uint32 = r.uint32le; 409 | const uint64 = new r.Struct({ lo: uint32, hi: uint32 }); 410 | 411 | const wasi_snapshot_preview1_filestat_t = new r.Struct({ 412 | dev: uint64, // 0 413 | ino: uint64, // 8 414 | ftype: uint8, // 16 415 | pad0: new r.Reserved(uint8, 7), 416 | nlink: uint64, // 24 417 | size: uint64, // 32 418 | atim: uint64, // 40 419 | mtim: uint64, // 48 420 | ctim: uint64 // 56 421 | }); // size = 64 422 | 423 | const wasi_unstable_filestat_t = new r.Struct({ 424 | dev: uint64, // 0 425 | ino: uint64, // 8 426 | ftype: uint8, // 16 427 | pad0: new r.Reserved(uint8, 3), 428 | nlink: uint32, // 20 429 | size: uint64, // 24 430 | atim: uint64, // 32 431 | mtim: uint64, // 40 432 | ctim: uint64 // 48 433 | }); // size = 56 434 | 435 | imports.wasi_unstable.fd_seek = function(fd, offset, whence, result) { 436 | switch (whence) { 437 | case 0: whence = 1; break; // cur 438 | case 1: whence = 2; break; // end 439 | case 2: whence = 0; break; // set 440 | default: throw "Invalid whence"; 441 | } 442 | const res = wasiImport.fd_seek(fd, offset, whence, result); 443 | return res; 444 | } 445 | imports.wasi_unstable.fd_filestat_get = function(fd, buf) { 446 | const mem = new Uint8Array(ctx.memory.buffer); 447 | const backup = mem.slice(buf+56, buf+(64-56)); 448 | 449 | const res = wasiImport.fd_filestat_get(fd, buf); 450 | 451 | const modified = encodeStruct(wasi_unstable_filestat_t, 452 | decodeStruct(wasi_snapshot_preview1_filestat_t, 453 | mem.slice(buf, buf+64))); 454 | mem.set(modified, buf); // write new struct 455 | mem.set(backup, buf+56); // restore backup 456 | return res; 457 | } 458 | imports.wasi_unstable.path_filestat_get = function(fd, flags, path, path_len, buf) { 459 | const mem = new Uint8Array(ctx.memory.buffer); 460 | const backup = mem.slice(buf+56, buf+(64-56)); 461 | 462 | const res = wasiImport.path_filestat_get(fd, flags, path, path_len, buf); 463 | 464 | const modified = encodeStruct(wasi_unstable_filestat_t, 465 | decodeStruct(wasi_snapshot_preview1_filestat_t, 466 | mem.slice(buf, buf+64))); 467 | mem.set(modified, buf); // write new struct 468 | mem.set(backup, buf+56); // restore backup 469 | return res; 470 | } 471 | } else { 472 | fatal(`Unsupported WASI version: ${wasmInfo.wasiVersion}`); 473 | } 474 | } 475 | 476 | // TODO: WASI API + structures decoding (generate from witx?) 477 | if (argv.trace) { 478 | function traceGeneric(name, f) { 479 | return function (...args) { 480 | try { 481 | let res = f.apply(this, args); 482 | log(`${name} ${args.join()} => ${res}`); 483 | return res; 484 | } catch (e) { 485 | log(`${name} ${args.join()} => ${e}`); 486 | throw e; 487 | } 488 | } 489 | } 490 | 491 | let newimports = {} 492 | for (const [modname, mod] of Object.entries(imports)) { 493 | newimports[modname] = {} 494 | for (const [funcname, func] of Object.entries(mod)) { 495 | newimports[modname][funcname] = traceGeneric(`${modname}!${funcname}`, func); 496 | } 497 | } 498 | 499 | imports = newimports; 500 | } 501 | 502 | /* 503 | * Execute 504 | */ 505 | 506 | try { 507 | let instance = await WebAssembly.instantiate(module, imports); 508 | 509 | // If no WASI is detected, and no func specified -> try to run the only function 510 | if (!argv.invoke && !wasmInfo.wasiVersion && wasmInfo.exportedFuncs.length == 1) { 511 | argv.invoke = wasmInfo.exportedFuncs[0]; 512 | } 513 | 514 | if (argv.invoke) { 515 | if (!wasmInfo.exportedFuncs.includes(argv.invoke)) { 516 | fatal(`Function not found: ${argv.invoke}`); 517 | } 518 | let args = argv._.slice(1) 519 | 520 | let wasmInfo2 = await parseWasmInfo(binary); 521 | //console.log(JSON.stringify(wasmInfo2)); 522 | let funcInfo = wasmInfo2.funcsByName[argv.invoke]; 523 | 524 | for (let i = 0; i < funcInfo.params.length; i++) { 525 | switch (funcInfo.params[i]) { 526 | case 'i32': args[i] = parseInt(args[i]); break; 527 | case 'i64': args[i] = BigInt(args[i]); break; 528 | case 'f32': 529 | case 'f64': args[i] = parseFloat(args[i]); break; 530 | } 531 | } 532 | 533 | log(`Running ${argv.invoke}(${args})...`); 534 | let func = instance.exports[argv.invoke]; 535 | let result = func(...args); 536 | log(`Result: ${result}`); 537 | printGasUsed(); 538 | } else { 539 | ctx.memory = instance.exports.memory; 540 | let exitcode = ctx.wasi.start(instance); 541 | if (exitcode) { 542 | log(`Exit code: ${exitcode}`); 543 | } 544 | printGasUsed(); 545 | process.exit(exitcode); 546 | } 547 | } catch (e) { 548 | fatal(e); 549 | } 550 | })(); 551 | --------------------------------------------------------------------------------