Skip to content

Commit df7c6cc

Browse files
rvolosatovsdicej
andauthored
add tcp-p3 example and test (#204)
* add `tcp-p3` example and test Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * keep send side open while receiving in `tcp-p3` example Netcat likes to close the connection if the receive half is closed by the remote host, regardless of whether stdin has been closed, and there's no obvious way to change that behavior. So on the client side we hold both halves open until we've received something, which matches the p2 example behavior. --------- Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> Co-authored-by: Joel Dice <joel.dice@akamai.com>
1 parent b3adeb7 commit df7c6cc

File tree

5 files changed

+163
-7
lines changed

5 files changed

+163
-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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
await send_tx.write_all(b"hello, world!")
73+
74+
recv_rx, recv_fut = sock.receive()
75+
async def read() -> None:
76+
with recv_rx:
77+
data = await recv_rx.read(1024)
78+
print(f"received: {str(data)}")
79+
send_tx.__exit__(None, None, None)
80+
await asyncio.gather(recv_fut.read(), read(), sock.send(send_rx), write())

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)