implement fuzzing for component types (#4537)
This addresses #4307. For the static API we generate 100 arbitrary test cases at build time, each of which includes 0-5 parameter types, a result type, and a WAT fragment containing an imported function and an exported function. The exported function calls the imported function, which is implemented by the host. At runtime, the fuzz test selects a test case at random and feeds it zero or more sets of arbitrary parameters and results, checking that values which flow host-to-guest and guest-to-host make the transition unchanged. The fuzz test for the dynamic API follows a similar pattern, the only difference being that test cases are generated at runtime. Signed-off-by: Joel Dice <joel.dice@fermyon.com>
This commit is contained in:
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -441,6 +441,17 @@ dependencies = [
|
|||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "component-fuzz-util"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"arbitrary",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"wasmtime-component-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "component-macro-test"
|
name = "component-macro-test"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -450,6 +461,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "component-test-util"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"arbitrary",
|
||||||
|
"env_logger 0.9.0",
|
||||||
|
"wasmtime",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@@ -3415,6 +3436,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"clap 3.2.8",
|
"clap 3.2.8",
|
||||||
"component-macro-test",
|
"component-macro-test",
|
||||||
|
"component-test-util",
|
||||||
"criterion",
|
"criterion",
|
||||||
"env_logger 0.9.0",
|
"env_logger 0.9.0",
|
||||||
"filecheck",
|
"filecheck",
|
||||||
@@ -3434,6 +3456,7 @@ dependencies = [
|
|||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wasmtime-cache",
|
"wasmtime-cache",
|
||||||
"wasmtime-cli-flags",
|
"wasmtime-cli-flags",
|
||||||
|
"wasmtime-component-util",
|
||||||
"wasmtime-cranelift",
|
"wasmtime-cranelift",
|
||||||
"wasmtime-environ",
|
"wasmtime-environ",
|
||||||
"wasmtime-runtime",
|
"wasmtime-runtime",
|
||||||
@@ -3520,6 +3543,7 @@ name = "wasmtime-environ-fuzz"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arbitrary",
|
"arbitrary",
|
||||||
|
"component-fuzz-util",
|
||||||
"env_logger 0.9.0",
|
"env_logger 0.9.0",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
"wasmparser",
|
"wasmparser",
|
||||||
@@ -3543,6 +3567,10 @@ dependencies = [
|
|||||||
name = "wasmtime-fuzz"
|
name = "wasmtime-fuzz"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"arbitrary",
|
||||||
|
"component-fuzz-util",
|
||||||
|
"component-test-util",
|
||||||
"cranelift-codegen",
|
"cranelift-codegen",
|
||||||
"cranelift-filetests",
|
"cranelift-filetests",
|
||||||
"cranelift-fuzzgen",
|
"cranelift-fuzzgen",
|
||||||
@@ -3550,6 +3578,9 @@ dependencies = [
|
|||||||
"cranelift-reader",
|
"cranelift-reader",
|
||||||
"cranelift-wasm",
|
"cranelift-wasm",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rand 0.8.5",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wasmtime-fuzzing",
|
"wasmtime-fuzzing",
|
||||||
@@ -3561,6 +3592,8 @@ version = "0.19.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arbitrary",
|
"arbitrary",
|
||||||
|
"component-fuzz-util",
|
||||||
|
"component-test-util",
|
||||||
"env_logger 0.9.0",
|
"env_logger 0.9.0",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ once_cell = "1.9.0"
|
|||||||
rayon = "1.5.0"
|
rayon = "1.5.0"
|
||||||
component-macro-test = { path = "crates/misc/component-macro-test" }
|
component-macro-test = { path = "crates/misc/component-macro-test" }
|
||||||
wasmtime-wast = { path = "crates/wast", version = "=0.40.0", features = ['component-model'] }
|
wasmtime-wast = { path = "crates/wast", version = "=0.40.0", features = ['component-model'] }
|
||||||
|
component-test-util = { path = "crates/misc/component-test-util" }
|
||||||
|
wasmtime-component-util = { path = "crates/component-util" }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dev-dependencies]
|
[target.'cfg(windows)'.dev-dependencies]
|
||||||
windows-sys = { version = "0.36.0", features = ["Win32_System_Memory"] }
|
windows-sys = { version = "0.36.0", features = ["Win32_System_Memory"] }
|
||||||
@@ -110,7 +112,11 @@ memory-init-cow = ["wasmtime/memory-init-cow", "wasmtime-cli-flags/memory-init-c
|
|||||||
pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"]
|
pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"]
|
||||||
all-arch = ["wasmtime/all-arch"]
|
all-arch = ["wasmtime/all-arch"]
|
||||||
posix-signals-on-macos = ["wasmtime/posix-signals-on-macos"]
|
posix-signals-on-macos = ["wasmtime/posix-signals-on-macos"]
|
||||||
component-model = ["wasmtime/component-model", "wasmtime-wast/component-model", "wasmtime-cli-flags/component-model"]
|
component-model = [
|
||||||
|
"wasmtime/component-model",
|
||||||
|
"wasmtime-wast/component-model",
|
||||||
|
"wasmtime-cli-flags/component-model"
|
||||||
|
]
|
||||||
|
|
||||||
# Stub feature that does nothing, for Cargo-features compatibility: the new
|
# Stub feature that does nothing, for Cargo-features compatibility: the new
|
||||||
# backend is the default now.
|
# backend is the default now.
|
||||||
|
|||||||
2
build.rs
2
build.rs
@@ -41,7 +41,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"cargo:warning=The spec testsuite is disabled. To enable, run `git submodule \
|
"cargo:warning=The spec testsuite is disabled. To enable, run `git submodule \
|
||||||
update --remote`."
|
update --remote`."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -885,7 +885,10 @@ impl Expander for ComponentTypeExpander {
|
|||||||
const SIZE32: usize = {
|
const SIZE32: usize = {
|
||||||
let mut size = 0;
|
let mut size = 0;
|
||||||
#sizes
|
#sizes
|
||||||
#internal::align_to(#discriminant_size as usize, Self::ALIGN32) + size
|
#internal::align_to(
|
||||||
|
#internal::align_to(#discriminant_size as usize, Self::ALIGN32) + size,
|
||||||
|
Self::ALIGN32
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
const ALIGN32: u32 = {
|
const ALIGN32: u32 = {
|
||||||
|
|||||||
@@ -77,3 +77,62 @@ impl FlagsSize {
|
|||||||
fn ceiling_divide(n: usize, d: usize) -> usize {
|
fn ceiling_divide(n: usize, d: usize) -> usize {
|
||||||
(n + d - 1) / d
|
(n + d - 1) / d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A simple bump allocator which can be used with modules
|
||||||
|
pub const REALLOC_AND_FREE: &str = r#"
|
||||||
|
(global $last (mut i32) (i32.const 8))
|
||||||
|
(func $realloc (export "realloc")
|
||||||
|
(param $old_ptr i32)
|
||||||
|
(param $old_size i32)
|
||||||
|
(param $align i32)
|
||||||
|
(param $new_size i32)
|
||||||
|
(result i32)
|
||||||
|
|
||||||
|
;; Test if the old pointer is non-null
|
||||||
|
local.get $old_ptr
|
||||||
|
if
|
||||||
|
;; If the old size is bigger than the new size then
|
||||||
|
;; this is a shrink and transparently allow it
|
||||||
|
local.get $old_size
|
||||||
|
local.get $new_size
|
||||||
|
i32.gt_u
|
||||||
|
if
|
||||||
|
local.get $old_ptr
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
;; ... otherwise this is unimplemented
|
||||||
|
unreachable
|
||||||
|
end
|
||||||
|
|
||||||
|
;; align up `$last`
|
||||||
|
(global.set $last
|
||||||
|
(i32.and
|
||||||
|
(i32.add
|
||||||
|
(global.get $last)
|
||||||
|
(i32.add
|
||||||
|
(local.get $align)
|
||||||
|
(i32.const -1)))
|
||||||
|
(i32.xor
|
||||||
|
(i32.add
|
||||||
|
(local.get $align)
|
||||||
|
(i32.const -1))
|
||||||
|
(i32.const -1))))
|
||||||
|
|
||||||
|
;; save the current value of `$last` as the return value
|
||||||
|
global.get $last
|
||||||
|
|
||||||
|
;; ensure anything necessary is set to valid data by spraying a bit
|
||||||
|
;; pattern that is invalid
|
||||||
|
global.get $last
|
||||||
|
i32.const 0xde
|
||||||
|
local.get $new_size
|
||||||
|
memory.fill
|
||||||
|
|
||||||
|
;; bump our pointer
|
||||||
|
(global.set $last
|
||||||
|
(i32.add
|
||||||
|
(global.get $last)
|
||||||
|
(local.get $new_size)))
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ libfuzzer-sys = "0.4"
|
|||||||
wasmparser = "0.88.0"
|
wasmparser = "0.88.0"
|
||||||
wasmprinter = "0.2.37"
|
wasmprinter = "0.2.37"
|
||||||
wasmtime-environ = { path = ".." }
|
wasmtime-environ = { path = ".." }
|
||||||
|
component-fuzz-util = { path = "../../misc/component-fuzz-util", optional = true }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "fact-valid-module"
|
name = "fact-valid-module"
|
||||||
@@ -24,4 +25,4 @@ doc = false
|
|||||||
required-features = ["component-model"]
|
required-features = ["component-model"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
component-model = ["wasmtime-environ/component-model"]
|
component-model = ["wasmtime-environ/component-model", "dep:component-fuzz-util"]
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use arbitrary::{Arbitrary, Unstructured};
|
use arbitrary::Arbitrary;
|
||||||
|
use component_fuzz_util::Type as ValType;
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use std::fmt;
|
|
||||||
use wasmparser::{Validator, WasmFeatures};
|
use wasmparser::{Validator, WasmFeatures};
|
||||||
use wasmtime_environ::component::*;
|
use wasmtime_environ::component::*;
|
||||||
use wasmtime_environ::fact::Module;
|
use wasmtime_environ::fact::Module;
|
||||||
@@ -38,34 +38,6 @@ struct FuncType {
|
|||||||
result: ValType,
|
result: ValType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Arbitrary, Debug)]
|
|
||||||
enum ValType {
|
|
||||||
Unit,
|
|
||||||
U8,
|
|
||||||
S8,
|
|
||||||
U16,
|
|
||||||
S16,
|
|
||||||
U32,
|
|
||||||
S32,
|
|
||||||
U64,
|
|
||||||
S64,
|
|
||||||
Float32,
|
|
||||||
Float64,
|
|
||||||
Char,
|
|
||||||
List(Box<ValType>),
|
|
||||||
Record(Vec<ValType>),
|
|
||||||
// Up to 65 flags to exercise up to 3 u32 values
|
|
||||||
Flags(UsizeInRange<0, 65>),
|
|
||||||
Tuple(Vec<ValType>),
|
|
||||||
Variant(NonZeroLenVec<ValType>),
|
|
||||||
Union(NonZeroLenVec<ValType>),
|
|
||||||
// at least one enum variant but no more than what's necessary to inflate to
|
|
||||||
// 16 bits to keep this reasonably sized
|
|
||||||
Enum(UsizeInRange<1, 257>),
|
|
||||||
Option(Box<ValType>),
|
|
||||||
Expected(Box<ValType>, Box<ValType>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Arbitrary, Debug)]
|
#[derive(Copy, Clone, Arbitrary, Debug)]
|
||||||
enum GenStringEncoding {
|
enum GenStringEncoding {
|
||||||
Utf8,
|
Utf8,
|
||||||
@@ -73,39 +45,9 @@ enum GenStringEncoding {
|
|||||||
CompactUtf16,
|
CompactUtf16,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NonZeroLenVec<T>(Vec<T>);
|
fuzz_target!(|module: GenAdapterModule| { drop(target(module)) });
|
||||||
|
|
||||||
impl<'a, T: Arbitrary<'a>> Arbitrary<'a> for NonZeroLenVec<T> {
|
fn target(module: GenAdapterModule) -> Result<(), ()> {
|
||||||
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
|
||||||
let mut items = Vec::arbitrary(u)?;
|
|
||||||
if items.is_empty() {
|
|
||||||
items.push(u.arbitrary()?);
|
|
||||||
}
|
|
||||||
Ok(NonZeroLenVec(items))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: fmt::Debug> fmt::Debug for NonZeroLenVec<T> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UsizeInRange<const L: usize, const H: usize>(usize);
|
|
||||||
|
|
||||||
impl<'a, const L: usize, const H: usize> Arbitrary<'a> for UsizeInRange<L, H> {
|
|
||||||
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
|
||||||
Ok(UsizeInRange(u.int_in_range(L..=H)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const L: usize, const H: usize> fmt::Debug for UsizeInRange<L, H> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fuzz_target!(|module: GenAdapterModule| {
|
|
||||||
drop(env_logger::try_init());
|
drop(env_logger::try_init());
|
||||||
|
|
||||||
let mut types = ComponentTypesBuilder::default();
|
let mut types = ComponentTypesBuilder::default();
|
||||||
@@ -148,9 +90,9 @@ fuzz_target!(|module: GenAdapterModule| {
|
|||||||
for adapter in module.adapters.iter() {
|
for adapter in module.adapters.iter() {
|
||||||
let mut params = Vec::new();
|
let mut params = Vec::new();
|
||||||
for param in adapter.ty.params.iter() {
|
for param in adapter.ty.params.iter() {
|
||||||
params.push((None, intern(&mut types, param)));
|
params.push((None, intern(&mut types, param)?));
|
||||||
}
|
}
|
||||||
let result = intern(&mut types, &adapter.ty.result);
|
let result = intern(&mut types, &adapter.ty.result)?;
|
||||||
let signature = types.add_func_type(TypeFunc {
|
let signature = types.add_func_type(TypeFunc {
|
||||||
params: params.into(),
|
params: params.into(),
|
||||||
result,
|
result,
|
||||||
@@ -201,7 +143,7 @@ fuzz_target!(|module: GenAdapterModule| {
|
|||||||
.validate_all(&wasm);
|
.validate_all(&wasm);
|
||||||
|
|
||||||
let err = match result {
|
let err = match result {
|
||||||
Ok(_) => return,
|
Ok(_) => return Ok(()),
|
||||||
Err(e) => e,
|
Err(e) => e,
|
||||||
};
|
};
|
||||||
eprintln!("invalid wasm module: {err:?}");
|
eprintln!("invalid wasm module: {err:?}");
|
||||||
@@ -215,11 +157,12 @@ fuzz_target!(|module: GenAdapterModule| {
|
|||||||
}
|
}
|
||||||
|
|
||||||
panic!()
|
panic!()
|
||||||
});
|
}
|
||||||
|
|
||||||
fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> InterfaceType {
|
fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result<InterfaceType, ()> {
|
||||||
match ty {
|
Ok(match ty {
|
||||||
ValType::Unit => InterfaceType::Unit,
|
ValType::Unit => InterfaceType::Unit,
|
||||||
|
ValType::Bool => InterfaceType::Bool,
|
||||||
ValType::U8 => InterfaceType::U8,
|
ValType::U8 => InterfaceType::U8,
|
||||||
ValType::S8 => InterfaceType::S8,
|
ValType::S8 => InterfaceType::S8,
|
||||||
ValType::U16 => InterfaceType::U16,
|
ValType::U16 => InterfaceType::U16,
|
||||||
@@ -232,7 +175,7 @@ fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> InterfaceType {
|
|||||||
ValType::Float64 => InterfaceType::Float64,
|
ValType::Float64 => InterfaceType::Float64,
|
||||||
ValType::Char => InterfaceType::Char,
|
ValType::Char => InterfaceType::Char,
|
||||||
ValType::List(ty) => {
|
ValType::List(ty) => {
|
||||||
let ty = intern(types, ty);
|
let ty = intern(types, ty)?;
|
||||||
InterfaceType::List(types.add_interface_type(ty))
|
InterfaceType::List(types.add_interface_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Record(tys) => {
|
ValType::Record(tys) => {
|
||||||
@@ -240,61 +183,72 @@ fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> InterfaceType {
|
|||||||
fields: tys
|
fields: tys
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, ty)| RecordField {
|
.map(|(i, ty)| {
|
||||||
name: format!("f{i}"),
|
Ok(RecordField {
|
||||||
ty: intern(types, ty),
|
name: format!("f{i}"),
|
||||||
|
ty: intern(types, ty)?,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect::<Result<_, _>>()?,
|
||||||
};
|
};
|
||||||
InterfaceType::Record(types.add_record_type(ty))
|
InterfaceType::Record(types.add_record_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Flags(size) => {
|
ValType::Flags(size) => {
|
||||||
let ty = TypeFlags {
|
let ty = TypeFlags {
|
||||||
names: (0..size.0).map(|i| format!("f{i}")).collect(),
|
names: (0..size.as_usize()).map(|i| format!("f{i}")).collect(),
|
||||||
};
|
};
|
||||||
InterfaceType::Flags(types.add_flags_type(ty))
|
InterfaceType::Flags(types.add_flags_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Tuple(tys) => {
|
ValType::Tuple(tys) => {
|
||||||
let ty = TypeTuple {
|
let ty = TypeTuple {
|
||||||
types: tys.iter().map(|ty| intern(types, ty)).collect(),
|
types: tys
|
||||||
|
.iter()
|
||||||
|
.map(|ty| intern(types, ty))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
};
|
};
|
||||||
InterfaceType::Tuple(types.add_tuple_type(ty))
|
InterfaceType::Tuple(types.add_tuple_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Variant(NonZeroLenVec(cases)) => {
|
ValType::Variant(cases) => {
|
||||||
let ty = TypeVariant {
|
let ty = TypeVariant {
|
||||||
cases: cases
|
cases: cases
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, ty)| VariantCase {
|
.map(|(i, ty)| {
|
||||||
name: format!("c{i}"),
|
Ok(VariantCase {
|
||||||
ty: intern(types, ty),
|
name: format!("c{i}"),
|
||||||
|
ty: intern(types, ty)?,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect::<Result<_, _>>()?,
|
||||||
};
|
};
|
||||||
InterfaceType::Variant(types.add_variant_type(ty))
|
InterfaceType::Variant(types.add_variant_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Union(tys) => {
|
ValType::Union(tys) => {
|
||||||
let ty = TypeUnion {
|
let ty = TypeUnion {
|
||||||
types: tys.0.iter().map(|ty| intern(types, ty)).collect(),
|
types: tys
|
||||||
|
.iter()
|
||||||
|
.map(|ty| intern(types, ty))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
};
|
};
|
||||||
InterfaceType::Union(types.add_union_type(ty))
|
InterfaceType::Union(types.add_union_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Enum(size) => {
|
ValType::Enum(size) => {
|
||||||
let ty = TypeEnum {
|
let ty = TypeEnum {
|
||||||
names: (0..size.0).map(|i| format!("c{i}")).collect(),
|
names: (0..size.as_usize()).map(|i| format!("c{i}")).collect(),
|
||||||
};
|
};
|
||||||
InterfaceType::Enum(types.add_enum_type(ty))
|
InterfaceType::Enum(types.add_enum_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Option(ty) => {
|
ValType::Option(ty) => {
|
||||||
let ty = intern(types, ty);
|
let ty = intern(types, ty)?;
|
||||||
InterfaceType::Option(types.add_interface_type(ty))
|
InterfaceType::Option(types.add_interface_type(ty))
|
||||||
}
|
}
|
||||||
ValType::Expected(ok, err) => {
|
ValType::Expected { ok, err } => {
|
||||||
let ok = intern(types, ok);
|
let ok = intern(types, ok)?;
|
||||||
let err = intern(types, err);
|
let err = intern(types, err)?;
|
||||||
InterfaceType::Expected(types.add_expected_type(TypeExpected { ok, err }))
|
InterfaceType::Expected(types.add_expected_type(TypeExpected { ok, err }))
|
||||||
}
|
}
|
||||||
}
|
ValType::String => return Err(()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GenStringEncoding> for StringEncoding {
|
impl From<GenStringEncoding> for StringEncoding {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ license = "Apache-2.0 WITH LLVM-exception"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.22"
|
anyhow = "1.0.22"
|
||||||
arbitrary = { version = "1.1.0", features = ["derive"] }
|
arbitrary = { version = "1.1.0", features = ["derive"] }
|
||||||
|
component-test-util = { path = "../misc/component-test-util" }
|
||||||
|
component-fuzz-util = { path = "../misc/component-fuzz-util" }
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
rayon = "1.2.1"
|
rayon = "1.2.1"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
mod codegen_settings;
|
mod codegen_settings;
|
||||||
|
pub mod component_types;
|
||||||
mod config;
|
mod config;
|
||||||
mod instance_allocation_strategy;
|
mod instance_allocation_strategy;
|
||||||
mod instance_limits;
|
mod instance_limits;
|
||||||
|
|||||||
189
crates/fuzzing/src/generators/component_types.rs
Normal file
189
crates/fuzzing/src/generators/component_types.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
//! This module generates test cases for the Wasmtime component model function APIs,
|
||||||
|
//! e.g. `wasmtime::component::func::Func` and `TypedFunc`.
|
||||||
|
//!
|
||||||
|
//! Each case includes a list of arbitrary interface types to use as parameters, plus another one to use as a
|
||||||
|
//! result, and a component which exports a function and imports a function. The exported function forwards its
|
||||||
|
//! parameters to the imported one and forwards the result back to the caller. This serves to excercise Wasmtime's
|
||||||
|
//! lifting and lowering code and verify the values remain intact during both processes.
|
||||||
|
|
||||||
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use component_fuzz_util::{Declarations, EXPORT_FUNCTION, IMPORT_FUNCTION};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::ops::ControlFlow;
|
||||||
|
use wasmtime::component::{self, Component, Lift, Linker, Lower, Val};
|
||||||
|
use wasmtime::{Config, Engine, Store, StoreContextMut};
|
||||||
|
|
||||||
|
/// Minimum length of an arbitrary list value generated for a test case
|
||||||
|
const MIN_LIST_LENGTH: u32 = 0;
|
||||||
|
|
||||||
|
/// Maximum length of an arbitrary list value generated for a test case
|
||||||
|
const MAX_LIST_LENGTH: u32 = 10;
|
||||||
|
|
||||||
|
/// Generate an arbitrary instance of the specified type.
|
||||||
|
pub fn arbitrary_val(ty: &component::Type, input: &mut Unstructured) -> arbitrary::Result<Val> {
|
||||||
|
use component::Type;
|
||||||
|
|
||||||
|
Ok(match ty {
|
||||||
|
Type::Unit => Val::Unit,
|
||||||
|
Type::Bool => Val::Bool(input.arbitrary()?),
|
||||||
|
Type::S8 => Val::S8(input.arbitrary()?),
|
||||||
|
Type::U8 => Val::U8(input.arbitrary()?),
|
||||||
|
Type::S16 => Val::S16(input.arbitrary()?),
|
||||||
|
Type::U16 => Val::U16(input.arbitrary()?),
|
||||||
|
Type::S32 => Val::S32(input.arbitrary()?),
|
||||||
|
Type::U32 => Val::U32(input.arbitrary()?),
|
||||||
|
Type::S64 => Val::S64(input.arbitrary()?),
|
||||||
|
Type::U64 => Val::U64(input.arbitrary()?),
|
||||||
|
Type::Float32 => Val::Float32(input.arbitrary::<f32>()?.to_bits()),
|
||||||
|
Type::Float64 => Val::Float64(input.arbitrary::<f64>()?.to_bits()),
|
||||||
|
Type::Char => Val::Char(input.arbitrary()?),
|
||||||
|
Type::String => Val::String(input.arbitrary()?),
|
||||||
|
Type::List(list) => {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
input.arbitrary_loop(Some(MIN_LIST_LENGTH), Some(MAX_LIST_LENGTH), |input| {
|
||||||
|
values.push(arbitrary_val(&list.ty(), input)?);
|
||||||
|
|
||||||
|
Ok(ControlFlow::Continue(()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
list.new_val(values.into()).unwrap()
|
||||||
|
}
|
||||||
|
Type::Record(record) => record
|
||||||
|
.new_val(
|
||||||
|
record
|
||||||
|
.fields()
|
||||||
|
.map(|field| Ok((field.name, arbitrary_val(&field.ty, input)?)))
|
||||||
|
.collect::<arbitrary::Result<Vec<_>>>()?,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Type::Tuple(tuple) => tuple
|
||||||
|
.new_val(
|
||||||
|
tuple
|
||||||
|
.types()
|
||||||
|
.map(|ty| arbitrary_val(&ty, input))
|
||||||
|
.collect::<arbitrary::Result<_>>()?,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Type::Variant(variant) => {
|
||||||
|
let mut cases = variant.cases();
|
||||||
|
let discriminant = input.int_in_range(0..=cases.len() - 1)?;
|
||||||
|
variant
|
||||||
|
.new_val(
|
||||||
|
&format!("C{discriminant}"),
|
||||||
|
arbitrary_val(&cases.nth(discriminant).unwrap().ty, input)?,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Type::Enum(en) => {
|
||||||
|
let discriminant = input.int_in_range(0..=en.names().len() - 1)?;
|
||||||
|
en.new_val(&format!("C{discriminant}")).unwrap()
|
||||||
|
}
|
||||||
|
Type::Union(un) => {
|
||||||
|
let mut types = un.types();
|
||||||
|
let discriminant = input.int_in_range(0..=types.len() - 1)?;
|
||||||
|
un.new_val(
|
||||||
|
discriminant.try_into().unwrap(),
|
||||||
|
arbitrary_val(&types.nth(discriminant).unwrap(), input)?,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Type::Option(option) => {
|
||||||
|
let discriminant = input.int_in_range(0..=1)?;
|
||||||
|
option
|
||||||
|
.new_val(match discriminant {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(arbitrary_val(&option.ty(), input)?),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Type::Expected(expected) => {
|
||||||
|
let discriminant = input.int_in_range(0..=1)?;
|
||||||
|
expected
|
||||||
|
.new_val(match discriminant {
|
||||||
|
0 => Ok(arbitrary_val(&expected.ok(), input)?),
|
||||||
|
1 => Err(arbitrary_val(&expected.err(), input)?),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Type::Flags(flags) => flags
|
||||||
|
.new_val(
|
||||||
|
&flags
|
||||||
|
.names()
|
||||||
|
.filter_map(|name| {
|
||||||
|
input
|
||||||
|
.arbitrary()
|
||||||
|
.map(|p| if p { Some(name) } else { None })
|
||||||
|
.transpose()
|
||||||
|
})
|
||||||
|
.collect::<arbitrary::Result<Box<[_]>>>()?,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! define_static_api_test {
|
||||||
|
($name:ident $(($param:ident $param_name:ident $param_expected_name:ident))*) => {
|
||||||
|
#[allow(unused_parens)]
|
||||||
|
/// Generate zero or more sets of arbitrary argument and result values and execute the test using those
|
||||||
|
/// values, asserting that they flow from host-to-guest and guest-to-host unchanged.
|
||||||
|
pub fn $name<'a, $($param,)* R>(
|
||||||
|
input: &mut Unstructured<'a>,
|
||||||
|
declarations: &Declarations,
|
||||||
|
) -> arbitrary::Result<()>
|
||||||
|
where
|
||||||
|
$($param: Lift + Lower + Clone + PartialEq + Debug + Arbitrary<'a> + 'static,)*
|
||||||
|
R: Lift + Lower + Clone + PartialEq + Debug + Arbitrary<'a> + 'static
|
||||||
|
{
|
||||||
|
crate::init_fuzzing();
|
||||||
|
|
||||||
|
let mut config = Config::new();
|
||||||
|
config.wasm_component_model(true);
|
||||||
|
let engine = Engine::new(&config).unwrap();
|
||||||
|
let component = Component::new(
|
||||||
|
&engine,
|
||||||
|
declarations.make_component().as_bytes()
|
||||||
|
).unwrap();
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
linker
|
||||||
|
.root()
|
||||||
|
.func_wrap(
|
||||||
|
IMPORT_FUNCTION,
|
||||||
|
|cx: StoreContextMut<'_, ($(Option<$param>,)* Option<R>)>,
|
||||||
|
$($param_name: $param,)*|
|
||||||
|
{
|
||||||
|
let ($($param_expected_name,)* result) = cx.data();
|
||||||
|
$(assert_eq!($param_name, *$param_expected_name.as_ref().unwrap());)*
|
||||||
|
Ok(result.as_ref().unwrap().clone())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut store = Store::new(&engine, Default::default());
|
||||||
|
let instance = linker.instantiate(&mut store, &component).unwrap();
|
||||||
|
let func = instance
|
||||||
|
.get_typed_func::<($($param,)*), R, _>(&mut store, EXPORT_FUNCTION)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
while input.arbitrary()? {
|
||||||
|
$(let $param_name = input.arbitrary::<$param>()?;)*
|
||||||
|
let result = input.arbitrary::<R>()?;
|
||||||
|
*store.data_mut() = ($(Some($param_name.clone()),)* Some(result.clone()));
|
||||||
|
|
||||||
|
assert_eq!(func.call(&mut store, ($($param_name,)*)).unwrap(), result);
|
||||||
|
func.post_return(&mut store).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
define_static_api_test!(static_api_test0);
|
||||||
|
define_static_api_test!(static_api_test1 (P0 p0 p0_expected));
|
||||||
|
define_static_api_test!(static_api_test2 (P0 p0 p0_expected) (P1 p1 p1_expected));
|
||||||
|
define_static_api_test!(static_api_test3 (P0 p0 p0_expected) (P1 p1 p1_expected) (P2 p2 p2_expected));
|
||||||
|
define_static_api_test!(static_api_test4 (P0 p0 p0_expected) (P1 p1 p1_expected) (P2 p2 p2_expected)
|
||||||
|
(P3 p3 p3_expected));
|
||||||
|
define_static_api_test!(static_api_test5 (P0 p0 p0_expected) (P1 p1 p1_expected) (P2 p2 p2_expected)
|
||||||
|
(P3 p3 p3_expected) (P4 p4 p4_expected));
|
||||||
@@ -1073,3 +1073,60 @@ fn set_fuel<T>(store: &mut Store<T>, fuel: u64) {
|
|||||||
// double-check that the store has the expected amount of fuel remaining
|
// double-check that the store has the expected amount of fuel remaining
|
||||||
assert_eq!(store.consume_fuel(0).unwrap(), fuel);
|
assert_eq!(store.consume_fuel(0).unwrap(), fuel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate and execute a `crate::generators::component_types::TestCase` using the specified `input` to create
|
||||||
|
/// arbitrary types and values.
|
||||||
|
pub fn dynamic_component_api_target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
|
||||||
|
use crate::generators::component_types;
|
||||||
|
use anyhow::Result;
|
||||||
|
use component_fuzz_util::{TestCase, EXPORT_FUNCTION, IMPORT_FUNCTION};
|
||||||
|
use component_test_util::FuncExt;
|
||||||
|
use wasmtime::component::{Component, Linker, Val};
|
||||||
|
|
||||||
|
crate::init_fuzzing();
|
||||||
|
|
||||||
|
let case = input.arbitrary::<TestCase>()?;
|
||||||
|
|
||||||
|
let engine = component_test_util::engine();
|
||||||
|
let mut store = Store::new(&engine, (Box::new([]) as Box<[Val]>, None));
|
||||||
|
let component =
|
||||||
|
Component::new(&engine, case.declarations().make_component().as_bytes()).unwrap();
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
|
||||||
|
linker
|
||||||
|
.root()
|
||||||
|
.func_new(&component, IMPORT_FUNCTION, {
|
||||||
|
move |cx: StoreContextMut<'_, (Box<[Val]>, Option<Val>)>, args: &[Val]| -> Result<Val> {
|
||||||
|
let (expected_args, result) = cx.data();
|
||||||
|
assert_eq!(args.len(), expected_args.len());
|
||||||
|
for (expected, actual) in expected_args.iter().zip(args) {
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
Ok(result.as_ref().unwrap().clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let instance = linker.instantiate(&mut store, &component).unwrap();
|
||||||
|
let func = instance.get_func(&mut store, EXPORT_FUNCTION).unwrap();
|
||||||
|
let params = func.params(&store);
|
||||||
|
let result = func.result(&store);
|
||||||
|
|
||||||
|
while input.arbitrary()? {
|
||||||
|
let args = params
|
||||||
|
.iter()
|
||||||
|
.map(|ty| component_types::arbitrary_val(ty, input))
|
||||||
|
.collect::<arbitrary::Result<Box<[_]>>>()?;
|
||||||
|
|
||||||
|
let result = component_types::arbitrary_val(&result, input)?;
|
||||||
|
|
||||||
|
*store.data_mut() = (args.clone(), Some(result.clone()));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
func.call_and_post_return(&mut store, &args).unwrap(),
|
||||||
|
result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
14
crates/misc/component-fuzz-util/Cargo.toml
Normal file
14
crates/misc/component-fuzz-util/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "component-fuzz-util"
|
||||||
|
authors = ["The Wasmtime Project Developers"]
|
||||||
|
license = "Apache-2.0 WITH LLVM-exception"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { version = "1.0.19" }
|
||||||
|
arbitrary = { version = "1.1.0", features = ["derive"] }
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
quote = "1.0"
|
||||||
|
wasmtime-component-util = { path = "../../component-util" }
|
||||||
800
crates/misc/component-fuzz-util/src/lib.rs
Normal file
800
crates/misc/component-fuzz-util/src/lib.rs
Normal file
@@ -0,0 +1,800 @@
|
|||||||
|
//! This module generates test cases for the Wasmtime component model function APIs,
|
||||||
|
//! e.g. `wasmtime::component::func::Func` and `TypedFunc`.
|
||||||
|
//!
|
||||||
|
//! Each case includes a list of arbitrary interface types to use as parameters, plus another one to use as a
|
||||||
|
//! result, and a component which exports a function and imports a function. The exported function forwards its
|
||||||
|
//! parameters to the imported one and forwards the result back to the caller. This serves to excercise Wasmtime's
|
||||||
|
//! lifting and lowering code and verify the values remain intact during both processes.
|
||||||
|
|
||||||
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use proc_macro2::{Ident, TokenStream};
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use std::fmt::{self, Debug, Write};
|
||||||
|
use std::iter;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use wasmtime_component_util::{DiscriminantSize, FlagsSize, REALLOC_AND_FREE};
|
||||||
|
|
||||||
|
const MAX_FLAT_PARAMS: usize = 16;
|
||||||
|
const MAX_FLAT_RESULTS: usize = 1;
|
||||||
|
const MAX_ARITY: usize = 5;
|
||||||
|
|
||||||
|
/// The name of the imported host function which the generated component will call
|
||||||
|
pub const IMPORT_FUNCTION: &str = "echo";
|
||||||
|
|
||||||
|
/// The name of the exported guest function which the host should call
|
||||||
|
pub const EXPORT_FUNCTION: &str = "echo";
|
||||||
|
|
||||||
|
/// Maximum length of an arbitrary tuple type. As of this writing, the `wasmtime::component::func::typed` module
|
||||||
|
/// only implements the `ComponentType` trait for tuples up to this length.
|
||||||
|
const MAX_TUPLE_LENGTH: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
enum CoreType {
|
||||||
|
I32,
|
||||||
|
I64,
|
||||||
|
F32,
|
||||||
|
F64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoreType {
|
||||||
|
/// This is the `join` operation specified in [the canonical
|
||||||
|
/// ABI](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening) for
|
||||||
|
/// variant types.
|
||||||
|
fn join(self, other: Self) -> Self {
|
||||||
|
match (self, other) {
|
||||||
|
_ if self == other => self,
|
||||||
|
(Self::I32, Self::F32) | (Self::F32, Self::I32) => Self::I32,
|
||||||
|
_ => Self::I64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CoreType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::I32 => f.write_str("i32"),
|
||||||
|
Self::I64 => f.write_str("i64"),
|
||||||
|
Self::F32 => f.write_str("f32"),
|
||||||
|
Self::F64 => f.write_str("f64"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UsizeInRange<const L: usize, const H: usize>(usize);
|
||||||
|
|
||||||
|
impl<const L: usize, const H: usize> UsizeInRange<L, H> {
|
||||||
|
pub fn as_usize(&self) -> usize {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const L: usize, const H: usize> Arbitrary<'a> for UsizeInRange<L, H> {
|
||||||
|
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||||
|
Ok(UsizeInRange(u.int_in_range(L..=H)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a `Box<[T]>` and provides an `Arbitrary` implementation that always generates non-empty slices
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NonEmptyArray<T>(Box<[T]>);
|
||||||
|
|
||||||
|
impl<'a, T: Arbitrary<'a>> Arbitrary<'a> for NonEmptyArray<T> {
|
||||||
|
fn arbitrary(input: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||||
|
Ok(Self(
|
||||||
|
iter::once(input.arbitrary())
|
||||||
|
.chain(input.arbitrary_iter()?)
|
||||||
|
.collect::<arbitrary::Result<_>>()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for NonEmptyArray<T> {
|
||||||
|
type Target = [T];
|
||||||
|
|
||||||
|
fn deref(&self) -> &[T] {
|
||||||
|
self.0.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a `Box<[T]>` and provides an `Arbitrary` implementation that always generates slices of length less than
|
||||||
|
/// or equal to the longest tuple for which Wasmtime generates a `ComponentType` impl
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TupleArray<T>(Box<[T]>);
|
||||||
|
|
||||||
|
impl<'a, T: Arbitrary<'a>> Arbitrary<'a> for TupleArray<T> {
|
||||||
|
fn arbitrary(input: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||||
|
Ok(Self(
|
||||||
|
input
|
||||||
|
.arbitrary_iter()?
|
||||||
|
.take(MAX_TUPLE_LENGTH)
|
||||||
|
.collect::<arbitrary::Result<_>>()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for TupleArray<T> {
|
||||||
|
type Target = [T];
|
||||||
|
|
||||||
|
fn deref(&self) -> &[T] {
|
||||||
|
self.0.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a component model interface type
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Arbitrary, Debug)]
|
||||||
|
pub enum Type {
|
||||||
|
Unit,
|
||||||
|
Bool,
|
||||||
|
S8,
|
||||||
|
U8,
|
||||||
|
S16,
|
||||||
|
U16,
|
||||||
|
S32,
|
||||||
|
U32,
|
||||||
|
S64,
|
||||||
|
U64,
|
||||||
|
Float32,
|
||||||
|
Float64,
|
||||||
|
Char,
|
||||||
|
String,
|
||||||
|
List(Box<Type>),
|
||||||
|
Record(Box<[Type]>),
|
||||||
|
Tuple(TupleArray<Type>),
|
||||||
|
Variant(NonEmptyArray<Type>),
|
||||||
|
Enum(UsizeInRange<1, 257>),
|
||||||
|
Union(NonEmptyArray<Type>),
|
||||||
|
Option(Box<Type>),
|
||||||
|
Expected { ok: Box<Type>, err: Box<Type> },
|
||||||
|
Flags(UsizeInRange<0, 65>),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_record<'a>(types: impl Iterator<Item = &'a Type>, vec: &mut Vec<CoreType>) {
|
||||||
|
for ty in types {
|
||||||
|
ty.lower(vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_variant<'a>(types: impl Iterator<Item = &'a Type>, vec: &mut Vec<CoreType>) {
|
||||||
|
vec.push(CoreType::I32);
|
||||||
|
let offset = vec.len();
|
||||||
|
for ty in types {
|
||||||
|
for (index, ty) in ty.lowered().iter().enumerate() {
|
||||||
|
let index = offset + index;
|
||||||
|
if index < vec.len() {
|
||||||
|
vec[index] = vec[index].join(*ty);
|
||||||
|
} else {
|
||||||
|
vec.push(*ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32_count_from_flag_count(count: usize) -> usize {
|
||||||
|
match FlagsSize::from_count(count) {
|
||||||
|
FlagsSize::Size0 => 0,
|
||||||
|
FlagsSize::Size1 | FlagsSize::Size2 => 1,
|
||||||
|
FlagsSize::Size4Plus(n) => n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SizeAndAlignment {
|
||||||
|
size: usize,
|
||||||
|
alignment: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Type {
|
||||||
|
fn lowered(&self) -> Vec<CoreType> {
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
self.lower(&mut vec);
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower(&self, vec: &mut Vec<CoreType>) {
|
||||||
|
match self {
|
||||||
|
Type::Unit => (),
|
||||||
|
Type::Bool
|
||||||
|
| Type::U8
|
||||||
|
| Type::S8
|
||||||
|
| Type::S16
|
||||||
|
| Type::U16
|
||||||
|
| Type::S32
|
||||||
|
| Type::U32
|
||||||
|
| Type::Char
|
||||||
|
| Type::Enum(_) => vec.push(CoreType::I32),
|
||||||
|
Type::S64 | Type::U64 => vec.push(CoreType::I64),
|
||||||
|
Type::Float32 => vec.push(CoreType::F32),
|
||||||
|
Type::Float64 => vec.push(CoreType::F64),
|
||||||
|
Type::String | Type::List(_) => {
|
||||||
|
vec.push(CoreType::I32);
|
||||||
|
vec.push(CoreType::I32);
|
||||||
|
}
|
||||||
|
Type::Record(types) => lower_record(types.iter(), vec),
|
||||||
|
Type::Tuple(types) => lower_record(types.0.iter(), vec),
|
||||||
|
Type::Variant(types) | Type::Union(types) => lower_variant(types.0.iter(), vec),
|
||||||
|
Type::Option(ty) => lower_variant([&Type::Unit, ty].into_iter(), vec),
|
||||||
|
Type::Expected { ok, err } => lower_variant([ok.deref(), err].into_iter(), vec),
|
||||||
|
Type::Flags(count) => {
|
||||||
|
vec.extend(iter::repeat(CoreType::I32).take(u32_count_from_flag_count(count.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_and_alignment(&self) -> SizeAndAlignment {
|
||||||
|
match self {
|
||||||
|
Type::Unit => SizeAndAlignment {
|
||||||
|
size: 0,
|
||||||
|
alignment: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::Bool | Type::S8 | Type::U8 => SizeAndAlignment {
|
||||||
|
size: 1,
|
||||||
|
alignment: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::S16 | Type::U16 => SizeAndAlignment {
|
||||||
|
size: 2,
|
||||||
|
alignment: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::S32 | Type::U32 | Type::Char | Type::Float32 => SizeAndAlignment {
|
||||||
|
size: 4,
|
||||||
|
alignment: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::S64 | Type::U64 | Type::Float64 => SizeAndAlignment {
|
||||||
|
size: 8,
|
||||||
|
alignment: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::String | Type::List(_) => SizeAndAlignment {
|
||||||
|
size: 8,
|
||||||
|
alignment: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::Record(types) => record_size_and_alignment(types.iter()),
|
||||||
|
|
||||||
|
Type::Tuple(types) => record_size_and_alignment(types.0.iter()),
|
||||||
|
|
||||||
|
Type::Variant(types) | Type::Union(types) => variant_size_and_alignment(types.0.iter()),
|
||||||
|
|
||||||
|
Type::Enum(count) => variant_size_and_alignment((0..count.0).map(|_| &Type::Unit)),
|
||||||
|
|
||||||
|
Type::Option(ty) => variant_size_and_alignment([&Type::Unit, ty].into_iter()),
|
||||||
|
|
||||||
|
Type::Expected { ok, err } => variant_size_and_alignment([ok.deref(), err].into_iter()),
|
||||||
|
|
||||||
|
Type::Flags(count) => match FlagsSize::from_count(count.0) {
|
||||||
|
FlagsSize::Size0 => SizeAndAlignment {
|
||||||
|
size: 0,
|
||||||
|
alignment: 1,
|
||||||
|
},
|
||||||
|
FlagsSize::Size1 => SizeAndAlignment {
|
||||||
|
size: 1,
|
||||||
|
alignment: 1,
|
||||||
|
},
|
||||||
|
FlagsSize::Size2 => SizeAndAlignment {
|
||||||
|
size: 2,
|
||||||
|
alignment: 2,
|
||||||
|
},
|
||||||
|
FlagsSize::Size4Plus(n) => SizeAndAlignment {
|
||||||
|
size: n * 4,
|
||||||
|
alignment: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_to(a: usize, align: u32) -> usize {
|
||||||
|
let align = align as usize;
|
||||||
|
(a + (align - 1)) & !(align - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_size_and_alignment<'a>(types: impl Iterator<Item = &'a Type>) -> SizeAndAlignment {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut align = 1;
|
||||||
|
for ty in types {
|
||||||
|
let SizeAndAlignment { size, alignment } = ty.size_and_alignment();
|
||||||
|
offset = align_to(offset, alignment) + size;
|
||||||
|
align = align.max(alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeAndAlignment {
|
||||||
|
size: align_to(offset, align),
|
||||||
|
alignment: align,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variant_size_and_alignment<'a>(
|
||||||
|
types: impl ExactSizeIterator<Item = &'a Type>,
|
||||||
|
) -> SizeAndAlignment {
|
||||||
|
let discriminant_size = DiscriminantSize::from_count(types.len()).unwrap();
|
||||||
|
let mut alignment = u32::from(discriminant_size);
|
||||||
|
let mut size = 0;
|
||||||
|
for ty in types {
|
||||||
|
let size_and_alignment = ty.size_and_alignment();
|
||||||
|
alignment = alignment.max(size_and_alignment.alignment);
|
||||||
|
size = size.max(size_and_alignment.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeAndAlignment {
|
||||||
|
size: align_to(
|
||||||
|
align_to(usize::from(discriminant_size), alignment) + size,
|
||||||
|
alignment,
|
||||||
|
),
|
||||||
|
alignment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_import_and_export(params: &[Type], result: &Type) -> Box<str> {
|
||||||
|
let params_lowered = params
|
||||||
|
.iter()
|
||||||
|
.flat_map(|ty| ty.lowered())
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
let result_lowered = result.lowered();
|
||||||
|
|
||||||
|
let mut core_params = String::new();
|
||||||
|
let mut gets = String::new();
|
||||||
|
|
||||||
|
if params_lowered.len() <= MAX_FLAT_PARAMS {
|
||||||
|
for (index, param) in params_lowered.iter().enumerate() {
|
||||||
|
write!(&mut core_params, " {param}").unwrap();
|
||||||
|
write!(&mut gets, "local.get {index} ").unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(&mut core_params, " i32").unwrap();
|
||||||
|
write!(&mut gets, "local.get 0 ").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_core_params = if params_lowered.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("(param{core_params})")
|
||||||
|
};
|
||||||
|
|
||||||
|
if result_lowered.len() <= MAX_FLAT_RESULTS {
|
||||||
|
let mut core_results = String::new();
|
||||||
|
for result in result_lowered.iter() {
|
||||||
|
write!(&mut core_results, " {result}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_core_results = if result_lowered.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("(result{core_results})")
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
(func $f (import "host" "{IMPORT_FUNCTION}") {maybe_core_params} {maybe_core_results})
|
||||||
|
|
||||||
|
(func (export "{EXPORT_FUNCTION}") {maybe_core_params} {maybe_core_results}
|
||||||
|
{gets}
|
||||||
|
|
||||||
|
call $f
|
||||||
|
)"#
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let SizeAndAlignment { size, alignment } = result.size_and_alignment();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
(func $f (import "host" "{IMPORT_FUNCTION}") (param{core_params} i32))
|
||||||
|
|
||||||
|
(func (export "{EXPORT_FUNCTION}") {maybe_core_params} (result i32)
|
||||||
|
(local $base i32)
|
||||||
|
(local.set $base
|
||||||
|
(call $realloc
|
||||||
|
(i32.const 0)
|
||||||
|
(i32.const 0)
|
||||||
|
(i32.const {alignment})
|
||||||
|
(i32.const {size})))
|
||||||
|
{gets}
|
||||||
|
local.get $base
|
||||||
|
|
||||||
|
call $f
|
||||||
|
|
||||||
|
local.get $base
|
||||||
|
)"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_rust_name(name_counter: &mut u32) -> Ident {
|
||||||
|
let name = format_ident!("Foo{name_counter}");
|
||||||
|
*name_counter += 1;
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a [`TokenStream`] containing the rust type name for a type.
|
||||||
|
///
|
||||||
|
/// The `name_counter` parameter is used to generate names for each recursively visited type. The `declarations`
|
||||||
|
/// parameter is used to accumulate declarations for each recursively visited type.
|
||||||
|
pub fn rust_type(ty: &Type, name_counter: &mut u32, declarations: &mut TokenStream) -> TokenStream {
|
||||||
|
match ty {
|
||||||
|
Type::Unit => quote!(()),
|
||||||
|
Type::Bool => quote!(bool),
|
||||||
|
Type::S8 => quote!(i8),
|
||||||
|
Type::U8 => quote!(u8),
|
||||||
|
Type::S16 => quote!(i16),
|
||||||
|
Type::U16 => quote!(u16),
|
||||||
|
Type::S32 => quote!(i32),
|
||||||
|
Type::U32 => quote!(u32),
|
||||||
|
Type::S64 => quote!(i64),
|
||||||
|
Type::U64 => quote!(u64),
|
||||||
|
Type::Float32 => quote!(Float32),
|
||||||
|
Type::Float64 => quote!(Float64),
|
||||||
|
Type::Char => quote!(char),
|
||||||
|
Type::String => quote!(Box<str>),
|
||||||
|
Type::List(ty) => {
|
||||||
|
let ty = rust_type(ty, name_counter, declarations);
|
||||||
|
quote!(Vec<#ty>)
|
||||||
|
}
|
||||||
|
Type::Record(types) => {
|
||||||
|
let fields = types
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, ty)| {
|
||||||
|
let name = format_ident!("f{index}");
|
||||||
|
let ty = rust_type(ty, name_counter, declarations);
|
||||||
|
quote!(#name: #ty,)
|
||||||
|
})
|
||||||
|
.collect::<TokenStream>();
|
||||||
|
|
||||||
|
let name = make_rust_name(name_counter);
|
||||||
|
|
||||||
|
declarations.extend(quote! {
|
||||||
|
#[derive(ComponentType, Lift, Lower, PartialEq, Debug, Clone, Arbitrary)]
|
||||||
|
#[component(record)]
|
||||||
|
struct #name {
|
||||||
|
#fields
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote!(#name)
|
||||||
|
}
|
||||||
|
Type::Tuple(types) => {
|
||||||
|
let fields = types
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|ty| {
|
||||||
|
let ty = rust_type(ty, name_counter, declarations);
|
||||||
|
quote!(#ty,)
|
||||||
|
})
|
||||||
|
.collect::<TokenStream>();
|
||||||
|
|
||||||
|
quote!((#fields))
|
||||||
|
}
|
||||||
|
Type::Variant(types) | Type::Union(types) => {
|
||||||
|
let cases = types
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, ty)| {
|
||||||
|
let name = format_ident!("C{index}");
|
||||||
|
let ty = rust_type(ty, name_counter, declarations);
|
||||||
|
quote!(#name(#ty),)
|
||||||
|
})
|
||||||
|
.collect::<TokenStream>();
|
||||||
|
|
||||||
|
let name = make_rust_name(name_counter);
|
||||||
|
|
||||||
|
let which = if let Type::Variant(_) = ty {
|
||||||
|
quote!(variant)
|
||||||
|
} else {
|
||||||
|
quote!(union)
|
||||||
|
};
|
||||||
|
|
||||||
|
declarations.extend(quote! {
|
||||||
|
#[derive(ComponentType, Lift, Lower, PartialEq, Debug, Clone, Arbitrary)]
|
||||||
|
#[component(#which)]
|
||||||
|
enum #name {
|
||||||
|
#cases
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote!(#name)
|
||||||
|
}
|
||||||
|
Type::Enum(count) => {
|
||||||
|
let cases = (0..count.0)
|
||||||
|
.map(|index| {
|
||||||
|
let name = format_ident!("C{index}");
|
||||||
|
quote!(#name,)
|
||||||
|
})
|
||||||
|
.collect::<TokenStream>();
|
||||||
|
|
||||||
|
let name = make_rust_name(name_counter);
|
||||||
|
|
||||||
|
declarations.extend(quote! {
|
||||||
|
#[derive(ComponentType, Lift, Lower, PartialEq, Debug, Clone, Arbitrary)]
|
||||||
|
#[component(enum)]
|
||||||
|
enum #name {
|
||||||
|
#cases
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote!(#name)
|
||||||
|
}
|
||||||
|
Type::Option(ty) => {
|
||||||
|
let ty = rust_type(ty, name_counter, declarations);
|
||||||
|
quote!(Option<#ty>)
|
||||||
|
}
|
||||||
|
Type::Expected { ok, err } => {
|
||||||
|
let ok = rust_type(ok, name_counter, declarations);
|
||||||
|
let err = rust_type(err, name_counter, declarations);
|
||||||
|
quote!(Result<#ok, #err>)
|
||||||
|
}
|
||||||
|
Type::Flags(count) => {
|
||||||
|
let type_name = make_rust_name(name_counter);
|
||||||
|
|
||||||
|
let mut flags = TokenStream::new();
|
||||||
|
let mut names = TokenStream::new();
|
||||||
|
|
||||||
|
for index in 0..count.0 {
|
||||||
|
let name = format_ident!("F{index}");
|
||||||
|
flags.extend(quote!(const #name;));
|
||||||
|
names.extend(quote!(#type_name::#name,))
|
||||||
|
}
|
||||||
|
|
||||||
|
declarations.extend(quote! {
|
||||||
|
wasmtime::component::flags! {
|
||||||
|
#type_name {
|
||||||
|
#flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Arbitrary<'a> for #type_name {
|
||||||
|
fn arbitrary(input: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||||
|
let mut flags = #type_name::default();
|
||||||
|
for flag in [#names] {
|
||||||
|
if input.arbitrary()? {
|
||||||
|
flags |= flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote!(#type_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_component_name(name_counter: &mut u32) -> String {
|
||||||
|
let name = format!("$Foo{name_counter}");
|
||||||
|
*name_counter += 1;
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_component_type(
|
||||||
|
ty: &Type,
|
||||||
|
f: &mut String,
|
||||||
|
name_counter: &mut u32,
|
||||||
|
declarations: &mut String,
|
||||||
|
) {
|
||||||
|
match ty {
|
||||||
|
Type::Unit => f.push_str("unit"),
|
||||||
|
Type::Bool => f.push_str("bool"),
|
||||||
|
Type::S8 => f.push_str("s8"),
|
||||||
|
Type::U8 => f.push_str("u8"),
|
||||||
|
Type::S16 => f.push_str("s16"),
|
||||||
|
Type::U16 => f.push_str("u16"),
|
||||||
|
Type::S32 => f.push_str("s32"),
|
||||||
|
Type::U32 => f.push_str("u32"),
|
||||||
|
Type::S64 => f.push_str("s64"),
|
||||||
|
Type::U64 => f.push_str("u64"),
|
||||||
|
Type::Float32 => f.push_str("float32"),
|
||||||
|
Type::Float64 => f.push_str("float64"),
|
||||||
|
Type::Char => f.push_str("char"),
|
||||||
|
Type::String => f.push_str("string"),
|
||||||
|
Type::List(ty) => {
|
||||||
|
let mut case = String::new();
|
||||||
|
write_component_type(ty, &mut case, name_counter, declarations);
|
||||||
|
let name = make_component_name(name_counter);
|
||||||
|
write!(declarations, "(type {name} (list {case}))").unwrap();
|
||||||
|
f.push_str(&name);
|
||||||
|
}
|
||||||
|
Type::Record(types) => {
|
||||||
|
let mut fields = String::new();
|
||||||
|
for (index, ty) in types.iter().enumerate() {
|
||||||
|
write!(fields, r#" (field "f{index}" "#).unwrap();
|
||||||
|
write_component_type(ty, &mut fields, name_counter, declarations);
|
||||||
|
fields.push_str(")");
|
||||||
|
}
|
||||||
|
let name = make_component_name(name_counter);
|
||||||
|
write!(declarations, "(type {name} (record{fields}))").unwrap();
|
||||||
|
f.push_str(&name);
|
||||||
|
}
|
||||||
|
Type::Tuple(types) => {
|
||||||
|
let mut fields = String::new();
|
||||||
|
for ty in types.0.iter() {
|
||||||
|
fields.push_str(" ");
|
||||||
|
write_component_type(ty, &mut fields, name_counter, declarations);
|
||||||
|
}
|
||||||
|
let name = make_component_name(name_counter);
|
||||||
|
write!(declarations, "(type {name} (tuple{fields}))").unwrap();
|
||||||
|
f.push_str(&name);
|
||||||
|
}
|
||||||
|
Type::Variant(types) => {
|
||||||
|
let mut cases = String::new();
|
||||||
|
for (index, ty) in types.0.iter().enumerate() {
|
||||||
|
write!(cases, r#" (case "C{index}" "#).unwrap();
|
||||||
|
write_component_type(ty, &mut cases, name_counter, declarations);
|
||||||
|
cases.push_str(")");
|
||||||
|
}
|
||||||
|
let name = make_component_name(name_counter);
|
||||||
|
write!(declarations, "(type {name} (variant{cases}))").unwrap();
|
||||||
|
f.push_str(&name);
|
||||||
|
}
|
||||||
|
Type::Enum(count) => {
|
||||||
|
f.push_str("(enum");
|
||||||
|
for index in 0..count.0 {
|
||||||
|
write!(f, r#" "C{index}""#).unwrap();
|
||||||
|
}
|
||||||
|
f.push_str(")");
|
||||||
|
}
|
||||||
|
Type::Union(types) => {
|
||||||
|
let mut cases = String::new();
|
||||||
|
for ty in types.0.iter() {
|
||||||
|
cases.push_str(" ");
|
||||||
|
write_component_type(ty, &mut cases, name_counter, declarations);
|
||||||
|
}
|
||||||
|
let name = make_component_name(name_counter);
|
||||||
|
write!(declarations, "(type {name} (union{cases}))").unwrap();
|
||||||
|
f.push_str(&name);
|
||||||
|
}
|
||||||
|
Type::Option(ty) => {
|
||||||
|
let mut case = String::new();
|
||||||
|
write_component_type(ty, &mut case, name_counter, declarations);
|
||||||
|
let name = make_component_name(name_counter);
|
||||||
|
write!(declarations, "(type {name} (option {case}))").unwrap();
|
||||||
|
f.push_str(&name);
|
||||||
|
}
|
||||||
|
Type::Expected { ok, err } => {
|
||||||
|
let mut cases = String::new();
|
||||||
|
write_component_type(ok, &mut cases, name_counter, declarations);
|
||||||
|
cases.push_str(" ");
|
||||||
|
write_component_type(err, &mut cases, name_counter, declarations);
|
||||||
|
let name = make_component_name(name_counter);
|
||||||
|
write!(declarations, "(type {name} (expected {cases}))").unwrap();
|
||||||
|
f.push_str(&name);
|
||||||
|
}
|
||||||
|
Type::Flags(count) => {
|
||||||
|
f.push_str("(flags");
|
||||||
|
for index in 0..count.0 {
|
||||||
|
write!(f, r#" "F{index}""#).unwrap();
|
||||||
|
}
|
||||||
|
f.push_str(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents custom fragments of a WAT file which may be used to create a component for exercising [`TestCase`]s
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Declarations {
|
||||||
|
/// Type declarations (if any) referenced by `params` and/or `result`
|
||||||
|
pub types: Box<str>,
|
||||||
|
/// Parameter declarations used for the imported and exported functions
|
||||||
|
pub params: Box<str>,
|
||||||
|
/// Result declaration used for the imported and exported functions
|
||||||
|
pub result: Box<str>,
|
||||||
|
/// A WAT fragment representing the core function import and export to use for testing
|
||||||
|
pub import_and_export: Box<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Declarations {
|
||||||
|
/// Generate a complete WAT file based on the specified fragments.
|
||||||
|
pub fn make_component(&self) -> Box<str> {
|
||||||
|
let Self {
|
||||||
|
types,
|
||||||
|
params,
|
||||||
|
result,
|
||||||
|
import_and_export,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
(component
|
||||||
|
(core module $libc
|
||||||
|
(memory (export "memory") 1)
|
||||||
|
{REALLOC_AND_FREE}
|
||||||
|
)
|
||||||
|
|
||||||
|
(core instance $libc (instantiate $libc))
|
||||||
|
|
||||||
|
{types}
|
||||||
|
|
||||||
|
(import "{IMPORT_FUNCTION}" (func $f {params} {result}))
|
||||||
|
|
||||||
|
(core func $f_lower (canon lower
|
||||||
|
(func $f)
|
||||||
|
(memory $libc "memory")
|
||||||
|
(realloc (func $libc "realloc"))
|
||||||
|
))
|
||||||
|
|
||||||
|
(core module $m
|
||||||
|
(memory (import "libc" "memory") 1)
|
||||||
|
(func $realloc (import "libc" "realloc") (param i32 i32 i32 i32) (result i32))
|
||||||
|
|
||||||
|
{import_and_export}
|
||||||
|
)
|
||||||
|
|
||||||
|
(core instance $i (instantiate $m
|
||||||
|
(with "libc" (instance $libc))
|
||||||
|
(with "host" (instance (export "{IMPORT_FUNCTION}" (func $f_lower))))
|
||||||
|
))
|
||||||
|
|
||||||
|
(func (export "echo") {params} {result}
|
||||||
|
(canon lift
|
||||||
|
(core func $i "echo")
|
||||||
|
(memory $libc "memory")
|
||||||
|
(realloc (func $libc "realloc"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)"#,
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a test case for calling a component function
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TestCase {
|
||||||
|
/// The types of parameters to pass to the function
|
||||||
|
pub params: Box<[Type]>,
|
||||||
|
/// The type of the result to be returned by the function
|
||||||
|
pub result: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase {
|
||||||
|
/// Generate a `Declarations` for this `TestCase` which may be used to build a component to execute the case.
|
||||||
|
pub fn declarations(&self) -> Declarations {
|
||||||
|
let mut types = String::new();
|
||||||
|
let name_counter = &mut 0;
|
||||||
|
|
||||||
|
let params = self
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|ty| {
|
||||||
|
let mut tmp = String::new();
|
||||||
|
write_component_type(ty, &mut tmp, name_counter, &mut types);
|
||||||
|
format!("(param {tmp})")
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>()
|
||||||
|
.join(" ")
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let result = {
|
||||||
|
let mut tmp = String::new();
|
||||||
|
write_component_type(&self.result, &mut tmp, name_counter, &mut types);
|
||||||
|
format!("(result {tmp})")
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let import_and_export = make_import_and_export(&self.params, &self.result);
|
||||||
|
|
||||||
|
Declarations {
|
||||||
|
types: types.into(),
|
||||||
|
params,
|
||||||
|
result,
|
||||||
|
import_and_export,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Arbitrary<'a> for TestCase {
|
||||||
|
/// Generate an arbitrary [`TestCase`].
|
||||||
|
fn arbitrary(input: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
params: input
|
||||||
|
.arbitrary_iter()?
|
||||||
|
.take(MAX_ARITY)
|
||||||
|
.collect::<arbitrary::Result<Box<[_]>>>()?,
|
||||||
|
result: input.arbitrary()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/misc/component-test-util/Cargo.toml
Normal file
13
crates/misc/component-test-util/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "component-test-util"
|
||||||
|
authors = ["The Wasmtime Project Developers"]
|
||||||
|
license = "Apache-2.0 WITH LLVM-exception"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
env_logger = "0.9.0"
|
||||||
|
anyhow = "1.0.19"
|
||||||
|
arbitrary = { version = "1.1.0", features = ["derive"] }
|
||||||
|
wasmtime = { path = "../../wasmtime", features = ["component-model"] }
|
||||||
112
crates/misc/component-test-util/src/lib.rs
Normal file
112
crates/misc/component-test-util/src/lib.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use arbitrary::Arbitrary;
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
|
use wasmtime::component::__internal::{
|
||||||
|
ComponentTypes, InterfaceType, Memory, MemoryMut, Options, StoreOpaque,
|
||||||
|
};
|
||||||
|
use wasmtime::component::{ComponentParams, ComponentType, Func, Lift, Lower, TypedFunc, Val};
|
||||||
|
use wasmtime::{AsContextMut, Config, Engine, StoreContextMut};
|
||||||
|
|
||||||
|
pub trait TypedFuncExt<P, R> {
|
||||||
|
fn call_and_post_return(&self, store: impl AsContextMut, params: P) -> Result<R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, R> TypedFuncExt<P, R> for TypedFunc<P, R>
|
||||||
|
where
|
||||||
|
P: ComponentParams + Lower,
|
||||||
|
R: Lift,
|
||||||
|
{
|
||||||
|
fn call_and_post_return(&self, mut store: impl AsContextMut, params: P) -> Result<R> {
|
||||||
|
let result = self.call(&mut store, params)?;
|
||||||
|
self.post_return(&mut store)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FuncExt {
|
||||||
|
fn call_and_post_return(&self, store: impl AsContextMut, args: &[Val]) -> Result<Val>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FuncExt for Func {
|
||||||
|
fn call_and_post_return(&self, mut store: impl AsContextMut, args: &[Val]) -> Result<Val> {
|
||||||
|
let result = self.call(&mut store, args)?;
|
||||||
|
self.post_return(&mut store)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn engine() -> Engine {
|
||||||
|
drop(env_logger::try_init());
|
||||||
|
|
||||||
|
let mut config = Config::new();
|
||||||
|
config.wasm_component_model(true);
|
||||||
|
|
||||||
|
// When `WASMTIME_TEST_NO_HOG_MEMORY` is set it means we're in qemu. The
|
||||||
|
// component model tests create a disproportionate number of instances so
|
||||||
|
// try to cut down on virtual memory usage by avoiding 4G reservations.
|
||||||
|
if std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() {
|
||||||
|
config.static_memory_maximum_size(0);
|
||||||
|
config.dynamic_memory_guard_size(0);
|
||||||
|
}
|
||||||
|
Engine::new(&config).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Newtype wrapper for `f32` whose `PartialEq` impl considers NaNs equal to each other.
|
||||||
|
#[derive(Copy, Clone, Debug, Arbitrary)]
|
||||||
|
pub struct Float32(pub f32);
|
||||||
|
|
||||||
|
/// Newtype wrapper for `f64` whose `PartialEq` impl considers NaNs equal to each other.
|
||||||
|
#[derive(Copy, Clone, Debug, Arbitrary)]
|
||||||
|
pub struct Float64(pub f64);
|
||||||
|
|
||||||
|
macro_rules! forward_impls {
|
||||||
|
($($a:ty => $b:ty,)*) => ($(
|
||||||
|
unsafe impl ComponentType for $a {
|
||||||
|
type Lower = <$b as ComponentType>::Lower;
|
||||||
|
|
||||||
|
const SIZE32: usize = <$b as ComponentType>::SIZE32;
|
||||||
|
const ALIGN32: u32 = <$b as ComponentType>::ALIGN32;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn typecheck(ty: &InterfaceType, types: &ComponentTypes) -> Result<()> {
|
||||||
|
<$b as ComponentType>::typecheck(ty, types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Lower for $a {
|
||||||
|
fn lower<U>(
|
||||||
|
&self,
|
||||||
|
store: &mut StoreContextMut<U>,
|
||||||
|
options: &Options,
|
||||||
|
dst: &mut MaybeUninit<Self::Lower>,
|
||||||
|
) -> Result<()> {
|
||||||
|
<$b as Lower>::lower(&self.0, store, options, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store<U>(&self, memory: &mut MemoryMut<'_, U>, offset: usize) -> Result<()> {
|
||||||
|
<$b as Lower>::store(&self.0, memory, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Lift for $a {
|
||||||
|
fn lift(store: &StoreOpaque, options: &Options, src: &Self::Lower) -> Result<Self> {
|
||||||
|
Ok(Self(<$b as Lift>::lift(store, options, src)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(memory: &Memory<'_>, bytes: &[u8]) -> Result<Self> {
|
||||||
|
Ok(Self(<$b as Lift>::load(memory, bytes)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for $a {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.0 == other.0 || (self.0.is_nan() && other.0.is_nan())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*)
|
||||||
|
}
|
||||||
|
|
||||||
|
forward_impls! {
|
||||||
|
Float32 => f32,
|
||||||
|
Float64 => f64,
|
||||||
|
}
|
||||||
@@ -257,6 +257,12 @@ impl Func {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the result type for this function.
|
||||||
|
pub fn result(&self, store: impl AsContext) -> Type {
|
||||||
|
let data = &store.as_context()[self.0];
|
||||||
|
Type::from(&data.types[data.ty].result, &data.types)
|
||||||
|
}
|
||||||
|
|
||||||
/// Invokes this function with the `params` given and returns the result.
|
/// Invokes this function with the `params` given and returns the result.
|
||||||
///
|
///
|
||||||
/// The `params` here must match the type signature of this `Func`, or this will return an error. If a trap
|
/// The `params` here must match the type signature of this `Func`, or this will return an error. If a trap
|
||||||
@@ -307,11 +313,14 @@ impl Func {
|
|||||||
self.store_args(store, &options, ¶ms, args, dst)
|
self.store_args(store, &options, ¶ms, args, dst)
|
||||||
} else {
|
} else {
|
||||||
dst.write([ValRaw::u64(0); MAX_FLAT_PARAMS]);
|
dst.write([ValRaw::u64(0); MAX_FLAT_PARAMS]);
|
||||||
let dst = unsafe {
|
|
||||||
|
let dst = &mut unsafe {
|
||||||
mem::transmute::<_, &mut [MaybeUninit<ValRaw>; MAX_FLAT_PARAMS]>(dst)
|
mem::transmute::<_, &mut [MaybeUninit<ValRaw>; MAX_FLAT_PARAMS]>(dst)
|
||||||
};
|
}
|
||||||
|
.iter_mut();
|
||||||
|
|
||||||
args.iter()
|
args.iter()
|
||||||
.try_for_each(|arg| arg.lower(store, &options, &mut dst.iter_mut()))
|
.try_for_each(|arg| arg.lower(store, &options, dst))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|store, options, src: &[ValRaw; MAX_FLAT_RESULTS]| {
|
|store, options, src: &[ValRaw; MAX_FLAT_RESULTS]| {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use crate::component::func::{Memory, MemoryMut, Options};
|
use crate::component::func::{Memory, MemoryMut, Options};
|
||||||
use crate::component::{ComponentParams, ComponentType, Lift, Lower};
|
use crate::component::types::SizeAndAlignment;
|
||||||
|
use crate::component::{ComponentParams, ComponentType, Lift, Lower, Type, Val};
|
||||||
use crate::{AsContextMut, StoreContextMut, ValRaw};
|
use crate::{AsContextMut, StoreContextMut, ValRaw};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::mem::MaybeUninit;
|
use std::mem::{self, MaybeUninit};
|
||||||
use std::panic::{self, AssertUnwindSafe};
|
use std::panic::{self, AssertUnwindSafe};
|
||||||
use std::ptr::NonNull;
|
use std::ptr::NonNull;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -43,7 +44,7 @@ pub trait IntoComponentFunc<T, Params, Return> {
|
|||||||
|
|
||||||
pub struct HostFunc {
|
pub struct HostFunc {
|
||||||
entrypoint: VMLoweringCallee,
|
entrypoint: VMLoweringCallee,
|
||||||
typecheck: fn(TypeFuncIndex, &ComponentTypes) -> Result<()>,
|
typecheck: Box<dyn (Fn(TypeFuncIndex, &Arc<ComponentTypes>) -> Result<()>) + Send + Sync>,
|
||||||
func: Box<dyn Any + Send + Sync>,
|
func: Box<dyn Any + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,17 +52,54 @@ impl HostFunc {
|
|||||||
fn new<F, P, R>(func: F, entrypoint: VMLoweringCallee) -> Arc<HostFunc>
|
fn new<F, P, R>(func: F, entrypoint: VMLoweringCallee) -> Arc<HostFunc>
|
||||||
where
|
where
|
||||||
F: Send + Sync + 'static,
|
F: Send + Sync + 'static,
|
||||||
P: ComponentParams + Lift,
|
P: ComponentParams + Lift + 'static,
|
||||||
R: Lower,
|
R: Lower + 'static,
|
||||||
{
|
{
|
||||||
Arc::new(HostFunc {
|
Arc::new(HostFunc {
|
||||||
entrypoint,
|
entrypoint,
|
||||||
typecheck: typecheck::<P, R>,
|
typecheck: Box::new(typecheck::<P, R>),
|
||||||
func: Box::new(func),
|
func: Box::new(func),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn typecheck(&self, ty: TypeFuncIndex, types: &ComponentTypes) -> Result<()> {
|
pub(crate) fn new_dynamic<
|
||||||
|
T,
|
||||||
|
F: Fn(StoreContextMut<'_, T>, &[Val]) -> Result<Val> + Send + Sync + 'static,
|
||||||
|
>(
|
||||||
|
func: F,
|
||||||
|
index: TypeFuncIndex,
|
||||||
|
types: &Arc<ComponentTypes>,
|
||||||
|
) -> Arc<HostFunc> {
|
||||||
|
let ty = &types[index];
|
||||||
|
|
||||||
|
Arc::new(HostFunc {
|
||||||
|
entrypoint: dynamic_entrypoint::<T, F>,
|
||||||
|
typecheck: Box::new({
|
||||||
|
let types = types.clone();
|
||||||
|
|
||||||
|
move |expected_index, expected_types| {
|
||||||
|
if index == expected_index && Arc::ptr_eq(&types, expected_types) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("function type mismatch"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
func: Box::new(DynamicContext {
|
||||||
|
func,
|
||||||
|
types: Types {
|
||||||
|
params: ty
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|(_, ty)| Type::from(ty, types))
|
||||||
|
.collect(),
|
||||||
|
result: Type::from(&ty.result, types),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn typecheck(&self, ty: TypeFuncIndex, types: &Arc<ComponentTypes>) -> Result<()> {
|
||||||
(self.typecheck)(ty, types)
|
(self.typecheck)(ty, types)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +112,7 @@ impl HostFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn typecheck<P, R>(ty: TypeFuncIndex, types: &ComponentTypes) -> Result<()>
|
fn typecheck<P, R>(ty: TypeFuncIndex, types: &Arc<ComponentTypes>) -> Result<()>
|
||||||
where
|
where
|
||||||
P: ComponentParams + Lift,
|
P: ComponentParams + Lift,
|
||||||
R: Lower,
|
R: Lower,
|
||||||
@@ -256,8 +294,8 @@ macro_rules! impl_into_component_func {
|
|||||||
impl<T, F, $($args,)* R> IntoComponentFunc<T, ($($args,)*), R> for F
|
impl<T, F, $($args,)* R> IntoComponentFunc<T, ($($args,)*), R> for F
|
||||||
where
|
where
|
||||||
F: Fn($($args),*) -> Result<R> + Send + Sync + 'static,
|
F: Fn($($args),*) -> Result<R> + Send + Sync + 'static,
|
||||||
($($args,)*): ComponentParams + Lift,
|
($($args,)*): ComponentParams + Lift + 'static,
|
||||||
R: Lower,
|
R: Lower + 'static,
|
||||||
{
|
{
|
||||||
extern "C" fn entrypoint(
|
extern "C" fn entrypoint(
|
||||||
cx: *mut VMOpaqueContext,
|
cx: *mut VMOpaqueContext,
|
||||||
@@ -294,8 +332,8 @@ macro_rules! impl_into_component_func {
|
|||||||
impl<T, F, $($args,)* R> IntoComponentFunc<T, (StoreContextMut<'_, T>, $($args,)*), R> for F
|
impl<T, F, $($args,)* R> IntoComponentFunc<T, (StoreContextMut<'_, T>, $($args,)*), R> for F
|
||||||
where
|
where
|
||||||
F: Fn(StoreContextMut<'_, T>, $($args),*) -> Result<R> + Send + Sync + 'static,
|
F: Fn(StoreContextMut<'_, T>, $($args),*) -> Result<R> + Send + Sync + 'static,
|
||||||
($($args,)*): ComponentParams + Lift,
|
($($args,)*): ComponentParams + Lift + 'static,
|
||||||
R: Lower,
|
R: Lower + 'static,
|
||||||
{
|
{
|
||||||
extern "C" fn entrypoint(
|
extern "C" fn entrypoint(
|
||||||
cx: *mut VMOpaqueContext,
|
cx: *mut VMOpaqueContext,
|
||||||
@@ -330,3 +368,154 @@ macro_rules! impl_into_component_func {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for_each_function_signature!(impl_into_component_func);
|
for_each_function_signature!(impl_into_component_func);
|
||||||
|
|
||||||
|
unsafe fn call_host_dynamic<T, F>(
|
||||||
|
Types { params, result }: &Types,
|
||||||
|
cx: *mut VMOpaqueContext,
|
||||||
|
mut flags: InstanceFlags,
|
||||||
|
memory: *mut VMMemoryDefinition,
|
||||||
|
realloc: *mut VMCallerCheckedAnyfunc,
|
||||||
|
string_encoding: StringEncoding,
|
||||||
|
storage: &mut [ValRaw],
|
||||||
|
closure: F,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
F: FnOnce(StoreContextMut<'_, T>, &[Val]) -> Result<Val>,
|
||||||
|
{
|
||||||
|
let cx = VMComponentContext::from_opaque(cx);
|
||||||
|
let instance = (*cx).instance();
|
||||||
|
let mut cx = StoreContextMut::from_raw((*instance).store());
|
||||||
|
|
||||||
|
let options = Options::new(
|
||||||
|
cx.0.id(),
|
||||||
|
NonNull::new(memory),
|
||||||
|
NonNull::new(realloc),
|
||||||
|
string_encoding,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform a dynamic check that this instance can indeed be left. Exiting
|
||||||
|
// the component is disallowed, for example, when the `realloc` function
|
||||||
|
// calls a canonical import.
|
||||||
|
if !flags.may_leave() {
|
||||||
|
bail!("cannot leave component instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
let param_count = params.iter().map(|ty| ty.flatten_count()).sum::<usize>();
|
||||||
|
|
||||||
|
let args;
|
||||||
|
let ret_index;
|
||||||
|
|
||||||
|
if param_count <= MAX_FLAT_PARAMS {
|
||||||
|
let iter = &mut storage.iter();
|
||||||
|
args = params
|
||||||
|
.iter()
|
||||||
|
.map(|ty| Val::lift(ty, cx.0, &options, iter))
|
||||||
|
.collect::<Result<Box<[_]>>>()?;
|
||||||
|
ret_index = param_count;
|
||||||
|
} else {
|
||||||
|
let param_layout = {
|
||||||
|
let mut size = 0;
|
||||||
|
let mut alignment = 1;
|
||||||
|
for ty in params.iter() {
|
||||||
|
alignment = alignment.max(ty.size_and_alignment().alignment);
|
||||||
|
ty.next_field(&mut size);
|
||||||
|
}
|
||||||
|
SizeAndAlignment { size, alignment }
|
||||||
|
};
|
||||||
|
|
||||||
|
let memory = Memory::new(cx.0, &options);
|
||||||
|
let mut offset = validate_inbounds_dynamic(param_layout, memory.as_slice(), &storage[0])?;
|
||||||
|
args = params
|
||||||
|
.iter()
|
||||||
|
.map(|ty| {
|
||||||
|
Val::load(
|
||||||
|
ty,
|
||||||
|
&memory,
|
||||||
|
&memory.as_slice()[ty.next_field(&mut offset)..]
|
||||||
|
[..ty.size_and_alignment().size],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Box<[_]>>>()?;
|
||||||
|
ret_index = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ret = closure(cx.as_context_mut(), &args)?;
|
||||||
|
flags.set_may_leave(false);
|
||||||
|
result.check(&ret)?;
|
||||||
|
|
||||||
|
let result_count = result.flatten_count();
|
||||||
|
if result_count <= MAX_FLAT_RESULTS {
|
||||||
|
let dst = mem::transmute::<&mut [ValRaw], &mut [MaybeUninit<ValRaw>]>(storage);
|
||||||
|
ret.lower(&mut cx, &options, &mut dst.iter_mut())?;
|
||||||
|
} else {
|
||||||
|
let ret_ptr = &storage[ret_index];
|
||||||
|
let mut memory = MemoryMut::new(cx.as_context_mut(), &options);
|
||||||
|
let ptr =
|
||||||
|
validate_inbounds_dynamic(result.size_and_alignment(), memory.as_slice_mut(), ret_ptr)?;
|
||||||
|
ret.store(&mut memory, ptr)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.set_may_leave(true);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_inbounds_dynamic(
|
||||||
|
SizeAndAlignment { size, alignment }: SizeAndAlignment,
|
||||||
|
memory: &[u8],
|
||||||
|
ptr: &ValRaw,
|
||||||
|
) -> Result<usize> {
|
||||||
|
// FIXME: needs memory64 support
|
||||||
|
let ptr = usize::try_from(ptr.get_u32())?;
|
||||||
|
if ptr % usize::try_from(alignment)? != 0 {
|
||||||
|
bail!("pointer not aligned");
|
||||||
|
}
|
||||||
|
let end = match ptr.checked_add(size) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => bail!("pointer size overflow"),
|
||||||
|
};
|
||||||
|
if end > memory.len() {
|
||||||
|
bail!("pointer out of bounds")
|
||||||
|
}
|
||||||
|
Ok(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Types {
|
||||||
|
params: Box<[Type]>,
|
||||||
|
result: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DynamicContext<F> {
|
||||||
|
func: F,
|
||||||
|
types: Types,
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn dynamic_entrypoint<
|
||||||
|
T,
|
||||||
|
F: Fn(StoreContextMut<'_, T>, &[Val]) -> Result<Val> + Send + Sync + 'static,
|
||||||
|
>(
|
||||||
|
cx: *mut VMOpaqueContext,
|
||||||
|
data: *mut u8,
|
||||||
|
flags: InstanceFlags,
|
||||||
|
memory: *mut VMMemoryDefinition,
|
||||||
|
realloc: *mut VMCallerCheckedAnyfunc,
|
||||||
|
string_encoding: StringEncoding,
|
||||||
|
storage: *mut ValRaw,
|
||||||
|
storage_len: usize,
|
||||||
|
) {
|
||||||
|
let data = data as *const DynamicContext<F>;
|
||||||
|
unsafe {
|
||||||
|
handle_result(|| {
|
||||||
|
call_host_dynamic::<T, _>(
|
||||||
|
&(*data).types,
|
||||||
|
cx,
|
||||||
|
flags,
|
||||||
|
memory,
|
||||||
|
realloc,
|
||||||
|
string_encoding,
|
||||||
|
std::slice::from_raw_parts_mut(storage, storage_len),
|
||||||
|
|store, values| ((*data).func)(store, values),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1731,7 +1731,7 @@ macro_rules! impl_component_ty_for_tuples {
|
|||||||
_size = align_to(_size, $t::ALIGN32);
|
_size = align_to(_size, $t::ALIGN32);
|
||||||
_size += $t::SIZE32;
|
_size += $t::SIZE32;
|
||||||
)*
|
)*
|
||||||
_size
|
align_to(_size, Self::ALIGN32)
|
||||||
};
|
};
|
||||||
|
|
||||||
const ALIGN32: u32 = {
|
const ALIGN32: u32 = {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ impl Instance {
|
|||||||
/// Looks up a function by name within this [`Instance`].
|
/// Looks up a function by name within this [`Instance`].
|
||||||
///
|
///
|
||||||
/// This is a convenience method for calling [`Instance::exports`] followed
|
/// This is a convenience method for calling [`Instance::exports`] followed
|
||||||
/// by [`ExportInstance::get_func`].
|
/// by [`ExportInstance::func`].
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use crate::component::func::HostFunc;
|
use crate::component::func::HostFunc;
|
||||||
use crate::component::instance::RuntimeImport;
|
use crate::component::instance::RuntimeImport;
|
||||||
use crate::component::matching::TypeChecker;
|
use crate::component::matching::TypeChecker;
|
||||||
use crate::component::{Component, Instance, InstancePre, IntoComponentFunc};
|
use crate::component::{Component, Instance, InstancePre, IntoComponentFunc, Val};
|
||||||
use crate::{AsContextMut, Engine, Module};
|
use crate::{AsContextMut, Engine, Module, StoreContextMut};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use std::collections::hash_map::{Entry, HashMap};
|
use std::collections::hash_map::{Entry, HashMap};
|
||||||
use std::marker;
|
use std::marker;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use wasmtime_environ::component::TypeDef;
|
||||||
use wasmtime_environ::PrimaryMap;
|
use wasmtime_environ::PrimaryMap;
|
||||||
|
|
||||||
/// A type used to instantiate [`Component`]s.
|
/// A type used to instantiate [`Component`]s.
|
||||||
@@ -230,6 +231,37 @@ impl<T> LinkerInstance<'_, T> {
|
|||||||
self.insert(name, Definition::Func(func.into_host_func()))
|
self.insert(name, Definition::Func(func.into_host_func()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Define a new host-provided function using dynamic types.
|
||||||
|
///
|
||||||
|
/// `name` must refer to a function type import in `component`. If and when
|
||||||
|
/// that import is invoked by the component, the specified `func` will be
|
||||||
|
/// called, which must return a `Val` which is an instance of the result
|
||||||
|
/// type of the import.
|
||||||
|
pub fn func_new<
|
||||||
|
F: Fn(StoreContextMut<'_, T>, &[Val]) -> Result<Val> + Send + Sync + 'static,
|
||||||
|
>(
|
||||||
|
&mut self,
|
||||||
|
component: &Component,
|
||||||
|
name: &str,
|
||||||
|
func: F,
|
||||||
|
) -> Result<()> {
|
||||||
|
for (import_name, ty) in component.env_component().import_types.values() {
|
||||||
|
if name == import_name {
|
||||||
|
if let TypeDef::ComponentFunc(index) = ty {
|
||||||
|
let name = self.strings.intern(name);
|
||||||
|
return self.insert(
|
||||||
|
name,
|
||||||
|
Definition::Func(HostFunc::new_dynamic(func, *index, component.types())),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
bail!("import `{name}` has the wrong type (expected a function)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("import `{name}` not found"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Defines a [`Module`] within this instance.
|
/// Defines a [`Module`] within this instance.
|
||||||
///
|
///
|
||||||
/// This can be used to provide a core wasm [`Module`] as an import to a
|
/// This can be used to provide a core wasm [`Module`] as an import to a
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ use crate::component::linker::{Definition, NameMap, Strings};
|
|||||||
use crate::types::matching;
|
use crate::types::matching;
|
||||||
use crate::Module;
|
use crate::Module;
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use std::sync::Arc;
|
||||||
use wasmtime_environ::component::{
|
use wasmtime_environ::component::{
|
||||||
ComponentTypes, TypeComponentInstance, TypeDef, TypeFuncIndex, TypeModule,
|
ComponentTypes, TypeComponentInstance, TypeDef, TypeFuncIndex, TypeModule,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TypeChecker<'a> {
|
pub struct TypeChecker<'a> {
|
||||||
pub types: &'a ComponentTypes,
|
pub types: &'a Arc<ComponentTypes>,
|
||||||
pub strings: &'a Strings,
|
pub strings: &'a Strings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ impl Flags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the size and alignment requirements of the heap-serialized form of a type
|
/// Represents the size and alignment requirements of the heap-serialized form of a type
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) struct SizeAndAlignment {
|
pub(crate) struct SizeAndAlignment {
|
||||||
pub(crate) size: usize,
|
pub(crate) size: usize,
|
||||||
pub(crate) alignment: u32,
|
pub(crate) alignment: u32,
|
||||||
@@ -662,7 +663,10 @@ fn variant_size_and_alignment(types: impl ExactSizeIterator<Item = Type>) -> Siz
|
|||||||
}
|
}
|
||||||
|
|
||||||
SizeAndAlignment {
|
SizeAndAlignment {
|
||||||
size: func::align_to(usize::from(discriminant_size), alignment) + size,
|
size: func::align_to(
|
||||||
|
func::align_to(usize::from(discriminant_size), alignment) + size,
|
||||||
|
alignment,
|
||||||
|
),
|
||||||
alignment,
|
alignment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -604,8 +604,9 @@ impl Val {
|
|||||||
}
|
}
|
||||||
Type::Flags(handle) => {
|
Type::Flags(handle) => {
|
||||||
let count = u32::try_from(handle.names().len()).unwrap();
|
let count = u32::try_from(handle.names().len()).unwrap();
|
||||||
assert!(count <= 32);
|
let value = iter::repeat_with(|| u32::lift(store, options, next(src)))
|
||||||
let value = iter::once(u32::lift(store, options, next(src))?).collect();
|
.take(u32_count_for_flag_count(count.try_into()?))
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
Val::Flags(Flags {
|
Val::Flags(Flags {
|
||||||
ty: handle.clone(),
|
ty: handle.clone(),
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ publish = false
|
|||||||
cargo-fuzz = true
|
cargo-fuzz = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = { version = "1.0.19" }
|
||||||
|
arbitrary = { version = "1.1.0", features = ["derive"] }
|
||||||
cranelift-codegen = { path = "../cranelift/codegen" }
|
cranelift-codegen = { path = "../cranelift/codegen" }
|
||||||
cranelift-reader = { path = "../cranelift/reader" }
|
cranelift-reader = { path = "../cranelift/reader" }
|
||||||
cranelift-wasm = { path = "../cranelift/wasm" }
|
cranelift-wasm = { path = "../cranelift/wasm" }
|
||||||
@@ -19,6 +21,16 @@ libfuzzer-sys = "0.4.0"
|
|||||||
target-lexicon = "0.12"
|
target-lexicon = "0.12"
|
||||||
wasmtime = { path = "../crates/wasmtime" }
|
wasmtime = { path = "../crates/wasmtime" }
|
||||||
wasmtime-fuzzing = { path = "../crates/fuzzing" }
|
wasmtime-fuzzing = { path = "../crates/fuzzing" }
|
||||||
|
component-test-util = { path = "../crates/misc/component-test-util" }
|
||||||
|
component-fuzz-util = { path = "../crates/misc/component-fuzz-util" }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
anyhow = "1.0.19"
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
arbitrary = { version = "1.1.0", features = ["derive"] }
|
||||||
|
rand = { version = "0.8.0" }
|
||||||
|
quote = "1.0"
|
||||||
|
component-fuzz-util = { path = "../crates/misc/component-fuzz-util" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ['fuzz-spec-interpreter']
|
default = ['fuzz-spec-interpreter']
|
||||||
@@ -102,3 +114,9 @@ name = "instantiate-many"
|
|||||||
path = "fuzz_targets/instantiate-many.rs"
|
path = "fuzz_targets/instantiate-many.rs"
|
||||||
test = false
|
test = false
|
||||||
doc = false
|
doc = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "component_api"
|
||||||
|
path = "fuzz_targets/component_api.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
|||||||
144
fuzz/build.rs
Normal file
144
fuzz/build.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
component::generate_static_api_tests()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
mod component {
|
||||||
|
use anyhow::{anyhow, Context, Error, Result};
|
||||||
|
use arbitrary::{Arbitrary, Unstructured};
|
||||||
|
use component_fuzz_util::{self, Declarations, TestCase};
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
use std::env;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::fs;
|
||||||
|
use std::iter;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
pub fn generate_static_api_tests() -> Result<()> {
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
let out_dir = PathBuf::from(
|
||||||
|
env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
write_static_api_tests(&mut out)?;
|
||||||
|
|
||||||
|
let output = out_dir.join("static_component_api.rs");
|
||||||
|
fs::write(&output, out)?;
|
||||||
|
|
||||||
|
drop(Command::new("rustfmt").arg(&output).status());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_static_api_tests(out: &mut String) -> Result<()> {
|
||||||
|
let seed = if let Ok(seed) = env::var("WASMTIME_FUZZ_SEED") {
|
||||||
|
seed.parse::<u64>()
|
||||||
|
.with_context(|| anyhow!("expected u64 in WASMTIME_FUZZ_SEED"))?
|
||||||
|
} else {
|
||||||
|
StdRng::from_entropy().gen()
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"using seed {seed} (set WASMTIME_FUZZ_SEED={seed} in your environment to reproduce)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rng = StdRng::seed_from_u64(seed);
|
||||||
|
|
||||||
|
const TEST_CASE_COUNT: usize = 100;
|
||||||
|
|
||||||
|
let mut tests = TokenStream::new();
|
||||||
|
|
||||||
|
let name_counter = &mut 0;
|
||||||
|
|
||||||
|
let mut declarations = TokenStream::new();
|
||||||
|
|
||||||
|
for index in 0..TEST_CASE_COUNT {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
|
||||||
|
let case = loop {
|
||||||
|
let count = rng.gen_range(1000..2000);
|
||||||
|
bytes.extend(iter::repeat_with(|| rng.gen::<u8>()).take(count));
|
||||||
|
|
||||||
|
match TestCase::arbitrary(&mut Unstructured::new(&bytes)) {
|
||||||
|
Ok(case) => break case,
|
||||||
|
Err(arbitrary::Error::NotEnoughData) => (),
|
||||||
|
Err(error) => return Err(Error::from(error)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Declarations {
|
||||||
|
types,
|
||||||
|
params,
|
||||||
|
result,
|
||||||
|
import_and_export,
|
||||||
|
} = case.declarations();
|
||||||
|
|
||||||
|
let test = format_ident!("static_api_test{}", case.params.len());
|
||||||
|
|
||||||
|
let rust_params = case
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|ty| {
|
||||||
|
let ty = component_fuzz_util::rust_type(&ty, name_counter, &mut declarations);
|
||||||
|
quote!(#ty,)
|
||||||
|
})
|
||||||
|
.collect::<TokenStream>();
|
||||||
|
|
||||||
|
let rust_result =
|
||||||
|
component_fuzz_util::rust_type(&case.result, name_counter, &mut declarations);
|
||||||
|
|
||||||
|
let test = quote!(#index => component_types::#test::<#rust_params #rust_result>(
|
||||||
|
input,
|
||||||
|
&Declarations {
|
||||||
|
types: #types.into(),
|
||||||
|
params: #params.into(),
|
||||||
|
result: #result.into(),
|
||||||
|
import_and_export: #import_and_export.into()
|
||||||
|
}
|
||||||
|
),);
|
||||||
|
|
||||||
|
tests.extend(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
let module = quote! {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
fn static_component_api_target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
|
||||||
|
use anyhow::Result;
|
||||||
|
use arbitrary::{Unstructured, Arbitrary};
|
||||||
|
use component_test_util::{self, Float32, Float64};
|
||||||
|
use component_fuzz_util::Declarations;
|
||||||
|
use std::sync::{Arc, Once};
|
||||||
|
use wasmtime::component::{ComponentType, Lift, Lower};
|
||||||
|
use wasmtime_fuzzing::generators::component_types;
|
||||||
|
|
||||||
|
const SEED: u64 = #seed;
|
||||||
|
|
||||||
|
static ONCE: Once = Once::new();
|
||||||
|
|
||||||
|
ONCE.call_once(|| {
|
||||||
|
eprintln!(
|
||||||
|
"Seed {SEED} was used to generate static component API fuzz tests.\n\
|
||||||
|
Set WASMTIME_FUZZ_SEED={SEED} in your environment at build time to reproduce."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
#declarations
|
||||||
|
|
||||||
|
match input.int_in_range(0..=(#TEST_CASE_COUNT-1))? {
|
||||||
|
#tests
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(out, "{module}")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
22
fuzz/fuzz_targets/component_api.rs
Normal file
22
fuzz/fuzz_targets/component_api.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use wasmtime_fuzzing::oracles;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/static_component_api.rs"));
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
fn target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
|
||||||
|
if input.arbitrary()? {
|
||||||
|
static_component_api_target(input)
|
||||||
|
} else {
|
||||||
|
oracles::dynamic_component_api_target(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzz_target!(|bytes: &[u8]| {
|
||||||
|
match target(&mut arbitrary::Unstructured::new(bytes)) {
|
||||||
|
Ok(()) | Err(arbitrary::Error::NotEnoughData) => (),
|
||||||
|
Err(error) => panic!("{}", error),
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use component_test_util::{engine, TypedFuncExt};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use wasmtime::component::{Component, ComponentParams, Lift, Lower, TypedFunc};
|
use wasmtime::component::Component;
|
||||||
use wasmtime::{AsContextMut, Config, Engine};
|
use wasmtime_component_util::REALLOC_AND_FREE;
|
||||||
|
|
||||||
mod dynamic;
|
mod dynamic;
|
||||||
mod func;
|
mod func;
|
||||||
@@ -12,97 +13,6 @@ mod macros;
|
|||||||
mod nested;
|
mod nested;
|
||||||
mod post_return;
|
mod post_return;
|
||||||
|
|
||||||
trait TypedFuncExt<P, R> {
|
|
||||||
fn call_and_post_return(&self, store: impl AsContextMut, params: P) -> Result<R>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P, R> TypedFuncExt<P, R> for TypedFunc<P, R>
|
|
||||||
where
|
|
||||||
P: ComponentParams + Lower,
|
|
||||||
R: Lift,
|
|
||||||
{
|
|
||||||
fn call_and_post_return(&self, mut store: impl AsContextMut, params: P) -> Result<R> {
|
|
||||||
let result = self.call(&mut store, params)?;
|
|
||||||
self.post_return(&mut store)?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A simple bump allocator which can be used with modules
|
|
||||||
const REALLOC_AND_FREE: &str = r#"
|
|
||||||
(global $last (mut i32) (i32.const 8))
|
|
||||||
(func $realloc (export "realloc")
|
|
||||||
(param $old_ptr i32)
|
|
||||||
(param $old_size i32)
|
|
||||||
(param $align i32)
|
|
||||||
(param $new_size i32)
|
|
||||||
(result i32)
|
|
||||||
|
|
||||||
;; Test if the old pointer is non-null
|
|
||||||
local.get $old_ptr
|
|
||||||
if
|
|
||||||
;; If the old size is bigger than the new size then
|
|
||||||
;; this is a shrink and transparently allow it
|
|
||||||
local.get $old_size
|
|
||||||
local.get $new_size
|
|
||||||
i32.gt_u
|
|
||||||
if
|
|
||||||
local.get $old_ptr
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
;; ... otherwise this is unimplemented
|
|
||||||
unreachable
|
|
||||||
end
|
|
||||||
|
|
||||||
;; align up `$last`
|
|
||||||
(global.set $last
|
|
||||||
(i32.and
|
|
||||||
(i32.add
|
|
||||||
(global.get $last)
|
|
||||||
(i32.add
|
|
||||||
(local.get $align)
|
|
||||||
(i32.const -1)))
|
|
||||||
(i32.xor
|
|
||||||
(i32.add
|
|
||||||
(local.get $align)
|
|
||||||
(i32.const -1))
|
|
||||||
(i32.const -1))))
|
|
||||||
|
|
||||||
;; save the current value of `$last` as the return value
|
|
||||||
global.get $last
|
|
||||||
|
|
||||||
;; ensure anything necessary is set to valid data by spraying a bit
|
|
||||||
;; pattern that is invalid
|
|
||||||
global.get $last
|
|
||||||
i32.const 0xde
|
|
||||||
local.get $new_size
|
|
||||||
memory.fill
|
|
||||||
|
|
||||||
;; bump our pointer
|
|
||||||
(global.set $last
|
|
||||||
(i32.add
|
|
||||||
(global.get $last)
|
|
||||||
(local.get $new_size)))
|
|
||||||
)
|
|
||||||
"#;
|
|
||||||
|
|
||||||
fn engine() -> Engine {
|
|
||||||
drop(env_logger::try_init());
|
|
||||||
|
|
||||||
let mut config = Config::new();
|
|
||||||
config.wasm_component_model(true);
|
|
||||||
|
|
||||||
// When pooling allocator tests are skipped it means we're in qemu. The
|
|
||||||
// component model tests create a disproportionate number of instances so
|
|
||||||
// try to cut down on virtual memory usage by avoiding 4G reservations.
|
|
||||||
if crate::skip_pooling_allocator_tests() {
|
|
||||||
config.static_memory_maximum_size(0);
|
|
||||||
config.dynamic_memory_guard_size(0);
|
|
||||||
}
|
|
||||||
Engine::new(&config).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn components_importing_modules() -> Result<()> {
|
fn components_importing_modules() -> Result<()> {
|
||||||
let engine = engine();
|
let engine = engine();
|
||||||
@@ -113,49 +23,49 @@ fn components_importing_modules() -> Result<()> {
|
|||||||
Component::new(
|
Component::new(
|
||||||
&engine,
|
&engine,
|
||||||
r#"
|
r#"
|
||||||
(component
|
(component
|
||||||
(import "" (core module))
|
(import "" (core module))
|
||||||
)
|
)
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Component::new(
|
Component::new(
|
||||||
&engine,
|
&engine,
|
||||||
r#"
|
r#"
|
||||||
(component
|
(component
|
||||||
(import "" (core module $m1
|
(import "" (core module $m1
|
||||||
(import "" "" (func))
|
(import "" "" (func))
|
||||||
(import "" "x" (global i32))
|
(import "" "x" (global i32))
|
||||||
|
|
||||||
(export "a" (table 1 funcref))
|
(export "a" (table 1 funcref))
|
||||||
(export "b" (memory 1))
|
(export "b" (memory 1))
|
||||||
(export "c" (func (result f32)))
|
(export "c" (func (result f32)))
|
||||||
(export "d" (global i64))
|
(export "d" (global i64))
|
||||||
))
|
))
|
||||||
|
|
||||||
(core module $m2
|
(core module $m2
|
||||||
(func (export ""))
|
(func (export ""))
|
||||||
(global (export "x") i32 i32.const 0)
|
(global (export "x") i32 i32.const 0)
|
||||||
)
|
|
||||||
(core instance $i2 (instantiate (module $m2)))
|
|
||||||
(core instance $i1 (instantiate (module $m1) (with "" (instance $i2))))
|
|
||||||
|
|
||||||
(core module $m3
|
|
||||||
(import "mod" "1" (memory 1))
|
|
||||||
(import "mod" "2" (table 1 funcref))
|
|
||||||
(import "mod" "3" (global i64))
|
|
||||||
(import "mod" "4" (func (result f32)))
|
|
||||||
)
|
|
||||||
|
|
||||||
(core instance $i3 (instantiate (module $m3)
|
|
||||||
(with "mod" (instance
|
|
||||||
(export "1" (memory $i1 "b"))
|
|
||||||
(export "2" (table $i1 "a"))
|
|
||||||
(export "3" (global $i1 "d"))
|
|
||||||
(export "4" (func $i1 "c"))
|
|
||||||
))
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
|
(core instance $i2 (instantiate (module $m2)))
|
||||||
|
(core instance $i1 (instantiate (module $m1) (with "" (instance $i2))))
|
||||||
|
|
||||||
|
(core module $m3
|
||||||
|
(import "mod" "1" (memory 1))
|
||||||
|
(import "mod" "2" (table 1 funcref))
|
||||||
|
(import "mod" "3" (global i64))
|
||||||
|
(import "mod" "4" (func (result f32)))
|
||||||
|
)
|
||||||
|
|
||||||
|
(core instance $i3 (instantiate (module $m3)
|
||||||
|
(with "mod" (instance
|
||||||
|
(export "1" (memory $i1 "b"))
|
||||||
|
(export "2" (table $i1 "a"))
|
||||||
|
(export "3" (global $i1 "d"))
|
||||||
|
(export "4" (func $i1 "c"))
|
||||||
|
))
|
||||||
|
))
|
||||||
|
)
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
use super::{make_echo_component, make_echo_component_with_params, Param, Type};
|
use super::{make_echo_component, make_echo_component_with_params, Param, Type};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use wasmtime::component::{self, Component, Func, Linker, Val};
|
use component_test_util::FuncExt;
|
||||||
use wasmtime::{AsContextMut, Store};
|
use wasmtime::component::{self, Component, Linker, Val};
|
||||||
|
use wasmtime::Store;
|
||||||
trait FuncExt {
|
|
||||||
fn call_and_post_return(&self, store: impl AsContextMut, args: &[Val]) -> Result<Val>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FuncExt for Func {
|
|
||||||
fn call_and_post_return(&self, mut store: impl AsContextMut, args: &[Val]) -> Result<Val> {
|
|
||||||
let result = self.call(&mut store, args)?;
|
|
||||||
self.post_return(&mut store)?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn primitives() -> Result<()> {
|
fn primitives() -> Result<()> {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::REALLOC_AND_FREE;
|
use super::REALLOC_AND_FREE;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use std::ops::Deref;
|
||||||
use wasmtime::component::*;
|
use wasmtime::component::*;
|
||||||
use wasmtime::{Store, StoreContextMut, Trap};
|
use wasmtime::{Store, StoreContextMut, Trap};
|
||||||
|
|
||||||
@@ -117,6 +118,12 @@ fn simple() -> Result<()> {
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let engine = super::engine();
|
let engine = super::engine();
|
||||||
|
let component = Component::new(&engine, component)?;
|
||||||
|
let mut store = Store::new(&engine, None);
|
||||||
|
assert!(store.data().is_none());
|
||||||
|
|
||||||
|
// First, test the static API
|
||||||
|
|
||||||
let mut linker = Linker::new(&engine);
|
let mut linker = Linker::new(&engine);
|
||||||
linker.root().func_wrap(
|
linker.root().func_wrap(
|
||||||
"",
|
"",
|
||||||
@@ -127,15 +134,36 @@ fn simple() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let component = Component::new(&engine, component)?;
|
|
||||||
let mut store = Store::new(&engine, None);
|
|
||||||
let instance = linker.instantiate(&mut store, &component)?;
|
let instance = linker.instantiate(&mut store, &component)?;
|
||||||
assert!(store.data().is_none());
|
|
||||||
instance
|
instance
|
||||||
.get_typed_func::<(), (), _>(&mut store, "call")?
|
.get_typed_func::<(), (), _>(&mut store, "call")?
|
||||||
.call(&mut store, ())?;
|
.call(&mut store, ())?;
|
||||||
assert_eq!(store.data().as_ref().unwrap(), "hello world");
|
assert_eq!(store.data().as_ref().unwrap(), "hello world");
|
||||||
|
|
||||||
|
// Next, test the dynamic API
|
||||||
|
|
||||||
|
*store.data_mut() = None;
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
linker.root().func_new(
|
||||||
|
&component,
|
||||||
|
"",
|
||||||
|
|mut store: StoreContextMut<'_, Option<String>>, args| {
|
||||||
|
if let Val::String(s) = &args[0] {
|
||||||
|
assert!(store.data().is_none());
|
||||||
|
*store.data_mut() = Some(s.to_string());
|
||||||
|
Ok(Val::Unit)
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let instance = linker.instantiate(&mut store, &component)?;
|
||||||
|
instance
|
||||||
|
.get_func(&mut store, "call")
|
||||||
|
.unwrap()
|
||||||
|
.call(&mut store, &[])?;
|
||||||
|
assert_eq!(store.data().as_ref().unwrap(), "hello world");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,15 +327,20 @@ fn attempt_to_reenter_during_host() -> Result<()> {
|
|||||||
)
|
)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
struct State {
|
let engine = super::engine();
|
||||||
|
let component = Component::new(&engine, component)?;
|
||||||
|
|
||||||
|
// First, test the static API
|
||||||
|
|
||||||
|
struct StaticState {
|
||||||
func: Option<TypedFunc<(), ()>>,
|
func: Option<TypedFunc<(), ()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let engine = super::engine();
|
let mut store = Store::new(&engine, StaticState { func: None });
|
||||||
let mut linker = Linker::new(&engine);
|
let mut linker = Linker::new(&engine);
|
||||||
linker.root().func_wrap(
|
linker.root().func_wrap(
|
||||||
"thunk",
|
"thunk",
|
||||||
|mut store: StoreContextMut<'_, State>| -> Result<()> {
|
|mut store: StoreContextMut<'_, StaticState>| -> Result<()> {
|
||||||
let func = store.data_mut().func.take().unwrap();
|
let func = store.data_mut().func.take().unwrap();
|
||||||
let trap = func.call(&mut store, ()).unwrap_err();
|
let trap = func.call(&mut store, ()).unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -319,12 +352,39 @@ fn attempt_to_reenter_during_host() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let component = Component::new(&engine, component)?;
|
|
||||||
let mut store = Store::new(&engine, State { func: None });
|
|
||||||
let instance = linker.instantiate(&mut store, &component)?;
|
let instance = linker.instantiate(&mut store, &component)?;
|
||||||
let func = instance.get_typed_func::<(), (), _>(&mut store, "run")?;
|
let func = instance.get_typed_func::<(), (), _>(&mut store, "run")?;
|
||||||
store.data_mut().func = Some(func);
|
store.data_mut().func = Some(func);
|
||||||
func.call(&mut store, ())?;
|
func.call(&mut store, ())?;
|
||||||
|
|
||||||
|
// Next, test the dynamic API
|
||||||
|
|
||||||
|
struct DynamicState {
|
||||||
|
func: Option<Func>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut store = Store::new(&engine, DynamicState { func: None });
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
linker.root().func_new(
|
||||||
|
&component,
|
||||||
|
"thunk",
|
||||||
|
|mut store: StoreContextMut<'_, DynamicState>, _| {
|
||||||
|
let func = store.data_mut().func.take().unwrap();
|
||||||
|
let trap = func.call(&mut store, &[]).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
trap.to_string()
|
||||||
|
.contains("cannot reenter component instance"),
|
||||||
|
"bad trap: {}",
|
||||||
|
trap,
|
||||||
|
);
|
||||||
|
Ok(Val::Unit)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let instance = linker.instantiate(&mut store, &component)?;
|
||||||
|
let func = instance.get_func(&mut store, "run").unwrap();
|
||||||
|
store.data_mut().func = Some(func);
|
||||||
|
func.call(&mut store, &[])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +526,11 @@ fn stack_and_heap_args_and_rets() -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let engine = super::engine();
|
let engine = super::engine();
|
||||||
|
let component = Component::new(&engine, component)?;
|
||||||
|
let mut store = Store::new(&engine, ());
|
||||||
|
|
||||||
|
// First, test the static API
|
||||||
|
|
||||||
let mut linker = Linker::new(&engine);
|
let mut linker = Linker::new(&engine);
|
||||||
linker.root().func_wrap("f1", |x: u32| -> Result<u32> {
|
linker.root().func_wrap("f1", |x: u32| -> Result<u32> {
|
||||||
assert_eq!(x, 1);
|
assert_eq!(x, 1);
|
||||||
@@ -515,12 +580,60 @@ fn stack_and_heap_args_and_rets() -> Result<()> {
|
|||||||
Ok("xyz".to_string())
|
Ok("xyz".to_string())
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let component = Component::new(&engine, component)?;
|
|
||||||
let mut store = Store::new(&engine, ());
|
|
||||||
let instance = linker.instantiate(&mut store, &component)?;
|
let instance = linker.instantiate(&mut store, &component)?;
|
||||||
instance
|
instance
|
||||||
.get_typed_func::<(), (), _>(&mut store, "run")?
|
.get_typed_func::<(), (), _>(&mut store, "run")?
|
||||||
.call(&mut store, ())?;
|
.call(&mut store, ())?;
|
||||||
|
|
||||||
|
// Next, test the dynamic API
|
||||||
|
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
linker.root().func_new(&component, "f1", |_, args| {
|
||||||
|
if let Val::U32(x) = &args[0] {
|
||||||
|
assert_eq!(*x, 1);
|
||||||
|
Ok(Val::U32(2))
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
linker.root().func_new(&component, "f2", |_, args| {
|
||||||
|
if let Val::Tuple(tuple) = &args[0] {
|
||||||
|
if let Val::String(s) = &tuple.values()[0] {
|
||||||
|
assert_eq!(s.deref(), "abc");
|
||||||
|
Ok(Val::U32(3))
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
linker.root().func_new(&component, "f3", |_, args| {
|
||||||
|
if let Val::U32(x) = &args[0] {
|
||||||
|
assert_eq!(*x, 8);
|
||||||
|
Ok(Val::String("xyz".into()))
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
linker.root().func_new(&component, "f4", |_, args| {
|
||||||
|
if let Val::Tuple(tuple) = &args[0] {
|
||||||
|
if let Val::String(s) = &tuple.values()[0] {
|
||||||
|
assert_eq!(s.deref(), "abc");
|
||||||
|
Ok(Val::String("xyz".into()))
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let instance = linker.instantiate(&mut store, &component)?;
|
||||||
|
instance
|
||||||
|
.get_func(&mut store, "run")
|
||||||
|
.unwrap()
|
||||||
|
.call(&mut store, &[])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,6 +761,9 @@ fn no_actual_wasm_code() -> Result<()> {
|
|||||||
let engine = super::engine();
|
let engine = super::engine();
|
||||||
let component = Component::new(&engine, component)?;
|
let component = Component::new(&engine, component)?;
|
||||||
let mut store = Store::new(&engine, 0);
|
let mut store = Store::new(&engine, 0);
|
||||||
|
|
||||||
|
// First, test the static API
|
||||||
|
|
||||||
let mut linker = Linker::new(&engine);
|
let mut linker = Linker::new(&engine);
|
||||||
linker
|
linker
|
||||||
.root()
|
.root()
|
||||||
@@ -663,5 +779,23 @@ fn no_actual_wasm_code() -> Result<()> {
|
|||||||
thunk.call(&mut store, ())?;
|
thunk.call(&mut store, ())?;
|
||||||
assert_eq!(*store.data(), 1);
|
assert_eq!(*store.data(), 1);
|
||||||
|
|
||||||
|
// Next, test the dynamic API
|
||||||
|
|
||||||
|
*store.data_mut() = 0;
|
||||||
|
let mut linker = Linker::new(&engine);
|
||||||
|
linker
|
||||||
|
.root()
|
||||||
|
.func_new(&component, "f", |mut store: StoreContextMut<'_, u32>, _| {
|
||||||
|
*store.data_mut() += 1;
|
||||||
|
Ok(Val::Unit)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let instance = linker.instantiate(&mut store, &component)?;
|
||||||
|
let thunk = instance.get_func(&mut store, "thunk").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*store.data(), 0);
|
||||||
|
thunk.call(&mut store, &[])?;
|
||||||
|
assert_eq!(*store.data(), 1);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user