Skip to content

Commit bceb577

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

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-p3/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
@@ -171,6 +171,25 @@ fn lint_tcp_bindings() -> anyhow::Result<()> {
171171
Ok(())
172172
}
173173

174+
#[test]
175+
fn lint_tcp_p3_bindings() -> anyhow::Result<()> {
176+
let dir = tempfile::tempdir()?;
177+
fs_extra::copy_items(
178+
&["./examples/tcp-p3", "./wit"],
179+
dir.path(),
180+
&CopyOptions::new(),
181+
)?;
182+
let path = dir.path().join("tcp-p3");
183+
184+
generate_bindings(&path, "wasi:cli/command@0.3.0-rc-2026-01-06")?;
185+
186+
assert!(predicate::path::is_dir().eval(&path.join("wit_world")));
187+
188+
mypy_check(&path, ["--strict", "-m", "app"]);
189+
190+
Ok(())
191+
}
192+
174193
fn generate_bindings(path: &Path, world: &str) -> Result<Assert, anyhow::Error> {
175194
Ok(cargo::cargo_bin_cmd!("componentize-py")
176195
.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},
@@ -241,21 +242,30 @@ fn sandbox_example() -> anyhow::Result<()> {
241242

242243
#[test]
243244
fn tcp_example() -> anyhow::Result<()> {
245+
test_tcp_example("tcp", "wasi:cli/command@0.2.0")
246+
}
247+
248+
#[test]
249+
fn tcp_p3_example() -> anyhow::Result<()> {
250+
test_tcp_example("tcp-p3", "wasi:cli/command@0.3.0-rc-2026-01-06")
251+
}
252+
253+
fn test_tcp_example(name: &str, world: &str) -> anyhow::Result<()> {
244254
let dir = tempfile::tempdir()?;
245255
fs_extra::copy_items(
246-
&["./examples/tcp", "./wit"],
256+
&[format!("./examples/{name}").as_str(), "./wit"],
247257
dir.path(),
248258
&CopyOptions::new(),
249259
)?;
250-
let path = dir.path().join("tcp");
260+
let path = dir.path().join(name);
251261

252262
cargo::cargo_bin_cmd!("componentize-py")
253263
.current_dir(&path)
254264
.args([
255265
"-d",
256266
"../wit",
257267
"-w",
258-
"wasi:cli/command@0.2.0",
268+
world,
259269
"componentize",
260270
"app",
261271
"-o",
@@ -265,16 +275,17 @@ fn tcp_example() -> anyhow::Result<()> {
265275
.success()
266276
.stdout("Component built successfully\n");
267277

268-
let listener = std::net::TcpListener::bind("127.0.0.1:3456")?;
278+
let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
279+
let port = listener.local_addr()?.port();
269280

270281
let tcp_handle = std::process::Command::new("wasmtime")
271282
.current_dir(&path)
272283
.args([
273284
"run",
274-
"--wasi",
275-
"inherit-network",
285+
"-Sp3,inherit-network",
286+
"-Wcomponent-model-async",
276287
"tcp.wasm",
277-
"127.0.0.1:3456",
288+
&format!("127.0.0.1:{port}"),
278289
])
279290
.stdout(Stdio::piped())
280291
.spawn()?;

0 commit comments

Comments
 (0)