Skip to content
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

Make wasm-bindgen compatible with the standard C ABI #3595

Merged
merged 7 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
When exported constructors return `Self`.
[#3562](https://github.com/rustwasm/wasm-bindgen/pull/3562)

* Made `wasm-bindgen` forwards-compatible with the standard C ABI.
[#3595](https://github.com/rustwasm/wasm-bindgen/pull/3595)
Liamolucko marked this conversation as resolved.
Show resolved Hide resolved

### Fixed

* Fixed bindings and comments for `Atomics.wait`.
Expand Down
124 changes: 86 additions & 38 deletions crates/backend/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::encode;
use crate::Diagnostic;
use once_cell::sync::Lazy;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::format_ident;
use quote::quote_spanned;
use quote::{quote, ToTokens};
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -142,15 +143,15 @@ impl TryToTokens for ast::LinkToModule {
let link_function_name = self.0.link_function_name(0);
let name = Ident::new(&link_function_name, Span::call_site());
let wasm_bindgen = &self.0.wasm_bindgen;
let abi_ret = quote! { <std::string::String as #wasm_bindgen::convert::FromWasmAbi>::Abi };
let abi_ret = quote! { #wasm_bindgen::convert::WasmRet<<std::string::String as #wasm_bindgen::convert::FromWasmAbi>::Abi> };
let extern_fn = extern_fn(&name, &[], &[], &[], abi_ret);
(quote! {
{
#program
#extern_fn

unsafe {
<std::string::String as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#name())
<std::string::String as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#name().join())
}
}
})
Expand Down Expand Up @@ -401,7 +402,7 @@ impl ToTokens for ast::StructField {
#[cfg_attr(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))), no_mangle)]
#[doc(hidden)]
pub unsafe extern "C" fn #getter(js: u32)
-> <#ty as #wasm_bindgen::convert::IntoWasmAbi>::Abi
-> #wasm_bindgen::convert::WasmRet<<#ty as #wasm_bindgen::convert::IntoWasmAbi>::Abi>
{
use #wasm_bindgen::__rt::{WasmRefCell, assert_not_null};
use #wasm_bindgen::convert::IntoWasmAbi;
Expand All @@ -412,7 +413,7 @@ impl ToTokens for ast::StructField {
let js = js as *mut WasmRefCell<#struct_name>;
assert_not_null(js);
let val = #val;
<#ty as IntoWasmAbi>::into_abi(val)
<#ty as IntoWasmAbi>::into_abi(val).into()
}
};
})
Expand All @@ -432,6 +433,9 @@ impl ToTokens for ast::StructField {
return;
}

let abi = quote! { <#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi };
let (args, names) = splat(wasm_bindgen, &Ident::new("val", rust_name.span()), &abi);

(quote! {
#[cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))]
#[automatically_derived]
Expand All @@ -440,13 +444,14 @@ impl ToTokens for ast::StructField {
#[doc(hidden)]
pub unsafe extern "C" fn #setter(
js: u32,
val: <#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi,
#(#args,)*
) {
use #wasm_bindgen::__rt::{WasmRefCell, assert_not_null};
use #wasm_bindgen::convert::FromWasmAbi;

let js = js as *mut WasmRefCell<#struct_name>;
assert_not_null(js);
let val = <#abi as #wasm_bindgen::convert::WasmAbi>::join(#(#names),*);
let val = <#ty as FromWasmAbi>::from_abi(val);
(*js).borrow_mut().#rust_name = val;
}
Expand Down Expand Up @@ -525,52 +530,61 @@ impl TryToTokens for ast::Export {
elem,
..
}) => {
args.push(quote! {
#ident: <#elem as #wasm_bindgen::convert::RefMutFromWasmAbi>::Abi
});
let abi = quote! { <#elem as #wasm_bindgen::convert::RefMutFromWasmAbi>::Abi };
let (prim_args, prim_names) = splat(wasm_bindgen, &ident, &abi);
args.extend(prim_args);
arg_conversions.push(quote! {
let mut #ident = unsafe {
<#elem as #wasm_bindgen::convert::RefMutFromWasmAbi>
::ref_mut_from_abi(#ident)
::ref_mut_from_abi(
<#abi as #wasm_bindgen::convert::WasmAbi>::join(#(#prim_names),*)
)
};
let #ident = &mut *#ident;
});
}
syn::Type::Reference(syn::TypeReference { elem, .. }) => {
if self.function.r#async {
args.push(quote! {
#ident: <#elem as #wasm_bindgen::convert::LongRefFromWasmAbi>::Abi
});
let abi =
quote! { <#elem as #wasm_bindgen::convert::LongRefFromWasmAbi>::Abi };
let (prim_args, prim_names) = splat(wasm_bindgen, &ident, &abi);
args.extend(prim_args);
arg_conversions.push(quote! {
let #ident = unsafe {
<#elem as #wasm_bindgen::convert::LongRefFromWasmAbi>
::long_ref_from_abi(#ident)
::long_ref_from_abi(
<#abi as #wasm_bindgen::convert::WasmAbi>::join(#(#prim_names),*)
)
};
let #ident = <<#elem as #wasm_bindgen::convert::LongRefFromWasmAbi>
::Anchor as core::borrow::Borrow<#elem>>
::borrow(&#ident);
});
} else {
args.push(quote! {
#ident: <#elem as #wasm_bindgen::convert::RefFromWasmAbi>::Abi
});
let abi = quote! { <#elem as #wasm_bindgen::convert::RefFromWasmAbi>::Abi };
let (prim_args, prim_names) = splat(wasm_bindgen, &ident, &abi);
args.extend(prim_args);
arg_conversions.push(quote! {
let #ident = unsafe {
<#elem as #wasm_bindgen::convert::RefFromWasmAbi>
::ref_from_abi(#ident)
::ref_from_abi(
<#abi as #wasm_bindgen::convert::WasmAbi>::join(#(#prim_names),*)
)
};
let #ident = &*#ident;
});
}
}
_ => {
args.push(quote! {
#ident: <#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi
});
let abi = quote! { <#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi };
let (prim_args, prim_names) = splat(wasm_bindgen, &ident, &abi);
args.extend(prim_args);
arg_conversions.push(quote! {
let #ident = unsafe {
<#ty as #wasm_bindgen::convert::FromWasmAbi>
::from_abi(#ident)
::from_abi(
<#abi as #wasm_bindgen::convert::WasmAbi>::join(#(#prim_names),*)
)
};
});
}
Expand Down Expand Up @@ -642,7 +656,7 @@ impl TryToTokens for ast::Export {
}

let projection = quote! { <#ret_ty as #wasm_bindgen::convert::ReturnWasmAbi> };
let convert_ret = quote! { #projection::return_abi(#ret) };
let convert_ret = quote! { #projection::return_abi(#ret).into() };
let describe_ret = quote! {
<#ret_ty as WasmDescribe>::describe();
<#inner_ret_ty as WasmDescribe>::describe();
Expand All @@ -664,7 +678,7 @@ impl TryToTokens for ast::Export {
all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))),
export_name = #export_name,
)]
pub unsafe extern "C" fn #generated_name(#(#args),*) -> #projection::Abi {
pub unsafe extern "C" fn #generated_name(#(#args),*) -> #wasm_bindgen::convert::WasmRet<#projection::Abi> {
#start_check

let #ret = #call;
Expand Down Expand Up @@ -1147,10 +1161,11 @@ impl TryToTokens for ast::ImportFunction {
),
};

abi_argument_names.push(name.clone());
abi_arguments.push(quote! {
#name: <#ty as #wasm_bindgen::convert::IntoWasmAbi>::Abi
});
let abi = quote! { <#ty as #wasm_bindgen::convert::IntoWasmAbi>::Abi };
let (prim_args, prim_names) = splat(wasm_bindgen, &name, &abi);
abi_arguments.extend(prim_args);
abi_argument_names.extend(prim_names.iter().cloned());

let var = if i == 0 && is_method {
quote! { self }
} else {
Expand All @@ -1160,6 +1175,7 @@ impl TryToTokens for ast::ImportFunction {
arg_conversions.push(quote! {
let #name = <#ty as #wasm_bindgen::convert::IntoWasmAbi>
::into_abi(#var);
let (#(#prim_names),*) = <#abi as #wasm_bindgen::convert::WasmAbi>::split(#name);
});
}
let abi_ret;
Expand All @@ -1173,12 +1189,13 @@ impl TryToTokens for ast::ImportFunction {
}
Some(ref ty) => {
if self.function.r#async {
abi_ret =
quote! { <js_sys::Promise as #wasm_bindgen::convert::FromWasmAbi>::Abi };
abi_ret = quote! {
#wasm_bindgen::convert::WasmRet<<js_sys::Promise as #wasm_bindgen::convert::FromWasmAbi>::Abi>
};
let future = quote! {
#wasm_bindgen_futures::JsFuture::from(
<js_sys::Promise as #wasm_bindgen::convert::FromWasmAbi>
::from_abi(#ret_ident)
::from_abi(#ret_ident.join())
).await
};
convert_ret = if self.catch {
Expand All @@ -1188,22 +1205,23 @@ impl TryToTokens for ast::ImportFunction {
};
} else {
abi_ret = quote! {
<#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi
#wasm_bindgen::convert::WasmRet<<#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi>
};
convert_ret = quote! {
<#ty as #wasm_bindgen::convert::FromWasmAbi>
::from_abi(#ret_ident)
::from_abi(#ret_ident.join())
};
}
}
None => {
if self.function.r#async {
abi_ret =
quote! { <js_sys::Promise as #wasm_bindgen::convert::FromWasmAbi>::Abi };
abi_ret = quote! {
#wasm_bindgen::convert::WasmRet<<js_sys::Promise as #wasm_bindgen::convert::FromWasmAbi>::Abi>
};
let future = quote! {
#wasm_bindgen_futures::JsFuture::from(
<js_sys::Promise as #wasm_bindgen::convert::FromWasmAbi>
::from_abi(#ret_ident)
::from_abi(#ret_ident.join())
).await
};
convert_ret = if self.catch {
Expand Down Expand Up @@ -1421,23 +1439,27 @@ impl ToTokens for ast::ImportStatic {
let shim_name = &self.shim;
let vis = &self.vis;
let wasm_bindgen = &self.wasm_bindgen;

let abi_ret = quote! {
#wasm_bindgen::convert::WasmRet<<#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi>
};
(quote! {
#[automatically_derived]
#vis static #name: #wasm_bindgen::JsStatic<#ty> = {
fn init() -> #ty {
#[link(wasm_import_module = "__wbindgen_placeholder__")]
#[cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))]
extern "C" {
fn #shim_name() -> <#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi;
fn #shim_name() -> #abi_ret;
}

#[cfg(not(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi")))))]
unsafe fn #shim_name() -> <#ty as wasm_bindgen::convert::FromWasmAbi>::Abi {
unsafe fn #shim_name() -> #abi_ret {
panic!("cannot access imported statics on non-wasm targets")
}

unsafe {
<#ty as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#shim_name())
<#ty as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#shim_name().join())
}
}
thread_local!(static _VAL: #ty = init(););
Expand Down Expand Up @@ -1540,6 +1562,32 @@ fn extern_fn(
}
}

/// Splats an argument with the given name and ABI type into 4 arguments, one
/// for each primitive that the ABI type splits into.
///
/// Returns an `(args, names)` pair, where `args` is the list of arguments to
/// be inserted into the function signature, and `names` is a list of the names
/// of those arguments.
fn splat(
wasm_bindgen: &syn::Path,
name: &Ident,
abi: &TokenStream,
) -> (Vec<TokenStream>, Vec<Ident>) {
let mut args = Vec::new();
let mut names = Vec::new();

for n in 1..=4 {
let arg_name = format_ident!("{name}_{n}");
let prim_name = format_ident!("Prim{n}");
args.push(quote! {
#arg_name: <#abi as #wasm_bindgen::convert::WasmAbi>::#prim_name
});
names.push(arg_name);
}

(args, names)
}

/// Converts `span` into a stream of tokens, and attempts to ensure that `input`
/// has all the appropriate span information so errors in it point to `span`.
fn respan(input: TokenStream, span: &dyn ToTokens) -> TokenStream {
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/tests/reference/anyref-import-catch.wat
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
(type (;3;) (func (param i32) (result i32)))
(import "./reference_test_bg.js" "__wbindgen_init_externref_table" (func (;0;) (type 0)))
(func $__wbindgen_exn_store (;1;) (type 2) (param i32))
(func $__externref_table_dealloc (;2;) (type 2) (param i32))
(func $exported (;3;) (type 2) (param i32))
(func $exported (;2;) (type 2) (param i32))
(func $__externref_table_dealloc (;3;) (type 2) (param i32))
(func $__externref_table_alloc (;4;) (type 1) (result i32))
(func $__wbindgen_add_to_stack_pointer (;5;) (type 3) (param i32) (result i32))
(table (;0;) 128 externref)
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/tests/reference/builder.wat
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32)))
(func $__wbg_classbuilder_free (;0;) (type 1) (param i32))
(func $classbuilder_builder (;1;) (type 0) (result i32))
(func $classbuilder_builder (;0;) (type 0) (result i32))
(func $__wbg_classbuilder_free (;1;) (type 1) (param i32))
(memory (;0;) 17)
(export "memory" (memory 0))
(export "__wbg_classbuilder_free" (func $__wbg_classbuilder_free))
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/tests/reference/constructor.wat
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32)))
(func $__wbg_classconstructor_free (;0;) (type 1) (param i32))
(func $classconstructor_new (;1;) (type 0) (result i32))
(func $classconstructor_new (;0;) (type 0) (result i32))
(func $__wbg_classconstructor_free (;1;) (type 1) (param i32))
(memory (;0;) 17)
(export "memory" (memory 0))
(export "__wbg_classconstructor_free" (func $__wbg_classconstructor_free))
Expand Down
32 changes: 24 additions & 8 deletions guide/src/contributing/design/rust-type-conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,41 @@ a Rust value to a JS one. There's a few points here:

* We'll get to `WasmDescribe` later in this section.

* The associated type `Abi` is what will actually be generated as an argument /
return type for the `extern "C"` functions used to declare wasm imports/exports.
* The associated type `Abi` is the type of the raw data that we actually want to pass to JS.
The bound `WasmAbi` is implemented for primitive types like `u32` and `f64`,
which can be represented directly as WebAssembly values, as well of a couple
of `#[repr(C)]` types like `WasmSlice`:
of other types like `WasmSlice`:

```rust
#[repr(C)]
pub struct WasmSlice {
pub ptr: u32,
pub len: u32,
}
```

This struct, which is how things like strings are represented in FFI, isn't
a WebAssembly primitive type and so isn't mapped directly to a WebAssembly
parameter / return value; instead, the C ABI flattens it out into two arguments
or stores it on the stack.
a WebAssembly primitive type, and so it can't be mapped directly to a
WebAssembly parameter / return value. This is why `WasmAbi` lets types specify
how they can be split up into multiple WebAssembly parameters:

```rust
impl WasmAbi for WasmSlice {
fn split(self) -> (u32, u32, (), ()) {
(self.ptr, self.len, (), ())
}

// some other details to specify return type of `split`, go in the other direction
}
```

This means that a `WasmSlice` gets split up into two `u32` parameters.
The extra unit types on the end are there because Rust doesn't let us make
`WasmAbi` generic over variable-length tuples, so we just take tuples of 4
elements. The unit types still end up getting passed to/from JS, but the C ABI
just completely ignores them and doesn't generate any arguments.

Since we can't return multiple values, when returning a `WasmSlice` we instead
put the two `u32`s into a `#[repr(C)]` struct and return that.

* And finally we have the `into_abi` function, returning the `Abi` associated
type which will be actually passed to JS.
Expand Down Expand Up @@ -94,4 +111,3 @@ and remain anonymous.
The `From*` family of traits are used for converting the Rust arguments in Rust
exported functions to JS. They are also used for the return value in JS
functions imported into Rust.