-
-
Notifications
You must be signed in to change notification settings - Fork 20
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(wasm): run a "hello world" wasm under an interpreter #58
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
use std::env; | ||
use std::fs; | ||
use std::path::PathBuf; | ||
|
||
fn main() -> Result<(), Box<std::error::Error>> { | ||
let out_dir = PathBuf::from(env::var("OUT_DIR")?); | ||
|
||
// Build our helloworld.wast into binary. | ||
let binary = wat::parse_file("src/helloworld.wast")?; | ||
fs::write(out_dir.join("helloworld.wasm"), &binary)?; | ||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
;; Copied from the wasmtime WASI tutorial | ||
;; https://github.com/bytecodealliance/wasmtime/blob/1f8921ef09667dc4319c1b65ebb1ce993e19badf/docs/WASI-tutorial.md#web-assembly-text-example | ||
;; | ||
;; wat2wasm helloworld.wast | ||
|
||
(module | ||
;; Import the required fd_write WASI function which will write the given io vectors to stdout | ||
;; The function signature for fd_write is: | ||
;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written | ||
(import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) | ||
|
||
(memory 1) | ||
(export "memory" (memory 0)) | ||
|
||
;; Write 'hello world\n' to memory at an offset of 8 bytes | ||
;; Note the trailing newline which is required for the text to appear | ||
(data (i32.const 8) "hello world\n") | ||
|
||
(func $main (export "_start") | ||
;; Creating a new io vector within linear memory | ||
(i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string | ||
(i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string | ||
|
||
(call $fd_write | ||
(i32.const 1) ;; file_descriptor - 1 for stdout | ||
(i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0 | ||
(i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one. | ||
(i32.const 20) ;; nwritten - A place in memory to store the number of bytes written | ||
) | ||
drop ;; Discard the number of bytes written from the top of the stack | ||
) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,10 @@ use hal_core::{boot::BootInfo, mem, Architecture}; | |
|
||
use alloc::vec::Vec; | ||
|
||
mod wasm; | ||
|
||
const HELLOWORLD_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/helloworld.wasm")); | ||
|
||
pub fn kernel_main<A>(bootinfo: &impl BootInfo<Arch = A>) -> ! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this clippy lint fires a lot of false positives on tracing macros, we should just turn it off |
||
where | ||
A: Architecture, | ||
|
@@ -73,6 +77,16 @@ where | |
assert_eq!(v.pop(), Some(5)); | ||
} | ||
|
||
{ | ||
let span = tracing::info_span!("wasm test"); | ||
let _enter = span.enter(); | ||
|
||
match wasm::run_wasm(HELLOWORLD_WASM) { | ||
Ok(()) => tracing::info!("wasm test Ok!"), | ||
Err(err) => tracing::error!(?err, "wasm test Err"), | ||
} | ||
} | ||
|
||
// if this function returns we would boot loop. Hang, instead, so the debug | ||
// output can be read. | ||
// | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
use core::fmt; | ||
|
||
#[derive(Debug, Copy, Clone)] | ||
pub struct ConvertError; | ||
impl fmt::Display for ConvertError { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
write!(f, "conversion error") | ||
} | ||
} | ||
impl From<ConvertError> for wasmi::Trap { | ||
fn from(_: ConvertError) -> wasmi::Trap { | ||
wasmi::TrapKind::UnexpectedSignature.into() | ||
} | ||
} | ||
|
||
/// Trait describing a type which can be used in wasm method signatures. | ||
// XXX: Should this require `wasmi::FromRuntimeValue` and | ||
// `Into<wasmi::RuntimeValue>`? | ||
pub trait WasmPrimitive: Sized { | ||
const TYPE: wasmi::ValueType; | ||
|
||
fn from_wasm_value(value: wasmi::RuntimeValue) -> Result<Self, ConvertError>; | ||
fn into_wasm_value(self) -> wasmi::RuntimeValue; | ||
} | ||
|
||
macro_rules! impl_wasm_primitive { | ||
($($rust:ty = $wasm:ident;)*) => { | ||
$( | ||
impl WasmPrimitive for $rust { | ||
const TYPE: wasmi::ValueType = wasmi::ValueType::$wasm; | ||
|
||
fn from_wasm_value(value: wasmi::RuntimeValue) -> Result<Self, ConvertError> { | ||
value.try_into().ok_or(ConvertError) | ||
} | ||
|
||
fn into_wasm_value(self) -> wasmi::RuntimeValue { | ||
self.into() | ||
} | ||
} | ||
)* | ||
} | ||
} | ||
|
||
impl_wasm_primitive! { | ||
i8 = I32; | ||
i16 = I32; | ||
i32 = I32; | ||
i64 = I64; | ||
u8 = I32; | ||
u16 = I32; | ||
u32 = I32; | ||
u64 = I64; | ||
wasmi::nan_preserving_float::F32 = F32; | ||
wasmi::nan_preserving_float::F64 = F64; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
use alloc::borrow::ToOwned; | ||
use core::convert::TryFrom; | ||
use core::fmt; | ||
|
||
mod convert; | ||
mod wasi; | ||
|
||
use self::convert::WasmPrimitive; | ||
|
||
macro_rules! option_helper { | ||
(Some $rt:expr) => { | ||
Some($rt) | ||
}; | ||
(Some) => { | ||
None | ||
}; | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct Host { | ||
pub module: wasmi::ModuleRef, | ||
// FIXME: The wasmi crate currently provides no way to determine the | ||
// caller's module or memory from a host function. This effectively means | ||
// that if a module imports and calls a function from another module, and | ||
// that module calls a host function, the memory region of the first module | ||
// will be used. | ||
// | ||
// We will need to either modify the wasmi interpreter and host function API | ||
// to pass in function context when calling host methods, or use host | ||
// function trampolines which change the `Host` value, to avoid this issue. | ||
pub memory: wasmi::MemoryRef, | ||
} | ||
|
||
impl Host { | ||
// Create a new host for the given instance. | ||
// NOTE: The instance may not have been started yet. | ||
fn new(instance: &wasmi::ModuleRef) -> Result<Self, wasmi::Error> { | ||
let memory = match instance.export_by_name("memory") { | ||
Some(wasmi::ExternVal::Memory(memory)) => memory, | ||
_ => { | ||
return Err(wasmi::Error::Instantiation( | ||
"required memory export".to_owned(), | ||
)) | ||
} | ||
}; | ||
|
||
Ok(Host { | ||
module: instance.clone(), | ||
memory, | ||
}) | ||
} | ||
} | ||
|
||
macro_rules! host_funcs { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. am i correct that this macro is generating all the wasi host calls (eventually)? it would be nice to have a little more documentation of all the stuff this generates & how it should be invoked (eventually) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. I threw together this macro because I needed to write a boatload of boilerplate to declare even a single host method, and didn't want to have to do it again. We might actually want to use |
||
($( | ||
fn $module:literal :: $name:literal ($($p:ident : $t:ident),*) $( -> $rt:ident)? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoa, TIL that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it's super handy, especially because it has an open FOLLOW set, so you don't need to be as careful with what you put after it! |
||
as $variant:ident impl $method:path; | ||
)*) => { | ||
#[repr(usize)] | ||
#[derive(Copy, Clone, Debug, Eq, PartialEq)] | ||
enum HostFunc { | ||
$($variant),* | ||
} | ||
|
||
impl HostFunc { | ||
fn resolve_func( | ||
module_name: &str, | ||
field_name: &str, | ||
_signature: &wasmi::Signature, | ||
) -> Result<HostFunc, wasmi::Error> { | ||
match (module_name, field_name) { | ||
$(($module, $name) => Ok(HostFunc::$variant),)* | ||
_ => Err(wasmi::Error::Instantiation("unresolved func import".to_owned())) | ||
} | ||
} | ||
|
||
fn signature(self) -> wasmi::Signature { | ||
match self { | ||
$( | ||
HostFunc::$variant => wasmi::Signature::new( | ||
&[$(<$t as WasmPrimitive>::TYPE),*][..], | ||
option_helper!(Some $(<$rt as WasmPrimitive>::TYPE)?), | ||
) | ||
),* | ||
} | ||
} | ||
|
||
fn func_ref(self) -> wasmi::FuncRef { | ||
wasmi::FuncInstance::alloc_host(self.signature(), self as usize) | ||
} | ||
|
||
fn module_name(self) -> &'static str { | ||
match self { | ||
$(HostFunc::$variant => $module),* | ||
} | ||
} | ||
|
||
fn field_name(self) -> &'static str { | ||
match self { | ||
$(HostFunc::$variant => $name),* | ||
} | ||
} | ||
} | ||
|
||
impl TryFrom<usize> for HostFunc { | ||
type Error = wasmi::Trap; | ||
fn try_from(x: usize) -> Result<Self, Self::Error> { | ||
$( | ||
if x == (HostFunc::$variant as usize) { | ||
return Ok(HostFunc::$variant); | ||
} | ||
)* | ||
Err(wasmi::TrapKind::UnexpectedSignature.into()) | ||
} | ||
} | ||
|
||
impl fmt::Display for HostFunc { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
write!(f, "{}::{}", self.module_name(), self.field_name()) | ||
} | ||
} | ||
|
||
impl wasmi::Externals for Host { | ||
fn invoke_index( | ||
&mut self, | ||
index: usize, | ||
args: wasmi::RuntimeArgs, | ||
) -> Result<Option<wasmi::RuntimeValue>, wasmi::Trap> { | ||
let span = tracing::trace_span!("invoke_index", index, ?args); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIOLI: seems like we could just slap a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the method which I was poking you on Discord about yesterday which breaks when annotated with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah gotcha. it would be nice to have a tracing issue for that (even though it's a compiler error), just so we can reference the related issue against the compiler? |
||
let _enter = span.enter(); | ||
|
||
match HostFunc::try_from(index)? { | ||
$( | ||
HostFunc::$variant => match args.as_ref() { | ||
[$($p),*] => { | ||
let _result = $method( | ||
self, | ||
$(<$t as WasmPrimitive>::from_wasm_value(*$p)?),* | ||
)?; | ||
Ok(option_helper!( | ||
Some $(<$rt as WasmPrimitive>::into_wasm_value(_result))? | ||
)) | ||
} | ||
_ => Err(wasmi::TrapKind::UnexpectedSignature.into()), | ||
} | ||
),* | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
host_funcs! { | ||
fn "wasi_unstable"::"fd_write"(fd: u32, iovs: u32, iovs_len: u32, nwritten: u32) -> u16 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIOLI: seems like these could be idents rather than quoted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shrug probably. |
||
as FdWrite impl wasi::fd_write; | ||
} | ||
|
||
struct HostResolver; | ||
impl wasmi::ImportResolver for HostResolver { | ||
fn resolve_func( | ||
&self, | ||
module_name: &str, | ||
field_name: &str, | ||
signature: &wasmi::Signature, | ||
) -> Result<wasmi::FuncRef, wasmi::Error> { | ||
let host_fn = HostFunc::resolve_func(module_name, field_name, signature)?; | ||
Ok(host_fn.func_ref()) | ||
} | ||
|
||
fn resolve_global( | ||
&self, | ||
module_name: &str, | ||
field_name: &str, | ||
descriptor: &wasmi::GlobalDescriptor, | ||
) -> Result<wasmi::GlobalRef, wasmi::Error> { | ||
tracing::error!( | ||
module_name, | ||
field_name, | ||
?descriptor, | ||
"unresolved global import" | ||
); | ||
Err(wasmi::Error::Instantiation( | ||
"unresolved global import".to_owned(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there's no way for these error strings to be static strings, is there? I'm assuming the wasmi error type is not ours... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup :-/. Every |
||
)) | ||
} | ||
|
||
fn resolve_memory( | ||
&self, | ||
module_name: &str, | ||
field_name: &str, | ||
descriptor: &wasmi::MemoryDescriptor, | ||
) -> Result<wasmi::MemoryRef, wasmi::Error> { | ||
tracing::error!( | ||
module_name, | ||
field_name, | ||
?descriptor, | ||
"unresolved memory import" | ||
); | ||
Err(wasmi::Error::Instantiation( | ||
"unresolved memory import".to_owned(), | ||
)) | ||
} | ||
|
||
fn resolve_table( | ||
&self, | ||
module_name: &str, | ||
field_name: &str, | ||
descriptor: &wasmi::TableDescriptor, | ||
) -> Result<wasmi::TableRef, wasmi::Error> { | ||
tracing::error!( | ||
module_name, | ||
field_name, | ||
?descriptor, | ||
"unresolved table import" | ||
); | ||
Err(wasmi::Error::Instantiation( | ||
"unresolved table import".to_owned(), | ||
)) | ||
} | ||
} | ||
|
||
pub fn run_wasm(binary: &[u8]) -> Result<(), wasmi::Error> { | ||
let module = wasmi::Module::from_buffer(binary)?; | ||
|
||
// Instantiate the module and it's corresponding `Host` instance. | ||
let instance = wasmi::ModuleInstance::new(&module, &HostResolver)?; | ||
let mut host = Host::new(instance.not_started_instance())?; | ||
let instance = instance.run_start(&mut host)?; | ||
|
||
// FIXME: We should probably use resumable calls here. | ||
instance.invoke_export("_start", &[], &mut host)?; | ||
Ok(()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we wat2wasm when building the kernel? it seems super easy to accidentally end up with a binary wasm dep in the kernel that doesn't match some associated source
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That'd be pretty solid. We could also use a pure-rust wat2wasm converter (there are a few iirc) and just run it in our build script.
I just did it this way because it was the easiest, but not checking in binaries is probably a good plan.