Skip to content

Commit 1670783

Browse files
committed
add tcp-p3 example and test
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
1 parent 07d0afb commit 1670783

File tree

5 files changed

+164
-7
lines changed

5 files changed

+164
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ examples/http/.spin
1212
examples/http/http.wasm
1313
examples/http/proxy
1414
examples/http/poll_loop.py
15+
examples/tcp-p3/tcp.wasm
1516
examples/tcp/tcp.wasm
1617
examples/tcp/command
1718
examples/cli/cli.wasm

examples/tcp-p3/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Example: `tcp-p3`
2+
3+
This is an example of how to use [componentize-py] and [Wasmtime] to build and
4+
run a Python-based component targetting version `0.3.0-rc-2026-01-06` of the
5+
[wasi-cli] `command` world and making an outbound TCP request using [wasi-sockets].
6+
7+
[componentize-py]: https://github.com/bytecodealliance/componentize-py
8+
[Wasmtime]: https://github.com/bytecodealliance/wasmtime
9+
[wasi-cli]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-01-06/proposals/cli/wit-0.3.0-draft
10+
[wasi-sockets]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-01-06/proposals/sockets/wit-0.3.0-draft
11+
12+
## Prerequisites
13+
14+
* `Wasmtime` 41.0.3
15+
* `componentize-py` 0.21.0
16+
17+
Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If
18+
you don't have `cargo`, you can download and install from
19+
https://github.com/bytecodealliance/wasmtime/releases/tag/v41.0.3.
20+
21+
```
22+
cargo install --version 41.0.3 wasmtime-cli
23+
pip install componentize-py==0.21.0
24+
```
25+
26+
## Running the demo
27+
28+
First, in a separate terminal, run `netcat`, telling it to listen for incoming
29+
TCP connections. You can choose any port you like.
30+
31+
```
32+
nc -l 127.0.0.1 3456
33+
```
34+
35+
Now, build and run the example, using the same port you gave to `netcat`.
36+
37+
```
38+
componentize-py -d ../../wit -w wasi:cli/command@0.3.0-rc-2026-01-06 componentize app -o tcp.wasm
39+
wasmtime run -Sp3 -Sinherit-network -Wcomponent-model-async tcp.wasm 127.0.0.1:3456
40+
```
41+
42+
The program will open a TCP connection, send a message, and wait to receive a
43+
response before exiting. You can give it a response by typing anything you like
44+
into the terminal where `netcat` is running and then pressing the `Enter` key on
45+
your keyboard.

examples/tcp-p3/app.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import sys
2+
import asyncio
3+
import ipaddress
4+
from ipaddress import IPv4Address, IPv6Address
5+
import wit_world
6+
from wit_world import exports
7+
from wit_world.imports.wasi_sockets_types import (
8+
TcpSocket,
9+
IpSocketAddress_Ipv4,
10+
IpSocketAddress_Ipv6,
11+
Ipv4SocketAddress,
12+
Ipv6SocketAddress,
13+
IpAddressFamily,
14+
)
15+
from typing import Tuple
16+
17+
18+
IPAddress = IPv4Address | IPv6Address
19+
20+
class Run(exports.Run):
21+
async def run(self) -> None:
22+
args = sys.argv[1:]
23+
if len(args) != 1:
24+
print("usage: tcp-p3 <address>:<port>", file=sys.stderr)
25+
exit(-1)
26+
27+
address, port = parse_address_and_port(args[0])
28+
await send_and_receive(address, port)
29+
30+
31+
def parse_address_and_port(address_and_port: str) -> Tuple[IPAddress, int]:
32+
ip, separator, port = address_and_port.rpartition(":")
33+
assert separator
34+
return (ipaddress.ip_address(ip.strip("[]")), int(port))
35+
36+
37+
def make_socket_address(address: IPAddress, port: int) -> IpSocketAddress_Ipv4 | IpSocketAddress_Ipv6:
38+
if isinstance(address, IPv4Address):
39+
octets = address.packed
40+
return IpSocketAddress_Ipv4(Ipv4SocketAddress(
41+
port=port,
42+
address=(octets[0], octets[1], octets[2], octets[3]),
43+
))
44+
else:
45+
b = address.packed
46+
return IpSocketAddress_Ipv6(Ipv6SocketAddress(
47+
port=port,
48+
flow_info=0,
49+
address=(
50+
(b[0] << 8) | b[1],
51+
(b[2] << 8) | b[3],
52+
(b[4] << 8) | b[5],
53+
(b[6] << 8) | b[7],
54+
(b[8] << 8) | b[9],
55+
(b[10] << 8) | b[11],
56+
(b[12] << 8) | b[13],
57+
(b[14] << 8) | b[15],
58+
),
59+
scope_id=0,
60+
))
61+
62+
63+
async def send_and_receive(address: IPAddress, port: int) -> None:
64+
family = IpAddressFamily.IPV4 if isinstance(address, IPv4Address) else IpAddressFamily.IPV6
65+
66+
sock = TcpSocket.create(family)
67+
68+
await sock.connect(make_socket_address(address, port))
69+
70+
send_tx, send_rx = wit_world.byte_stream()
71+
async def write() -> None:
72+
with send_tx:
73+
await send_tx.write_all(b"hello, world!")
74+
await asyncio.gather(sock.send(send_rx), write())
75+
76+
recv_rx, recv_fut = sock.receive()
77+
async def read() -> None:
78+
with recv_rx:
79+
data = await recv_rx.read(1024)
80+
print(f"received: {str(data)}")
81+
await asyncio.gather(recv_fut.read(), read())

tests/bindings.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,25 @@ fn lint_tcp_bindings() -> anyhow::Result<()> {
150150
Ok(())
151151
}
152152

153+
#[test]
154+
fn lint_tcp_p3_bindings() -> anyhow::Result<()> {
155+
let dir = tempfile::tempdir()?;
156+
fs_extra::copy_items(
157+
&["./examples/tcp-p3", "./wit"],
158+
dir.path(),
159+
&CopyOptions::new(),
160+
)?;
161+
let path = dir.path().join("tcp-p3");
162+
163+
generate_bindings(&path, "wasi:cli/command@0.3.0-rc-2026-01-06")?;
164+
165+
assert!(predicate::path::is_dir().eval(&path.join("wit_world")));
166+
167+
mypy_check(&path, ["--strict", "-m", "app"]);
168+
169+
Ok(())
170+
}
171+
153172
fn generate_bindings(path: &Path, world: &str) -> Result<Assert, anyhow::Error> {
154173
Ok(cargo::cargo_bin_cmd!("componentize-py")
155174
.current_dir(path)

tests/componentize.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::net::Ipv4Addr;
12
use std::{
23
io::Write,
34
path::{Path, PathBuf},
@@ -232,21 +233,30 @@ fn sandbox_example() -> anyhow::Result<()> {
232233

233234
#[test]
234235
fn tcp_example() -> anyhow::Result<()> {
236+
test_tcp_example("tcp", "wasi:cli/command@0.2.0")
237+
}
238+
239+
#[test]
240+
fn tcp_p3_example() -> anyhow::Result<()> {
241+
test_tcp_example("tcp-p3", "wasi:cli/command@0.3.0-rc-2026-01-06")
242+
}
243+
244+
fn test_tcp_example(name: &str, world: &str) -> anyhow::Result<()> {
235245
let dir = tempfile::tempdir()?;
236246
fs_extra::copy_items(
237-
&["./examples/tcp", "./wit"],
247+
&[format!("./examples/{name}").as_str(), "./wit"],
238248
dir.path(),
239249
&CopyOptions::new(),
240250
)?;
241-
let path = dir.path().join("tcp");
251+
let path = dir.path().join(name);
242252

243253
cargo::cargo_bin_cmd!("componentize-py")
244254
.current_dir(&path)
245255
.args([
246256
"-d",
247257
"../wit",
248258
"-w",
249-
"wasi:cli/command@0.2.0",
259+
world,
250260
"componentize",
251261
"app",
252262
"-o",
@@ -256,16 +266,17 @@ fn tcp_example() -> anyhow::Result<()> {
256266
.success()
257267
.stdout("Component built successfully\n");
258268

259-
let listener = std::net::TcpListener::bind("127.0.0.1:3456")?;
269+
let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
270+
let port = listener.local_addr()?.port();
260271

261272
let tcp_handle = std::process::Command::new("wasmtime")
262273
.current_dir(&path)
263274
.args([
264275
"run",
265-
"--wasi",
266-
"inherit-network",
276+
"-Sp3,inherit-network",
277+
"-Wcomponent-model-async",
267278
"tcp.wasm",
268-
"127.0.0.1:3456",
279+
&format!("127.0.0.1:{port}"),
269280
])
270281
.stdout(Stdio::piped())
271282
.spawn()?;

0 commit comments

Comments
 (0)