Skip to content
Open
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.
Comment on lines +43 to +45
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not actually happen, but I'm assuming that's a temporary bug

81 changes: 81 additions & 0 deletions examples/tcp-p3/app.py
Original file line number Diff line number Diff line change
@@ -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 <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:
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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By some reason, this does not block on empty buffers - I assumed that this would only ever return a non-zero buffer or an error.

For example, if nc is invoked like so:

echo test | nc -l 127.0.0.1 3456

The component receives test.

There is no way to send data to the component via nc after the component has connected.

I am assuming this is a bug in componentize-py?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the spec (https://github.com/WebAssembly/component-model/blob/c7176a512c0bbe4654849f4ba221c1a71c7cf514/design/mvp/Concurrency.md#stream-readiness):

When passed a non-zero-length buffer, the stream.read and stream.write built-ins are "completion-based" (in the style of, e.g., Overlapped I/O or io_uring) in that they complete only once one or more values have been copied to or from the memory buffer passed in at the start of the operation.

print(f"received: {str(data)}")
await asyncio.gather(recv_fut.read(), read())
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