Stream: jco

Topic: understanding WASI/Storage options with JCO


view this post on Zulip Tylr (Dec 09 2024 at 22:19):

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?

JS -> WebAssembly Component. Contribute to bytecodealliance/ComponentizeJS development by creating an account on GitHub.
JS -> WebAssembly Component. Contribute to bytecodealliance/ComponentizeJS development by creating an account on GitHub.

view this post on Zulip Victor Adossi (Dec 10 2024 at 01:57):

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.

A lightweight WebAssembly runtime that is fast, secure, and standards-compliant - bytecodealliance/wasmtime

view this post on Zulip Tylr (Dec 10 2024 at 18:14):

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.

Contribute to WebAssembly/wasi-keyvalue development by creating an account on GitHub.
A lightweight WebAssembly runtime that is fast, secure, and standards-compliant - bytecodealliance/wasmtime

view this post on Zulip Tylr (Dec 10 2024 at 18:15):

Been reading all I can find before typing out a reply. Sorry slow response :sweat_smile:

view this post on Zulip Tylr (Dec 10 2024 at 18:56):

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.

JavaScript toolchain for working with WebAssembly Components - bytecodealliance/jco

view this post on Zulip Victor Adossi (Dec 11 2024 at 01:39):

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!

view this post on Zulip Victor Adossi (Dec 11 2024 at 01:44):

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 upwill 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.

view this post on Zulip Tylr (Dec 12 2024 at 18:15):

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?

JavaScript toolchain for working with WebAssembly Components - bytecodealliance/jco

view this post on Zulip Victor Adossi (Dec 16 2024 at 15:29):

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 imports 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?

view this post on Zulip Tylr (Dec 16 2024 at 18:13):

Ill commit the src later today. Thank you for the reply.

view this post on Zulip Tylr (Dec 17 2024 at 02:39):

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.

Contribute to tylerhjones/wasm-lambda development by creating an account on GitHub.

view this post on Zulip Victor Adossi (Dec 17 2024 at 15:25):

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 namespaces), you'd need code that looks like this I think:

import * as WasiKeyvalueStore from "wasi:keyvalue/store@0.2.0-draft";
wasmCloud is an open source Cloud Native Computing Foundation (CNCF) project that enables teams to build, manage, and scale polyglot apps across any cloud, K8s, or edge. - wasmCloud/wasmCloud
Contribute to tylerhjones/wasm-lambda development by creating an account on GitHub.
Adds a --declare-imports option to the jco types command. Closes #439

view this post on Zulip Victor Adossi (Dec 17 2024 at 15:25):

Let me try it and make a PR to your project real quick

view this post on Zulip Victor Adossi (Dec 17 2024 at 15:53):

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.

Contribute to WebAssembly/wasi-keyvalue development by creating an account on GitHub.
We read every piece of feedback, and take your input very seriously.

view this post on Zulip Tylr (Dec 17 2024 at 15:58):

Thanks! Ill take another attempt with these changes today

view this post on Zulip Tylr (Dec 18 2024 at 00:05):

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?

view this post on Zulip Victor Adossi (Dec 18 2024 at 05:10):

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 exporting functionality versus importing 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.

view this post on Zulip Tylr (Dec 18 2024 at 16:45):

gotcha, thanks for the explanations :bow:


Last updated: Dec 23 2024 at 13:07 UTC