diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ccaff779ce..e20e5577f16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## **[Unreleased]** +### Added + +* [#1867](https://github.com/wasmerio/wasmer/pull/1867) Added `Metering::get_remaining_points` and `Metering::set_remaining_points` + ## 1.0.0-beta1 - 2020-12-01 ### Added diff --git a/Cargo.toml b/Cargo.toml index 79ebcef8c14..776ac60c713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -255,3 +255,8 @@ required-features = ["cranelift"] name = "hello-world" path = "examples/hello_world.rs" required-features = ["cranelift"] + +[[example]] +name = "metering" +path = "examples/metering.rs" +required-features = ["cranelift"] diff --git a/examples/metering.rs b/examples/metering.rs new file mode 100644 index 00000000000..8bb794030dd --- /dev/null +++ b/examples/metering.rs @@ -0,0 +1,165 @@ +//! Wasmer will let you easily run Wasm module in a Rust host. +//! +//! This example illustrates the basics of using Wasmer metering features: +//! +//! 1. How to enable metering in a module +//! 2. How to meter a specific function call +//! 3. How to make execution fails if cost exceeds a given limit +//! +//! You can run the example directly by executing in Wasmer root: +//! +//! ```shell +//! cargo run --example metering --release --features "cranelift" +//! ``` +//! +//! Ready? + +use anyhow::bail; +use std::sync::Arc; +use wasmer::wasmparser::Operator; +use wasmer::CompilerConfig; +use wasmer::{imports, wat2wasm, Instance, Module, Store}; +use wasmer_compiler_cranelift::Cranelift; +use wasmer_engine_jit::JIT; +use wasmer_middlewares::Metering; + +fn main() -> anyhow::Result<()> { + // Let's declare the Wasm module. + // + // We are using the text representation of the module here but you can also load `.wasm` + // files using the `include_bytes!` macro. + let wasm_bytes = wat2wasm( + br#" +(module + (type $add_t (func (param i32) (result i32))) + (func $add_one_f (type $add_t) (param $value i32) (result i32) + local.get $value + i32.const 1 + i32.add) + (export "add_one" (func $add_one_f))) +"#, + )?; + + // Let's define our cost function. + // + // This function will be called for each `Operator` encountered during + // the Wasm module execution. It should return the cost of the operator + // that it received as it first argument. + let cost_function = |operator: &Operator| -> u64 { + match operator { + Operator::LocalGet { .. } | Operator::I32Const { .. } => 1, + Operator::I32Add { .. } => 2, + _ => 0, + } + }; + + // Now let's create our metering middleware. + // + // `Metering` needs to be configured with a limit (the gas limit) and + // a cost function. + // + // For each `Operator`, the metering middleware will call the cost + // function and subtract the cost from the gas. + let metering = Arc::new(Metering::new(10, cost_function)); + let mut compiler_config = Cranelift::default(); + compiler_config.push_middleware(metering.clone()); + + // Create a Store. + // + // We use our previously create compiler configuration + // with the JIT engine. + let store = Store::new(&JIT::new(&compiler_config).engine()); + + println!("Compiling module..."); + // Let's compile the Wasm module. + let module = Module::new(&store, wasm_bytes)?; + + // Create an empty import object. + let import_object = imports! {}; + + println!("Instantiating module..."); + // Let's instantiate the Wasm module. + let instance = Instance::new(&module, &import_object)?; + + // We now have an instance ready to be used. + // + // Our module exports a single `add_one` function. We want to + // measure the cost of executing this function. + let add_one = instance + .exports + .get_function("add_one")? + .native::()?; + + println!("Calling `add_one` function once..."); + add_one.call(1)?; + + // As you can see here, after the first call we have 6 remaining gas points. + // + // This is correct, here are the details of how it has been computed: + // * `local.get $value` is a `Operator::LocalGet` which costs 1 point; + // * `i32.const` is a `Operator::I32Const` which costs 1 point; + // * `i32.add` is a `Operator::I32Add` which costs 2 points. + let remaining_points_after_first_call = metering.get_remaining_points(&instance); + assert_eq!(remaining_points_after_first_call, 6); + + println!( + "Remaining points after the first call: {:?}", + remaining_points_after_first_call + ); + + println!("Calling `add_one` function twice..."); + add_one.call(1)?; + + // We spent 4 more gas points with the second call. + // We have 2 remaining points. + let remaining_points_after_second_call = metering.get_remaining_points(&instance); + assert_eq!(remaining_points_after_second_call, 2); + + println!( + "Remaining points after the second call: {:?}", + remaining_points_after_second_call + ); + + // Because calling our `add_one` function consumes 4 gas points, + // calling it a third time will fail: we already consume 8 gas + // points, there are only two remaining. + println!("Calling `add_one` function a third time..."); + match add_one.call(1) { + Ok(result) => { + bail!( + "Expected failure while calling `add_one`, found: {}", + result + ); + } + Err(_) => { + println!("Calling `add_one` failed: not enough gas points remaining."); + } + } + + // Becasue the previous call failed, it did not consume any gas point. + // We still have 2 remaining points. + let remaining_points_after_third_call = metering.get_remaining_points(&instance); + assert_eq!(remaining_points_after_third_call, 2); + + println!( + "Remaining points after third call: {:?}", + remaining_points_after_third_call + ); + + // Now let's see how we can set a new limit... + println!("Set new remaining points points to 10"); + let new_limit = 10; + metering.set_remaining_points(&instance, new_limit); + + let remaining_points = metering.get_remaining_points(&instance); + assert_eq!(remaining_points, new_limit); + + println!("Remaining points: {:?}", remaining_points); + + Ok(()) +} + +#[test] +fn test_metering() -> anyhow::Result<()> { + main() +} diff --git a/lib/api/src/lib.rs b/lib/api/src/lib.rs index 01c2683ba08..9ffca5f1624 100644 --- a/lib/api/src/lib.rs +++ b/lib/api/src/lib.rs @@ -85,7 +85,7 @@ pub use wasmer_engine::{ NamedResolverChain, Resolver, RuntimeError, SerializeError, }; pub use wasmer_types::{ - Atomically, Bytes, GlobalInit, LocalFunctionIndex, MemoryView, Pages, ValueType, + Atomically, Bytes, ExportIndex, GlobalInit, LocalFunctionIndex, MemoryView, Pages, ValueType, WASM_MAX_PAGES, WASM_MIN_PAGES, WASM_PAGE_SIZE, }; diff --git a/lib/middlewares/src/metering.rs b/lib/middlewares/src/metering.rs index 68a010a9794..335026c20f6 100644 --- a/lib/middlewares/src/metering.rs +++ b/lib/middlewares/src/metering.rs @@ -7,8 +7,8 @@ use wasmer::wasmparser::{ Operator, Result as WpResult, Type as WpType, TypeOrFuncType as WpTypeOrFuncType, }; use wasmer::{ - FunctionMiddleware, GlobalInit, GlobalType, LocalFunctionIndex, MiddlewareReaderState, - ModuleMiddleware, Mutability, Type, + ExportIndex, FunctionMiddleware, GlobalInit, GlobalType, Instance, LocalFunctionIndex, + MiddlewareReaderState, ModuleMiddleware, Mutability, Type, Value, }; use wasmer_types::GlobalIndex; use wasmer_vm::ModuleInfo; @@ -52,6 +52,30 @@ impl u64 + Copy + Clone + Send + Sync> Metering { remaining_points_index: Mutex::new(None), } } + + /// Get the remaining points in an Instance. + /// + /// Important: the instance Module must been processed with the `Metering` middleware. + pub fn get_remaining_points(&self, instance: &Instance) -> u64 { + instance + .exports + .get_global("remaining_points") + .expect("Can't get `remaining_points` from Instance") + .get() + .unwrap_i64() as _ + } + + /// Set the provided remaining points in an Instance. + /// + /// Important: the instance Module must been processed with the `Metering` middleware. + pub fn set_remaining_points(&self, instance: &Instance, points: u64) { + instance + .exports + .get_global("remaining_points") + .expect("Can't get `remaining_points` from Instance") + .set(Value::I64(points as _)) + .expect("Can't set `remaining_points` in Instance"); + } } impl u64 + Copy + Clone + Send + Sync> fmt::Debug for Metering { @@ -86,14 +110,18 @@ impl u64 + Copy + Clone + Send + Sync + 'static> ModuleMiddl } // Append a global for remaining points and initialize it. - *remaining_points_index = Some( - module_info - .globals - .push(GlobalType::new(Type::I64, Mutability::Var)), - ); + let global_index = module_info + .globals + .push(GlobalType::new(Type::I64, Mutability::Var)); + *remaining_points_index = Some(global_index.clone()); module_info .global_initializers .push(GlobalInit::I64Const(self.initial_limit as i64)); + + module_info.exports.insert( + "remaining_points".to_string(), + ExportIndex::Global(global_index), + ); } }