Stream: wit-bindgen

Topic: Dependency Injection/Inversion


view this post on Zulip Mitchell (Aug 05 2023 at 00:50):

Hey. I'm wondering if there has been any discussion about designing the outputs of wit_bindgen::generate!() with dependency injection in mind for the guest code. Specifically I think it would be nice to have the host imports for the plugin code be mockable so you can test in native Rust without needing a runtime.

To be clear, I have been able to do this with the existing code, just with some serious drawbacks.

This is the sort of thing I'm doing right now:

pub trait Imports {
    fn foo(something: &str);
    fn bar(other_thing: u8) ->  u8;
}

pub struct Plugin<I: Imports> {
    _inputs: PhantomData<I>,
}

impl<I: Imports> MyWorld for Plugin<I> {
    fn run() {
        I::foo("howdy");
        I::bar(1)
    }
}

then provide a live adapter to Imports:

struct MyWorldImports;

impl Imports for MyWorldImports {
    fn foo(something: &str) {
        foo(something);
    }
    fn bar(other_thing: u8) -> u8 {
        bar(other_thing)
    }
}

and then I need an alias to export:

type MyPlugin = Plugin<MyWorldInputs;

export_my_world!(MyPlugin);

It would be nice if the imports were also defined as traits and just came with a default impl instead of the free-floating foo and bar, but that's not the biggest issue.
It's challenging to write mocks when the trait functions aren't methods, but associated functions. It's possible, yes, with lazy_static and stuff, but I definitely don't consider that idiomatic Rust.

Thoughts?

TL;DR
It would be nice if wit_bindgen gave you traits like this, both for your imports and exports:

pub trait Imports {
    fn foo(&self, something: &str);
    fn bar(&self, other_thing: u8) ->  u8;
}

pub trait MyWorld{
    fn run(&self);
}

Where those traits were all methods instead of associated functions.
This would make dependency injection easier for tests in your plugin/guest code.

view this post on Zulip Robin Brown (Aug 05 2023 at 14:49):

I think in the long run, people will dependency injection test their components... using the component model + runtimes. The benefit of this being that you're testing the component exactly as it is through the exact interface it will have when you use it, not a mock of it.

Tools catering to this don't really exist yet, but you could do it today by writing some tests that use wasmtime + host bindings to initialize your component with its imports mapped to whatever you want. In the future, I expect people (myself included) will make testing harnesses/languages/frameworks that make this very simple and easy to do.

view this post on Zulip Mitchell (Aug 05 2023 at 20:10):

That's an option too. I'd still want mocks in that case though, since we could be dealing with expensive IO or processes we don't have vision into. So, the mock dependencies could be injected into the Runtime for testing imports. That just means a heavier testing framework than what I was suggesting.

As a rule of thumb, I try to push my testing "left", or as early in the process as possible. I'd rather do native Rust tests than a hybrid.

view this post on Zulip Mitchell (Aug 05 2023 at 20:12):

Also, for some projects, your component could be run natively in some contexts and as Wasm in others as well. The tests would need to be on the Rust side in that case.

view this post on Zulip Robin Brown (Aug 05 2023 at 20:51):

What's interesting is that if you do it using a runtime, they aren't actually "mocks" they're real virtualizations of the host facilities your components make use of.

view this post on Zulip Mitchell (Aug 05 2023 at 21:32):

I'm not sure I follow.

I'm saying I want to be able to mock things. I want to be able to hit the components' exports with something that resembles a real request and see that it behaves in the expected way. What the host does with the component is out of scope.

view this post on Zulip Ramon Klass (Aug 05 2023 at 22:01):

the argument for emulating network cards instead of mocking response data is that you actually test 100% of the code that runs on production, there is no mocking code in your program, just the emulated network that answers exactly in the reliable expected way

view this post on Zulip Robin Brown (Aug 05 2023 at 22:08):

It's not really about what the "host does with the component". I'm saying that you can implement what your host will eventually provide to the component (e.g. file system, network) as either another component (called a virtualization) or on the host itself in a sandboxed way. Doing so means (like what Ramon said) that you're actually running the exact component code you will run in production against a network facility/capability/resource implementation that conforms to the same spec and behavior as the real one will.

view this post on Zulip Mitchell (Aug 05 2023 at 22:25):

view this post on Zulip Mitchell (Aug 05 2023 at 22:27):

What you're describing sounds great. And I think I would want to use that for integration testing. I want to push left as much as possible and the more tests I can have as unit tests the better.

view this post on Zulip Robin Brown (Aug 05 2023 at 22:40):

Fair enough, but I don't expect there's going to be a lot of interest in working on building an additional dependency injection framework into the bindgen when the component model is essentially a dependency injection framework. If you want to see this happen, you'd probably need to prototype it yourself since everyone's focused on getting things ready for preview2.

view this post on Zulip Robin Brown (Aug 05 2023 at 22:42):

Best place to start would probably be a wit-bindgen issue proposing your design.

view this post on Zulip Mitchell (Aug 05 2023 at 23:18):

It wouldn't be a framework. It would just be exposing the imports as a trait + trait impl rather than floating functions and making the export trait methods instead of assoc functions.

Possibly very little work in the macro, but it would be a breaking change for sure.

view this post on Zulip Christof Petig (Aug 06 2023 at 13:14):

I deployed a similar idea to test my component without a wasm runtime by just compiling them to native - but then I ran into the following problems:

So I guess the best recommendation is to test them as a wasm component, by using a runtime - from a scripting language (jco and componentize-py might help here, sadly I need more than they support at the moment)
… But as long as I was able to keep it working the debugging experience was great, because I could singlestep between tester and testee in the same debugging environment.

view this post on Zulip Mitchell (Aug 06 2023 at 18:03):

Oh nice. Debugging was another thing I thought of where this might help, but I've never done any debugging with Wasm, so was reticent to bring up.

I don't anticipate my idea to have issues with naming. It should have the same naming scheme as the current bindgen. The only think I'm worried about is I would need to provide the export macro an instance of a struct, rather than just the struct type. This would have implications on how the component is generated under the hood and could indeed impact the bindings in other languages.

view this post on Zulip Christof Petig (Aug 08 2023 at 16:25):

As far as debugging is concerned I can tell that both chrome and wamr work fine even with source level debugging (e.g. setting break points at C/++ or rust lines and inspecting variables inside the guest), but setting up the debugging system is still tricky.


Last updated: Dec 23 2024 at 12:05 UTC