Skip to content

Add exporting Rust functions as variadic JS functions #2954

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 22, 2022
2 changes: 2 additions & 0 deletions crates/backend/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ pub struct Function {
pub r#async: bool,
/// Whether to generate a typescript definition for this function
pub generate_typescript: bool,
/// Whether this is a function with a variadict parameter
pub variadic: bool,
}

/// Information about a Struct being exported
Expand Down
1 change: 1 addition & 0 deletions crates/backend/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Functi
asyncness: func.r#async,
name: &func.name,
generate_typescript: func.generate_typescript,
variadic: func.variadic,
}
}

Expand Down
45 changes: 41 additions & 4 deletions crates/cli-support/src/js/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl<'a, 'b> Builder<'a, 'b> {
instructions: &[InstructionData],
explicit_arg_names: &Option<Vec<String>>,
asyncness: bool,
variadic: bool,
) -> Result<JsFunction, Error> {
if self
.cx
Expand Down Expand Up @@ -193,7 +194,17 @@ impl<'a, 'b> Builder<'a, 'b> {

let mut code = String::new();
code.push_str("(");
code.push_str(&function_args.join(", "));
if variadic {
if let Some((last, non_variadic_args)) = function_args.split_last() {
code.push_str(&non_variadic_args.join(", "));
if non_variadic_args.len() > 0 {
code.push_str(", ");
}
code.push_str((String::from("...") + last).as_str())
}
} else {
code.push_str(&function_args.join(", "));
}
code.push_str(") {\n");

let mut call = js.prelude;
Expand Down Expand Up @@ -227,8 +238,9 @@ impl<'a, 'b> Builder<'a, 'b> {
&adapter.inner_results,
&mut might_be_optional_field,
asyncness,
variadic,
);
let js_doc = self.js_doc_comments(&function_args, &arg_tys, &ts_ret_ty);
let js_doc = self.js_doc_comments(&function_args, &arg_tys, &ts_ret_ty, variadic);

Ok(JsFunction {
code,
Expand All @@ -254,6 +266,7 @@ impl<'a, 'b> Builder<'a, 'b> {
result_tys: &[AdapterType],
might_be_optional_field: &mut bool,
asyncness: bool,
variadic: bool,
) -> (String, Vec<String>, Option<String>) {
// Build up the typescript signature as well
let mut omittable = true;
Expand Down Expand Up @@ -284,7 +297,19 @@ impl<'a, 'b> Builder<'a, 'b> {
}
ts_args.reverse();
ts_arg_tys.reverse();
let mut ts = format!("({})", ts_args.join(", "));
let mut ts = String::from("(");
if variadic {
if let Some((last, non_variadic_args)) = ts_args.split_last() {
ts.push_str(&non_variadic_args.join(", "));
if non_variadic_args.len() > 0 {
ts.push_str(", ");
}
ts.push_str((String::from("...") + last).as_str())
}
} else {
ts.push_str(&format!("{}", ts_args.join(", ")));
};
ts.push_str(")");

// If this function is an optional field's setter, it should have only
// one arg, and omittable should be `true`.
Expand Down Expand Up @@ -318,15 +343,27 @@ impl<'a, 'b> Builder<'a, 'b> {
arg_names: &[String],
arg_tys: &[&AdapterType],
ts_ret: &Option<String>,
variadic: bool,
) -> String {
let mut ret = String::new();
for (name, ty) in arg_names.iter().zip(arg_tys) {
let (variadic_arg, fn_arg_names) = match arg_names.split_last() {
Some((last, args)) if variadic => (Some(last), args),
_ => (None, arg_names),
};
for (name, ty) in fn_arg_names.iter().zip(arg_tys) {
ret.push_str("@param {");
adapter2ts(ty, &mut ret);
ret.push_str("} ");
ret.push_str(name);
ret.push_str("\n");
}
if let (Some(name), Some(ty)) = (variadic_arg, arg_tys.last()) {
ret.push_str("@param {...");
adapter2ts(ty, &mut ret);
ret.push_str("} ");
ret.push_str(name);
ret.push_str("\n");
}
if let Some(ts) = ts_ret {
if ts != "void" {
ret.push_str(&format!("@returns {{{}}}", ts));
Expand Down
4 changes: 3 additions & 1 deletion crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2511,10 +2511,12 @@ impl<'a> Context<'a> {
builder.catch(catch);
let mut arg_names = &None;
let mut asyncness = false;
let mut variadic = false;
match kind {
Kind::Export(export) => {
arg_names = &export.arg_names;
asyncness = export.asyncness;
variadic = export.variadic;
match &export.kind {
AuxExportKind::Function(_) => {}
AuxExportKind::StaticFunction { .. } => {}
Expand All @@ -2539,7 +2541,7 @@ impl<'a> Context<'a> {
catch,
log_error,
} = builder
.process(&adapter, instrs, arg_names, asyncness)
.process(&adapter, instrs, arg_names, asyncness, variadic)
.with_context(|| match kind {
Kind::Export(e) => format!("failed to generate bindings for `{}`", e.debug_name),
Kind::Import(i) => {
Expand Down
4 changes: 4 additions & 0 deletions crates/cli-support/src/wit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ impl<'a> Context<'a> {
asyncness: export.function.asyncness,
kind,
generate_typescript: export.function.generate_typescript,
variadic: export.function.variadic,
},
);
Ok(())
Expand Down Expand Up @@ -822,6 +823,7 @@ impl<'a> Context<'a> {
consumed: false,
},
generate_typescript: field.generate_typescript,
variadic: false,
},
);

Expand Down Expand Up @@ -851,6 +853,7 @@ impl<'a> Context<'a> {
consumed: false,
},
generate_typescript: field.generate_typescript,
variadic: false,
},
);
}
Expand Down Expand Up @@ -1085,6 +1088,7 @@ impl<'a> Context<'a> {
asyncness: false,
kind,
generate_typescript: true,
variadic: false,
};
assert!(self.aux.export_map.insert(id, export).is_none());
}
Expand Down
2 changes: 2 additions & 0 deletions crates/cli-support/src/wit/nonstandard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub struct AuxExport {
pub kind: AuxExportKind,
/// Whether typescript bindings should be generated for this export.
pub generate_typescript: bool,
/// Whether typescript bindings should be generated for this export.
pub variadic: bool,
}

/// All possible kinds of exports from a wasm module.
Expand Down
16 changes: 2 additions & 14 deletions crates/macro-support/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,6 @@ impl<'a> ConvertToAst<BindgenAttrs> for &'a mut syn::ItemStruct {
};

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

fn convert(self, attrs: BindgenAttrs) -> Result<Self::Target, Diagnostic> {
assert_not_variadic(&attrs)?;
let js_name = attrs
.js_name()
.map(|s| s.0)
Expand Down Expand Up @@ -678,7 +676,7 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a ast::ImportModule)> for syn::ForeignIte
if self.mutability.is_some() {
bail_span!(self.mutability, "cannot import mutable globals yet")
}
assert_not_variadic(&opts)?;

let default_name = self.ident.to_string();
let js_name = opts
.js_name()
Expand Down Expand Up @@ -718,7 +716,6 @@ impl ConvertToAst<BindgenAttrs> for syn::ItemFn {
if self.sig.unsafety.is_some() {
bail_span!(self.sig.unsafety, "can only #[wasm_bindgen] safe functions");
}
assert_not_variadic(&attrs)?;

let ret = function_from_decl(
&self.sig.ident,
Expand Down Expand Up @@ -859,6 +856,7 @@ fn function_from_decl(
rust_vis: vis,
r#async: sig.asyncness.is_some(),
generate_typescript: opts.skip_typescript().is_none(),
variadic: opts.variadic().is_some(),
},
method_self,
))
Expand Down Expand Up @@ -1558,16 +1556,6 @@ fn assert_no_lifetimes(sig: &syn::Signature) -> Result<(), Diagnostic> {
Diagnostic::from_vec(walk.diagnostics)
}

/// This method always fails if the BindgenAttrs contain variadic
fn assert_not_variadic(attrs: &BindgenAttrs) -> Result<(), Diagnostic> {
if let Some(span) = attrs.variadic() {
let msg = "the `variadic` attribute can only be applied to imported \
(`extern`) functions";
return Err(Diagnostic::span_error(*span, msg));
}
Ok(())
}

/// Extracts the last ident from the path
fn extract_path_ident(path: &syn::Path) -> Result<Ident, Diagnostic> {
for segment in path.segments.iter() {
Expand Down
1 change: 1 addition & 0 deletions crates/shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ macro_rules! shared_api {
asyncness: bool,
name: &'a str,
generate_typescript: bool,
variadic: bool,
}

struct Struct<'a> {
Expand Down
2 changes: 1 addition & 1 deletion crates/shared/src/schema_hash_approval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// If the schema in this library has changed then:
// 1. Bump the version in `crates/shared/Cargo.toml`
// 2. Change the `SCHEMA_VERSION` in this library to this new Cargo.toml version
const APPROVED_SCHEMA_FILE_HASH: &'static str = "3468290064813615840";
const APPROVED_SCHEMA_FILE_HASH: &'static str = "16056751188521403565";

#[test]
fn schema_version() {
Expand Down
5 changes: 5 additions & 0 deletions crates/typescript-tests/src/simple_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ pub fn greet(_: &str) {}
pub fn take_and_return_bool(_: bool) -> bool {
true
}

#[wasm_bindgen(variadic)]
pub fn variadic_function(arr: &JsValue) -> JsValue {
arr.into()
}
2 changes: 2 additions & 0 deletions crates/typescript-tests/src/simple_fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ import * as wasm from '../pkg/typescript_tests_bg.wasm';
const wbg_greet: (a: string) => void = wbg.greet;
const wasm_greet: (a: number, b: number) => void = wasm.greet;
const take_and_return_bool: (a: boolean) => boolean = wbg.take_and_return_bool;
const wbg_variadic_function: (...arr: any) => any = wbg.variadic_function;
const wasm_variadic_function: (arr: number) => number = wasm.variadic_function;
15 changes: 15 additions & 0 deletions guide/src/reference/attributes/on-js-imports/variadic.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ extern "C" {

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


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:

```rust
#[wasm_bindgen(variadic)]
pub fn variadic_function(arr: &JsValue) -> JsValue {
arr.into()
}
```

will generate the following TS interface

```ts
export function variadic_function(...arr: any): any;
```
30 changes: 21 additions & 9 deletions tests/wasm/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,36 @@ exports.js_works = () => {
assert.strictEqual(wasm.api_get_false(), false);
wasm.api_test_bool(true, false, 1.0);

assert.strictEqual(typeof(wasm.api_mk_symbol()), 'symbol');
assert.strictEqual(typeof(wasm.api_mk_symbol2('a')), 'symbol');
assert.strictEqual(typeof (wasm.api_mk_symbol()), 'symbol');
assert.strictEqual(typeof (wasm.api_mk_symbol2('a')), 'symbol');
assert.strictEqual(Symbol.keyFor(wasm.api_mk_symbol()), undefined);
assert.strictEqual(Symbol.keyFor(wasm.api_mk_symbol2('b')), undefined);

wasm.api_assert_symbols(Symbol(), 'a');
wasm.api_acquire_string('foo', null);
assert.strictEqual(wasm.api_acquire_string2(''), '');
assert.strictEqual(wasm.api_acquire_string2('a'), 'a');

let arr = [1, 2, 3, 4, {}, ['a', 'b', 'c']]
wasm.api_completely_variadic(...arr).forEach((element, index) => {
assert.strictEqual(element, arr[index]);
});
assert.strictEqual(
wasm.api_completely_variadic().length,
0
);
wasm.api_variadic_with_prefixed_params([], {}, ...arr).forEach((element, index) => {
assert.strictEqual(element, arr[index]);
});
};

exports.js_eq_works = () => {
assert.strictEqual(wasm.eq_test('a', 'a'), true);
assert.strictEqual(wasm.eq_test('a', 'b'), false);
assert.strictEqual(wasm.eq_test(NaN, NaN), false);
assert.strictEqual(wasm.eq_test({a: 'a'}, {a: 'a'}), false);
assert.strictEqual(wasm.eq_test({ a: 'a' }, { a: 'a' }), false);
assert.strictEqual(wasm.eq_test1(NaN), false);
let x = {a: 'a'};
let x = { a: 'a' };
assert.strictEqual(wasm.eq_test(x, x), true);
assert.strictEqual(wasm.eq_test1(x), true);
};
Expand All @@ -48,16 +60,16 @@ exports.debug_values = () => ([
0,
1.0,
true,
[1,2,3],
[1, 2, 3],
"string",
{test: "object"},
{ test: "object" },
[1.0, [2.0, 3.0]],
() => (null),
new Set(),
]);

exports.assert_function_table = (x, i) => {
const rawWasm = require('wasm-bindgen-test.js').__wasm;
assert.ok(x instanceof WebAssembly.Table);
assert.strictEqual(x.get(i), rawWasm.function_table_lookup);
const rawWasm = require('wasm-bindgen-test.js').__wasm;
assert.ok(x instanceof WebAssembly.Table);
assert.strictEqual(x.get(i), rawWasm.function_table_lookup);
};
14 changes: 14 additions & 0 deletions tests/wasm/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ pub fn eq_test1(a: &JsValue) -> bool {
a == a
}

#[wasm_bindgen(variadic)]
pub fn api_completely_variadic(args: &JsValue) -> JsValue {
args.into()
}

#[wasm_bindgen(variadic)]
pub fn api_variadic_with_prefixed_params(
first: &JsValue,
second: &JsValue,
args: &JsValue,
) -> JsValue {
args.into()
}

#[wasm_bindgen_test]
fn null_keeps_working() {
assert_null(JsValue::null());
Expand Down