bytecodealliance / wasmtime

A fast and secure runtime for WebAssembly
https://wasmtime.dev/
Apache License 2.0
15.17k stars 1.27k forks source link

Evolving the API of the `wasmtime` crate #708

Closed alexcrichton closed 4 years ago

alexcrichton commented 4 years ago

I've been reviewing the wasmtime crate from a Rust API perspective and ended up realizing that there's actually quite a few changes that I would like to make to the crate. I think that these changes are far too large so simply send in a PR, so I wanted to make sure that we had some discussion of this first!

In this issue I hope to lay out a vision for an end-state wasmtime crate and what the API might look like. I'm assuming that we can incrementally reach this end-goal over time and the exact route through which we get here isn't too too important. In any case, I'm curious if others have thoughts on all this!

Some of these items below may warrant their own separate issue as well, but I wanted to make sure that I had this all written down in one location first

High-level changes

Config

Engine

Store

Module

Instance

Global, Table, Memory

These types are only accessed via a reference when fetched through an Instance.

Func

This type I think needs a lot of improvements. I think it's best to separate out these concerns into a separate issue, however. Some high-level unbaked thoughts are:

MemoryType, TableType, GlobalType

ImportType, ExportType

FuncType

Trap

Val

WASI

This is now split off to https://github.com/bytecodealliance/wasmtime/issues/727

Name Resolution

This is now split off to https://github.com/bytecodealliance/wasmtime/issues/727

Instantiation is a bit wonky where you have to line up imports 1:1 with the expected imports of the module. We should explore ideas where we have a more name resolution based mechanism which leverages the module system. Would perhaps make it much easier to slot in WASI or slot in a module. Pretty tricky API though so we'd have to think elsewhere about this.

eminence commented 4 years ago

One of the things I'm interested in doing is creating a custom sandbox, where I provide some custom functions for a wasm module to import, and some wasi functions (but maybe not all of the wasi functions). I think your wasi_unstable "bare minimum" function handles this case, by allowing me to pick from the returned HashMap and use whatever ones I want during instantiation. I can definitely see how this would interact with name resolution (I'd like to be able to detect if the module I'm running is importing a wasi function I'm unwilling to provide them)

One of the things that confused me about the current version of Callable is how you store your results in a &mut [Val]. Just based off the function signature, one of the things I tried first was:

    fn call(&self, _params: &[Val], results: &mut [Val]) -> Result<(), HostRef<Trap>> {
        if let Some(Val::I32(v)) = results.get_mut(0) {
            *v = 42;
        } // else return trap
    }

The runtime error you get here is fairly inscrutable. I don't have a concrete proposal here, but I wanted to mention this minor papercut.

cedric-h commented 4 years ago

As I was hiding HostRef in the Module API in my PR #696 I noticed that once I removed the #[derive(Clone)] from the ModuleInner, the compiler began to issue a complaint, namely that the store field wasn't actually ever being used. I've considered maybe adding a #[allow(dead_code)] to the field for now, but perhaps I should just go ahead and add a stores accessor (as you suggest up there) instead?

cedric-h commented 4 years ago

I've been looking for a scripting language to embed into some of my Rust game development projects, to facilitate scripting (with hot reloading!) custom logic and events for levels. Along the way, I've tried a bunch of different languages and have developed opinions about their APIs. So without further ado: A review of some Rusty embeddable scripting language APIs.

Dyon

https://github.com/PistonDevelopers/dyon/blob/master/examples/call.rs I very much dislike Dyon's scripting API. In terms of features, most things are there (Dyon can return Rust structs to your Rust host code) but the parts that you'd like to be able to control, like deciding how Dyon objects are turned into Rust structs, are hidden from you using really arcane macros, and the parts that you don't care so much about, like wrapping everything in Arc and declaring function type signatures by hand using Dyon's enums for that... aren't. It makes exposing a Rust API to Dyon scripts really annoying, and ultimately drove me away from using Dyon ever again.

Forge

https://github.com/zesterer/forge/blob/master/examples/callbacks.rs I'm actually very fond of Forge's embedding API. You can directly pass globals and functions to Forge's API using the builder pattern, which is patently Rusty and very ergonomic. I ended up not using Forge simply because the language itself lacks many features, and because the embedding doesn't yet support returning Rust structs from Forge, one of the things I can't do without for my use case.

Rhai

https://github.com/jonathandturner/rhai Rhai has very good documentation on their embedding API right in their README.MD. The API is a little bit messy and could potentially be made cleaner by leveraging Rust's type system better, but the core concepts behind it are similar to Forge's (with a useful distinction between adding functions directly to the interpreting Engine and adding them to particular modules). Their embedding API also supports instantiating and manipulating Rust structs from Rhai. I ended up choosing not to use Rhai, however, because of the lack of features in the language (no for in loop) which prevented it from being ergonomic to use.

PyO3

https://pyo3.rs/master/python_from_rust.html I was looking for an easily embeddable pure Rust solution for my scripting needs, but ultimately I found that the pure Rust ecosystem just wasn't quite there yet. PyO3's wrapper around Python's C FFI fulfilled all of my needs and then some, and the macros were very helpful and seemed to work with me instead of against me as was the case with Dyon. I was able to hack together an abuse of Rust's type system which allows me to take a Rust struct that's returned from Python, make sure it's one of the ECS components in my game, and then introduce it into the simulation. So far I've used this to allow Python to override objects in my map files as they're being loaded, so that a Python script can i.e. choose one of several possible enemy formations stored in the map file and choose to load just one. Aside from that, I haven't tried this yet, but all of the machinery is also there for enemy formations and even rooms and such to be generated from scratch (instead of from a map file) in Python. https://github.com/cedric-h/hauntfall/blob/master/serv/src/config/level.rs

The ultimate solution to this problem, of course, is wasmtime, which is why I'm excited to see the Interface Types proposal implemented; that should remove most of the barriers that prevent its use in my (and the rest of the Rust gamedev community's) projects.

joshtriplett commented 4 years ago

Instantiation is a bit wonky where you have to line up imports 1:1 with the expected imports of the module. We should explore ideas where we have a more name resolution based mechanism which leverages the module system. Would perhaps make it much easier to slot in WASI or slot in a module. Pretty tricky API though so we'd have to think elsewhere about this.

I would love to see functions provided from the host/embedder in the style of module exports, with interface types and names, and then have wasmtime handle name resolution.

Once we have linking, what if we have a new type for a "HostModule", with a module name and a set of named exported functions with types?

alexcrichton commented 4 years ago

@eminence good point! I think that the details of Func and how that's created are definitely not ergonomic right now and it's something we want to improve. I think though it's probably best to separate out discussion from this issue since I think that will largely be a separable concern. I'm hoping we can get a lot of the skeleton of the API improved through this issue, and then we can do deep dives like https://github.com/bytecodealliance/wasmtime/issues/727 into different APIs.

@cedric-h I don't think we'll want to add allow(dead_code) but I think we largely just need to be careful about our incremental progress. So long as the tests all continue passing I think we're all good :)

@joshtriplett I've started a dedicated discussion for that at https://github.com/bytecodealliance/wasmtime/issues/727 since I think it'll be a big topic, want to take a look at the proposal there and see if it sounds like it'll meet your needs?

alexcrichton commented 4 years ago

With https://github.com/bytecodealliance/wasmtime/pull/814 I believe everything here is either fixed or split out into a separate issue, so I'm going to close!