Feature
Add a Config/Tunables option that exempts the synthesized ModuleStartup function from fuel metering, restoring the ≤ v36 behavior where Instance::new with store.set_fuel(0) succeeds for modules that have no (start ...) function. Since #13487 ("Move most module initialization to compiled code"), module initialization (globals, segments, tables, etc.) runs as compiled Wasm inside ModuleStartup (crates/cranelift/src/func_environ.rs) and is intentionally fuel-metered, so Instance::new now consumes fuel even for modules with no (start ...) and a small fuel budget makes instantiation itself trap with Trap::OutOfFuel. Fuel is meant to bound Wasm function execution, not to forbid instantiation.
Example
A module that requires a startup function but has no (start ...) — here a passive element segment, which is initialized once during instantiation:
(module
(func $f (result i32) i32.const 42)
(table 1 funcref)
(elem $passive func $f))
// Cargo.toml: [dependencies] wasmtime = "48"
use wasmtime::*;
const MODULE: &str = r#"
(module
(func $f (result i32) i32.const 42)
(table 1 funcref)
(elem $passive func $f))
"#;
fn main() -> Result<()> {
let mut config = Config::new();
config.consume_fuel(true);
let engine = Engine::new(&config)?;
let module = Module::new(&engine, MODULE)?;
// No `(start ...)`, but the passive element segment forces Wasmtime to
// synthesize a `ModuleStartup` function, and that function is fuel-metered.
for fuel in [0, 1, 2] {
let mut store = Store::new(&engine, ());
store.set_fuel(fuel)?;
match Instance::new(&mut store, &module, &[]) {
Ok(_) => {
let consumed = fuel - store.get_fuel()?;
println!("fuel={fuel}: instantiation succeeded, consumed {consumed} unit(s)");
}
Err(e) => println!("fuel={fuel}: instantiation failed: {e}"),
}
}
Ok(())
}
Output (reproduced on main at f1412a598f, Wasmtime 48.0.0, Linux x86_64):
fuel=0: instantiation failed: wasm trap: all fuel consumed by WebAssembly
fuel=1: instantiation failed: wasm trap: all fuel consumed by WebAssembly
fuel=2: instantiation succeeded, consumed 1 unit(s)
fuel=0 traps inside Instance::new even though the module has no (start ...), and fuel=1 traps as well because the startup function's flat entry charge of 1 is mandatory; only fuel=2 succeeds, showing that instantiation consumes exactly 1 unit for this module. In ≤ v36, set_fuel(0) + Instance::new succeeded here, with fuel spent only when running an exported function. A module that needs no startup function still instantiates fine at fuel=0 today.
Why a cost parameter alone is insufficient
With store.set_fuel(0) the fuel counter is 0. fuel_check (func_environ.rs:626) traps once the counter becomes >= 0, and fuel_function_entry runs that check plus the mandatory initial fuel_consumed: 1 (func_environ.rs:296) at the entry of every compiled function, including ModuleStartup. So even setting every startup cost to 0 still leaves 0 + 1 >= 0 → trap. Only skipping the fuel entry/exit handling for ModuleStartup restores the v36 behavior.
Benefit
- v36 compatibility: instantiation with
set_fuel(0) works again. Today it traps for any module whose initialization cannot be constant-folded or precomputed (measured: a passive elem, a complicated global, and an active externref table — none with (start ...) — each consume 1 unit of fuel on Instance::new, and trap when the budget is exhausted). Fuel should bound Wasm function execution, not forbid instantiation.
Implementation
- Add
Config::consume_fuel_during_module_initialization(bool) (or an equivalent Tunables field), defaulting to true for current behavior. When false, compile FuncKey::ModuleStartup with fuel accounting disabled for that function: skip fuel_function_entry/fuel_function_exit so it neither charges the flat entry cost (fuel_consumed: 1) nor runs the entry >= 0 check that makes set_fuel(0) trap.
- The
(start ...) call itself remains ordinary Wasm (it consumed fuel in v36 as well); scope the exemption to the synthesized initialization body.
- Tests:
set_fuel(0) + Instance::new on a module that needs a startup function (e.g. a passive elem); option on → traps, option off → succeeds.
Alternatives
- Configurable startup cost: add a dedicated cost knob for the startup function, e.g.
OperatorCost::module_startup (a flat per-instance charge, defaulting to 1 to preserve current metering), replacing the hardcoded fuel_consumed: 1 entry charge that ModuleStartup currently pays. Setting it to 0 restores the v36 set_fuel(0) behavior, and values > 0 keep metering while letting embedders charge a custom amount for instantiation-time work — strictly more flexible than the boolean flag. The required crutch: zeroing the cost is not enough on its own, because fuel_check (func_environ.rs:626) traps once the counter becomes >= 0 and fuel_function_entry runs that check unconditionally at ModuleStartup entry. So the 0 case must also skip fuel_function_entry/fuel_function_exit for FuncKey::ModuleStartup — the same surgical skip the boolean flag needs, but keyed on cost == 0 rather than a separate option.
Feature
Add a
Config/Tunablesoption that exempts the synthesizedModuleStartupfunction from fuel metering, restoring the ≤ v36 behavior whereInstance::newwithstore.set_fuel(0)succeeds for modules that have no(start ...)function. Since #13487 ("Move most module initialization to compiled code"), module initialization (globals, segments, tables, etc.) runs as compiled Wasm insideModuleStartup(crates/cranelift/src/func_environ.rs) and is intentionally fuel-metered, soInstance::newnow consumes fuel even for modules with no(start ...)and a small fuel budget makes instantiation itself trap withTrap::OutOfFuel. Fuel is meant to bound Wasm function execution, not to forbid instantiation.Example
A module that requires a startup function but has no
(start ...)— here a passive element segment, which is initialized once during instantiation:Output (reproduced on
mainatf1412a598f, Wasmtime 48.0.0, Linux x86_64):fuel=0traps insideInstance::neweven though the module has no(start ...), andfuel=1traps as well because the startup function's flat entry charge of1is mandatory; onlyfuel=2succeeds, showing that instantiation consumes exactly 1 unit for this module. In ≤ v36,set_fuel(0)+Instance::newsucceeded here, with fuel spent only when running an exported function. A module that needs no startup function still instantiates fine atfuel=0today.Why a cost parameter alone is insufficient
With
store.set_fuel(0)the fuel counter is0.fuel_check(func_environ.rs:626) traps once the counter becomes>= 0, andfuel_function_entryruns that check plus the mandatory initialfuel_consumed: 1(func_environ.rs:296) at the entry of every compiled function, includingModuleStartup. So even setting every startup cost to0still leaves0 + 1 >= 0→ trap. Only skipping the fuel entry/exit handling forModuleStartuprestores the v36 behavior.Benefit
set_fuel(0)works again. Today it traps for any module whose initialization cannot be constant-folded or precomputed (measured: a passiveelem, a complicated global, and an activeexternreftable — none with(start ...)— each consume 1 unit of fuel onInstance::new, and trap when the budget is exhausted). Fuel should bound Wasm function execution, not forbid instantiation.Implementation
Config::consume_fuel_during_module_initialization(bool)(or an equivalentTunablesfield), defaulting totruefor current behavior. Whenfalse, compileFuncKey::ModuleStartupwith fuel accounting disabled for that function: skipfuel_function_entry/fuel_function_exitso it neither charges the flat entry cost (fuel_consumed: 1) nor runs the entry>= 0check that makesset_fuel(0)trap.(start ...)call itself remains ordinary Wasm (it consumed fuel in v36 as well); scope the exemption to the synthesized initialization body.set_fuel(0)+Instance::newon a module that needs a startup function (e.g. a passiveelem); option on → traps, option off → succeeds.Alternatives
OperatorCost::module_startup(a flat per-instance charge, defaulting to1to preserve current metering), replacing the hardcodedfuel_consumed: 1entry charge thatModuleStartupcurrently pays. Setting it to0restores the v36set_fuel(0)behavior, and values> 0keep metering while letting embedders charge a custom amount for instantiation-time work — strictly more flexible than the boolean flag. The required crutch: zeroing the cost is not enough on its own, becausefuel_check(func_environ.rs:626) traps once the counter becomes>= 0andfuel_function_entryruns that check unconditionally atModuleStartupentry. So the0case must also skipfuel_function_entry/fuel_function_exitforFuncKey::ModuleStartup— the same surgical skip the boolean flag needs, but keyed oncost == 0rather than a separate option.