Skip to content

Commit df1b591

Browse files
authored
Add exporting Rust functions as variadic JS functions (#2954)
* add variadic argument into function structures * bind variadic function last argument * comment variadic assertions * add variadic functions tests * fix variadic var name * add new schema file hash in the shared crate * add variadic param in JS doc * remove assert_not_variadic references * add variadic TS tests * update variadic docs
1 parent ebe6587 commit df1b591

File tree

14 files changed

+114
-29
lines changed

14 files changed

+114
-29
lines changed

crates/backend/src/ast.rs

+2
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ pub struct Function {
303303
pub r#async: bool,
304304
/// Whether to generate a typescript definition for this function
305305
pub generate_typescript: bool,
306+
/// Whether this is a function with a variadict parameter
307+
pub variadic: bool,
306308
}
307309

308310
/// Information about a Struct being exported

crates/backend/src/encode.rs

+1
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Functi
212212
asyncness: func.r#async,
213213
name: &func.name,
214214
generate_typescript: func.generate_typescript,
215+
variadic: func.variadic,
215216
}
216217
}
217218

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

+41-4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ impl<'a, 'b> Builder<'a, 'b> {
102102
instructions: &[InstructionData],
103103
explicit_arg_names: &Option<Vec<String>>,
104104
asyncness: bool,
105+
variadic: bool,
105106
) -> Result<JsFunction, Error> {
106107
if self
107108
.cx
@@ -193,7 +194,17 @@ impl<'a, 'b> Builder<'a, 'b> {
193194

194195
let mut code = String::new();
195196
code.push_str("(");
196-
code.push_str(&function_args.join(", "));
197+
if variadic {
198+
if let Some((last, non_variadic_args)) = function_args.split_last() {
199+
code.push_str(&non_variadic_args.join(", "));
200+
if non_variadic_args.len() > 0 {
201+
code.push_str(", ");
202+
}
203+
code.push_str((String::from("...") + last).as_str())
204+
}
205+
} else {
206+
code.push_str(&function_args.join(", "));
207+
}
197208
code.push_str(") {\n");
198209

199210
let mut call = js.prelude;
@@ -227,8 +238,9 @@ impl<'a, 'b> Builder<'a, 'b> {
227238
&adapter.inner_results,
228239
&mut might_be_optional_field,
229240
asyncness,
241+
variadic,
230242
);
231-
let js_doc = self.js_doc_comments(&function_args, &arg_tys, &ts_ret_ty);
243+
let js_doc = self.js_doc_comments(&function_args, &arg_tys, &ts_ret_ty, variadic);
232244

233245
Ok(JsFunction {
234246
code,
@@ -254,6 +266,7 @@ impl<'a, 'b> Builder<'a, 'b> {
254266
result_tys: &[AdapterType],
255267
might_be_optional_field: &mut bool,
256268
asyncness: bool,
269+
variadic: bool,
257270
) -> (String, Vec<String>, Option<String>) {
258271
// Build up the typescript signature as well
259272
let mut omittable = true;
@@ -284,7 +297,19 @@ impl<'a, 'b> Builder<'a, 'b> {
284297
}
285298
ts_args.reverse();
286299
ts_arg_tys.reverse();
287-
let mut ts = format!("({})", ts_args.join(", "));
300+
let mut ts = String::from("(");
301+
if variadic {
302+
if let Some((last, non_variadic_args)) = ts_args.split_last() {
303+
ts.push_str(&non_variadic_args.join(", "));
304+
if non_variadic_args.len() > 0 {
305+
ts.push_str(", ");
306+
}
307+
ts.push_str((String::from("...") + last).as_str())
308+
}
309+
} else {
310+
ts.push_str(&format!("{}", ts_args.join(", ")));
311+
};
312+
ts.push_str(")");
288313

289314
// If this function is an optional field's setter, it should have only
290315
// one arg, and omittable should be `true`.
@@ -318,15 +343,27 @@ impl<'a, 'b> Builder<'a, 'b> {
318343
arg_names: &[String],
319344
arg_tys: &[&AdapterType],
320345
ts_ret: &Option<String>,
346+
variadic: bool,
321347
) -> String {
322348
let mut ret = String::new();
323-
for (name, ty) in arg_names.iter().zip(arg_tys) {
349+
let (variadic_arg, fn_arg_names) = match arg_names.split_last() {
350+
Some((last, args)) if variadic => (Some(last), args),
351+
_ => (None, arg_names),
352+
};
353+
for (name, ty) in fn_arg_names.iter().zip(arg_tys) {
324354
ret.push_str("@param {");
325355
adapter2ts(ty, &mut ret);
326356
ret.push_str("} ");
327357
ret.push_str(name);
328358
ret.push_str("\n");
329359
}
360+
if let (Some(name), Some(ty)) = (variadic_arg, arg_tys.last()) {
361+
ret.push_str("@param {...");
362+
adapter2ts(ty, &mut ret);
363+
ret.push_str("} ");
364+
ret.push_str(name);
365+
ret.push_str("\n");
366+
}
330367
if let Some(ts) = ts_ret {
331368
if ts != "void" {
332369
ret.push_str(&format!("@returns {{{}}}", ts));

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -2511,10 +2511,12 @@ impl<'a> Context<'a> {
25112511
builder.catch(catch);
25122512
let mut arg_names = &None;
25132513
let mut asyncness = false;
2514+
let mut variadic = false;
25142515
match kind {
25152516
Kind::Export(export) => {
25162517
arg_names = &export.arg_names;
25172518
asyncness = export.asyncness;
2519+
variadic = export.variadic;
25182520
match &export.kind {
25192521
AuxExportKind::Function(_) => {}
25202522
AuxExportKind::StaticFunction { .. } => {}
@@ -2539,7 +2541,7 @@ impl<'a> Context<'a> {
25392541
catch,
25402542
log_error,
25412543
} = builder
2542-
.process(&adapter, instrs, arg_names, asyncness)
2544+
.process(&adapter, instrs, arg_names, asyncness, variadic)
25432545
.with_context(|| match kind {
25442546
Kind::Export(e) => format!("failed to generate bindings for `{}`", e.debug_name),
25452547
Kind::Import(i) => {

crates/cli-support/src/wit/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ impl<'a> Context<'a> {
464464
asyncness: export.function.asyncness,
465465
kind,
466466
generate_typescript: export.function.generate_typescript,
467+
variadic: export.function.variadic,
467468
},
468469
);
469470
Ok(())
@@ -822,6 +823,7 @@ impl<'a> Context<'a> {
822823
consumed: false,
823824
},
824825
generate_typescript: field.generate_typescript,
826+
variadic: false,
825827
},
826828
);
827829

@@ -851,6 +853,7 @@ impl<'a> Context<'a> {
851853
consumed: false,
852854
},
853855
generate_typescript: field.generate_typescript,
856+
variadic: false,
854857
},
855858
);
856859
}
@@ -1085,6 +1088,7 @@ impl<'a> Context<'a> {
10851088
asyncness: false,
10861089
kind,
10871090
generate_typescript: true,
1091+
variadic: false,
10881092
};
10891093
assert!(self.aux.export_map.insert(id, export).is_none());
10901094
}

crates/cli-support/src/wit/nonstandard.rs

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ pub struct AuxExport {
7979
pub kind: AuxExportKind,
8080
/// Whether typescript bindings should be generated for this export.
8181
pub generate_typescript: bool,
82+
/// Whether typescript bindings should be generated for this export.
83+
pub variadic: bool,
8284
}
8385

8486
/// All possible kinds of exports from a wasm module.

crates/macro-support/src/parser.rs

+2-14
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,6 @@ impl<'a> ConvertToAst<BindgenAttrs> for &'a mut syn::ItemStruct {
411411
};
412412

413413
let attrs = BindgenAttrs::find(&mut field.attrs)?;
414-
assert_not_variadic(&attrs)?;
415414
if attrs.skip().is_some() {
416415
attrs.check_used()?;
417416
continue;
@@ -627,7 +626,6 @@ impl ConvertToAst<BindgenAttrs> for syn::ForeignItemType {
627626
type Target = ast::ImportKind;
628627

629628
fn convert(self, attrs: BindgenAttrs) -> Result<Self::Target, Diagnostic> {
630-
assert_not_variadic(&attrs)?;
631629
let js_name = attrs
632630
.js_name()
633631
.map(|s| s.0)
@@ -678,7 +676,7 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a ast::ImportModule)> for syn::ForeignIte
678676
if self.mutability.is_some() {
679677
bail_span!(self.mutability, "cannot import mutable globals yet")
680678
}
681-
assert_not_variadic(&opts)?;
679+
682680
let default_name = self.ident.to_string();
683681
let js_name = opts
684682
.js_name()
@@ -718,7 +716,6 @@ impl ConvertToAst<BindgenAttrs> for syn::ItemFn {
718716
if self.sig.unsafety.is_some() {
719717
bail_span!(self.sig.unsafety, "can only #[wasm_bindgen] safe functions");
720718
}
721-
assert_not_variadic(&attrs)?;
722719

723720
let ret = function_from_decl(
724721
&self.sig.ident,
@@ -859,6 +856,7 @@ fn function_from_decl(
859856
rust_vis: vis,
860857
r#async: sig.asyncness.is_some(),
861858
generate_typescript: opts.skip_typescript().is_none(),
859+
variadic: opts.variadic().is_some(),
862860
},
863861
method_self,
864862
))
@@ -1558,16 +1556,6 @@ fn assert_no_lifetimes(sig: &syn::Signature) -> Result<(), Diagnostic> {
15581556
Diagnostic::from_vec(walk.diagnostics)
15591557
}
15601558

1561-
/// This method always fails if the BindgenAttrs contain variadic
1562-
fn assert_not_variadic(attrs: &BindgenAttrs) -> Result<(), Diagnostic> {
1563-
if let Some(span) = attrs.variadic() {
1564-
let msg = "the `variadic` attribute can only be applied to imported \
1565-
(`extern`) functions";
1566-
return Err(Diagnostic::span_error(*span, msg));
1567-
}
1568-
Ok(())
1569-
}
1570-
15711559
/// Extracts the last ident from the path
15721560
fn extract_path_ident(path: &syn::Path) -> Result<Ident, Diagnostic> {
15731561
for segment in path.segments.iter() {

crates/shared/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ macro_rules! shared_api {
118118
asyncness: bool,
119119
name: &'a str,
120120
generate_typescript: bool,
121+
variadic: bool,
121122
}
122123

123124
struct Struct<'a> {

crates/shared/src/schema_hash_approval.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// If the schema in this library has changed then:
99
// 1. Bump the version in `crates/shared/Cargo.toml`
1010
// 2. Change the `SCHEMA_VERSION` in this library to this new Cargo.toml version
11-
const APPROVED_SCHEMA_FILE_HASH: &'static str = "3468290064813615840";
11+
const APPROVED_SCHEMA_FILE_HASH: &'static str = "16056751188521403565";
1212

1313
#[test]
1414
fn schema_version() {

crates/typescript-tests/src/simple_fn.rs

+5
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ pub fn greet(_: &str) {}
77
pub fn take_and_return_bool(_: bool) -> bool {
88
true
99
}
10+
11+
#[wasm_bindgen(variadic)]
12+
pub fn variadic_function(arr: &JsValue) -> JsValue {
13+
arr.into()
14+
}

crates/typescript-tests/src/simple_fn.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ import * as wasm from '../pkg/typescript_tests_bg.wasm';
44
const wbg_greet: (a: string) => void = wbg.greet;
55
const wasm_greet: (a: number, b: number) => void = wasm.greet;
66
const take_and_return_bool: (a: boolean) => boolean = wbg.take_and_return_bool;
7+
const wbg_variadic_function: (...arr: any) => any = wbg.variadic_function;
8+
const wasm_variadic_function: (arr: number) => number = wasm.variadic_function;

guide/src/reference/attributes/on-js-imports/variadic.md

+15
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,18 @@ extern "C" {
3636

3737
when we call this function, the last argument will be expanded as the javascript expects.
3838

39+
40+
To export a rust function to javascript with a variadic argument, we will use the same bindgen variadic attribute and assume that the last argument will be the variadic array. For example the following rust function:
41+
42+
```rust
43+
#[wasm_bindgen(variadic)]
44+
pub fn variadic_function(arr: &JsValue) -> JsValue {
45+
arr.into()
46+
}
47+
```
48+
49+
will generate the following TS interface
50+
51+
```ts
52+
export function variadic_function(...arr: any): any;
53+
```

tests/wasm/api.js

+21-9
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,36 @@ exports.js_works = () => {
2020
assert.strictEqual(wasm.api_get_false(), false);
2121
wasm.api_test_bool(true, false, 1.0);
2222

23-
assert.strictEqual(typeof(wasm.api_mk_symbol()), 'symbol');
24-
assert.strictEqual(typeof(wasm.api_mk_symbol2('a')), 'symbol');
23+
assert.strictEqual(typeof (wasm.api_mk_symbol()), 'symbol');
24+
assert.strictEqual(typeof (wasm.api_mk_symbol2('a')), 'symbol');
2525
assert.strictEqual(Symbol.keyFor(wasm.api_mk_symbol()), undefined);
2626
assert.strictEqual(Symbol.keyFor(wasm.api_mk_symbol2('b')), undefined);
2727

2828
wasm.api_assert_symbols(Symbol(), 'a');
2929
wasm.api_acquire_string('foo', null);
3030
assert.strictEqual(wasm.api_acquire_string2(''), '');
3131
assert.strictEqual(wasm.api_acquire_string2('a'), 'a');
32+
33+
let arr = [1, 2, 3, 4, {}, ['a', 'b', 'c']]
34+
wasm.api_completely_variadic(...arr).forEach((element, index) => {
35+
assert.strictEqual(element, arr[index]);
36+
});
37+
assert.strictEqual(
38+
wasm.api_completely_variadic().length,
39+
0
40+
);
41+
wasm.api_variadic_with_prefixed_params([], {}, ...arr).forEach((element, index) => {
42+
assert.strictEqual(element, arr[index]);
43+
});
3244
};
3345

3446
exports.js_eq_works = () => {
3547
assert.strictEqual(wasm.eq_test('a', 'a'), true);
3648
assert.strictEqual(wasm.eq_test('a', 'b'), false);
3749
assert.strictEqual(wasm.eq_test(NaN, NaN), false);
38-
assert.strictEqual(wasm.eq_test({a: 'a'}, {a: 'a'}), false);
50+
assert.strictEqual(wasm.eq_test({ a: 'a' }, { a: 'a' }), false);
3951
assert.strictEqual(wasm.eq_test1(NaN), false);
40-
let x = {a: 'a'};
52+
let x = { a: 'a' };
4153
assert.strictEqual(wasm.eq_test(x, x), true);
4254
assert.strictEqual(wasm.eq_test1(x), true);
4355
};
@@ -48,16 +60,16 @@ exports.debug_values = () => ([
4860
0,
4961
1.0,
5062
true,
51-
[1,2,3],
63+
[1, 2, 3],
5264
"string",
53-
{test: "object"},
65+
{ test: "object" },
5466
[1.0, [2.0, 3.0]],
5567
() => (null),
5668
new Set(),
5769
]);
5870

5971
exports.assert_function_table = (x, i) => {
60-
const rawWasm = require('wasm-bindgen-test.js').__wasm;
61-
assert.ok(x instanceof WebAssembly.Table);
62-
assert.strictEqual(x.get(i), rawWasm.function_table_lookup);
72+
const rawWasm = require('wasm-bindgen-test.js').__wasm;
73+
assert.ok(x instanceof WebAssembly.Table);
74+
assert.strictEqual(x.get(i), rawWasm.function_table_lookup);
6375
};

tests/wasm/api.rs

+14
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ pub fn eq_test1(a: &JsValue) -> bool {
126126
a == a
127127
}
128128

129+
#[wasm_bindgen(variadic)]
130+
pub fn api_completely_variadic(args: &JsValue) -> JsValue {
131+
args.into()
132+
}
133+
134+
#[wasm_bindgen(variadic)]
135+
pub fn api_variadic_with_prefixed_params(
136+
first: &JsValue,
137+
second: &JsValue,
138+
args: &JsValue,
139+
) -> JsValue {
140+
args.into()
141+
}
142+
129143
#[wasm_bindgen_test]
130144
fn null_keeps_working() {
131145
assert_null(JsValue::null());

0 commit comments

Comments
 (0)