Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions examples/tcp-p3/README.md
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 80 additions & 0 deletions examples/tcp-p3/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 <address>:<port>", 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:
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)}")
send_tx.__exit__(None, None, None)
await asyncio.gather(recv_fut.read(), read(), sock.send(send_rx), write())
19 changes: 19 additions & 0 deletions tests/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Assert, anyhow::Error> {
Ok(cargo::cargo_bin_cmd!("componentize-py")
.current_dir(path)
Expand Down
25 changes: 18 additions & 7 deletions tests/componentize.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use core::net::Ipv4Addr;
use std::{
io::Write,
path::{Path, PathBuf},
Expand Down Expand Up @@ -232,21 +233,30 @@ 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)
.args([
"-d",
"../wit",
"-w",
"wasi:cli/command@0.2.0",
world,
"componentize",
"app",
"-o",
Expand All @@ -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()?;
Expand Down