Closed jschilli closed 4 months ago
Thanks for raising this question!
I'm assuming world
s will be specialized to boards (or families of boards). The run function in my prototype just takes an output-pin
as an argument, but the idea is, on more complex boards the run
function could have arguments for all the devices/buses/etc. on the board the world
is specialized for. If a board has exactly one I2C bus, the run
function in its world
will have a single i2c
handle as an argument. If more dynamism is needed, it could also take list
s of handles, or even list
s of tuples of handles and identifiers, or other things. But in a lot of boards, there is exactly one I2C bus and it can be really simple.
Looking at the constructor here, it takes a string "identifier". How should an application know what string value to pass? I imagine it's either: the application just hard-codes a string, like "/dev/i2c-1", or: the application has to read a string as input somewhere, that it will then pass to the constructor.
With a hard-coded string we get into the question of, "how does the application know it wants "/dev/i2c-1" versus "/dev/i2c-2" versus something else? Making this choice requires knowledge of the system configuration, which gets awkward.
On the other hand, if the application is going to get passed a string and just blindly pass that string into the constructor, then the string doesn't seem to be adding much value; it's a thing that you have to convert into the thing you want, instead of just being the thing you want. And if there are multiple devices or buses, and they all need strings, then it seems we're either talking a per-board run
function anyway, or something like list<string>
and some extra work at startup to figure out which strings are which.
And either way, there's the question of whether we need to surface errors in case the passed-in string doesn't name a valid device on the host, or the permissions aren't sufficient, or something else.
As you observe, there are other concerns in play when it comes to filesystems and so on in WASI. I'll discuss those more in #3. But for these embedded APIs, we don't have major compatibility constraints, so we can consider the simplest solutions. Taking an i2c
handle argument is a great realization of the "do one thing and do it well" philosophy: the app gets an i2c
bus handle and can immediately start doing what it's there to do.
If upstream Wasmtime decides to bundle in wasi-i2c and other embedded API host support, I imagine we can do something similar to wasmtime serve
, and have a specialized CLI subcommand with options that configure the world
and, on Linux, specify which device file(s) to open and pass in, and so on (and it'll open devices and report any errors before invoking the component).
Does that make sense?
Dan, first of all, thanks for chiming in - I appreciate you helping to move the ball down the court on this.
I like (and share) your vision for world
s specialized to boards and board families. The tricky bits of this question are manifest in both the complexity (or range) of boards out there and that is directly reflected in the (existing hal) driver signatures.
WRT this:
Looking at the constructor here, it takes a string "identifier". How should an application know what string value to pass? I imagine it's either: the application just hard-codes a string, like "/dev/i2c-1", or: the application has to read a string as input somewhere, that it will then pass to the constructor.
100% agree that a string is insufficient for the general case (although it worked perfectly for our admittedly narrow one). We'll be able to quickly absorb the WASI standard side of the proposed interface and then (as you suggest above) create a device specific HAL/world
to handle the initialization part of this.
Thanks again.
Ultimately, this "many worlds" approach means we have a lot of flexibility. We can put driver initialization code in the guest when we want to. For things like opening "/dev/i2c-1", I tend to default to putting them in the host, because there's a lot of simplicity when guests can be just handed an i2c
handle that they can immediately start using. But, we do have the flexibility to build worlds that work either way.
Thanks for your efforts here. We have a simple implementation of (an almost standard) i2c interface - our focus (initially) was narrower than the standard.
See https://github.com/wasmrobotics/wasm-i2c-compare for our test repo where we built out i2c functionality a few ways - one of which is using a WIT defined I2C interface.
In both Dan Gohman's original code and the samples I've seen (e.g. https://github.com/Zelzahn/i2c-wasm-components/blob/fac68fe2ecd4f0db562b246875250f72eddb9321/wasmtime/host/src/device.rs#L145) the initialization of the I2c connection happens by directly creating the "driver" instance and pushing it into the
ResourceTable
and then passing thatResource
to the component that requires it.You'll see here that we've made a constructor for the resource as well as a resource creator playing around a bit with what feels more natural.
My concern with the standard as written is that it avoids the issue of resource construction. I think there are a few ways to look @ this
I understand the vagaries of the embedded world enough to know that there may not be an obvious way to build a common constructor (or factory function) but I think it's worth surfacing the concern. The linux i2c space is probably the easiest because the resource is described as a device. That simplicity disappears in the face of an embedded device with pin mappings etc.
Why does this matter?
In our case we're building out a runtime that loads compiled WASM components and as such we need a way for components to self-source their I2c connection. No one would want to recompile
wasmtime
or ??? just to leverage an I2c component.I'd love to hear thoughts on this.