415 lines
15 KiB
Rust
Raw Normal View History

Rewrite wasm-bindgen with updated interface types proposal (#1882) This commit is a pretty large scale rewrite of the internals of wasm-bindgen. No user-facing changes are expected as a result of this PR, but due to the scale of changes here it's likely inevitable that at least something will break. I'm hoping to get more testing in though before landing! The purpose of this PR is to update wasm-bindgen to the current state of the interface types proposal. The wasm-bindgen tool was last updated when it was still called "WebIDL bindings" so it's been awhile! All support is now based on https://github.com/bytecodealliance/wasm-interface-types which defines parsers/binary format/writers/etc for wasm-interface types. This is a pretty massive PR and unfortunately can't really be split up any more afaik. I don't really expect realistic review of all the code here (or commits), but some high-level changes are: * Interface types now consists of a set of "adapter functions". The IR in wasm-bindgen is modeled the same way not. * Each adapter function has a list of instructions, and these instructions work at a higher level than wasm itself, for example with strings. * The wasm-bindgen tool has a suite of instructions which are specific to it and not present in the standard. (like before with webidl bindings) * The anyref/multi-value transformations are now greatly simplified. They're simply "optimization passes" over adapter functions, removing instructions that are otherwise present. This way we don't have to juggle so much all over the place, and instructions always have the same meaning.
2019-12-03 11:16:44 -06:00
//! Support for generating a standard wasm interface types
//!
//! This module has all the necessary support for generating a full-fledged
//! standard wasm interface types section as defined by the `wit_walrus`
//! 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::wit::{AdapterId, AdapterJsImportKind, AdapterType, Instruction};
use crate::wit::{AdapterKind, NonstandardWitSection, WasmBindgenAux};
use crate::wit::{AuxExport, InstructionData};
use crate::wit::{AuxExportKind, AuxImport, AuxValue, JsImport, JsImportName};
use anyhow::{anyhow, bail, Context, Error};
use std::collections::HashMap;
use walrus::Module;
pub fn add(
module: &mut Module,
aux: &WasmBindgenAux,
nonstandard: &NonstandardWitSection,
) -> Result<(), Error> {
let mut section = wit_walrus::WasmInterfaceTypes::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,
anyref_table: _, // not relevant
anyref_alloc: _, // not relevant
anyref_drop_slice: _, // not relevant
} = aux;
let adapter_context = |id: AdapterId| {
if let Some((name, _)) = nonstandard.exports.iter().find(|p| p.1 == id) {
return format!("in function export `{}`", name);
}
if let Some((core, _)) = nonstandard.implements.iter().find(|p| p.1 == id) {
let import = module.imports.get(*core);
return format!(
"in function import from `{}::{}`",
import.module, import.name
);
}
unreachable!()
};
let mut us2walrus = HashMap::new();
for (us, func) in nonstandard.adapters.iter() {
if let Some(export) = export_map.get(us) {
check_standard_export(export).context(adapter_context(*us))?;
}
if let Some(import) = import_map.get(us) {
check_standard_import(import).context(adapter_context(*us))?;
}
let params = translate_tys(&func.params).context(adapter_context(*us))?;
let results = translate_tys(&func.results).context(adapter_context(*us))?;
let ty = section.types.add(params, results);
let walrus = match &func.kind {
AdapterKind::Local { .. } => section.funcs.add_local(ty, Vec::new()),
AdapterKind::Import {
module,
name,
kind: AdapterJsImportKind::Normal,
} => section.add_import_func(module, name, ty).0,
AdapterKind::Import {
module,
name,
kind: AdapterJsImportKind::Constructor,
} => {
bail!(
"interfaces types doesn't support import of `{}::{}` \
as a constructor",
module,
name
);
}
AdapterKind::Import {
module,
name,
kind: AdapterJsImportKind::Method,
} => {
bail!(
"interfaces types doesn't support import of `{}::{}` \
as a method",
module,
name
);
}
};
us2walrus.insert(*us, walrus);
}
for (core, adapter) in nonstandard.implements.iter() {
let core = match module.imports.get(*core).kind {
walrus::ImportKind::Function(f) => f,
_ => bail!("cannot implement a non-function"),
};
section.implements.add(us2walrus[adapter], core);
}
for (name, adapter) in nonstandard.exports.iter() {
section.exports.add(name, us2walrus[adapter]);
}
for (id, func) in nonstandard.adapters.iter() {
let instructions = match &func.kind {
AdapterKind::Local { instructions } => instructions,
AdapterKind::Import { .. } => continue,
};
let result = match &mut section.funcs.get_mut(us2walrus[id]).kind {
wit_walrus::FuncKind::Local(i) => i,
_ => unreachable!(),
};
for instruction in instructions {
result.push(
translate_instruction(instruction, &us2walrus, module)
.with_context(|| adapter_context(*id))?,
);
}
}
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(id) = imports_with_catch.iter().next() {
bail!(
"{}\ngenerating a bindings section is currently incompatible with \
`#[wasm_bindgen(catch)]`",
adapter_context(*id),
);
}
if let Some(id) = imports_with_variadic.iter().next() {
bail!(
"{}\ngenerating a bindings section is currently incompatible with \
`#[wasm_bindgen(variadic)]`",
adapter_context(*id),
);
}
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,
);
}
module.customs.add(section);
Ok(())
}
fn translate_instruction(
instr: &InstructionData,
us2walrus: &HashMap<AdapterId, wit_walrus::FuncId>,
module: &Module,
) -> Result<wit_walrus::Instruction, Error> {
use Instruction::*;
match &instr.instr {
Standard(s) => Ok(s.clone()),
CallAdapter(id) => {
let id = us2walrus[id];
Ok(wit_walrus::Instruction::CallAdapter(id))
}
CallExport(e) => match module.exports.get(*e).item {
walrus::ExportItem::Function(f) => Ok(wit_walrus::Instruction::CallCore(f)),
_ => bail!("can only call exported functions"),
},
CallTableElement(e) => {
let table = module
.tables
.main_function_table()?
.ok_or_else(|| anyhow!("no function table found in module"))?;
let functions = match &module.tables.get(table).kind {
walrus::TableKind::Function(f) => f,
_ => unreachable!(),
};
match functions.elements.get(*e as usize) {
Some(Some(f)) => Ok(wit_walrus::Instruction::CallCore(*f)),
_ => bail!("expected to find an element of the function table"),
}
}
StoreRetptr { .. } | LoadRetptr { .. } | Retptr => {
bail!("return pointers aren't supported in wasm interface types");
}
I32FromBool | BoolFromI32 => {
bail!("booleans aren't supported in wasm interface types");
}
I32FromStringFirstChar | StringFromChar => {
bail!("chars aren't supported in wasm interface types");
}
I32FromAnyrefOwned | I32FromAnyrefBorrow | AnyrefLoadOwned | TableGet => {
bail!("anyref pass failed to sink into wasm module");
}
I32FromAnyrefRustOwned { .. } | I32FromAnyrefRustBorrow { .. } | RustFromI32 { .. } => {
bail!("rust types aren't supported in wasm interface types");
}
I32Split64 { .. } | I64FromLoHi { .. } => {
bail!("64-bit integers aren't supported in wasm-bindgen");
}
I32SplitOption64 { .. }
| I32FromOptionAnyref
| I32FromOptionU32Sentinel
| I32FromOptionRust { .. }
| I32FromOptionBool
| I32FromOptionChar
| I32FromOptionEnum { .. }
| FromOptionNative { .. }
| OptionVector { .. }
| OptionRustFromI32 { .. }
| OptionVectorLoad { .. }
| OptionView { .. }
| OptionU32Sentinel
| ToOptionNative { .. }
| OptionBoolFromI32
| OptionCharFromI32
| OptionEnumFromI32 { .. }
| Option64FromI32 { .. } => {
bail!("optional types aren't supported in wasm bindgen");
}
MutableSliceToMemory { .. } | VectorToMemory { .. } | VectorLoad { .. } | View { .. } => {
bail!("vector slices aren't supported in wasm interface types yet");
}
CachedStringLoad { .. } => {
bail!("cached strings aren't supported in wasm interface types");
}
StackClosure { .. } => {
bail!("closures aren't supported in wasm interface types");
}
}
}
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
);
}
fn check_standard_export(export: &AuxExport) -> Result<(), Error> {
// First up make sure this is something that's actually valid to export
// form a vanilla WebAssembly module with WebIDL bindings.
match &export.kind {
AuxExportKind::Function(_) => Ok(()),
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
);
}
}
}
fn translate_tys(tys: &[AdapterType]) -> Result<Vec<wit_walrus::ValType>, Error> {
tys.iter()
.map(|ty| {
ty.to_wit()
.ok_or_else(|| anyhow!("type {:?} isn't supported in standard interface types", ty))
})
.collect()
}