From 6a20a4008d730c12ffc55155b6f6d07b8110e63e Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Wed, 18 Feb 2026 14:23:33 +0100 Subject: [PATCH 1/2] add `tcp-p3` example and test Signed-off-by: Roman Volosatovs --- .gitignore | 1 + examples/tcp-p3/README.md | 45 ++++++++++++++++++++++ examples/tcp-p3/app.py | 81 +++++++++++++++++++++++++++++++++++++++ tests/bindings.rs | 19 +++++++++ tests/componentize.rs | 25 ++++++++---- 5 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 examples/tcp-p3/README.md create mode 100644 examples/tcp-p3/app.py diff --git a/.gitignore b/.gitignore index d59c72e..e440618 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ examples/http/.spin examples/http/http.wasm examples/http/proxy examples/http/poll_loop.py +examples/tcp-p3/tcp.wasm examples/tcp/tcp.wasm examples/tcp/command examples/cli/cli.wasm diff --git a/examples/tcp-p3/README.md b/examples/tcp-p3/README.md new file mode 100644 index 0000000..3e744b3 --- /dev/null +++ b/examples/tcp-p3/README.md @@ -0,0 +1,45 @@ +# Example: `tcp-p3` + +This is an example of how to use [componentize-py] and [Wasmtime] to build and +run a Python-based component targetting version `0.3.0-rc-2026-01-06` of the +[wasi-cli] `command` world and making an outbound TCP request using [wasi-sockets]. + +[componentize-py]: https://github.com/bytecodealliance/componentize-py +[Wasmtime]: https://github.com/bytecodealliance/wasmtime +[wasi-cli]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-01-06/proposals/cli/wit-0.3.0-draft +[wasi-sockets]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-01-06/proposals/sockets/wit-0.3.0-draft + +## Prerequisites + +* `Wasmtime` 41.0.3 +* `componentize-py` 0.21.0 + +Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If +you don't have `cargo`, you can download and install from +https://github.com/bytecodealliance/wasmtime/releases/tag/v41.0.3. + +``` +cargo install --version 41.0.3 wasmtime-cli +pip install componentize-py==0.21.0 +``` + +## Running the demo + +First, in a separate terminal, run `netcat`, telling it to listen for incoming +TCP connections. You can choose any port you like. + +``` +nc -l 127.0.0.1 3456 +``` + +Now, build and run the example, using the same port you gave to `netcat`. + +``` +componentize-py -d ../../wit -w wasi:cli/command@0.3.0-rc-2026-01-06 componentize app -o tcp.wasm +wasmtime run -Sp3 -Sinherit-network -Wcomponent-model-async tcp.wasm 127.0.0.1:3456 +``` + +The program will open a TCP connection, send a message, and wait to receive a +response before exiting. You can give it a response by typing anything you like +into the terminal where `netcat` is running and then pressing the `Enter` key on +your keyboard. diff --git a/examples/tcp-p3/app.py b/examples/tcp-p3/app.py new file mode 100644 index 0000000..33e74c9 --- /dev/null +++ b/examples/tcp-p3/app.py @@ -0,0 +1,81 @@ +import sys +import asyncio +import ipaddress +from ipaddress import IPv4Address, IPv6Address +import wit_world +from wit_world import exports +from wit_world.imports.wasi_sockets_types import ( + TcpSocket, + IpSocketAddress_Ipv4, + IpSocketAddress_Ipv6, + Ipv4SocketAddress, + Ipv6SocketAddress, + IpAddressFamily, +) +from typing import Tuple + + +IPAddress = IPv4Address | IPv6Address + +class Run(exports.Run): + async def run(self) -> None: + args = sys.argv[1:] + if len(args) != 1: + print("usage: tcp-p3
:", file=sys.stderr) + exit(-1) + + address, port = parse_address_and_port(args[0]) + await send_and_receive(address, port) + + +def parse_address_and_port(address_and_port: str) -> Tuple[IPAddress, int]: + ip, separator, port = address_and_port.rpartition(":") + assert separator + return (ipaddress.ip_address(ip.strip("[]")), int(port)) + + +def make_socket_address(address: IPAddress, port: int) -> IpSocketAddress_Ipv4 | IpSocketAddress_Ipv6: + if isinstance(address, IPv4Address): + octets = address.packed + return IpSocketAddress_Ipv4(Ipv4SocketAddress( + port=port, + address=(octets[0], octets[1], octets[2], octets[3]), + )) + else: + b = address.packed + return IpSocketAddress_Ipv6(Ipv6SocketAddress( + port=port, + flow_info=0, + address=( + (b[0] << 8) | b[1], + (b[2] << 8) | b[3], + (b[4] << 8) | b[5], + (b[6] << 8) | b[7], + (b[8] << 8) | b[9], + (b[10] << 8) | b[11], + (b[12] << 8) | b[13], + (b[14] << 8) | b[15], + ), + scope_id=0, + )) + + +async def send_and_receive(address: IPAddress, port: int) -> None: + family = IpAddressFamily.IPV4 if isinstance(address, IPv4Address) else IpAddressFamily.IPV6 + + sock = TcpSocket.create(family) + + await sock.connect(make_socket_address(address, port)) + + send_tx, send_rx = wit_world.byte_stream() + async def write() -> None: + with send_tx: + await send_tx.write_all(b"hello, world!") + await asyncio.gather(sock.send(send_rx), write()) + + recv_rx, recv_fut = sock.receive() + async def read() -> None: + with recv_rx: + data = await recv_rx.read(1024) + print(f"received: {str(data)}") + await asyncio.gather(recv_fut.read(), read()) diff --git a/tests/bindings.rs b/tests/bindings.rs index 2b583d8..6631296 100644 --- a/tests/bindings.rs +++ b/tests/bindings.rs @@ -150,6 +150,25 @@ fn lint_tcp_bindings() -> anyhow::Result<()> { Ok(()) } +#[test] +fn lint_tcp_p3_bindings() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/tcp-p3", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("tcp-p3"); + + generate_bindings(&path, "wasi:cli/command@0.3.0-rc-2026-01-06")?; + + assert!(predicate::path::is_dir().eval(&path.join("wit_world"))); + + mypy_check(&path, ["--strict", "-m", "app"]); + + Ok(()) +} + fn generate_bindings(path: &Path, world: &str) -> Result { Ok(cargo::cargo_bin_cmd!("componentize-py") .current_dir(path) diff --git a/tests/componentize.rs b/tests/componentize.rs index e1fa63c..e387b39 100644 --- a/tests/componentize.rs +++ b/tests/componentize.rs @@ -1,3 +1,4 @@ +use core::net::Ipv4Addr; use std::{ io::Write, path::{Path, PathBuf}, @@ -232,13 +233,22 @@ fn sandbox_example() -> anyhow::Result<()> { #[test] fn tcp_example() -> anyhow::Result<()> { + test_tcp_example("tcp", "wasi:cli/command@0.2.0") +} + +#[test] +fn tcp_p3_example() -> anyhow::Result<()> { + test_tcp_example("tcp-p3", "wasi:cli/command@0.3.0-rc-2026-01-06") +} + +fn test_tcp_example(name: &str, world: &str) -> anyhow::Result<()> { let dir = tempfile::tempdir()?; fs_extra::copy_items( - &["./examples/tcp", "./wit"], + &[format!("./examples/{name}").as_str(), "./wit"], dir.path(), &CopyOptions::new(), )?; - let path = dir.path().join("tcp"); + let path = dir.path().join(name); cargo::cargo_bin_cmd!("componentize-py") .current_dir(&path) @@ -246,7 +256,7 @@ fn tcp_example() -> anyhow::Result<()> { "-d", "../wit", "-w", - "wasi:cli/command@0.2.0", + world, "componentize", "app", "-o", @@ -256,16 +266,17 @@ fn tcp_example() -> anyhow::Result<()> { .success() .stdout("Component built successfully\n"); - let listener = std::net::TcpListener::bind("127.0.0.1:3456")?; + let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?; + let port = listener.local_addr()?.port(); let tcp_handle = std::process::Command::new("wasmtime") .current_dir(&path) .args([ "run", - "--wasi", - "inherit-network", + "-Sp3,inherit-network", + "-Wcomponent-model-async", "tcp.wasm", - "127.0.0.1:3456", + &format!("127.0.0.1:{port}"), ]) .stdout(Stdio::piped()) .spawn()?; From 1a13fc9d3cf3753ed41a716a1ad46da462b3b44d Mon Sep 17 00:00:00 2001 From: Joel Dice Date: Mon, 23 Feb 2026 13:47:40 -0700 Subject: [PATCH 2/2] 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. --- examples/tcp-p3/app.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/tcp-p3/app.py b/examples/tcp-p3/app.py index 33e74c9..35410eb 100644 --- a/examples/tcp-p3/app.py +++ b/examples/tcp-p3/app.py @@ -69,13 +69,12 @@ async def send_and_receive(address: IPAddress, port: int) -> None: send_tx, send_rx = wit_world.byte_stream() async def write() -> None: - with send_tx: - await send_tx.write_all(b"hello, world!") - await asyncio.gather(sock.send(send_rx), write()) + await send_tx.write_all(b"hello, world!") recv_rx, recv_fut = sock.receive() async def read() -> None: with recv_rx: data = await recv_rx.read(1024) print(f"received: {str(data)}") - await asyncio.gather(recv_fut.read(), read()) + send_tx.__exit__(None, None, None) + await asyncio.gather(recv_fut.read(), read(), sock.send(send_rx), write())