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

Linker API Changes #88

Merged
merged 2 commits into from
Sep 25, 2024
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
12 changes: 8 additions & 4 deletions benches/hook_performance_impact.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};

use wasm::{hooks::HookSet, validate, RuntimeInstance};
use wasm::{hooks::HookSet, validate, RuntimeInstance, DEFAULT_MODULE};

fn criterion_benchmark(c: &mut Criterion) {
let wat = r#"
Expand Down Expand Up @@ -52,14 +52,18 @@ fn criterion_benchmark(c: &mut Criterion) {
}

let mut instance_non_empty_hookset =
RuntimeInstance::new_with_hooks(&validation_info, MyCustomHookSet).unwrap();
RuntimeInstance::new_with_hooks(DEFAULT_MODULE, &validation_info, MyCustomHookSet).unwrap();

let test_fn = instance_empty_hookset.get_function_by_index(0, 2).unwrap();
c.bench_function("invoke_func EmptyHookSet", |b| {
b.iter(|| instance_empty_hookset.invoke_func::<_, ()>(black_box(2), black_box(42_i32)))
b.iter(|| instance_empty_hookset.invoke::<_, ()>(&test_fn, black_box(42_i32)))
});

let test_fn = instance_non_empty_hookset
.get_function_by_index(0, 2)
.unwrap();
c.bench_function("invoke_func MyCustomHookSet", |b| {
b.iter(|| instance_non_empty_hookset.invoke_func::<_, ()>(black_box(2), black_box(42_i32)))
b.iter(|| instance_non_empty_hookset.invoke::<_, ()>(&test_fn, black_box(42_i32)))
});
}

Expand Down
19 changes: 15 additions & 4 deletions examples/stuff/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,26 @@ fn main() -> ExitCode {
}
};

let twelve: i32 = instance.invoke_func(1, (5, 7)).unwrap();
let twelve: i32 = instance
.invoke(&instance.get_function_by_index(0, 1).unwrap(), (5, 7))
.unwrap();
assert_eq!(twelve, 12);

let twelve_plus_one: i32 = instance.invoke_func(0, twelve).unwrap();
let twelve_plus_one: i32 = instance
.invoke(&instance.get_function_by_index(0, 0).unwrap(), twelve)
.unwrap();
assert_eq!(twelve_plus_one, 13);

instance.invoke_func::<_, ()>(2, 42_i32).unwrap();
instance
.invoke::<_, ()>(&instance.get_function_by_index(0, 2).unwrap(), 42_i32)
.unwrap();

assert_eq!(instance.invoke_func::<(), i32>(3, ()).unwrap(), 42_i32);
assert_eq!(
instance
.invoke::<(), i32>(&instance.get_function_by_index(0, 3).unwrap(), ())
.unwrap(),
42_i32
);

ExitCode::SUCCESS
}
37 changes: 37 additions & 0 deletions src/execution/function_ref.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use alloc::string::String;
use alloc::vec::Vec;

use crate::execution::{hooks::HookSet, value::InteropValueList, RuntimeInstance};
use crate::{RuntimeError, ValType, Value};

pub struct FunctionRef {
pub(crate) module_name: String,
pub(crate) function_name: String,
pub(crate) module_index: usize,
pub(crate) function_index: usize,
/// If the function is exported from the module or not. This is used to determine if the function name - index
/// mapping should be verified. The module name - index mapping is always verified.
///
/// If this is set to false then the user must make sure that the function reference will still be valid when the
/// function is called. This means that the module must not be unloaded.
pub(crate) exported: bool,
}

impl FunctionRef {
pub fn invoke<H: HookSet, Param: InteropValueList, Returns: InteropValueList>(
&self,
runtime: &mut RuntimeInstance<H>,
params: Param,
) -> Result<Returns, RuntimeError> {
runtime.invoke(self, params)
}
florianhartung marked this conversation as resolved.
Show resolved Hide resolved

pub fn invoke_dynamic<H: HookSet>(
&self,
runtime: &mut RuntimeInstance<H>,
params: Vec<Value>,
ret_types: &[ValType],
) -> Result<Vec<Value>, RuntimeError> {
runtime.invoke_dynamic(self, params, ret_types)
}
}
189 changes: 136 additions & 53 deletions src/execution/mod.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
use alloc::string::ToString;
use alloc::vec::Vec;

use const_interpreter_loop::run_const;
use function_ref::FunctionRef;
use interpreter_loop::run;
use locals::Locals;
use value_stack::Stack;

use crate::core::indices::FuncIdx;
use crate::core::reader::types::export::{Export, ExportDesc};
use crate::core::reader::types::{FuncType, ValType};
use crate::core::reader::types::FuncType;
use crate::core::reader::WasmReader;
use crate::execution::assert_validated::UnwrapValidatedExt;
use crate::execution::hooks::{EmptyHookSet, HookSet};
use crate::execution::store::{FuncInst, GlobalInst, MemInst, Store};
use crate::execution::value::Value;
use crate::validation::code::read_declared_locals;
use crate::value::InteropValueList;
use crate::{RuntimeError, ValidationInfo};
use crate::{RuntimeError, ValType, ValidationInfo};

// TODO
pub(crate) mod assert_validated;
mod const_interpreter_loop;
pub mod function_ref;
pub mod hooks;
mod interpreter_loop;
pub(crate) mod locals;
pub(crate) mod store;
pub mod value;
pub mod value_stack;

/// The default module name if a [RuntimeInstance] was created using [RuntimeInstance::new].
pub const DEFAULT_MODULE: &str = "__interpreter_default__";

pub struct RuntimeInstance<'b, H = EmptyHookSet>
where
H: HookSet,
Expand All @@ -40,7 +45,14 @@ where

impl<'b> RuntimeInstance<'b, EmptyHookSet> {
pub fn new(validation_info: &'_ ValidationInfo<'b>) -> Result<Self, RuntimeError> {
Self::new_with_hooks(validation_info, EmptyHookSet)
Self::new_with_hooks(DEFAULT_MODULE, validation_info, EmptyHookSet)
}

pub fn new_named(
module_name: &str,
validation_info: &'_ ValidationInfo<'b>,
) -> Result<Self, RuntimeError> {
Self::new_with_hooks(module_name, validation_info, EmptyHookSet)
}
}

Expand All @@ -49,6 +61,7 @@ where
H: HookSet,
{
pub fn new_with_hooks(
module_name: &str,
validation_info: &'_ ValidationInfo<'b>,
hook_set: H,
) -> Result<Self, RuntimeError> {
Expand All @@ -65,68 +78,81 @@ where
};

if let Some(start) = validation_info.start {
let result = instance.invoke_func::<(), ()>(start, ());
result?;
// "start" is not always exported, so we need create a non-API exposed function reference.
// Note: function name is not important here, as it is not used in the verification process.
let start_fn = FunctionRef {
module_name: module_name.to_string(),
function_name: "start".to_string(),
module_index: 0,
function_index: start,
exported: false,
};
instance.invoke::<(), ()>(&start_fn, ())?;
}

Ok(instance)
}

pub fn invoke_named<Param: InteropValueList, Returns: InteropValueList>(
&mut self,
func_name: &str,
param: Param,
) -> Result<Returns, RuntimeError> {
// TODO: Optimize this search for better than linear-time. Pre-processing will likely be required
let func_idx = self.exports.iter().find_map(|export| {
if export.name == func_name {
match export.desc {
ExportDesc::FuncIdx(idx) => Some(idx),
_ => None,
}
} else {
None
}
});
pub fn get_function_by_name(
&self,
module_name: &str,
function_name: &str,
) -> Result<FunctionRef, RuntimeError> {
let (module_idx, func_idx) = self.get_indicies(module_name, function_name)?;

Ok(FunctionRef {
module_name: module_name.to_string(),
function_name: function_name.to_string(),
module_index: module_idx,
function_index: func_idx,
exported: true,
})
}

if let Some(func_idx) = func_idx {
self.invoke_func(func_idx, param)
} else {
Err(RuntimeError::FunctionNotFound)
}
pub fn get_function_by_index(
&self,
module_idx: usize,
function_idx: usize,
) -> Result<FunctionRef, RuntimeError> {
// TODO: Module resolution
let function_name = self
.exports
.iter()
.find(|export| match &export.desc {
ExportDesc::FuncIdx(idx) => *idx == function_idx,
_ => false,
})
.map(|export| export.name.clone())
.ok_or(RuntimeError::FunctionNotFound)?;

Ok(FunctionRef {
// TODO: get the module name from the module index
module_name: DEFAULT_MODULE.to_string(),
george-cosma marked this conversation as resolved.
Show resolved Hide resolved
function_name,
module_index: module_idx,
function_index: function_idx,
exported: true,
})
}

pub fn invoke_named_dynamic(
// TODO: remove this annotation when implementing the function
#[allow(clippy::result_unit_err)]
pub fn add_module(
&mut self,
func_name: &str,
param: Vec<Value>,
ret_types: &[ValType],
) -> Result<Vec<Value>, RuntimeError> {
// TODO: Optimize this search for better than linear-time. Pre-processing will likely be required
let func_idx = self.exports.iter().find_map(|export| {
if export.name == func_name {
match export.desc {
ExportDesc::FuncIdx(idx) => Some(idx),
_ => None,
}
} else {
None
}
});

if let Some(func_idx) = func_idx {
self.invoke_dynamic(func_idx, param, ret_types)
} else {
Err(RuntimeError::FunctionNotFound)
}
_module_name: &str,
_validation_info: &'_ ValidationInfo<'b>,
) -> Result<(), ()> {
todo!("Implement module linking");
}

/// Can only invoke functions with signature `[t1] -> [t2]` as of now.
pub fn invoke_func<Param: InteropValueList, Returns: InteropValueList>(
pub fn invoke<Param: InteropValueList, Returns: InteropValueList>(
&mut self,
func_idx: FuncIdx,
function_ref: &FunctionRef,
params: Param,
) -> Result<Returns, RuntimeError> {
// First, verify that the function reference is valid
let (_module_idx, func_idx) = self.verify_function_ref(function_ref)?;

// -=-= Verification =-=-
let func_inst = self.store.funcs.get(func_idx).expect("valid FuncIdx");
let func_ty = self.types.get(func_inst.ty).unwrap_validated();
Expand Down Expand Up @@ -175,10 +201,13 @@ where
/// Invokes a function with the given parameters, and return types which are not known at compile time.
pub fn invoke_dynamic(
&mut self,
func_idx: FuncIdx,
function_ref: &FunctionRef,
params: Vec<Value>,
ret_types: &[ValType],
) -> Result<Vec<Value>, RuntimeError> {
// First, verify that the function reference is valid
let (_module_idx, func_idx) = self.verify_function_ref(function_ref)?;

// -=-= Verification =-=-
let func_inst = self.store.funcs.get(func_idx).expect("valid FuncIdx");
let func_ty = self.types.get(func_inst.ty).unwrap_validated();
Expand Down Expand Up @@ -227,6 +256,60 @@ where
Ok(ret)
}

// TODO: replace this with the lookup table when implmenting the linker
fn get_indicies(
&self,
_module_name: &str,
function_name: &str,
) -> Result<(usize, usize), RuntimeError> {
let func_idx = self
.exports
.iter()
.find_map(|export| {
if export.name == function_name {
match export.desc {
ExportDesc::FuncIdx(func_idx) => Some(func_idx),
_ => None,
}
} else {
None
}
})
.ok_or(RuntimeError::FunctionNotFound)?;

Ok((0, func_idx))
}

fn verify_function_ref(
&self,
function_ref: &FunctionRef,
) -> Result<(usize, usize), RuntimeError> {
if function_ref.exported {
let (module_idx, func_idx) =
self.get_indicies(&function_ref.module_name, &function_ref.function_name)?;

if module_idx != function_ref.module_index || func_idx != function_ref.function_index {
// TODO: should we return a different error here?
return Err(RuntimeError::FunctionNotFound);
}

Ok((module_idx, func_idx))
} else {
let (module_idx, func_idx) = (function_ref.module_index, function_ref.function_index);

// TODO: verify module named - index mapping.

// Sanity check that the function index is at least in the bounds of the store, though this doesn't mean
// that it's a valid function.
self.store
.funcs
.get(func_idx)
.ok_or(RuntimeError::FunctionNotFound)?;

Ok((module_idx, func_idx))
}
}

fn init_store(validation_info: &ValidationInfo) -> Store {
let function_instances: Vec<FuncInst> = {
let mut wasm_reader = WasmReader::new(validation_info.wasm);
Expand Down
Loading