Add initial support for fused adapter trampolines (#4501)

* Add initial support for fused adapter trampolines

This commit lands a significant new piece of functionality to Wasmtime's
implementation of the component model in the form of the implementation
of fused adapter trampolines. Internally within a component core wasm
modules can communicate with each other by having their exports
`canon lift`'d to get `canon lower`'d into a different component. This
signifies that two components are communicating through a statically
known interface via the canonical ABI at this time. Previously Wasmtime
was able to identify that this communication was happening but it simply
panicked with `unimplemented!` upon seeing it. This commit is the
beginning of filling out this panic location with an actual
implementation.

The implementation route chosen here for fused adapters is to use a
WebAssembly module itself for the implementation. This means that, at
compile time of a component, Wasmtime is generating core WebAssembly
modules which then get recursively compiled within Wasmtime as well. The
choice to use WebAssembly itself as the implementation of fused adapters
stems from a few motivations:

* This does not represent a significant increase in the "trusted
  compiler base" of Wasmtime. Getting the Wasm -> CLIF translation
  correct once is hard enough much less for an entirely different IR to
  CLIF. By generating WebAssembly no new interactions with Cranelift are
  added which drastically reduces the possibilities for mistakes.

* Using WebAssembly means that component adapters are insulated from
  miscompilations and mistakes. If something goes wrong it's defined
  well within the WebAssembly specification how it goes wrong and what
  happens as a result. This means that the "blast zone" for a wrong
  adapter is the component instance but not the entire host itself.
  Accesses to linear memory are guaranteed to be in-bounds and otherwise
  handled via well-defined traps.

* A fully-finished fused adapter compiler is expected to be a
  significant and quite complex component of Wasmtime. Functionality
  along these lines is expected to be needed for Web-based polyfills of
  the component model and by using core WebAssembly it provides the
  opportunity to share code between Wasmtime and these polyfills for the
  component model.

* Finally the runtime implementation of managing WebAssembly modules is
  already implemented and quite easy to integrate with, so representing
  fused adapters with WebAssembly results in very little extra support
  necessary for the runtime implementation of instantiating and managing
  a component.

The compiler added in this commit is dubbed Wasmtime's Fused Adapter
Compiler of Trampolines (FACT) because who doesn't like deriving a name
from an acronym. Currently the trampoline compiler is limited in its
support for interface types and only supports a few primitives. I plan
on filing future PRs to flesh out the support here for all the variants
of `InterfaceType`. For now this PR is primarily focused on all of the
other infrastructure for the addition of a trampoline compiler.

With the choice to use core WebAssembly to implement fused adapters it
means that adapters need to be inserted into a module. Unfortunately
adapters cannot all go into a single WebAssembly module because adapters
themselves have dependencies which may be provided transitively through
instances that were instantiated with other adapters. This means that a
significant chunk of this PR (`adapt.rs`) is dedicated to determining
precisely which adapters go into precisely which adapter modules. This
partitioning process attempts to make large modules wherever it can to
cut down on core wasm instantiations but is likely not optimal as
it's just a simple heuristic today.

With all of this added together it's now possible to start writing
`*.wast` tests that internally have adapted modules communicating with
one another. A `fused.wast` test suite was added as part of this PR
which is the beginning of tests for the support of the fused adapter
compiler added in this PR. Currently this is primarily testing some
various topologies of adapters along with direct/indirect modes. This
will grow many more tests over time as more types are supported.

Overall I'm not 100% satisfied with the testing story of this PR. When a
test fails it's very difficult to debug since everything is written in
the text format of WebAssembly meaning there's no "conveniences" to
print out the state of the world when things go wrong and easily debug.
I think this will become even more apparent as more tests are written
for more types in subsequent PRs. At this time though I know of no
better alternative other than leaning pretty heavily on fuzz-testing to
ensure this is all exercised.

* Fix an unused field warning

* Fix tests in `wasmtime-runtime`

* Add some more tests for compiled trampolines

* Remap exports when injecting adapters

The exports of a component were accidentally left unmapped which meant
that they indexed the instance indexes pre-adapter module insertion.

* Fix typo

* Rebase conflicts
This commit is contained in:
Alex Crichton
2022-07-25 18:13:26 -05:00
committed by GitHub
parent 78d3e0b693
commit 97894bc65e
33 changed files with 3182 additions and 170 deletions

290
crates/environ/src/fact.rs Normal file
View File

@@ -0,0 +1,290 @@
//! Wasmtime's Fused Adapter Compiler of Trampolines (FACT)
//!
//! This module contains a compiler which emits trampolines to implement fused
//! adatpers for the component model. A fused adapter is when a core wasm
//! function is lifted from one component instance and then lowered into another
//! component instance. This communication between components is well-defined by
//! the spec and ends up creating what's called a "fused adapter".
//!
//! Adapters are currently implemented with WebAssembly modules. This submodule
//! will generate a core wasm binary which contains the adapters specified
//! during compilation. The actual wasm is then later processed by standard
//! paths in Wasmtime to create native machine code and runtime representations
//! of modules.
//!
//! Note that identification of precisely what goes into an adapter module is
//! not handled in this file, instead that's all done in `translate/adapt.rs`.
//! Otherwise this module is only reponsible for taking a set of adapters and
//! their imports and then generating a core wasm module to implement all of
//! that.
use crate::component::{
Adapter, AdapterOptions, ComponentTypes, CoreDef, StringEncoding, TypeFuncIndex,
};
use crate::{FuncIndex, GlobalIndex, MemoryIndex};
use std::collections::HashMap;
use wasm_encoder::*;
mod core_types;
mod signature;
mod trampoline;
mod traps;
/// Representation of an adapter module.
pub struct Module<'a> {
/// Whether or not debug code is inserted into the adapters themselves.
debug: bool,
/// Type information from the creator of this `Module`
types: &'a ComponentTypes,
/// Core wasm type section that's incrementally built
core_types: core_types::CoreTypes,
/// Core wasm import section which is built as adapters are inserted. Note
/// that imports here are intern'd to avoid duplicate imports of the same
/// item.
core_imports: ImportSection,
/// Final list of imports that this module ended up using, in the same order
/// as the imports in the import section.
imports: Vec<CoreDef>,
/// Intern'd imports and what index they were assigned.
imported: HashMap<CoreDef, u32>,
// Current status of index spaces from the imports generated so far.
core_funcs: u32,
core_memories: u32,
core_globals: u32,
/// Adapters which will be compiled once they're all registered.
adapters: Vec<AdapterData>,
}
struct AdapterData {
/// Export name of this adapter
name: String,
/// Options specified during the `canon lift` operation
lift: Options,
/// Options specified during the `canon lower` operation
lower: Options,
/// The core wasm function that this adapter will be calling (the original
/// function that was `canon lift`'d)
callee: FuncIndex,
/// FIXME(#4185) should be plumbed and handled as part of the new reentrance
/// rules not yet implemented here.
called_as_export: bool,
}
struct Options {
ty: TypeFuncIndex,
string_encoding: StringEncoding,
flags: GlobalIndex,
memory64: bool,
memory: Option<MemoryIndex>,
realloc: Option<FuncIndex>,
post_return: Option<FuncIndex>,
}
enum Context {
Lift,
Lower,
}
impl<'a> Module<'a> {
pub fn new(types: &'a ComponentTypes, debug: bool) -> Module<'a> {
Module {
debug,
types,
core_types: Default::default(),
core_imports: Default::default(),
imported: Default::default(),
adapters: Default::default(),
imports: Default::default(),
core_funcs: 0,
core_memories: 0,
core_globals: 0,
}
}
/// Registers a new adapter within this adapter module.
///
/// The `name` provided is the export name of the adapter from the final
/// module, and `adapter` contains all metadata necessary for compilation.
pub fn adapt(&mut self, name: &str, adapter: &Adapter) {
// Import core wasm function which was lifted using its appropriate
// signature since the exported function this adapter generates will
// call the lifted function.
let signature = self.signature(adapter.lift_ty, Context::Lift);
let ty = self
.core_types
.function(&signature.params, &signature.results);
let callee = self.import_func("callee", name, ty, adapter.func.clone());
// Next import any items required by the various canonical options
// (memories, reallocs, etc)
let mut lift = self.import_options(adapter.lift_ty, &adapter.lift_options);
let lower = self.import_options(adapter.lower_ty, &adapter.lower_options);
// Handle post-return specifically here where we have `core_ty` and the
// results of `core_ty` are the parameters to the post-return function.
lift.post_return = adapter.lift_options.post_return.as_ref().map(|func| {
let ty = self.core_types.function(&signature.results, &[]);
self.import_func("post_return", name, ty, func.clone())
});
// Lowering options are not allowed to specify post-return as per the
// current canonical abi specification.
assert!(adapter.lower_options.post_return.is_none());
self.adapters.push(AdapterData {
name: name.to_string(),
lift,
lower,
callee,
// FIXME(#4185) should be plumbed and handled as part of the new
// reentrance rules not yet implemented here.
called_as_export: true,
});
}
fn import_options(&mut self, ty: TypeFuncIndex, options: &AdapterOptions) -> Options {
let AdapterOptions {
instance,
string_encoding,
memory,
realloc,
post_return: _, // handled above
} = options;
let memory64 = false; // FIXME(#4311) should be plumbed from somewhere
let flags = self.import_global(
"flags",
&format!("instance{}", instance.as_u32()),
GlobalType {
val_type: ValType::I32,
mutable: true,
},
CoreDef::InstanceFlags(*instance),
);
let memory = memory.as_ref().map(|memory| {
self.import_memory(
"memory",
"",
MemoryType {
minimum: 0,
maximum: None,
shared: false,
memory64,
},
memory.clone().into(),
)
});
let realloc = realloc.as_ref().map(|func| {
let ptr = if memory64 { ValType::I64 } else { ValType::I32 };
let ty = self.core_types.function(&[ptr, ptr, ptr, ptr], &[ptr]);
self.import_func("realloc", "", ty, func.clone())
});
Options {
ty,
string_encoding: *string_encoding,
flags,
memory64,
memory,
realloc,
post_return: None,
}
}
fn import_func(&mut self, module: &str, name: &str, ty: u32, def: CoreDef) -> FuncIndex {
FuncIndex::from_u32(
self.import(module, name, EntityType::Function(ty), def, |m| {
&mut m.core_funcs
}),
)
}
fn import_global(
&mut self,
module: &str,
name: &str,
ty: GlobalType,
def: CoreDef,
) -> GlobalIndex {
GlobalIndex::from_u32(self.import(module, name, EntityType::Global(ty), def, |m| {
&mut m.core_globals
}))
}
fn import_memory(
&mut self,
module: &str,
name: &str,
ty: MemoryType,
def: CoreDef,
) -> MemoryIndex {
MemoryIndex::from_u32(self.import(module, name, EntityType::Memory(ty), def, |m| {
&mut m.core_memories
}))
}
fn import(
&mut self,
module: &str,
name: &str,
ty: EntityType,
def: CoreDef,
new: impl FnOnce(&mut Self) -> &mut u32,
) -> u32 {
if let Some(prev) = self.imported.get(&def) {
return *prev;
}
let cnt = new(self);
*cnt += 1;
let ret = *cnt - 1;
self.core_imports.import(module, name, ty);
self.imported.insert(def.clone(), ret);
self.imports.push(def);
ret
}
pub fn encode(&mut self) -> Vec<u8> {
let mut funcs = FunctionSection::new();
let mut code = CodeSection::new();
let mut exports = ExportSection::new();
let mut traps = traps::TrapSection::default();
for adapter in self.adapters.iter() {
let idx = self.core_funcs + funcs.len();
exports.export(&adapter.name, ExportKind::Func, idx);
let signature = self.signature(adapter.lower.ty, Context::Lower);
let ty = self
.core_types
.function(&signature.params, &signature.results);
funcs.function(ty);
let (function, func_traps) = trampoline::compile(self, adapter);
code.raw(&function);
traps.append(idx, func_traps);
}
let traps = traps.finish();
let mut result = wasm_encoder::Module::new();
result.section(&self.core_types.section);
result.section(&self.core_imports);
result.section(&funcs);
result.section(&exports);
result.section(&code);
if self.debug {
result.section(&CustomSection {
name: "wasmtime-trampoline-traps",
data: &traps,
});
}
result.finish()
}
/// Returns the imports that were used, in order, to create this adapter
/// module.
pub fn imports(&self) -> &[CoreDef] {
&self.imports
}
}