├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── example ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── example.rs ├── example.webasm ├── index.html ├── js-embed.js ├── main.js └── rs-embed.js └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /target/ 3 | **/*.rs.bk 4 | /build -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "wasm-glue" 3 | version = "0.1.0" 4 | 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-glue" 3 | version = "0.1.0" 4 | description = "Get println! & panics to work in WebAssembly" 5 | authors = ["Sterling DeMille "] 6 | 7 | repository = "https://github.com/demille/wasm-glue" 8 | homepage = "https://github.com/demille/wasm-glue" 9 | license = "MIT" 10 | readme = "README.md" 11 | keywords = ["WebAssembly", "wasm"] 12 | 13 | [lib] 14 | path = "src/lib.rs" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Sterling DeMille 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-glue 2 | **Get println! & panics to work in WebAssembly** 3 | 4 | WebAssembly is cool and all, but Rust `println!`'s don't work out of the box, and when it crashes you're left with that unhelpful "unreachable" error in the stack trace instead of it telling you what actually went wrong. 5 | 6 | `wasm-glue` is glue code to hook up your stdout and stderr. 7 | 8 | 👉 [Live Example](https://demille.github.io/wasm-glue/example/) 9 | 10 |
11 | 12 | 13 | ## Usage 14 | Most basic usage, call `wasm_glue::hook()` once, near the start of whatever you are doing: 15 | 16 | ```rust 17 | extern crate wasm_glue; 18 | 19 | #[no_mangle] 20 | pub fn run_webassembly() { 21 | // hook stdout and stderr up once, before printing anything 22 | wasm_glue::hook(); 23 | 24 | println!("hello console!"); 25 | println!("I'm gunna crash:"); 26 | 27 | None::>.unwrap(); 28 | } 29 | ``` 30 | 31 | **Coordinating JavaScript:** 32 | You'll need 3 imported JavaScript functions for this to work: `print`, `eprint`, and `trace`: 33 | 34 | ```rust 35 | extern { 36 | fn print(ptr: *const c_char); // for stdout 37 | fn eprint(ptr: *const c_char); // for stderr 38 | fn trace(ptr: *const c_char); // specifically for panics 39 | } 40 | ``` 41 | 42 | A basic implementation of these functions would look like this: 43 | 44 | ```js 45 | // keep a WebAssembly memory reference for `readString` 46 | let memory; 47 | 48 | // read a null terminated c string at a wasm memory buffer index 49 | function readString(ptr) { 50 | const view = new Uint8Array(memory.buffer); 51 | 52 | let end = ptr; 53 | while (view[end]) ++end; 54 | 55 | const buf = new Uint8Array(view.subarray(ptr, end)); 56 | return (new TextDecoder()).decode(buf); 57 | } 58 | 59 | // `wasm_glue::hook()` requires all three 60 | const imports = { 61 | env: { 62 | print(ptr) { 63 | console.log(readString(ptr)); 64 | }, 65 | 66 | eprint(ptr) { 67 | console.warn(readString(ptr)); 68 | }, 69 | 70 | trace(ptr) { 71 | throw new Error(readString(ptr)); 72 | }, 73 | }, 74 | }; 75 | 76 | // ... 77 | 78 | WebAssembly.instantiate(buffer, imports).then((result) => { 79 | const exports = result.instance.exports; 80 | 81 | // update memory reference for readString() 82 | memory = exports.memory; 83 | exports.run_webassembly(); 84 | 85 | // ... 86 | }) 87 | ``` 88 | 89 | :boom: Boom! Fully working `println!`'s and helpful panics. 90 | 91 | See a complete example of this in the `/example` folder. 92 | 93 | _**Extra Credit:**_ demangle your stack traces. 94 | You can copy the [implementation here][demangle] to demangle the `.stack` property of the error you generate inside your `trace` function. Helps make debugging a little more readable. 95 | 96 | [demangle]: https://github.com/DeMille/wasm-ffi/blob/master/src/demangle.js 97 | 98 |
99 | 100 | 101 | ## What's happening? 102 | 103 | `wasm-glue` uses somewhat obscure std library functions: 104 | - `std::io::set_print()` 105 | - `std::io::set_panic()` 106 | - `std::panic::set_hook()` 107 | 108 | Check `lib.rs` to see what's going on. 109 | 110 |
111 | 112 | 113 | ## Options 114 | 115 | `wasm_glue::hook()` calls all 3 of those magic functions. But you can pick and choose if you'd rather. You can also set stdout / stderr to unbuffered instead of line buffered (the default). 116 | 117 | • **::set_stdout()** 118 | • **::set_stdout_unbuffered()** 119 | • **::set_stderr()** 120 | • **::set_stderr_unbuffered()** 121 | • **::set_panic_hook()** 122 | 123 | Alternatively, you can just use the macros for `print!` / `eprint`: 124 | 125 | ```rust 126 | #[macro_use] 127 | extern crate wasm_glue; 128 | ``` 129 |
130 | 131 | 132 | ## License 133 | MIT -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /target/ 3 | **/*.rs.bk 4 | /build -------------------------------------------------------------------------------- /example/Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "example" 3 | version = "0.0.0" 4 | dependencies = [ 5 | "wasm-glue 0.1.0", 6 | ] 7 | 8 | [[package]] 9 | name = "wasm-glue" 10 | version = "0.1.0" 11 | 12 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.0.0" 4 | publish = false 5 | 6 | [lib] 7 | path = "./example.rs" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies.wasm-glue] 11 | path = "../" 12 | 13 | [profile.dev] 14 | opt-level = 2 15 | -------------------------------------------------------------------------------- /example/example.rs: -------------------------------------------------------------------------------- 1 | extern crate wasm_glue; 2 | 3 | // hook up stdout and stderr. do this first. 4 | #[no_mangle] 5 | pub fn hook() { 6 | wasm_glue::hook(); 7 | } 8 | 9 | #[no_mangle] 10 | pub fn print_add(a: u32, b: u32) { 11 | println!("{} + {} = {}", a, b, a + b); 12 | } 13 | 14 | #[no_mangle] 15 | pub fn print_err_msg() { 16 | eprintln!(r"Danger! Danger! /!\"); 17 | } 18 | 19 | #[no_mangle] 20 | pub fn cause_panic() { 21 | None::>.unwrap(); 22 | } 23 | -------------------------------------------------------------------------------- /example/example.webasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeMille/wasm-glue/5836c938a6ee21f4c8427265347458525c547e2a/example/example.webasm -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wasm-glue example 6 | 7 | 13 | 14 | 15 | 16 |

Open console ↓

17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/js-embed.js: -------------------------------------------------------------------------------- 1 | document.write(''); 2 | document.write('
main.js 69 lines
// keep a WebAssembly memory reference for `readString`
let memory;
// read a null terminated c string at a wasm memory buffer index
function readString(ptr) {
const view = new Uint8Array(memory.buffer);
// find the end of the string (null)
let end = ptr;
while (view[end]) ++end;
// `subarray` uses the same underlying ArrayBuffer as the view
const buf = new Uint8Array(view.subarray(ptr, end));
const str = (new TextDecoder()).decode(buf); // (utf-8 by default)
return str;
}
// necessary imports under the `env` namespace (rust looks for exports here)
//
// `wasm_glue::hook()` requires all three
const imports = {
env: {
// needed for `wasm_glue::set_stdout()` or `#[macro_use(print)]`
print(ptr) {
// sidenote: this doesn't have to be a console.log(). You could put
// this up in the html too if you wanted.
console.log(readString(ptr));
},
// needed for `wasm_glue::set_stderr()` or `#[macro_use(eprint)]`
eprint(ptr) {
console.warn(readString(ptr));
},
// needed for `wasm_glue::set_panic_hook()`
trace(ptr) {
const err = new Error(readString(ptr));
// here is where you could demangle the stack trace with:
// https://github.com/demille/wasm-ffi/blob/master/src/demangle.js
throw err;
},
},
};
// fetch and instantiate the wasm module
fetch('example.wasm')
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, imports))
.then((result) => {
console.log('wasm loaded!: ', result);
const exports = result.instance.exports;
// update memory reference for readString
memory = exports.memory;
// wasm_glue::hook() need to be called before anything that will print
exports.hook();
// wasm calls: imports.env.print()
exports.print_add(2, 2);
// wasm calls: imports.env.eprint()
exports.print_err_msg();
// wasm panic will trigger: imports.env.trace()
exports.cause_panic();
})
.catch(err => setTimeout(() => { throw err; }));
'); 3 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | // keep a WebAssembly memory reference for `readString` 2 | let memory; 3 | 4 | // read a null terminated c string at a wasm memory buffer index 5 | function readString(ptr) { 6 | const view = new Uint8Array(memory.buffer); 7 | 8 | // find the end of the string (null) 9 | let end = ptr; 10 | while (view[end]) ++end; 11 | 12 | // `subarray` uses the same underlying ArrayBuffer as the view 13 | const buf = new Uint8Array(view.subarray(ptr, end)); 14 | const str = (new TextDecoder()).decode(buf); // (utf-8 by default) 15 | 16 | return str; 17 | } 18 | 19 | // necessary imports under the `env` namespace (rust looks for exports here) 20 | // 21 | // `wasm_glue::hook()` requires all three 22 | const imports = { 23 | env: { 24 | // needed for `wasm_glue::set_stdout()` or `#[macro_use(print)]` 25 | print(ptr) { 26 | // sidenote: this doesn't have to be a console.log(). You could put 27 | // this up in the html too if you wanted. 28 | console.log(readString(ptr)); 29 | }, 30 | 31 | // needed for `wasm_glue::set_stderr()` or `#[macro_use(eprint)]` 32 | eprint(ptr) { 33 | console.warn(readString(ptr)); 34 | }, 35 | 36 | // needed for `wasm_glue::set_panic_hook()` 37 | trace(ptr) { 38 | const err = new Error(readString(ptr)); 39 | // here is where you could demangle the stack trace with: 40 | // https://github.com/demille/wasm-ffi/blob/master/src/demangle.js 41 | throw err; 42 | }, 43 | }, 44 | }; 45 | 46 | // fetch and instantiate the wasm module 47 | fetch('example.webasm') 48 | .then(response => response.arrayBuffer()) 49 | .then(buffer => WebAssembly.instantiate(buffer, imports)) 50 | .then((result) => { 51 | console.log('wasm loaded!: ', result); 52 | const exports = result.instance.exports; 53 | 54 | // update memory reference for readString 55 | memory = exports.memory; 56 | 57 | // wasm_glue::hook() need to be called before anything that will print 58 | exports.hook(); 59 | 60 | // wasm calls: imports.env.print() 61 | exports.print_add(2, 2); 62 | 63 | // wasm calls: imports.env.eprint() 64 | exports.print_err_msg(); 65 | 66 | // wasm panic will trigger: imports.env.trace() 67 | exports.cause_panic(); 68 | }) 69 | .catch(err => setTimeout(() => { throw err; })); 70 | -------------------------------------------------------------------------------- /example/rs-embed.js: -------------------------------------------------------------------------------- 1 | document.write(''); 2 | document.write('
example.rs 22 lines
extern crate wasm_glue;
// hook up stdout and stderr. do this first.
#[no_mangle]
pub fn hook() {
wasm_glue::hook();
}
#[no_mangle]
pub fn print_add(a: u32, b: u32) {
println!("{} + {} = {}", a, b, a + b);
}
#[no_mangle]
pub fn print_err_msg() {
eprintln!(r"Danger! Danger! /!\\");
}
#[no_mangle]
pub fn cause_panic() {
None::<Option<u32>>.unwrap();
}
'); 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(set_stdio)] 2 | #![feature(panic_col)] 3 | 4 | use std::ffi::CString; 5 | use std::os::raw::c_char; 6 | use std::fmt; 7 | use std::fmt::Write; 8 | use std::panic; 9 | use std::io; 10 | 11 | 12 | // these are the functions you'll need to privide with JS 13 | extern { 14 | fn print(ptr: *const c_char); 15 | fn eprint(ptr: *const c_char); 16 | fn trace(ptr: *const c_char); 17 | } 18 | 19 | 20 | fn _print(buf: &str) -> io::Result<()> { 21 | let cstring = CString::new(buf)?; 22 | 23 | unsafe { 24 | print(cstring.as_ptr()); 25 | } 26 | 27 | Ok(()) 28 | } 29 | 30 | fn _eprint(buf: &str) -> io::Result<()> { 31 | let cstring = CString::new(buf)?; 32 | 33 | unsafe { 34 | eprint(cstring.as_ptr()); 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | /// Used by the `print` macro 41 | #[doc(hidden)] 42 | pub fn _print_args(args: fmt::Arguments) { 43 | let mut buf = String::new(); 44 | let _ = buf.write_fmt(args); 45 | let _ = _print(&buf); 46 | } 47 | 48 | /// Used by the `eprint` macro 49 | #[doc(hidden)] 50 | pub fn _eprint_args(args: fmt::Arguments) { 51 | let mut buf = String::new(); 52 | let _ = buf.write_fmt(args); 53 | let _ = _eprint(&buf); 54 | } 55 | 56 | /// Overrides the default `print!` macro. 57 | #[macro_export] 58 | macro_rules! print { 59 | ($($arg:tt)*) => ($crate::_print_args(format_args!($($arg)*))); 60 | } 61 | 62 | /// Overrides the default `eprint!` macro. 63 | #[macro_export] 64 | macro_rules! eprint { 65 | ($($arg:tt)*) => ($crate::_eprint_args(format_args!($($arg)*))); 66 | } 67 | 68 | 69 | type PrintFn = fn(&str) -> io::Result<()>; 70 | 71 | struct Printer { 72 | printfn: PrintFn, 73 | buffer: String, 74 | is_buffered: bool, 75 | } 76 | 77 | impl Printer { 78 | fn new(printfn: PrintFn, is_buffered: bool) -> Printer { 79 | Printer { 80 | buffer: String::new(), 81 | printfn, 82 | is_buffered, 83 | } 84 | } 85 | } 86 | 87 | impl io::Write for Printer { 88 | fn write(&mut self, buf: &[u8]) -> io::Result { 89 | self.buffer.push_str(&String::from_utf8_lossy(buf)); 90 | 91 | if !self.is_buffered { 92 | (self.printfn)(&self.buffer)?; 93 | self.buffer.clear(); 94 | 95 | return Ok(buf.len()); 96 | } 97 | 98 | if let Some(i) = self.buffer.rfind('\n') { 99 | let buffered = { 100 | let (first, last) = self.buffer.split_at(i); 101 | (self.printfn)(first)?; 102 | 103 | String::from(&last[1..]) 104 | }; 105 | 106 | self.buffer.clear(); 107 | self.buffer.push_str(&buffered); 108 | } 109 | 110 | Ok(buf.len()) 111 | } 112 | 113 | fn flush(&mut self) -> io::Result<()> { 114 | (self.printfn)(&self.buffer)?; 115 | self.buffer.clear(); 116 | 117 | Ok(()) 118 | } 119 | } 120 | 121 | 122 | /// Sets a line-buffered stdout, uses your JavaScript `print` function 123 | pub fn set_stdout() { 124 | let printer = Printer::new(_print, true); 125 | io::set_print(Some(Box::new(printer))); 126 | } 127 | 128 | /// Sets an unbuffered stdout, uses your JavaScript `print` function 129 | pub fn set_stdout_unbuffered() { 130 | let printer = Printer::new(_print, false); 131 | io::set_print(Some(Box::new(printer))); 132 | } 133 | 134 | /// Sets a line-buffered stderr, uses your JavaScript `eprint` function 135 | pub fn set_stderr() { 136 | let eprinter = Printer::new(_eprint, true); 137 | io::set_panic(Some(Box::new(eprinter))); 138 | } 139 | 140 | /// Sets an unbuffered stderr, uses your JavaScript `eprint` function 141 | pub fn set_stderr_unbuffered() { 142 | let eprinter = Printer::new(_eprint, false); 143 | io::set_panic(Some(Box::new(eprinter))); 144 | } 145 | 146 | /// Sets a custom panic hook, uses your JavaScript `trace` function 147 | pub fn set_panic_hook() { 148 | panic::set_hook(Box::new(|info| { 149 | let file = info.location().unwrap().file(); 150 | let line = info.location().unwrap().line(); 151 | let col = info.location().unwrap().column(); 152 | 153 | let msg = match info.payload().downcast_ref::<&'static str>() { 154 | Some(s) => *s, 155 | None => { 156 | match info.payload().downcast_ref::() { 157 | Some(s) => &s[..], 158 | None => "Box", 159 | } 160 | } 161 | }; 162 | 163 | let err_info = format!("Panicked at '{}', {}:{}:{}", msg, file, line, col); 164 | let cstring = CString::new(err_info).unwrap(); 165 | 166 | unsafe { 167 | trace(cstring.as_ptr()); 168 | } 169 | })); 170 | } 171 | 172 | /// Sets stdout, stderr, and a custom panic hook 173 | pub fn hook() { 174 | set_stdout(); 175 | set_stderr(); 176 | set_panic_hook(); 177 | } 178 | --------------------------------------------------------------------------------