Skip to content

Commit e34f133

Browse files
authored
various updates to support wasi:http@0.2.0-rc-2023-10-18 (#43)
* various updates to support `wasi:http@0.2.0-rc-2023-10-18` This fixes various issues: - Broken generated code indentation for resources in some cases - Type annotations that refer to non-yet-declared types confuse CPython, so we disable them - However, MyPy has no trouble with them, so we enable them by default for the `bindings` subcommand - Support WIT version annotations (i.e. pass them through to the generated component) - This partially addresses #19, but doesn't support importing or exporting multiple versions of the same interface - Update the `http` example to match `wasi:http@0.2.0-rc-2023-10-18` - Update to Wasmtime 14 and the latest `wit-parser`, `wit-component`, etc. - and update the WASI preview 1 adapter to match This also bumps the version to 0.6.0. Note that I've had to remove the `matrix-math` example since `wasmtime-py` does not yet support resources. Although the example itself doesn't use them, the new WASI Preview 1 adapter pulls them in as WASI Preview 2 imports, and there's no feasible way to work around that. Ideally, we'd provide the option to allow users to supply their own adapter, in which case we could use a pre-resource version of the adapter. However, that won't work given that pre-initialization is central to how `componentize-py` works. Hopefully we can bring back this example in the future, e.g. when `wasmtime-py` adds support for resources. Signed-off-by: Joel Dice <joel.dice@fermyon.com> bundle `poll_loop.py` to make it available during pre-init This module is useful enough that it makes sense to bundle it as part of `componentize-py`. Eventually, we may want to distribute it via PyPI as a helper library, but we'll settle for bundling for now. It shouldn't add any overhead for apps that don't `import` it. Signed-off-by: Joel Dice <joel.dice@fermyon.com> * update `http` example doc comments Signed-off-by: Joel Dice <joel.dice@fermyon.com> --------- Signed-off-by: Joel Dice <joel.dice@fermyon.com>
1 parent e023544 commit e34f133

72 files changed

Lines changed: 3221 additions & 1120 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 180 additions & 134 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "componentize-py"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
edition = "2021"
55
exclude = ["cpython"]
66

@@ -17,17 +17,15 @@ zstd = "0.11.1"
1717
componentize-py-shared = { path = "shared" }
1818
wasmparser = "0.107.0"
1919
wasm-encoder = "0.29.0"
20-
# TODO: switch to release once https://github.com/bytecodealliance/wasm-tools/pull/1226 is merged and released:
21-
wit-parser = { git = "https://github.com/dicej/wasm-tools", branch = "adapter-export-resources" }
22-
wit-component = { git = "https://github.com/dicej/wasm-tools", branch = "adapter-export-resources" }
20+
wit-parser = "0.12.2"
21+
wit-component = "0.17.0"
2322
indexmap = "2.0.0"
2423
bincode = "1.3.3"
2524
heck = "0.4.1"
2625
pyo3 = { version = "0.18.3", features = ["abi3-py37", "extension-module"], optional = true }
27-
# TODO: switch to Wasmtime 14 when released:
28-
wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", rev = "40c1f9b8b4f962ed763e47943e6ce0a3be8d1966" }
29-
wasi-common = { git = "https://github.com/bytecodealliance/wasmtime", rev = "40c1f9b8b4f962ed763e47943e6ce0a3be8d1966" }
30-
wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "40c1f9b8b4f962ed763e47943e6ce0a3be8d1966", features = [ "component-model" ] }
26+
wasmtime-wasi = "14.0.3"
27+
wasi-common = "14.0.3"
28+
wasmtime = { version = "14.0.3", features = [ "component-model" ] }
3129
once_cell = "1.17.1"
3230
component-init = { git = "https://github.com/dicej/component-init" }
3331
async-trait = "0.1.68"
-100 KB
Binary file not shown.

adapters/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The subdirectory of this directory contains a build of the WASI Preview 1
2+
component adapter. It was built from commit `e8766e49` of
3+
https://github.com/dicej/wasmtime using the
4+
`ci/build-wasi-preview1-component-adapter.sh` script.
5+
6+
TODO: Switch back to upstream once
7+
https://github.com/bytecodealliance/wasmtime/pull/7444 has been merged and
8+
released.
107 KB
Binary file not shown.

build.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ fn stubs_for_clippy(out_dir: &Path) -> Result<()> {
8080
.do_finish()?;
8181
}
8282

83+
let path = out_dir.join("bundled.tar.zst");
84+
85+
if !path.exists() {
86+
Builder::new(Encoder::new(File::create(path)?, ZSTD_COMPRESSION_LEVEL)?)
87+
.into_inner()?
88+
.do_finish()?;
89+
}
90+
8391
Ok(())
8492
}
8593

@@ -184,8 +192,24 @@ fn package_all_the_things(out_dir: &Path) -> Result<()> {
184192
} else {
185193
bail!("no such directory: {}", path.display())
186194
}
195+
196+
let path = repo_dir.join("bundled");
197+
198+
if path.exists() {
199+
let mut builder = Builder::new(Encoder::new(
200+
File::create(out_dir.join("bundled.tar.zst"))?,
201+
ZSTD_COMPRESSION_LEVEL,
202+
)?);
203+
204+
add(&mut builder, &path, &path)?;
205+
206+
builder.into_inner()?.do_finish()?;
207+
} else {
208+
bail!("no such directory: {}", path.display())
209+
}
210+
187211
compress(
188-
&repo_dir.join("adapters/40c1f9b8"),
212+
&repo_dir.join("adapters/e8766e49"),
189213
"wasi_snapshot_preview1.reactor.wasm",
190214
out_dir,
191215
false,
Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
"""Defines a custom `asyncio` event loop backed by WASI's `poll_oneoff`.
1+
"""Defines a custom `asyncio` event loop backed by `wasi:io/poll#poll-list`.
2+
3+
This also includes helper classes and functions for working with `wasi:http`.
24
35
As of WASI Preview 2, there is not yet a standard for first-class, composable
46
asynchronous functions and streams. We expect that little or none of this
@@ -9,68 +11,104 @@
911
import socket
1012
import subprocess
1113

12-
from proxy.imports import types2 as types, streams2 as streams, poll2 as poll
13-
from proxy.imports.streams2 import StreamStatus
14+
from proxy.types import Ok, Err
15+
from proxy.imports import types, streams, poll, outgoing_handler
16+
from proxy.imports.types import IncomingBody, OutgoingBody, OutgoingRequest, IncomingResponse
17+
from proxy.imports.streams import StreamErrorClosed, InputStream
18+
from proxy.imports.poll import Pollable
1419
from typing import Optional, cast
1520

1621
# Maximum number of bytes to read at a time
1722
READ_SIZE: int = 16 * 1024
1823

24+
async def send(request: OutgoingRequest) -> IncomingResponse:
25+
"""Send the specified request and wait asynchronously for the response."""
26+
27+
future = outgoing_handler.handle(request, None)
28+
29+
while True:
30+
response = future.get()
31+
if response is None:
32+
await register(cast(PollLoop, asyncio.get_event_loop()), future.subscribe())
33+
else:
34+
if isinstance(response, Ok):
35+
if isinstance(response.value, Ok):
36+
return response.value.value
37+
else:
38+
raise response.value
39+
else:
40+
raise response
41+
1942
class Stream:
20-
"""Reader abstraction over `wasi-cli`'s low-level stream pseudo-resource."""
21-
def __init__(self, stream: int):
22-
self.pollable = streams.subscribe_to_input_stream(stream)
23-
self.stream = stream
24-
self.saw_end = False
43+
"""Reader abstraction over `wasi:http/types#incoming-body`."""
44+
def __init__(self, body: IncomingBody):
45+
self.body: Optional[IncomingBody] = body
46+
self.stream: Optional[InputStream] = body.stream()
2547

2648
async def next(self) -> Optional[bytes]:
2749
"""Wait for the next chunk of data to arrive on the stream.
2850
2951
This will return `None` when the end of the stream has been reached.
3052
"""
31-
if self.saw_end:
32-
return None
33-
else:
34-
while True:
35-
buffer, status = streams.read(self.stream, READ_SIZE)
36-
if status == StreamStatus.ENDED:
37-
types.finish_incoming_stream(self.stream)
38-
self.saw_end = True
39-
40-
if buffer:
41-
return buffer
42-
elif status == StreamStatus.ENDED:
53+
while True:
54+
try:
55+
if self.stream is None:
4356
return None
4457
else:
45-
await register(cast(PollLoop, asyncio.get_event_loop()), self.pollable)
58+
buffer = self.stream.read(READ_SIZE)
59+
if len(buffer) == 0:
60+
await register(cast(PollLoop, asyncio.get_event_loop()), self.stream.subscribe())
61+
else:
62+
return buffer
63+
except Err as e:
64+
if isinstance(e.value, StreamErrorClosed):
65+
if self.stream is not None:
66+
self.stream.drop()
67+
self.stream = None
68+
if self.body is not None:
69+
IncomingBody.finish(self.body)
70+
self.body = None
71+
else:
72+
raise e
4673

4774
class Sink:
48-
"""Writer abstraction over `wasi-cli`'s low-level stream pseudo-resource."""
49-
def __init__(self, stream: int):
50-
self.pollable = streams.subscribe_to_output_stream(stream)
51-
self.stream = stream
75+
"""Writer abstraction over `wasi-http/types#outgoing-body`."""
76+
def __init__(self, body: OutgoingBody):
77+
self.body = body
78+
self.stream = body.write()
5279

5380
async def send(self, chunk: bytes):
54-
"""Write the specified bytes to the stream.
81+
"""Write the specified bytes to the sink.
5582
56-
This may need to yield according to the backpressure requirements of the stream.
83+
This may need to yield according to the backpressure requirements of the sink.
5784
"""
5885
offset = 0
86+
flushing = False
5987
while True:
60-
count = streams.write(self.stream, chunk[offset:])
61-
offset += count
62-
if offset == len(chunk):
63-
return
88+
count = self.stream.check_write()
89+
if count == 0:
90+
await register(cast(PollLoop, asyncio.get_event_loop()), self.stream.subscribe())
91+
elif offset == len(chunk):
92+
if flushing:
93+
return
94+
else:
95+
self.stream.flush()
96+
flushing = True
6497
else:
65-
await register(cast(PollLoop, asyncio.get_event_loop()), self.pollable)
98+
count = min(count, len(chunk) - offset)
99+
self.stream.write(chunk[offset:offset+count])
100+
offset += count
66101

67102
def close(self):
68103
"""Close the stream, indicating no further data will be written."""
69-
70-
types.finish_outgoing_stream(self.stream)
104+
105+
self.stream.drop()
106+
self.stream = None
107+
OutgoingBody.finish(self.body, None)
108+
self.body = None
71109

72110
class PollLoop(asyncio.AbstractEventLoop):
73-
"""Custom `asyncio` event loop backed by WASI's `poll_oneoff` function."""
111+
"""Custom `asyncio` event loop backed by `wasi:io/poll#poll-list`."""
74112

75113
def __init__(self):
76114
self.wakers = []
@@ -96,8 +134,13 @@ def run_until_complete(self, future):
96134
[pollables, wakers] = list(map(list, zip(*self.wakers)))
97135

98136
new_wakers = []
99-
for (ready, pollable), waker in zip(zip(poll.poll_oneoff(pollables), pollables), wakers):
137+
ready = [False] * len(pollables)
138+
for index in poll.poll_list(pollables):
139+
ready[index] = True
140+
141+
for (ready, pollable), waker in zip(zip(ready, pollables), wakers):
100142
if ready:
143+
pollable.drop()
101144
waker.set_result(None)
102145
else:
103146
new_wakers.append((pollable, waker))
@@ -319,7 +362,7 @@ def default_exception_handler(self, context):
319362
def set_debug(self, enabled):
320363
raise NotImplementedError
321364

322-
async def register(loop: PollLoop, pollable: int):
365+
async def register(loop: PollLoop, pollable: Pollable):
323366
waker = loop.create_future()
324367
loop.wakers.append((pollable, waker))
325368
await waker

examples/http/README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
# Example: `http`
22

3-
This is an example of how to use [componentize-py] and [Spin] to build and run a
4-
Python-based component targetting the [wasi-http] `proxy` world.
3+
This is an example of how to use [componentize-py] and [Wasmtime] to build and
4+
run a Python-based component targetting the [wasi-http] `proxy` world.
55

66
Note that, as of this writing, neither `wasi-http` nor the portions of
77
`wasi-cli` on which it is based have stabilized. Here we use a snapshot of both,
88
which may differ from later revisions.
99

1010
[componentize-py]: https://github.com/bytecodealliance/componentize-py
11-
[Spin]: https://github.com/fermyon/spin
11+
[Wasmtime]: https://github.com/bytecodealliance/wasmtime
1212
[wasi-http]: https://github.com/WebAssembly/wasi-http
1313

1414
## Prerequisites
1515

16-
* `dicej/spin` branch `wasi-http-wasmtime-2ad057d7`
17-
* `componentize-py` 0.5.0
18-
* `Rust`, for installing `Spin`
16+
* `Wasmtime` 14.0.3 (later versions may use a different, incompatible `wasi-http` snapshot)
17+
* `componentize-py` 0.6.0
18+
19+
Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If
20+
you don't have `cargo`, you can download and install from
21+
https://github.com/bytecodealliance/wasmtime/releases/tag/v14.0.3.
1922

2023
```
21-
cargo install --locked --git https://github.com/dicej/spin --branch wasi-http-wasmtime-2ad057d7 spin-cli
24+
cargo install --version 14.0.3 wasmtime-cli
2225
pip install componentize-py
2326
```
2427

@@ -27,13 +30,14 @@ pip install componentize-py
2730
First, build the app and run it:
2831

2932
```
30-
spin build --up
33+
componentize-py -d wit -w proxy componentize app -o http.wasm
34+
wasmtime serve http.wasm
3135
```
3236

3337
Then, in another terminal, use cURL to send a request to the app:
3438

3539
```
36-
curl -i -H 'content-type: text/plain' --data-binary @- http://127.0.0.1:3000/echo <<EOF
40+
curl -i -H 'content-type: text/plain' --data-binary @- http://127.0.0.1:8080/echo <<EOF
3741
’Twas brillig, and the slithy toves
3842
Did gyre and gimble in the wabe:
3943
All mimsy were the borogoves,
@@ -52,7 +56,7 @@ curl -i \
5256
-H 'url: https://webassembly.github.io/spec/core/' \
5357
-H 'url: https://www.w3.org/groups/wg/wasm/' \
5458
-H 'url: https://bytecodealliance.org/' \
55-
http://127.0.0.1:3000/hash-all
59+
http://127.0.0.1:8080/hash-all
5660
```
5761

5862
If you run into any problems, please file an issue!

0 commit comments

Comments
 (0)