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