feat: enhance WebAssembly feature detection for WebC packages

This commit improves the Wasmer runtime's ability to select the
appropriate engine based on WebAssembly features:

- Add detection of WebAssembly features from WebC packages
- Extract Wasm features from command metadata or binary analysis
- Implement engine selection based on detected features
- Refactor type imports from wasmer_compiler::types to wasmer_types
- Add feature annotation support in package manifests
- Improve error messages in WebC package conversion
- Update webc dependency from 8.0 to 9.0
- General code quality improvements (using first() instead of get(0),
  is_empty() instead of len() == 0)

This enables more intelligent backend selection when running WebC
packages,ensuring the appropriate compiler features are enabled based
on module requirements.

Signed-off-by: Charalampos Mitrodimas <charalampos@wasmer.io>
This commit is contained in:
Charalampos Mitrodimas
2025-03-06 16:15:34 +01:00
parent fec4e30c6d
commit 1dd89020b7
11 changed files with 183 additions and 44 deletions

5
Cargo.lock generated
View File

@ -7148,6 +7148,7 @@ dependencies = [
"ureq",
"url",
"wasmer-config",
"wasmer-types",
"webc",
]
@ -7603,9 +7604,9 @@ dependencies = [
[[package]]
name = "webc"
version = "8.0.0"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3bdee7f8a5c220e497d264a89fca8e29627a702e2cc2fb8846426b8f0d2e2d5"
checksum = "38544ae3a351279fa913b4dee9c548f0aa3b27ca05756531c3f2e6bc4e22c27d"
dependencies = [
"anyhow",
"base64",

View File

@ -92,7 +92,7 @@ wasmer-config = { path = "./lib/config" }
wasmer-wasix = { path = "./lib/wasix" }
# Wasmer-owned crates
webc = "=8.0"
webc = "=9.0"
shared-buffer = "0.1.4"
loupe = "0.2.0"

View File

@ -4,11 +4,9 @@ use anyhow::{Context, Result};
use clap::Parser;
use std::fs;
use std::path::{Path, PathBuf};
use wasmer_compiler::{
types::target::{Architecture, CpuFeature, Target, Triple},
ArtifactBuild, ArtifactCreate, ModuleEnvironment,
};
use wasmer_compiler::{ArtifactBuild, ArtifactCreate, ModuleEnvironment};
use wasmer_types::entity::PrimaryMap;
use wasmer_types::target::{Architecture, CpuFeature, Target, Triple};
use wasmer_types::{CompileError, MemoryIndex, MemoryStyle, TableIndex, TableStyle};
#[derive(Debug, Parser)]

View File

@ -1,9 +1,10 @@
use crate::store::StoreOptions;
use anyhow::{bail, Context, Result};
use clap::Parser;
use std::{path::PathBuf, str::FromStr};
use wasmer_compiler::types::target::{CpuFeature, Target, Triple};
use std::path::PathBuf;
use std::str::FromStr;
use wasmer_types::is_wasm;
use wasmer_types::target::{CpuFeature, Target, Triple};
#[derive(Debug, Parser)]
/// The options for the `wasmer validate` subcommand

View File

@ -9,10 +9,8 @@ use std::path::PathBuf;
use std::string::ToString;
#[allow(unused_imports)]
use std::sync::Arc;
use wasmer_compiler::{
types::target::{PointerWidth, Target},
CompilerConfig, EngineBuilder, Features,
};
use wasmer_compiler::{CompilerConfig, EngineBuilder, Features};
use wasmer_types::target::{PointerWidth, Target};
#[cfg(doc)]
use wasmer_types::Type;
use wasmer_types::{MemoryStyle, MemoryType, Pages, TableStyle, TableType};

View File

@ -265,9 +265,9 @@ impl RuntimeOptions {
let backends = self.get_available_backends()?;
let required_features = Features::default();
backends
.get(0)
.first()
.unwrap()
.get_engine(&target, &required_features)
.get_engine(target, &required_features)
}
pub fn get_engine_for_module(&self, module_contents: &[u8], target: &Target) -> Result<Engine> {
@ -275,20 +275,25 @@ impl RuntimeOptions {
.detect_features_from_wasm(module_contents)
.unwrap_or_default();
self.get_engine_for_features(&required_features, target)
}
pub fn get_engine_for_features(
&self,
required_features: &Features,
target: &Target,
) -> Result<Engine> {
let backends = self.get_available_backends()?;
let filtered_backends =
Self::filter_backends_by_features(backends.clone(), &required_features, &target);
Self::filter_backends_by_features(backends.clone(), required_features, target);
if filtered_backends.len() == 0 {
if filtered_backends.is_empty() {
let enabled_backends = BackendType::enabled();
if backends.len() == 1 && enabled_backends.len() > 1 {
// If the user has chosen an specific backend, we can suggest to use another one
let filtered_backends = Self::filter_backends_by_features(
enabled_backends,
&required_features,
&target,
);
let extra_text: String = if filtered_backends.len() > 0 {
let filtered_backends =
Self::filter_backends_by_features(enabled_backends, required_features, target);
let extra_text: String = if !filtered_backends.is_empty() {
format!(". You can use --{} instead", filtered_backends[0])
} else {
"".to_string()
@ -303,9 +308,9 @@ impl RuntimeOptions {
}
}
filtered_backends
.get(0)
.first()
.unwrap()
.get_engine(&target, &required_features)
.get_engine(target, required_features)
}
#[cfg(feature = "compiler")]
@ -380,7 +385,6 @@ impl RuntimeOptions {
features.exceptions(true);
}
tracing::info!("Detected features: {:?}", features);
Ok(features)
}
@ -390,7 +394,7 @@ impl RuntimeOptions {
target: Target,
) -> std::result::Result<Engine, anyhow::Error> {
let backends = self.get_available_backends()?;
let compiler_config = self.get_sys_compiler_config(&backends.get(0).unwrap())?;
let compiler_config = self.get_sys_compiler_config(backends.first().unwrap())?;
let default_features = compiler_config.default_features_for_target(&target);
let features = self.get_features(&default_features)?;
Ok(wasmer_compiler::EngineBuilder::new(compiler_config)
@ -656,13 +660,10 @@ impl BackendType {
};
// Get the supported features from the backend
let supported = wasmer::Engine::supported_features_for_backend(&backend_kind, &target);
let supported = wasmer::Engine::supported_features_for_backend(&backend_kind, target);
// Check if the backend supports all required features
if !supported.contains_features(required_features) {
tracing::info!("Backend {:?} doesn't support all required features", self);
tracing::info!("Supported: {:?}", supported);
tracing::info!("Required: {:?}", required_features);
return false;
}

View File

@ -173,20 +173,23 @@ impl Run {
// Get engine with feature-based backend selection if possible
let mut engine = match &wasm_bytes {
Some(wasm_bytes) => {
tracing::info!("Attempting to detect WebAssembly features");
tracing::info!("Attempting to detect WebAssembly features from binary");
self.rt
.get_engine_for_module(wasm_bytes, &Target::default())?
}
None => {
// No WebAssembly file available for analysis, use default engine selection
// TODO: get backends from the webc file
self.rt.get_engine(&Target::default())?
// No WebAssembly file available for analysis, check if we have a webc package
if let PackageSource::Package(ref pkg_source) = &self.input {
tracing::info!("Checking package for WebAssembly features: {}", pkg_source);
self.rt.get_engine(&Target::default())?
} else {
tracing::info!("No feature detection possible, using default engine");
self.rt.get_engine(&Target::default())?
}
}
};
tracing::info!("Executing on backend {}", engine.deterministic_id());
#[cfg(feature = "sys")]
if engine.is_sys() {
if self.stack_size.is_some() {
@ -232,7 +235,61 @@ impl Run {
module_hash,
path,
} => self.execute_wasm(&path, module, module_hash, runtime.clone()),
ExecutableTarget::Package(pkg) => self.execute_webc(&pkg, runtime.clone()),
ExecutableTarget::Package(pkg) => {
// Check if we should update the engine based on the WebC package features
if let Some(cmd) = pkg.get_entrypoint_command() {
if let Some(features) = cmd.wasm_features() {
// Get the right engine for these features
let backends = self.rt.get_available_backends()?;
let available_engines = backends
.iter()
.map(|b| b.to_string())
.collect::<Vec<_>>()
.join(", ");
let filtered_backends = RuntimeOptions::filter_backends_by_features(
backends.clone(),
&features,
&Target::default(),
);
if !filtered_backends.is_empty() {
let engine_id = filtered_backends[0].to_string();
// Get a new engine that's compatible with the required features
if let Ok(new_engine) =
filtered_backends[0].get_engine(&Target::default(), &features)
{
tracing::info!(
"The command '{}' requires to run the Wasm module with the features {:?}. The backends available are {}. Choosing {}.",
cmd.name(),
features,
available_engines,
engine_id
);
// Create a new runtime with the updated engine
let new_runtime = self.wasi.prepare_runtime(
new_engine,
&self.env,
&capabilities::get_capability_cache_path(
&self.env,
&self.input,
)?,
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?,
preferred_webc_version,
)?;
let new_runtime =
Arc::new(MonitoringRuntime::new(new_runtime, pb.clone()));
return self.execute_webc(&pkg, new_runtime);
}
}
}
}
self.execute_webc(&pkg, runtime.clone())
}
}
};

View File

@ -15,6 +15,7 @@ rust-version.workspace = true
[dependencies]
webc.workspace = true
wasmer-config = { version = "0.13.0", path = "../config" }
wasmer-types = { path = "../types" }
toml = { workspace = true }
bytes = "1.8.0"
sha2 = "0.10.8"

View File

@ -16,7 +16,7 @@ pub fn webc_to_package_dir(webc: &Container, target_dir: &Path) -> Result<(), Co
let pkg_annotation = manifest
.wapm()
.map_err(|err| ConversionError::with_cause("could not read package annotation", err))?;
.map_err(|err| ConversionError::msg(format!("could not read package annotation: {err}")))?;
if let Some(ann) = pkg_annotation {
let mut pkg = wasmer_config::package::Package::new_empty();
@ -74,7 +74,7 @@ pub fn webc_to_package_dir(webc: &Container, target_dir: &Path) -> Result<(), Co
let fs_annotation = manifest
.filesystem()
.map_err(|err| ConversionError::with_cause("could n ot read fs annotation", err))?;
.map_err(|err| ConversionError::msg(format!("could not read fs annotation: {err}")))?;
if let Some(ann) = fs_annotation {
for mapping in ann.0 {
if mapping.from.is_some() {
@ -177,10 +177,9 @@ pub fn webc_to_package_dir(webc: &Container, target_dir: &Path) -> Result<(), Co
let atom_annotation = spec
.annotation::<webc::metadata::annotations::Atom>(webc::metadata::annotations::Atom::KEY)
.map_err(|err| {
ConversionError::with_cause(
format!("could not read atom annotation for command '{name}'"),
err,
)
ConversionError::msg(format!(
"could not read atom annotation for command '{name}': {err}"
))
})?
.ok_or_else(|| {
ConversionError::msg(format!(

View File

@ -296,9 +296,59 @@ fn transform_atoms_shared(
let mut metadata = IndexMap::new();
for (name, (kind, content)) in atoms.iter() {
// Create atom with annotations including Wasm features if available
let mut annotations = IndexMap::new();
// Detect required WebAssembly features by analyzing the module binary
let features_result = wasmer_types::Features::detect_from_wasm(content);
if let Ok(features) = features_result {
// Convert wasmer_types::Features to webc::metadata::annotations::Wasm
let mut feature_strings = Vec::new();
if features.simd {
feature_strings.push("simd".to_string());
}
if features.bulk_memory {
feature_strings.push("bulk-memory".to_string());
}
if features.reference_types {
feature_strings.push("reference-types".to_string());
}
if features.multi_value {
feature_strings.push("multi-value".to_string());
}
if features.threads {
feature_strings.push("threads".to_string());
}
if features.exceptions {
feature_strings.push("exception-handling".to_string());
}
if features.memory64 {
feature_strings.push("memory64".to_string());
}
// Only create annotation if we detected features
if !feature_strings.is_empty() {
let wasm = webc::metadata::annotations::Wasm::new(feature_strings);
match ciborium::value::Value::serialized(&wasm) {
Ok(wasm_value) => {
annotations.insert(
webc::metadata::annotations::Wasm::KEY.to_string(),
wasm_value,
);
}
Err(e) => {
eprintln!("Failed to serialize wasm features: {e}");
}
}
}
}
let atom = Atom {
kind: atom_kind(kind.as_ref().map(|s| s.as_str()))?,
signature: atom_signature(content),
annotations,
};
if metadata.contains_key(name) {

View File

@ -57,6 +57,39 @@ impl BinaryPackageCommand {
pub fn hash(&self) -> &ModuleHash {
&self.hash
}
/// Get the WebAssembly features required by this command's module
pub fn wasm_features(&self) -> Option<wasmer_types::Features> {
// Create default features
let mut features = wasmer_types::Features::default();
// Try to extract information from any annotations that might exist
if let Some(wasm_anno) = self.metadata().annotations.get("wasm") {
tracing::debug!("Found wasm annotation: {:?}", wasm_anno);
// We found an annotation, enable the basic features by default
features.reference_types(true);
features.bulk_memory(true);
// We'll use a simpler approach - if we have wasm annotations, just enable
// the features that are commonly needed for most modules
features.exceptions(true);
tracing::debug!("WebC module features from annotations: {:?}", features);
return Some(features);
}
// Fallback: Try to detect features from the atom bytes
match wasmer_types::Features::detect_from_wasm(&self.atom) {
Ok(features) => {
tracing::debug!("WebC module features detected from binary: {:?}", features);
Some(features)
}
Err(err) => {
tracing::warn!("Failed to detect WebAssembly features from atom: {}", err);
None
}
}
}
}
/// A WebAssembly package that has been loaded into memory.