Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): Add metering example #1867

Merged
merged 3 commits into from
Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
165 changes: 165 additions & 0 deletions examples/metering.rs
Original file line number Diff line number Diff line change
@@ -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::<i32, i32>()?;

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()
}
2 changes: 1 addition & 1 deletion lib/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
42 changes: 35 additions & 7 deletions lib/middlewares/src/metering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,6 +52,30 @@ impl<F: Fn(&Operator) -> u64 + Copy + Clone + Send + Sync> Metering<F> {
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<F: Fn(&Operator) -> u64 + Copy + Clone + Send + Sync> fmt::Debug for Metering<F> {
Expand Down Expand Up @@ -86,14 +110,18 @@ impl<F: Fn(&Operator) -> 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),
);
}
}

Expand Down