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()?;