Files
wasm-bindgen/crates/cli-support/src/webidl/standard.rs
Alex Crichton 935f71afec Switch from failure to anyhow (#1851)
This commit switches all of `wasm-bindgen` from the `failure` crate to
`anyhow`. The `anyhow` crate should serve all the purposes that we
previously used `failure` for but has a few advantages:

* It's based on the standard `Error` trait rather than a custom `Fail`
  trait, improving ecosystem compatibility.
* We don't need a `#[derive(Fail)]`, which means that's less code to
  compile for `wasm-bindgen`. This notably helps the compile time of
  `web-sys` itself.
* Using `Result<()>` in `fn main` with `anyhow::Error` produces
  human-readable output, so we can use that natively.
2019-11-04 11:35:28 -06:00

563 lines
22 KiB
Rust

//! Support for generating a standard WebIDL custom section
//!
//! This module has all the necessary support for generating a full-fledged
//! standard WebIDL custom section as defined by the `wasm-webidl-bindings`
//! crate. This module also critically assumes that the WebAssembly module
//! being generated **must be standalone**. In this mode all sorts of features
//! supported by `#[wasm_bindgen]` aren't actually supported, such as closures,
//! imports of global js names, js getters/setters, exporting structs, etc.
//! These features may all eventually come to the standard bindings proposal,
//! but it will likely take some time. In the meantime this module simply focuses
//! on taking what's already a valid wasm module and letting it through with a
//! standard WebIDL custom section. All other modules generate an error during
//! this binding process.
//!
//! Note that when this function is called and used we're also not actually
//! generating any JS glue. Any JS glue currently generated is also invalid if
//! the module contains the wasm bindings section and it's actually respected.
use crate::descriptor::VectorKind;
use crate::webidl::{AuxExportKind, AuxImport, AuxValue, JsImport, JsImportName};
use crate::webidl::{NonstandardIncoming, NonstandardOutgoing};
use crate::webidl::{NonstandardWebidlSection, WasmBindgenAux};
use anyhow::{bail, Context, Error};
use walrus::Module;
use wasm_bindgen_multi_value_xform as multi_value_xform;
use wasm_bindgen_wasm_conventions as wasm_conventions;
use wasm_webidl_bindings::ast;
pub fn add_multi_value(
module: &mut Module,
bindings: &mut NonstandardWebidlSection,
) -> Result<(), Error> {
let mut to_xform = vec![];
for (id, binding) in &bindings.exports {
if let Some(ref results) = binding.return_via_outptr {
// LLVM currently always uses the first parameter for the return
// pointer. We hard code that here, since we have no better option.
let return_pointer_index = 0;
to_xform.push((*id, return_pointer_index, &results[..]));
}
}
if to_xform.is_empty() {
// Early exit to avoid failing if we don't have a memory or shadow stack
// pointer because this is a minimal module that doesn't use linear
// memory.
return Ok(());
}
let shadow_stack_pointer = wasm_conventions::get_shadow_stack_pointer(module)?;
let memory = wasm_conventions::get_memory(module)?;
multi_value_xform::run(module, memory, shadow_stack_pointer, &to_xform)?;
// Finally, unset `return_via_outptr`, fix up its incoming bindings'
// argument numberings, and update its function type.
for (id, binding) in &mut bindings.exports {
if binding.return_via_outptr.take().is_none() {
continue;
}
if binding.incoming.is_empty() {
bail!("missing incoming binding expression for return pointer parameter");
}
if !is_ret_ptr_bindings(binding.incoming.remove(0)) {
bail!("unexpected incoming binding expression for return pointer parameter");
}
fixup_binding_argument_gets(&mut binding.incoming)?;
let func = match module.exports.get(*id).item {
walrus::ExportItem::Function(f) => f,
_ => unreachable!(),
};
binding.wasm_ty = module.funcs.get(func).ty();
// Be sure to delete the out-param pointer from the WebIDL type as well.
let webidl_ty = bindings
.types
.get::<ast::WebidlFunction>(binding.webidl_ty)
.unwrap();
let mut new_ty = webidl_ty.clone();
new_ty.params.remove(0);
binding.webidl_ty = bindings.types.insert(new_ty);
}
Ok(())
}
fn is_ret_ptr_bindings(b: NonstandardIncoming) -> bool {
match b {
NonstandardIncoming::Standard(ast::IncomingBindingExpression::As(
ast::IncomingBindingExpressionAs {
ty: walrus::ValType::I32,
expr,
},
)) => match *expr {
ast::IncomingBindingExpression::Get(ast::IncomingBindingExpressionGet { idx: 0 }) => {
true
}
_ => false,
},
_ => false,
}
}
// Since we removed the first parameter (which was the return pointer) now all
// of the `Get` binding expression's are off by one. This function fixes these
// `Get`s.
fn fixup_binding_argument_gets(incoming: &mut [NonstandardIncoming]) -> Result<(), Error> {
for inc in incoming {
fixup_nonstandard_incoming(inc)?;
}
return Ok(());
fn fixup_nonstandard_incoming(inc: &mut NonstandardIncoming) -> Result<(), Error> {
match inc {
NonstandardIncoming::Standard(s) => fixup_standard_incoming(s),
_ => bail!("found usage of non-standard bindings when in standard-bindings-only mode"),
}
}
fn fixup_standard_incoming(s: &mut ast::IncomingBindingExpression) -> Result<(), Error> {
match s {
ast::IncomingBindingExpression::Get(e) => {
if e.idx == 0 {
bail!(
"found usage of removed return pointer parameter in \
non-return pointer bindings"
);
} else {
e.idx -= 1;
Ok(())
}
}
ast::IncomingBindingExpression::As(e) => fixup_standard_incoming(&mut e.expr),
ast::IncomingBindingExpression::AllocUtf8Str(e) => fixup_standard_incoming(&mut e.expr),
ast::IncomingBindingExpression::AllocCopy(e) => fixup_standard_incoming(&mut e.expr),
ast::IncomingBindingExpression::EnumToI32(e) => fixup_standard_incoming(&mut e.expr),
ast::IncomingBindingExpression::Field(e) => fixup_standard_incoming(&mut e.expr),
ast::IncomingBindingExpression::BindImport(e) => fixup_standard_incoming(&mut e.expr),
}
}
}
pub fn add_section(
module: &mut Module,
aux: &WasmBindgenAux,
nonstandard: &NonstandardWebidlSection,
) -> Result<(), Error> {
let mut section = ast::WebidlBindings::default();
let WasmBindgenAux {
extra_typescript: _, // ignore this even if it's specified
local_modules,
snippets,
package_jsons,
export_map,
import_map,
imports_with_catch,
imports_with_variadic,
imports_with_assert_no_shim: _, // not relevant for this purpose
enums,
structs,
} = aux;
for (export, binding) in nonstandard.exports.iter() {
// First up make sure this is something that's actually valid to export
// form a vanilla WebAssembly module with WebIDL bindings.
match &export_map[export].kind {
AuxExportKind::Function(_) => {}
AuxExportKind::Constructor(name) => {
bail!(
"cannot export `{}` constructor function when generating \
a standalone WebAssembly module with no JS glue",
name,
);
}
AuxExportKind::Getter { class, field } => {
bail!(
"cannot export `{}::{}` getter function when generating \
a standalone WebAssembly module with no JS glue",
class,
field,
);
}
AuxExportKind::Setter { class, field } => {
bail!(
"cannot export `{}::{}` setter function when generating \
a standalone WebAssembly module with no JS glue",
class,
field,
);
}
AuxExportKind::StaticFunction { class, name } => {
bail!(
"cannot export `{}::{}` static function when \
generating a standalone WebAssembly module with no \
JS glue",
class,
name
);
}
AuxExportKind::Method { class, name, .. } => {
bail!(
"cannot export `{}::{}` method when \
generating a standalone WebAssembly module with no \
JS glue",
class,
name
);
}
}
let name = &module.exports.get(*export).name;
let params = extract_incoming(&binding.incoming).with_context(|| {
format!(
"failed to map arguments for export `{}` to standard \
binding expressions",
name
)
})?;
let result = extract_outgoing(&binding.outgoing).with_context(|| {
format!(
"failed to map return value for export `{}` to standard \
binding expressions",
name
)
})?;
assert!(binding.return_via_outptr.is_none());
let binding = section.bindings.insert(ast::ExportBinding {
wasm_ty: binding.wasm_ty,
webidl_ty: copy_ty(
&mut section.types,
binding.webidl_ty.into(),
&nonstandard.types,
),
params: ast::IncomingBindingMap { bindings: params },
result: ast::OutgoingBindingMap { bindings: result },
});
let func = match module.exports.get(*export).item {
walrus::ExportItem::Function(f) => f,
_ => unreachable!(),
};
section.binds.insert(ast::Bind {
func,
binding: binding.into(),
});
}
for (import, binding) in nonstandard.imports.iter() {
check_standard_import(&import_map[import])?;
let (module_name, name) = {
let import = module.imports.get(*import);
(&import.module, &import.name)
};
let params = extract_outgoing(&binding.outgoing).with_context(|| {
format!(
"failed to map arguments of import `{}::{}` to standard \
binding expressions",
module_name, name,
)
})?;
let result = extract_incoming(&binding.incoming).with_context(|| {
format!(
"failed to map return value of import `{}::{}` to standard \
binding expressions",
module_name, name,
)
})?;
assert!(binding.return_via_outptr.is_none());
let binding = section.bindings.insert(ast::ImportBinding {
wasm_ty: binding.wasm_ty,
webidl_ty: copy_ty(
&mut section.types,
binding.webidl_ty.into(),
&nonstandard.types,
),
params: ast::OutgoingBindingMap { bindings: params },
result: ast::IncomingBindingMap { bindings: result },
});
let func = match module.imports.get(*import).kind {
walrus::ImportKind::Function(f) => f,
_ => unreachable!(),
};
section.binds.insert(ast::Bind {
func,
binding: binding.into(),
});
}
if let Some((name, _)) = local_modules.iter().next() {
bail!(
"generating a bindings section is currently incompatible with \
local JS modules being specified as well, `{}` cannot be used \
since a standalone wasm file is being generated",
name,
);
}
if let Some((name, _)) = snippets.iter().filter(|(_, v)| !v.is_empty()).next() {
bail!(
"generating a bindings section is currently incompatible with \
local JS snippets being specified as well, `{}` cannot be used \
since a standalone wasm file is being generated",
name,
);
}
if let Some(path) = package_jsons.iter().next() {
bail!(
"generating a bindings section is currently incompatible with \
package.json being consumed as well, `{}` cannot be used \
since a standalone wasm file is being generated",
path.display(),
);
}
if let Some(import) = imports_with_catch.iter().next() {
let import = module.imports.get(*import);
bail!(
"generating a bindings section is currently incompatible with \
`#[wasm_bindgen(catch)]` on the `{}::{}` import because a \
a standalone wasm file is being generated",
import.module,
import.name,
);
}
if let Some(import) = imports_with_variadic.iter().next() {
let import = module.imports.get(*import);
bail!(
"generating a bindings section is currently incompatible with \
`#[wasm_bindgen(variadic)]` on the `{}::{}` import because a \
a standalone wasm file is being generated",
import.module,
import.name,
);
}
if let Some(enum_) = enums.iter().next() {
bail!(
"generating a bindings section is currently incompatible with \
exporting an `enum` from the wasm file, cannot export `{}`",
enum_.name,
);
}
if let Some(struct_) = structs.iter().next() {
bail!(
"generating a bindings section is currently incompatible with \
exporting a `struct` from the wasm file, cannot export `{}`",
struct_.name,
);
}
if nonstandard.elems.len() > 0 {
// Note that this is a pretty cryptic error message, but we in theory
// shouldn't ever hit this since closures always show up as some form
// of nonstandard binding which was previously checked.
bail!("generating a standalone wasm file requires no table element bindings");
}
module.customs.add(section);
Ok(())
}
fn extract_incoming(
nonstandard: &[NonstandardIncoming],
) -> Result<Vec<ast::IncomingBindingExpression>, Error> {
let mut exprs = Vec::new();
for expr in nonstandard {
let desc = match expr {
NonstandardIncoming::Standard(e) => {
exprs.push(e.clone());
continue;
}
NonstandardIncoming::Int64 { .. } => "64-bit integer",
NonstandardIncoming::AllocCopyInt64 { .. } => "64-bit integer array",
NonstandardIncoming::AllocCopyAnyrefArray { .. } => "array of JsValue",
NonstandardIncoming::MutableSlice { .. } => "mutable slice",
NonstandardIncoming::OptionSlice { .. } => "optional slice",
NonstandardIncoming::OptionVector { .. } => "optional vector",
NonstandardIncoming::OptionAnyref { .. } => "optional anyref",
NonstandardIncoming::OptionNative { .. } => "optional integer",
NonstandardIncoming::OptionU32Sentinel { .. } => "optional integer",
NonstandardIncoming::OptionBool { .. } => "optional bool",
NonstandardIncoming::OptionChar { .. } => "optional char",
NonstandardIncoming::OptionIntegerEnum { .. } => "optional enum",
NonstandardIncoming::OptionInt64 { .. } => "optional integer",
NonstandardIncoming::RustType { .. } => "native Rust type",
NonstandardIncoming::RustTypeRef { .. } => "reference to Rust type",
NonstandardIncoming::OptionRustType { .. } => "optional Rust type",
NonstandardIncoming::Char { .. } => "character",
NonstandardIncoming::BorrowedAnyref { .. } => "borrowed anyref",
};
bail!(
"cannot represent {} with a standard bindings expression",
desc
);
}
Ok(exprs)
}
fn extract_outgoing(
nonstandard: &[NonstandardOutgoing],
) -> Result<Vec<ast::OutgoingBindingExpression>, Error> {
let mut exprs = Vec::new();
for expr in nonstandard {
let desc = match expr {
NonstandardOutgoing::Standard(e) => {
exprs.push(e.clone());
continue;
}
// ... yeah ... let's just leak strings
// see comment at top of this module about returning strings for
// what this is doing and why it's weird
NonstandardOutgoing::Vector {
offset,
length,
kind: VectorKind::String,
} => {
exprs.push(
ast::OutgoingBindingExpressionUtf8Str {
offset: *offset,
length: *length,
ty: ast::WebidlScalarType::DomString.into(),
}
.into(),
);
continue;
}
NonstandardOutgoing::RustType { .. } => "rust type",
NonstandardOutgoing::Char { .. } => "character",
NonstandardOutgoing::Number64 { .. } => "64-bit integer",
NonstandardOutgoing::BorrowedAnyref { .. } => "borrowed anyref",
NonstandardOutgoing::Vector { .. } => "vector",
NonstandardOutgoing::CachedString { .. } => "cached string",
NonstandardOutgoing::View64 { .. } => "64-bit slice",
NonstandardOutgoing::ViewAnyref { .. } => "anyref slice",
NonstandardOutgoing::OptionVector { .. } => "optional vector",
NonstandardOutgoing::OptionSlice { .. } => "optional slice",
NonstandardOutgoing::OptionNative { .. } => "optional integer",
NonstandardOutgoing::OptionU32Sentinel { .. } => "optional integer",
NonstandardOutgoing::OptionBool { .. } => "optional boolean",
NonstandardOutgoing::OptionChar { .. } => "optional character",
NonstandardOutgoing::OptionIntegerEnum { .. } => "optional enum",
NonstandardOutgoing::OptionInt64 { .. } => "optional 64-bit integer",
NonstandardOutgoing::OptionRustType { .. } => "optional rust type",
NonstandardOutgoing::StackClosure { .. } => "closures",
};
bail!(
"cannot represent {} with a standard bindings expression",
desc
);
}
Ok(exprs)
}
/// Recursively clones `ty` into` dst` where it originally indexes values in
/// `src`, returning a new type ref which indexes inside of `dst`.
pub fn copy_ty(
dst: &mut ast::WebidlTypes,
ty: ast::WebidlTypeRef,
src: &ast::WebidlTypes,
) -> ast::WebidlTypeRef {
let id = match ty {
ast::WebidlTypeRef::Id(id) => id,
ast::WebidlTypeRef::Scalar(_) => return ty,
};
let ty: &ast::WebidlCompoundType = src.get(id).unwrap();
match ty {
ast::WebidlCompoundType::Function(f) => {
let params = f
.params
.iter()
.map(|param| copy_ty(dst, *param, src))
.collect();
let result = f.result.map(|ty| copy_ty(dst, ty, src));
dst.insert(ast::WebidlFunction {
kind: f.kind.clone(),
params,
result,
})
.into()
}
_ => unimplemented!(),
}
}
fn check_standard_import(import: &AuxImport) -> Result<(), Error> {
let desc_js = |js: &JsImport| {
let mut extra = String::new();
for field in js.fields.iter() {
extra.push_str(".");
extra.push_str(field);
}
match &js.name {
JsImportName::Global { name } | JsImportName::VendorPrefixed { name, .. } => {
format!("global `{}{}`", name, extra)
}
JsImportName::Module { module, name } => {
format!("`{}{}` from '{}'", name, extra, module)
}
JsImportName::LocalModule { module, name } => {
format!("`{}{}` from local module '{}'", name, extra, module)
}
JsImportName::InlineJs {
unique_crate_identifier,
name,
..
} => format!(
"`{}{}` from inline js in '{}'",
name, extra, unique_crate_identifier
),
}
};
let item = match import {
AuxImport::Value(AuxValue::Bare(js)) => {
if js.fields.len() == 0 {
if let JsImportName::Module { .. } = js.name {
return Ok(());
}
}
desc_js(js)
}
AuxImport::Value(AuxValue::Getter(js, name))
| AuxImport::Value(AuxValue::Setter(js, name))
| AuxImport::Value(AuxValue::ClassGetter(js, name))
| AuxImport::Value(AuxValue::ClassSetter(js, name)) => {
format!("field access of `{}` for {}", name, desc_js(js))
}
AuxImport::ValueWithThis(js, method) => format!("method `{}.{}`", desc_js(js), method),
AuxImport::Instanceof(js) => format!("instance of check of {}", desc_js(js)),
AuxImport::Static(js) => format!("static js value {}", desc_js(js)),
AuxImport::StructuralMethod(name) => format!("structural method `{}`", name),
AuxImport::StructuralGetter(name)
| AuxImport::StructuralSetter(name)
| AuxImport::StructuralClassGetter(_, name)
| AuxImport::StructuralClassSetter(_, name) => {
format!("structural field access of `{}`", name)
}
AuxImport::IndexingDeleterOfClass(_)
| AuxImport::IndexingDeleterOfObject
| AuxImport::IndexingGetterOfClass(_)
| AuxImport::IndexingGetterOfObject
| AuxImport::IndexingSetterOfClass(_)
| AuxImport::IndexingSetterOfObject => format!("indexing getters/setters/deleters"),
AuxImport::WrapInExportedClass(name) => {
format!("wrapping a pointer in a `{}` js class wrapper", name)
}
AuxImport::Intrinsic(intrinsic) => {
format!("wasm-bindgen specific intrinsic `{}`", intrinsic.name())
}
AuxImport::Closure { .. } => format!("creating a `Closure` wrapper"),
};
bail!(
"cannot generate a standalone WebAssembly module which \
contains an import of {} since it requires JS glue",
item
);
}