I am trying to understand WASI/Storage options with JCO and I guess the current state of what is available. I am trying to figure out how to preserve some state between executions of incoming-request from my wasm component. My example use case is that I want to provide 'connector' objects which make API calls with keys, or oauth tokens. I need to allow customers to define the behaviors for these in javascript and wasm allows a way to sandbox the customer code, constrain access and resources without having to configure larger complex security stacks around containers, all while providing a browser based runtime for them to test/assert their code. To accomplish this I need to load some minimal state and allow the code to update this state (like for an oauth refresh token exchange it needs to set the new token). I see the following options available,
1) WASI HTTP + External Service
Send an http request to an external storage to load the needed state on each request. For example I could provide credentials via env args + wasi ctx and communicate to redis to load or update state, perform locks, etc. componentize js supports this binding and I can use the 'fetch' api within the js code and how the wasm runtime fulfill the request. The issue I dislike about this is that it requires more security configuration outside the runtime. (I would like to keep the security configurations and access setup close to the runtime and not split between an external system and the runtime).
2) WASI keyvalue
There is a wasi draft for keyvalue storage, while it appears some support for this is enabled in wasmtime, it is not in the list of features from componentize-js and I dont see it in the bindings setup for jco.
2) WASI filesystem
Its possible I could mock a virtual filesystem with the runtime and use the file access to load and set state. However it seems this is not in the features yet for componentizejs, and it also doesn't seem to be the direction others intend given the keyvalue draft exists.
4) Custom export/import
I could add my own keyvalue binding and update componentize-js with my own linker, but this is starting to become too complex for my current knowledge, and with changes in the lbiraries I predict this would become a pain point to maintain and is a more difficult approach if im still learning.
5) Implement the keyvalue component in another language and compose my own wasm file with this one. Ive been reading through the componentizejs bindgen.rs but I do not yet understand the relations to know what functions would be available and or how to reference them for import from the js ctx. Would step 5 be as simple as taking a wasm component which implemented keyvalue and transpile it, and assume the runtime has the feature enabled? (im not sure if it can just assume host provided functions like that and still handle the types correctly?) Maybe the componentEmbed is somehow involved?
Does anyone have a suggested approach for a simple way to allow state read and update from the wasm component + jco?
Hey Tylr, as you've noticed, you're going to have to rely on something for component-external storage. All the routes that you've outlined are possible (great writeup/exploration!) but since that isn't quite jco's concern, you do need to provide some sort of storage to the component.
One thing I want to note is that it is possible to reuse (though not recommended) the component for serving requests if you make an embedding/thing that runs the component (for example see the code behind wasmtime serve
, you could choose to re-use the instantiated module though it's really not recommended)
There are a few companies who add functionality to components (like storage) greater -- I'm biased of course since I work at the company that maintains wasmCloud, but this might be a great time to slot your component into the existing frameworks and give them a shot!
That said, maybe we can start to narrow down these choices by narrowing down what kind of storage you expect to be able to access from the component? What is the shape of the outside world you want to see? Obviously keyvalue and filesystem are very different expectations.
I think what i want is the bucket resource from the wasi keyvalue. I want the runtime to provide the interface so the user defined code does not need to know the implementations which fulfill the storage.
As for how this pertains to jco, I think is my confusion around how to import a function the host runtime will provide. It looks like wasmtime includes the wasi keyvalue interface in the linker. Ive added the wit file for the store and imported it in my component and have generated the types from it. However upon execution of the wasm component via wasmtime serve -S common
I get the following error
Error: component imports instance `wasi:keyvalue/store@0.2.0-draft`, but a matching implementation was not found in the linker
I think there are two possible issues,
1) I have the wrong import name for jco to know which function im referencing.
2) jco will not bind my function at all because it is not in the list of known host interface bindings.
Im not sure if either or both are the issue. Trying to read through all the code of the spidermonkey bindings crate, the jco bindings and shims to understand, but it's.. a lot to comprehend/learn :sweat_smile:
As for existing store examples you mentioned;
I see the vmware implementation appears to append the data in the request response. Im guessing their implementation predated the wasi k/v draft? But the issue I have with it they already admitted, it cannot handle concurrent requests with state changes. (they probably ensure ordered processing via a queue or something to work around, but i'd like to stay close to the proposed wasi specs if possible and not alter the incoming-request interface).
It looks like the wasmcloud implementation rolls its own approach with those wrpc interface files. I see some references to NATS in the wasmcloud site, so im guessing either the wasmcloud runtime implements the host function directly for import, or it just defined the tcp behavior to communicate to what ever protocol NATS is talking. My instinct is that wasmcloud is doing this second approach given it would allow a lot of flexibility without having to rework the runtime code to provide more interfaces from the host. If that second scenario is the case it falls into the same scenario for having to defer security to the mechanisms available in NATS, and doesn't provide a simple way to test it all without having the NATS running or some mock server.
Been reading all I can find before typing out a reply. Sorry slow response :sweat_smile:
Ah, I guess the nats approach would be simpler if you swap the component out for a mock and compose your wasm with something like this example https://github.com/bytecodealliance/jco/blob/main/examples/guides/04-importing-and-reusing-components.md
It wouldn't allow true state updates, but could be mocked with predefined responses for the test cases.
As for your wasmtime issue, though you're right that wasi keyvalue is built into the linker, it's actually not enabled by -S Common
, you need -S keyvalue
wasmtime serve -S help
Available wasi options:
-S cli[=y|n] -- Enable support for WASI CLI APIs, including filesystems, sockets, clocks, and random.
-S cli-exit-with-code[=y|n] -- Enable WASI APIs marked as: @unstable(feature = cli-exit-with-code)
-S common[=y|n] -- Deprecated alias for `cli`
-S nn[=y|n] -- Enable support for WASI neural network API (experimental)
-S threads[=y|n] -- Enable support for WASI threading API (experimental)
-S http[=y|n] -- Enable support for WASI HTTP API (experimental)
-S config[=y|n] -- Enable support for WASI config API (experimental)
-S keyvalue[=y|n] -- Enable support for WASI key-value API (experimental)
-S listenfd[=y|n] -- Inherit environment variables and file descriptors following the systemd listen fd specification (UNIX only)
-S tcplisten=val -- Grant access to the given TCP listen socket
-S preview2[=y|n] -- Implement WASI CLI APIs with preview2 primitives (experimental).
-S nn-graph=<format>::<dir> -- Pre-load machine learning graphs (i.e., models) for use by wasi-nn.
-S inherit-network[=y|n] -- Flag for WASI preview2 to inherit the host's network within the guest so it has full access to all addresses/ports/etc.
-S allow-ip-name-lookup[=y|n] -- Indicates whether `wasi:sockets/ip-name-lookup` is enabled or not.
-S tcp[=y|n] -- Indicates whether `wasi:sockets` TCP support is enabled or not.
-S udp[=y|n] -- Indicates whether `wasi:sockets` UDP support is enabled or not.
-S network-error-code[=y|n] -- Enable WASI APIs marked as: @unstable(feature = network-error-code)
-S preview0[=y|n] -- Allows imports from the `wasi_unstable` core wasm module.
-S inherit-env[=y|n] -- Inherit all environment variables from the parent process.
-S config-var=<name>=<val> -- Pass a wasi config variable to the program.
-S keyvalue-in-memory-data=<name>=<val> -- Preset data for the In-Memory provider of WASI key-value API.
pass `-S help-long` to see longer-form explanations
I've been using -S common
as a habit but looks like it's been deprecated!
I see the vmware implementation appears to append the data in the request response. Im guessing their implementation predated the wasi k/v draft?
I'm not sure/don't know what actually happened but but that seems like a plausible explanation...
It looks like the wasmcloud implementation rolls its own approach with those wrpc interface files. I see some references to NATS in the wasmcloud site, so im guessing either the wasmcloud runtime implements the host function directly for import, or it just defined the tcp behavior to communicate to what ever protocol NATS is talking. My instinct is that wasmcloud is doing this second approach given it would allow a lot of flexibility without having to rework the runtime code to provide more interfaces from the host. If that second scenario is the case it falls into the same scenario for having to defer security to the mechanisms available in NATS, and doesn't provide a simple way to test it all without having the NATS running or some mock server.
Ah so you don't need the wrpc
interface files -- you can use WASI as normal and be able to connect. That said, you're right about NATS being a dependency of wasmCloud, we take your component, and run it in a host that uses NATS for communication, but you don't have to manage the NATS instance for small deployments -- wash up
will get you started, and wash start component file:///....
will start your component. But I'll avoid getting into the details here.
And yes, the point of wasmCloud providers is to be able to dynamically provide interfaces to components that need them! The providers themselves are written in languages like Rust or Go, and they export interfaces that components import.
Agreed that it's also not the easiest/simplest way to test your component -- If you're trying to test in a purely offline fashion I think the composition approach is probably the easiest! Building a component that mocks the interface you're trying to import and using it during the test is a great way to do it and test quickly.
Im trying to figure out how to use some wasi implementations with jco and why I cannot currently use the keyvalue wasi spec.
My understanding is as follows, jco
calls componentize-js
. This library then splices js code into the source based on the declared imports and exports from the source js. I think this step is providing compatible C api interfaces to be able to invoke the functions from the host.
Componentize js then invokes weval
which invokes wizer
which runs wasmtime to execute the starling monkey
wasm (which is the spider monkey javascript runtime compiled into web assembly). I think it provides the javascript source files by sharing a directly created in /tmp with the wasmtime runtime and the embedding code from the componentize spliced js, and.... something at this point? The levels of this stack are harder to follow in this. Somehow the final component includes the source js embedded and the starling monkey runtime, after some 'tree shaking' from weval+wizer ? :melting_face:
Im not sure at what point in this chain the missing bindings for the the wasi spec I want are. It appears at times ive found it but then I see some examples from wasmCloud which seems like the host imports are magically found and bound just by declaring them in the imports (which would make sense else you would have to re-declare the whole way up for every custom function given by the host.
While jco might fail the typings I can skip the type check like documented in the wasmcloud notes on keyvalue store. But how do I get the steps in between to tell me which imports are available to know where I differ from the expected host provided wasi interfaces?
Note that weval
is only used if AOT is on -- if it's it's not then the functionality is mostly the same (wizer
is called).
Basically, the StarlingMonkey interpreter is compiled to WebAssembly (not interactively of course!) and your JS code is loaded along with polyfills and generated code in order to form the "full" component.
... the final component includes the source js embedded and the starling monkey runtime, after some 'tree shaking' from weval+wizer ? :melt:
Correct!
Im not sure at what point in this chain the missing bindings for the the wasi spec I want are. It appears at times ive found it but then I see some examples from wasmCloud which seems like the host imports are magically found and bound just by declaring them in the imports (which would make sense else you would have to re-declare the whole way up for every custom function given by the host.
Yes, so the bindings are "magically" found -- they're automatically generated when you run jco componentize
and inserted. The imports are what glues the generated code to your JS code that acts as the component.
Note that typings are not required -- they are for TS integration (and to be honest, the're not even for components to use, the current jco types
command was meant for host side usage, not "guest" -- a new subcommand is being added soon), you can ignore them if it makes things simpler and write JS.
Generally, the imports that are available map to the import
s in your WIT. Unfortunately exactly how all of the WIT maps to JS is not well documented, but this is where generating TS typings can help (even if they're kind of "wrong" for now).
Do you have this in a public repo anywhere where we could help?
Ill commit the src later today. Thank you for the reply.
Here is the src im working from https://github.com/tylerhjones/wasm-lambda/blob/main/index.ts#L8
ive tried various imports with draft, draft2, sans draft, etc.
Hey so @Tylr yup, so here's what's happening that is actually quite confusing.
The current jco types
output is actually meant to be used for host-side integrations, which means at present it's got a bit of a weird setup -- the bindings that are generated are somewhat painful to use. You end up having to import the namespace all at once sometimes.
If you look at the generated types, what turns out to be a namespace there should actually be a regular ES module if you're using it from "guest" (component) code... So actually the import should look like this:
import { open } from "wasi:keyvalue/store@0.2.0-draft";
tsc
is going to complain, but this code would work in regular JS.
We actually just landed a PR to fix this and introduce a new guest-types
command, so more improvements are coming to this area (once a new jco
release is made you can use the new guest-types
command and then get more reasonable looking modules)... BUT if you want to use the current iteration w/ the generated types (which are TS namespace
s), you'd need code that looks like this I think:
import * as WasiKeyvalueStore from "wasi:keyvalue/store@0.2.0-draft";
Let me try it and make a PR to your project real quick
Ah I also just found another issue... the wasi:keyvalue
WIT that was in use was the wrong type actually -- it was based off of what is on main
right now (that's a link to a function that actually changed!) and we needed wasi:keyvalue
WIT at the release time.
I updated the README to use wkg wit fetch
which is much easier than fetching them all individually, should have shown that from the start!
I also filled out package.json
's serve
Once I got your component building (which I think it was before...?), and ran it with wasmtime
it executed properly though of course it couldn't connect to redis.
Here's a PR with everything opened against your repo:
https://github.com/tylerhjones/wasm-lambda/pull/2
Apologies for the bumpy ride here, thanks for continuing to tinker -- you weren't far from the goal there, and the ergonomics are going to improve for this stuff soon.
Thanks! Ill take another attempt with these changes today
It works! But I need more reading before any followup. I don't really understand the guest-host differences and their relation to these js modules/types enough to know how you reached the conclusion the import was the issue.
The closest hint I got was when I included the declared import in the bundling, the resulting js was invalid. But the types imports appear (to my limited js knowledge) the same. Both declare a namespace and both export types and a function from that namespace. So what makes it different for the incoming/outgoing request usages? Is it that 'fetch' bindings are setup special and avoided me having to declare the import to use outgoing-request and the incoming-request is exported so it didn't need to resolve any import from the host?
It works! But I need more reading before any followup. I don't really understand the guest-host differences and their relation to these js modules/types enough to know how you reached the conclusion the import was the issue.
Yeah this is more about the kind of emedding you're doing. Currently jco types
is for use in transpiled environments (integrating with NodeJS or the browser) -- not when writing a guest WebAssembly component.
I happen to know that jco types
needs that kind of weird wrapping because I've run into it before working on components for wasmCloud (and my colleague @Lachlan Heywood actually contributed the new guest-types
subcommand to jco
), so outside of iterating and/or tsc
is happy and the component actually works (though again, jco types
is the wrong thing!) -- it's not surprising.
So what makes it different for the incoming/outgoing request usages?
Here, the difference is how the toolchain (componentize-js
underneath, and StarlingMonkey underneath that) deals with export
ing functionality versus import
ing functionality.
Is it that 'fetch' bindings are setup special and avoided me having to declare the import to use outgoing-request and the incoming-request is exported so it didn't need to resolve any import from the host?
Yes, this understanding is exactly right, fetch
bindings are a special case -- for example right now if you build any JS component, you always get an import wasi:http/outgoing-handler
automatically IIRC, this is because you could fetch()
at any time.
gotcha, thanks for the explanations :bow:
Last updated: Dec 23 2024 at 13:07 UTC