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(wasm): run a "hello world" wasm under an interpreter #58

Merged
merged 4 commits into from
Jan 12, 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
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ tracing = { version = "0.1", default_features = false }
bootloader = "0.8.0"
hal-x86_64 = { path = "hal-x86_64" }

[dependencies.wasmi]
git = "https://github.com/mystor/wasmi"
branch = "mycelium"
default-features = false
features = ["core"]

[build-dependencies]
wat = "1.0"

[package.metadata.bootimage]
default-target = "x86_64-mycelium.json"

Expand Down
12 changes: 12 additions & 0 deletions build.rs
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(())
}
32 changes: 32 additions & 0 deletions src/helloworld.wast
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
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

;; 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
)
)
14 changes: 14 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>) -> !
Copy link
Owner

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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.
//
Expand Down
55 changes: 55 additions & 0 deletions src/wasm/convert.rs
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;
}
233 changes: 233 additions & 0 deletions src/wasm/mod.rs
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 {
Copy link
Owner

Choose a reason for hiding this comment

The 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)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 witx files and auto-generate the binding logic (similar to how wasmtime does since bytecodealliance/wasmtime#707), but this was much easier to get started with.

($(
fn $module:literal :: $name:literal ($($p:ident : $t:ident),*) $( -> $rt:ident)?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoa, TIL that literal is an acceptable thing to match against in a macro!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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!
Been around since ~2016 sometime, but was stabilized in rust-lang/rust#56072.

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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIOLI: seems like we could just slap a #[tracing::instrument] on the generated function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 #[tracing::instrument] due to rust messing up spans.

Copy link
Owner

Choose a reason for hiding this comment

The 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIOLI: seems like these could be idents rather than quoted?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shrug probably.
I made them strings because I think theoretically wasm supports spaces in symbols. I'm not sure we'll ever declare a symbol which has spaces in it, though, so it's probably not reasonable.

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(),
Copy link
Owner

Choose a reason for hiding this comment

The 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...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup :-/. Every wasmi error has a string allocation.
if we decide to hard-fork wasmi and break the API, we could change the error type (and the lib in general) to be much less allocation-happy.

))
}

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(())
}
Loading