wasmir/
lib.rs

1#![feature(string_remove_matches)]
2
3//! A library for embedding high-performance WASM code directly in a Rust program.
4//! This package was created for people who absolutely hate writing Javascript.
5//! The goal of this library is to reduce the amount of overhead required to implement
6//! WASM by automatically compiling WASM modules and statically linking them to
7//! your binary. You will need to have [wasm-bindgen](https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm) installed.
8//! If your project stops building, please submit an issue. You may also try deleting .wasmir directory.
9
10//! # Usage
11//! Code must be declared inside a module. The typical usage is as follows:
12//! ```
13//! use wasmir::wasmir;
14//!
15//! #[wasmir]
16//! mod my_module {
17//!    use wasm_bindgen::prelude::*;
18//!
19//!    #[wasm_bindgen]
20//!    extern "C" {
21//!       pub fn alert(s: &str);
22//!    }
23//!
24//!    #[wasm_bindgen]
25//!    pub fn greet(name: &str) {
26//!       unsafe {
27//!          alert(&format!("Hello, {}!", name));
28//!       }
29//!    }
30//! }
31//! ```
32//! Once the proc_macro does its work, the above module will then contain two binary blob constants,
33//! `wasm` and `loader`. Serve loader from "my_module.js" and wasm from "my_module_bg.wasm"
34//! Then, in index.js, include the following code:
35//! ```js
36//! import init from './my_module_bg.js';
37//! import {greet} from './my_module_bg.js';
38//!
39//! function run() {
40//!    greet(\"World\");
41//! }
42//!
43//! init().then(run)
44//! ```
45//! You can also specify WASM-dependencies like so:
46//! ```toml
47//! #[wasmir(
48//! [dependencies]
49//! wasm-bindgen = "*"
50//! [dependencies.web-sys]
51//! version = "*"
52//! features = ["Document", "Node", "Element"]
53//! )]
54//! ```
55
56// Macro gets applied to module, function, struct, etc.
57// Macro calls compiler with web assembly target on code.
58// Macro puts the resulting binary in the code.
59
60use proc_macro::TokenStream;
61use proc_macro2::{Delimiter, Ident, Span, TokenStream as TokenStream2, TokenTree};
62use quote::quote;
63use std::env;
64use std::fs::{create_dir_all, File, OpenOptions};
65use std::io::prelude::*;
66use std::process::Command;
67use toml;
68use toml::Value;
69
70fn token_tree_to_toml(tree: TokenTree, prev: &Option<TokenTree>) -> String {
71	let mut buf = String::new();
72	let newline_group = if let Some(TokenTree::Punct(_)) = prev {
73		""
74	} else {
75		"\n"
76	};
77	buf = match tree {
78		TokenTree::Group(group) => match group.delimiter() {
79			Delimiter::Brace => format![
80				"{}{}{{{}}}",
81				newline_group,
82				buf,
83				token_stream_to_toml(group.stream())
84			],
85			Delimiter::Bracket => format![
86				"{}{}[{}]",
87				newline_group,
88				buf,
89				token_stream_to_toml(group.stream())
90			],
91			Delimiter::Parenthesis => {
92				format![
93					"{}{}({})",
94					newline_group,
95					buf,
96					token_stream_to_toml(group.stream())
97				]
98			}
99			Delimiter::None => format![
100				"{}{}{}",
101				newline_group,
102				buf,
103				token_stream_to_toml(group.stream())
104			],
105		},
106		TokenTree::Ident(ident) => {
107			if let Some(TokenTree::Group(_) | TokenTree::Literal(_)) = prev {
108				format!["\n{}{}", buf, ident.to_string()]
109			} else {
110				format!["{}{}", buf, ident.to_string()]
111			}
112		}
113		TokenTree::Literal(literal) => {
114			if let Some(TokenTree::Group(_)) = prev {
115				format!["\n{}{}", buf, literal.to_string()]
116			} else {
117				format!["{}{}", buf, literal.to_string()]
118			}
119		}
120		TokenTree::Punct(punct) => {
121			if let Some(TokenTree::Group(_)) = prev {
122				format!["\n{}{}", buf, punct.to_string()]
123			} else {
124				format!["{}{}", buf, punct.to_string()]
125			}
126		}
127	};
128	buf
129}
130
131fn token_stream_to_toml(tokens: TokenStream2) -> String {
132	let mut buf = String::new();
133	let mut prev = None;
134
135	for token in tokens.into_iter() {
136		buf = format!["{}{}", buf, token_tree_to_toml(token.clone(), &prev)];
137		prev = Some(token);
138	}
139
140	buf
141}
142
143#[proc_macro_attribute]
144pub fn wasmir(attr: TokenStream, input: TokenStream) -> TokenStream {
145	let project_root = std::path::PathBuf::from(
146		std::env::var("CARGO_MANIFEST_DIR")
147			.expect("couldn't read CARGO_MANIFEST_DIR environment variable"),
148	);
149	let wasmir_dir = std::path::PathBuf::from(project_root.clone()).join(".wasmir");
150	create_dir_all(wasmir_dir.clone()).expect("couldn't create WASMIR temp directory");
151
152	let attr = TokenStream2::from(attr);
153	let dependencies = token_stream_to_toml(attr);
154	println!["{}", dependencies];
155
156	let input = TokenStream2::from(input);
157	let mut module_name = String::new();
158	let mut module_stream: TokenStream2 = TokenStream2::new();
159
160	for item in input.clone().into_iter() {
161		match item {
162			TokenTree::Ident(ident) => match ident.to_string().as_str() {
163				"pub" => {
164					continue;
165				}
166				"mod" => {
167					continue;
168				}
169				name => {
170					module_name = name.to_string();
171				}
172			},
173			TokenTree::Group(group) => {
174				module_stream = group.stream();
175				break;
176			}
177			_ => {
178				continue;
179			}
180		}
181	}
182
183	let module_text = module_stream.to_string();
184	let module_root = wasmir_dir.join(module_name.clone());
185	// Create the module dir
186	env::set_current_dir(wasmir_dir.clone()).expect("could not set current directory");
187	match Command::new("cargo")
188		.arg("new")
189		.arg("--lib")
190		.arg(module_name.clone())
191		.output()
192	{
193		Ok(o) => {
194			println!["{}", String::from_utf8(o.stderr).unwrap()];
195		}
196		Err(_) => {}
197	};
198
199	// attempt to write to lib.rs in module root
200	let mut file = File::create(module_root.join("src").join("lib.rs"))
201		.expect("could not open lib.rs for editing");
202	let buf: Vec<u8> = module_text.as_bytes().iter().map(|b| *b).collect();
203	file.write_all(&buf).expect("could not write to lib.rs");
204
205	// Configure the module
206	let mut buf = String::new();
207	let mut file = OpenOptions::new()
208		.write(true)
209		.read(true)
210		.open(module_root.join("Cargo.toml"))
211		.expect("no Cargo.toml in module root");
212
213	file.read_to_string(&mut buf)
214		.expect("failed to read from Cargo.toml");
215
216	let mut cargo_toml: toml::Value =
217		toml::from_str(&mut buf.as_str()).expect("failed to parse toml for module");
218
219	let cdylib: Value = Value::Array(vec![toml::Value::String("cdylib".to_string())]);
220
221	match cargo_toml.get_mut("lib") {
222		Some(Value::Table(lib)) => {
223			lib.insert(
224				"crate-type".to_string(),
225				Value::Array(vec![Value::String("cdylib".to_string())]),
226			);
227		}
228		_ => {
229			if let Some(table) = cargo_toml.as_table_mut() {
230				let mut map = toml::map::Map::new();
231				map.insert("crate-type".to_string(), cdylib);
232				let map = Value::Table(map);
233				table.insert("lib".to_string(), map);
234			}
235		}
236	}
237
238	let wasm_bindgen_dep: Value = Value::String("*".to_string());
239
240	match cargo_toml.get_mut("dependencies") {
241		Some(Value::Table(lib)) => {
242			lib.insert("wasm-bindgen".to_string(), Value::String("*".to_string()));
243		}
244		_ => {
245			if let Some(table) = cargo_toml.as_table_mut() {
246				let mut map = toml::map::Map::new();
247				map.insert("wasm-bindgen".to_string(), wasm_bindgen_dep);
248				let map = Value::Table(map);
249				table.insert("dependencies".to_string(), map);
250			}
251		}
252	}
253
254	let mut dependencies_toml: Value =
255		toml::from_str(&dependencies).expect("failed to parse dependencies toml");
256	match dependencies_toml.get_mut("dependencies") {
257		Some(Value::Table(deps)) => {
258			if let Some(Value::Table(lib_deps)) = cargo_toml.get_mut("dependencies") {
259				lib_deps.extend(deps.iter().map(|(k, v)| (k.clone(), v.clone())));
260			}
261		}
262		_ => {}
263	}
264
265	let mut file =
266		File::create(module_root.join("Cargo.toml")).expect("failed to write toml/create file");
267	file.write(&format!["{}", cargo_toml].bytes().collect::<Vec<u8>>())
268		.expect("failed to write to Cargo.toml");
269
270	// Build the module using `wasm-pack build --target web`
271	env::set_current_dir(module_root.clone()).expect("could not set current directory");
272	match Command::new("wasm-pack")
273		.arg("build")
274		.arg("--target")
275		.arg("web")
276		.output()
277	{
278		Ok(o) => {
279			println!["{}", String::from_utf8(o.stderr).unwrap()];
280		}
281		Err(e) => {
282			panic!["could not build: {}", e];
283		}
284	}
285
286	let mut file = match File::open(
287		module_root
288			.join("pkg")
289			.join(format!["{}_bg.wasm", module_name.clone()]),
290	) {
291		Ok(file) => file,
292		Err(e) => panic!["could not open binary: {}", e],
293	};
294
295	let mut binary = vec![];
296
297	file.read_to_end(&mut binary)
298		.expect("could not read-in binary");
299
300	let binary_len = binary.len();
301
302	let mut file = match File::open(
303		module_root
304			.join("pkg")
305			.join(format!["{}.js", module_name.clone()]),
306	) {
307		Ok(file) => file,
308		Err(e) => panic!["could not open js: {}", e],
309	};
310
311	let mut js = String::new();
312
313	file.read_to_string(&mut js).expect("could not read-in js");
314
315	let module_name = Ident::new(module_name.as_str(), Span::call_site());
316
317	quote![
318	 mod #module_name {
319	   #input
320		pub const wasm: [u8; #binary_len] = [#(#binary),*];
321		pub const loader: &str = #js;
322	}]
323	.into()
324}