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

Ergonomics issues with defining host functions #2596

Closed
Michael-F-Bryan opened this issue Oct 6, 2021 · 3 comments
Closed

Ergonomics issues with defining host functions #2596

Michael-F-Bryan opened this issue Oct 6, 2021 · 3 comments
Labels
🎉 enhancement New feature! priority-medium Medium priority issue 🏚 stale Inactive issues or PR

Comments

@Michael-F-Bryan
Copy link
Contributor

Motivation

The process of creating host functions and passing them as imports to a WebAssembly module suffers from several ergonomics issues:

  • If a function wants to access state, you need to create an explicit type that implements WasmEnv and use something like Function::new_native_with_env(). If functions want to share state then they'll typically need to wrap the env (or just some of its fields) in Arc<Mutex<_>>
  • The host functions themselves are very low-level and each host function needs to be added to the ImportsObject explicitly
  • All argument marshalling needs to be done manually when inside the host function
  • Argument marshalling requires unsafe for all but the most trivial host functions
  • This results in a lot of boilerplate and error-prone unsafe code
  • Because exposing a function from the host to the guest is quite an involved process we've found there is a real disincentive to changing existing APIs or creating detailed interfaces
    • This also means engineers will tend to hack around poor design decisions in the host or guest instead of fixing the problem by changing/replacing the host function

See here for an example that implements these host functions.

Additionally, if you need to use multiple WebAssembly runtimes (e.g. because Wasmer doesn't support a platform you want to target - #2580, #217) all of this boilerplate needs to be duplicated and adapted to fit the other WebAssembly runtime.

Proposed solution

From the end user's perspective, the process of defining and registering host functions should be as seamless as possible.

Ideally, you could use a handful of custom derives and add an attribute to a trait definition, and the library will generate everything else.

For example, imagine this input:

#[wasmer::host_function]
trait Logging {
  fn log(&mut self, target: &str, level: LogLevel, message: &str);
  fn is_enabled(&self, target: &str, level: LogLevel);
}

#[derive(wasmer::WasmType)]
#[repr(u32)]
enum LogLevel {
  Error = 0,
  Warn = 1,
  Info = 2,
  Debug = 3,
}

Which would then expand to something like this:

Lots of boilerplate that programmers would normally write manually.
trait Logging {
  fn log(&mut self, target: &str, level: LogLevel, message: &str);
  fn is_enabled(&self, target: &str, level: LogLevel) -> bool;
}

#[repr(u32)]
enum LogLevel {
  Error = 0,
  Warn = 1,
  Info = 2,
  Debug = 3,
}

impl WasmType for LogLevel { ... }

#[cfg(target_family = "wasm")]
mod Logging_intrinsics {
  extern "C" log(target: *const u8, target_len: u32, level: u32, message: *const u8, message_len: u32);
  extern "C" is_enabled(target: *const u8, target_len: u32, level: u32) -> i32;
}

#[cfg(feature = "wasmer")]
mod Logging_wasmer {
  use wasmer::{ImportObject, Store};
  use std::{collections::HashMap, sync::{Arc, Mutex}};

  pub fn register<L>(store: &Store, import_object: &mut ImportObject, logging: L) 
    where L: Logging + Send + Sync + 'static
  {
    let mut namespace = HashMap::new();
    let env = Env { logging: Arc::new(Mutex::new(logging)), memory: LazyInit::new() };

    ns.insert("log", Function::new_native_with_env(store, env.clone(), log);
    ns.insert("is_enabled", Function::new_native_with_env(store, env.clone(), is_enabled);
  
    import_object.register("Logging", Box::new(ns));
  }

  #[derive(Clone, WasmerEnv)]
  struct Env {
    logging: Arc<Mutex<dyn Logging + Send +Sync + 'static>>,
    #[wasmer(export)]
    memory: LazyInit<Memory>,
  }

  fn log(
    env: &Env, 
    target: WasmPtr<u8, Array>, 
    target_len: u32,
    level: u32,
    message: WasmPtr<u8, Array>,
    message_len: u32,
  ) {
    let target: &str = ...;
    let level: LogLevel = ...;
    let message: &str = ...;

    env.logging.lock().expect("Lock was poisoned").log(target, level, message);
  }

  fn is_enabled(
    env: &Env, 
    target: WasmPtr<u8, Array>, 
    target_len: u32,
    level: u32,
  ) -> i32 { 
    ...
  }
}

#[cfg(feature = "wasm3")]
mod Logging_wasm3 {
  // copy-paste of Logging_wasmer with some wasm3-specific tweaks
}


#[cfg(feature = "wasmtime")]
mod Logging_wasmtime {
  // copy-paste of Logging_wasmer with some wasmtime-specific tweaks
}

// Miscellaneous static assertions
const _: () = {
  fn _assert_is_object_safe(_: &dyn Logging) { }
};

Using items exported by the WebAssembly module could be done by accepting them as arguments to the trait's methods. For example:

trait Foo {
  fn do_something_with_callback(
    &mut self, 
    #[export] memory: &Memory, 
    #[export(name = "real_name", alias = "_real_name)] callback: &mut NativeFunc<(i32, i32), i32>
  );
}

Alternatives

Interface Types

We can't touch on the topic of interop between guest and host without mentioning the Interface Types Proposal.

Ideally, the code being generated by these macros will be completely hidden from the user and could use interface types as the underlying mechanism for passing objects/functions around, or we could use manual code as a polyfill.

WITX + Wiggle

The wasmtime project has a crate called wiggle which will generate much the same code, except it uses a WITX file to define the interface instead of a Rust trait.

I raised the question of adding wasmer support to wiggle in the past, but it sounds like their end goal is to specialize wiggle to work with just wasmtime because supporting multiple runtimes added a lot of engineering work:

Wasmtime has nearly subsumed Lucet in our (Fastly) production use. Once we have EOL'd Lucet, we intend to refactor wiggle to specialize it to just work with Wasmtime. Having wiggle factored to support two different runtimes makes it harder to read & write, among other design compromises we've been forced into. So, wiggle may end up evolving so that it is not viable to use on a different engine.

bytecodealliance/wasmtime#2949 (comment)

Additional context

Another project which has done something similar is wasm-bindgen. Ideally, we could reuse a lot of their lessons and tricks with our own implementation, while also designing things in a way that doesn't lock users into a single runtime.

@Michael-F-Bryan Michael-F-Bryan added the 🎉 enhancement New feature! label Oct 6, 2021
@Amanieu
Copy link
Contributor

Amanieu commented Oct 11, 2021

I've added wasmer support to witx-bindgen in this branch. Give it a try and see if this is what you are looking for. Note that this is still a preview and the API of the generated bindings is still subject to change.

You'll have to use the master branch of wasmer for this to work since it uses some features that are not yet in the latest release. Use these in Cargo.toml:

wasmer = { git = "https://github.com/wasmerio/wasmer" }
wasmer-wasi = { git = "https://github.com/wasmerio/wasmer" }

@Amanieu Amanieu added the priority-medium Medium priority issue label Oct 20, 2021
@stale
Copy link

stale bot commented Oct 20, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the 🏚 stale Inactive issues or PR label Oct 20, 2022
@Michael-F-Bryan
Copy link
Contributor Author

After the ecosystem has matured a bit, it looks like this problem is better solved by tools like wit-bindgen.

@Michael-F-Bryan Michael-F-Bryan closed this as not planned Won't fix, can't repro, duplicate, stale Oct 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🎉 enhancement New feature! priority-medium Medium priority issue 🏚 stale Inactive issues or PR
Projects
None yet
Development

No branches or pull requests

2 participants