Skip to content

Commit 1c11c46

Browse files
authored
Merge pull request #132 from rustwasm/closure-more-fun-types
Support closures with "rich" arguments
2 parents c1df441 + c64f178 commit 1c11c46

File tree

7 files changed

+577
-466
lines changed

7 files changed

+577
-466
lines changed

README.md

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,8 @@ booted.then(main);
399399

400400
## Closures
401401

402-
The `#[wasm_bindgen]` attribute supports a limited subset of Rust closures being
403-
passed to JS at this time. There are plans to expand this support currently but
404-
it's not clear how to proceed unfortunately. In any case some examples of what
405-
you can do are:
402+
The `#[wasm_bindgen]` attribute supports some Rust closures being passed to JS.
403+
Examples of what you can do are:
406404

407405
```rust
408406
#[wasm_bindgen]
@@ -416,26 +414,23 @@ closure*. You can call this function with a `&Fn()` argument and JS will receive
416414
a JS function. When the `foo` function returns, however, the JS function will be
417415
invalidated and any future usage of it will raise an exception.
418416

419-
Closures also support arguments and return values native to the wasm type
420-
system, aka f32/u32:
417+
Closures also support arguments and return values like exports do, for example:
421418

422419
```rust
423420
#[wasm_bindgen]
424421
extern {
425-
fn bar(a: &Fn(u32, f32) -> f64);
422+
type Foo;
423+
424+
fn bar(a: &Fn(u32, String) -> Foo);
426425
}
427426
```
428427

429-
At this time [types like strings aren't supported][cbstr] unfortunately.
430-
431-
[cbstr]: https://github.com/rustwasm/wasm-bindgen/issues/104
432-
433428
Sometimes the stack behavior of these closures is not desired. For example you'd
434429
like to schedule a closure to be run on the next turn of the event loop in JS
435430
through `setTimeout`. For this you want the imported function to return but the
436431
JS closure still needs to be valid!
437432

438-
To support this use case you can also do:
433+
To support this use case you can do:
439434

440435
```rust
441436
use wasm_bindgen::prelude::*;
@@ -463,8 +458,6 @@ extern {
463458
}
464459
```
465460

466-
Like stack closures, however, only wasm types like u32/f32 are supported today.
467-
468461
At this time you cannot [pass a JS closure to Rust][cbjs], you can only pass a
469462
Rust closure to JS in limited circumstances.
470463

crates/cli-support/src/descriptor.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ pub enum Descriptor {
4848
F64,
4949
Boolean,
5050
Function(Box<Function>),
51-
Closure(Box<Function>),
51+
Closure(Box<Function>, bool),
5252
Ref(Box<Descriptor>),
5353
RefMut(Box<Descriptor>),
5454
Slice(Box<Descriptor>),
@@ -101,8 +101,9 @@ impl Descriptor {
101101
BOOLEAN => Descriptor::Boolean,
102102
FUNCTION => Descriptor::Function(Box::new(Function::decode(data))),
103103
CLOSURE => {
104+
let mutable = get(data) == REFMUT;
104105
assert_eq!(get(data), FUNCTION);
105-
Descriptor::Closure(Box::new(Function::decode(data)))
106+
Descriptor::Closure(Box::new(Function::decode(data)), mutable)
106107
}
107108
REF => Descriptor::Ref(Box::new(Descriptor::_decode(data))),
108109
REFMUT => Descriptor::RefMut(Box::new(Descriptor::_decode(data))),
@@ -152,16 +153,16 @@ impl Descriptor {
152153
}
153154
}
154155

155-
pub fn ref_closure(&self) -> Option<&Function> {
156+
pub fn ref_closure(&self) -> Option<(&Function, bool)> {
156157
match *self {
157158
Descriptor::Ref(ref s) => s.closure(),
158159
_ => None,
159160
}
160161
}
161162

162-
pub fn closure(&self) -> Option<&Function> {
163+
pub fn closure(&self) -> Option<(&Function, bool)> {
163164
match *self {
164-
Descriptor::Closure(ref s) => Some(s),
165+
Descriptor::Closure(ref s, mutable) => Some((s, mutable)),
165166
_ => None,
166167
}
167168
}

crates/cli-support/src/js/js2rust.rs

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
use super::Context;
2+
use descriptor::{Descriptor, Function};
3+
4+
/// Helper struct for manfuacturing a shim in JS used to translate JS types to
5+
/// Rust, aka pass from JS back into Rust
6+
pub struct Js2Rust<'a, 'b: 'a> {
7+
cx: &'a mut Context<'b>,
8+
9+
/// Arguments passed to the invocation of the wasm function, aka things that
10+
/// are only numbers.
11+
rust_arguments: Vec<String>,
12+
13+
/// Arguments and their types to the JS shim.
14+
js_arguments: Vec<(String, String)>,
15+
16+
/// Conversions that happen before we invoke the wasm function, such as
17+
/// converting a string to a ptr/length pair.
18+
prelude: String,
19+
20+
/// "Destructors" or cleanup that must happen after the wasm function
21+
/// finishes. This is scheduled in a `finally` block.
22+
finally: String,
23+
24+
/// Next global index to write to when passing arguments via the single
25+
/// global stack.
26+
global_idx: usize,
27+
28+
/// Index of the next argument for unique name generation purposes.
29+
arg_idx: usize,
30+
31+
/// Typescript expression representing the type of the return value of this
32+
/// function.
33+
ret_ty: String,
34+
35+
/// Expression used to generate the return value. The string "RET" in this
36+
/// expression is replaced with the actual wasm invocation eventually.
37+
ret_expr: String,
38+
39+
/// Name of the JS shim/function that we're generating, primarily for
40+
/// TypeScript right now.
41+
js_name: String,
42+
}
43+
44+
impl<'a, 'b> Js2Rust<'a, 'b> {
45+
pub fn new(js_name: &str, cx: &'a mut Context<'b>) -> Js2Rust<'a, 'b> {
46+
Js2Rust {
47+
cx,
48+
js_name: js_name.to_string(),
49+
rust_arguments: Vec::new(),
50+
js_arguments: Vec::new(),
51+
prelude: String::new(),
52+
finally: String::new(),
53+
global_idx: 0,
54+
arg_idx: 0,
55+
ret_ty: String::new(),
56+
ret_expr: String::new(),
57+
}
58+
}
59+
60+
/// Generates all bindings necessary for the signature in `Function`,
61+
/// creating necessary argument conversions and return value processing.
62+
pub fn process(&mut self, function: &Function) -> &mut Self {
63+
for arg in function.arguments.iter() {
64+
self.argument(arg);
65+
}
66+
self.ret(&function.ret);
67+
self
68+
}
69+
70+
/// Flag this shim as a method call into Rust, so the first Rust argument
71+
/// passed should be `this.ptr`.
72+
pub fn method(&mut self, method: bool) -> &mut Self {
73+
if method {
74+
self.rust_arguments.insert(0, "this.ptr".to_string());
75+
}
76+
self
77+
}
78+
79+
/// Add extra processing to the prelude of this shim.
80+
pub fn prelude(&mut self, s: &str) -> &mut Self {
81+
self.prelude.push_str(s);
82+
self
83+
}
84+
85+
/// Add extra processing to the finally block of this shim.
86+
pub fn finally(&mut self, s: &str) -> &mut Self {
87+
self.finally.push_str(s);
88+
self
89+
}
90+
91+
/// Add an Rust argument to be passed manually.
92+
pub fn rust_argument(&mut self, s: &str) -> &mut Self {
93+
self.rust_arguments.push(s.to_string());
94+
self
95+
}
96+
97+
fn argument(&mut self, arg: &Descriptor) {
98+
let i = self.arg_idx;
99+
self.arg_idx += 1;
100+
let name = format!("arg{}", i);
101+
102+
if let Some(kind) = arg.vector_kind() {
103+
self.js_arguments.push((name.clone(), kind.js_ty().to_string()));
104+
105+
let func = self.cx.pass_to_wasm_function(kind);
106+
self.cx.expose_set_global_argument();
107+
let global_idx = self.global_idx();
108+
self.prelude.push_str(&format!("\
109+
const [ptr{i}, len{i}] = {func}({arg});
110+
setGlobalArgument(len{i}, {global_idx});
111+
", i = i, func = func, arg = name, global_idx = global_idx));
112+
if arg.is_by_ref() {
113+
self.finally.push_str(&format!("\n\
114+
wasm.__wbindgen_free(ptr{i}, len{i} * {size});\n\
115+
", i = i, size = kind.size()));
116+
self.cx.required_internal_exports.insert(
117+
"__wbindgen_free",
118+
);
119+
}
120+
self.rust_arguments.push(format!("ptr{}", i));
121+
return
122+
}
123+
124+
if let Some(s) = arg.rust_struct() {
125+
self.js_arguments.push((name.clone(), s.to_string()));
126+
127+
if self.cx.config.debug {
128+
self.cx.expose_assert_class();
129+
self.prelude.push_str(&format!("\
130+
_assertClass({arg}, {struct_});
131+
", arg = name, struct_ = s));
132+
}
133+
134+
if arg.is_by_ref() {
135+
self.rust_arguments.push(format!("{}.ptr", name));
136+
} else {
137+
self.prelude.push_str(&format!("\
138+
const ptr{i} = {arg}.ptr;
139+
{arg}.ptr = 0;
140+
", i = i, arg = name));
141+
self.rust_arguments.push(format!("ptr{}", i));
142+
}
143+
return
144+
}
145+
146+
if arg.is_number() {
147+
self.js_arguments.push((name.clone(), "number".to_string()));
148+
149+
if self.cx.config.debug {
150+
self.cx.expose_assert_num();
151+
self.prelude.push_str(&format!("_assertNum({});\n", name));
152+
}
153+
154+
self.rust_arguments.push(name);
155+
return
156+
}
157+
158+
if arg.is_ref_anyref() {
159+
self.js_arguments.push((name.clone(), "any".to_string()));
160+
self.cx.expose_borrowed_objects();
161+
self.finally.push_str("stack.pop();\n");
162+
self.rust_arguments.push(format!("addBorrowedObject({})", name));
163+
return
164+
}
165+
166+
match *arg {
167+
Descriptor::Boolean => {
168+
self.js_arguments.push((name.clone(), "boolean".to_string()));
169+
if self.cx.config.debug {
170+
self.cx.expose_assert_bool();
171+
self.prelude.push_str(&format!("\
172+
_assertBoolean({name});
173+
", name = name));
174+
}
175+
self.rust_arguments.push(format!("arg{i} ? 1 : 0", i = i));
176+
}
177+
Descriptor::Anyref => {
178+
self.js_arguments.push((name.clone(), "any".to_string()));
179+
self.cx.expose_add_heap_object();
180+
self.rust_arguments.push(format!("addHeapObject({})", name));
181+
}
182+
_ => {
183+
panic!("unsupported argument to rust function {:?}", arg)
184+
}
185+
}
186+
}
187+
188+
fn ret(&mut self, ret: &Option<Descriptor>) {
189+
let ty = match *ret {
190+
Some(ref t) => t,
191+
None => {
192+
self.ret_ty = "void".to_string();
193+
self.ret_expr = format!("return RET;");
194+
return
195+
}
196+
};
197+
198+
if ty.is_ref_anyref() {
199+
self.ret_ty = "any".to_string();
200+
self.cx.expose_get_object();
201+
self.ret_expr = format!("return getObject(RET);");
202+
return
203+
}
204+
205+
if ty.is_by_ref() {
206+
panic!("cannot return references from Rust to JS yet")
207+
}
208+
209+
if let Some(ty) = ty.vector_kind() {
210+
self.ret_ty = ty.js_ty().to_string();
211+
let f = self.cx.expose_get_vector_from_wasm(ty);
212+
self.cx.expose_get_global_argument();
213+
self.cx.required_internal_exports.insert("__wbindgen_free");
214+
self.ret_expr = format!("
215+
const ret = RET;
216+
const len = getGlobalArgument(0);
217+
const realRet = {}(ret, len);
218+
wasm.__wbindgen_free(ret, len * {});
219+
return realRet;
220+
", f, ty.size());
221+
return
222+
}
223+
224+
if let Some(name) = ty.rust_struct() {
225+
self.ret_ty = name.to_string();
226+
self.ret_expr = format!("return {name}.__construct(RET);", name = name);
227+
return
228+
}
229+
230+
if ty.is_number() {
231+
self.ret_ty = "number".to_string();
232+
self.ret_expr = format!("return RET;");
233+
return
234+
}
235+
236+
match *ty {
237+
Descriptor::Boolean => {
238+
self.ret_ty = "boolean".to_string();
239+
self.ret_expr = format!("return (RET) !== 0;");
240+
}
241+
Descriptor::Anyref => {
242+
self.ret_ty = "any".to_string();
243+
self.cx.expose_take_object();
244+
self.ret_expr = format!("return takeObject(RET);");
245+
}
246+
_ => panic!("unsupported return from Rust to JS {:?}", ty),
247+
}
248+
}
249+
250+
/// Generate the actual function.
251+
///
252+
/// The `prefix` specified is typically the string "function" but may be
253+
/// different for classes. The `invoc` is the function expression that we're
254+
/// invoking, like `wasm.bar` or `this.f`.
255+
///
256+
/// Returns two strings, the first of which is the JS expression for the
257+
/// generated function shim and the second is a TyepScript signature of rthe
258+
/// JS expression.
259+
pub fn finish(&self, prefix: &str, invoc: &str) -> (String, String) {
260+
let js_args = self.js_arguments
261+
.iter()
262+
.map(|s| &s.0[..])
263+
.collect::<Vec<_>>()
264+
.join(", ");
265+
let mut js = format!("{}({}) {{\n", prefix, js_args);
266+
js.push_str(&self.prelude);
267+
let rust_args = self.rust_arguments.join(", ");
268+
269+
let invoc = self.ret_expr.replace("RET", &format!("{}({})", invoc, rust_args));
270+
if self.finally.len() == 0 {
271+
js.push_str(&invoc);
272+
} else {
273+
js.push_str(&format!("\
274+
try {{
275+
{}
276+
}} finally {{
277+
{}
278+
}}
279+
",
280+
invoc,
281+
self.finally,
282+
));
283+
}
284+
js.push_str("}");
285+
286+
let ts_args = self.js_arguments
287+
.iter()
288+
.map(|s| format!("{}: {}", s.0, s.1))
289+
.collect::<Vec<_>>()
290+
.join(", ");
291+
let ts = format!("{} {}({}): {};\n", prefix, self.js_name, ts_args, self.ret_ty);
292+
(js, ts)
293+
}
294+
295+
fn global_idx(&mut self) -> usize {
296+
let ret = self.global_idx;
297+
self.global_idx += 1;
298+
ret
299+
}
300+
}

0 commit comments

Comments
 (0)