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
2 changes: 1 addition & 1 deletion docs/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ Every file should start with a brief description:
# This module provides functions for making HTTP requests with support
# for keep-alive connections, retries, and custom headers.

from std.net.tcp import connect, read, write, close
from std.net.tcp import connect, read, write_all, close
```

### Import Organization
Expand Down
29 changes: 29 additions & 0 deletions docs/io_foundation.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ the io layer now includes:
`std.fs` now exposes stream-based `open`, `create`, and `open_append` on the
same foundation.

## write is one syscall, write_all is the loop

this is the one distinction in the layer worth learning before you use it.

`write` and `write_bytes` are a single `write(2)`. a socket has a bounded send
buffer, so a buffer larger than the space left in it is written *in part*: the
kernel takes what fits and returns that count, and the rest is not queued
anywhere. a short write is normal, not an error — but discarding the returned
count drops the tail silently, and the peer then waits for bytes that were never
written. a 1 MiB redis `SET` used to work and a 4 MiB one used to hang for
exactly this reason.

`write_all` and `write_all_bytes` are the looping form: they resume from where
the last write stopped and only stop early when a write accepts nothing at all,
which means the reader is gone. use them unless you have a specific reason to
handle the count yourself. the same pair exists at every level:

- fd level: `std.net.tcp`'s `write` / `write_all` / `write_all_bytes`
- stream level: `TcpStream`, `FileStream`, `ProcessStdin`
- tls: `Conn.write_bytes` is capped at one 16 KiB record, so it too is a partial
write by construction; `Conn.write_all_bytes` is the loop
- the buffered writers flush through `write_all`, so they are already correct

one subtlety the loop has to get right: the resume runs on **bytes**, never on
text. a send buffer fills at whatever byte offset it fills at, and that offset
can be in the middle of a multi-byte character. a `String` cannot be cut there —
slicing one at a non-boundary offset stops the process — so the text `write_all`
helpers encode once and resume through their bytes counterpart.

## why the adapters are handle-backed

pith structs are value types right now. that means a tiny adapter struct cannot
Expand Down
24 changes: 14 additions & 10 deletions examples/net_echo.pith
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# TCP echo server/client test

import std.net.tcp as tcp

fn run_server(port: Int) -> Int!:
s := tcp_listen("127.0.0.1", port)!
defer tcp_close(s)
c := tcp_accept(s)!
defer tcp_close(c)
data := tcp_read(c, 1024)!
tcp_write(c, "echo: {data}")!
s := tcp.listen("127.0.0.1", port)!
defer tcp.close(s)
c := tcp.accept(s)!
defer tcp.close(c)
data := tcp.read(c, 1024)!
# write_all, not write: one write syscall stops when the send buffer fills,
# and the bytes it did not take are simply not sent.
tcp.write_all(c, "echo: {data}")!
return 0

fn run() -> String!:
Expand All @@ -17,10 +21,10 @@ fn run() -> String!:
sleep(200)

# connect client
conn := tcp_connect("127.0.0.1", port)!
defer tcp_close(conn)
tcp_write(conn, "hello pith")!
response := tcp_read(conn, 1024)!
conn := tcp.connect("127.0.0.1", port)!
defer tcp.close(conn)
tcp.write_all(conn, "hello pith")!
response := tcp.read(conn, 1024)!
print(response)

await task
Expand Down
3 changes: 2 additions & 1 deletion examples/redis_client.pith
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# skip the mock entirely.

import std.redis as redis
import std.net.tcp as tcp

# a stand-in redis: accept one connection, then reply to each command in turn
# with a pre-baked RESP response. it does not parse the requests — the client
Expand All @@ -18,7 +19,7 @@ fn fake_redis(port: Int, replies: List[String]) -> Int!:
defer tcp_close(conn)
for reply in replies:
request := tcp_read(conn, 4096)!
tcp_write(conn, reply)!
tcp.write_all(conn, reply)!
return 0

fn main() -> Int!:
Expand Down
23 changes: 13 additions & 10 deletions examples/tcp_echo.pith
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# tcp echo — start a server, connect a client, echo a message

import std.net.tcp as tcp

fn server(port: Int) -> String!:
server_fd := tcp_listen("127.0.0.1", port)!
defer tcp_close(server_fd)
client_fd := tcp_accept(server_fd)!
defer tcp_close(client_fd)
msg := tcp_read(client_fd, 1024)!
tcp_write(client_fd, msg)!
server_fd := tcp.listen("127.0.0.1", port)!
defer tcp.close(server_fd)
client_fd := tcp.accept(server_fd)!
defer tcp.close(client_fd)
msg := tcp.read(client_fd, 1024)!
# write_all keeps going after a short write; write would drop the remainder.
tcp.write_all(client_fd, msg)!
return ""

fn run() -> String!:
Expand All @@ -22,10 +25,10 @@ fn run() -> String!:
sleep(100)

# connect and exchange data
connection_fd := tcp_connect("127.0.0.1", port)!
defer tcp_close(connection_fd)
tcp_write(connection_fd, "hello from pith")!
response := tcp_read(connection_fd, 1024)!
connection_fd := tcp.connect("127.0.0.1", port)!
defer tcp.close(connection_fd)
tcp.write_all(connection_fd, "hello from pith")!
response := tcp.read(connection_fd, 1024)!
print("echo response: {response}")

await server_task
Expand Down
6 changes: 3 additions & 3 deletions examples/web_h2.pith
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ impl FrameStream:
# (END_STREAM + END_HEADERS). :authority is a fixed "localhost" — the app does
# not route on it.
fn send_get(fd: Int, path: String) -> Int!:
tcp_write_bytes(fd, bytes.from_string_utf8(H2C_PREFACE))!
tcp.write_all_bytes(fd, bytes.from_string_utf8(H2C_PREFACE))!
no_settings: List[frames.Setting] := []
tcp_write_bytes(fd, frames.frame_bytes(frames.settings_frame(no_settings)!)!)!
tcp.write_all_bytes(fd, frames.frame_bytes(frames.settings_frame(no_settings)!)!)!
encoder := hpack.new_encoder(hpack.DEFAULT_HEADER_TABLE_SIZE, false)
fields := [hpack.header_field(":method", "GET"), hpack.header_field(":path", path), hpack.header_field(":scheme", "http"), hpack.header_field(":authority", "localhost")]
block := encoder.encode(fields)
tcp_write_bytes(fd, frames.frame_bytes(frames.headers_frame(1, block, true, true))!)!
tcp.write_all_bytes(fd, frames.frame_bytes(frames.headers_frame(1, block, true, true))!)!
return 0

# read response frames until stream 1 ends, returning the DATA payload as text.
Expand Down
55 changes: 10 additions & 45 deletions std/io.pith
Original file line number Diff line number Diff line change
Expand Up @@ -1091,38 +1091,19 @@ fn write_all_string(writer: StringBuffer, data: String) -> Int!:
remaining = remaining_after_write(remaining, wrote)
return total

# the text write_all loops all resume through their bytes counterpart rather
# than re-slicing the String. a write stops at whatever byte offset the send
# buffer or the pipe ran out at, and that offset can be in the middle of a
# multi-byte character — which a String cannot be cut at, so resuming as text
# would stop the process on the first non-ascii payload big enough to be split.
fn write_all_tcp_stream(writer: TcpStream, data: String) -> Int!:
mut total := 0
mut remaining := data
while remaining.len() > 0:
wrote := writer.write(remaining)!
if wrote <= 0:
fail "write returned 0"
total = total + wrote
remaining = remaining_after_write(remaining, wrote)
return total
return write_all_tcp_stream_bytes(writer, encode_utf8(data))

fn write_all_tcp_stream_ctx(writer: TcpStream, ctx: Context, data: String) -> Int!BlockingError:
mut total := 0
mut remaining := data
while remaining.len() > 0:
wrote := tcp_stream_write_ctx(writer, ctx, remaining)!
if wrote <= 0:
fail blocking_failed("write returned 0")
total = total + wrote
remaining = remaining_after_write(remaining, wrote)
return total
return write_all_tcp_stream_bytes_ctx(writer, ctx, encode_utf8(data))

fn write_all_file_stream(writer: FileStream, data: String) -> Int!:
mut total := 0
mut remaining := data
while remaining.len() > 0:
wrote := writer.write(remaining)!
if wrote <= 0:
fail "write returned 0"
total = total + wrote
remaining = remaining_after_write(remaining, wrote)
return total
return write_all_file_stream_bytes(writer, encode_utf8(data))

fn copy_string(reader: StringReader, writer: StringBuffer) -> Int!:
return copy_string_chunked(reader, writer, DEFAULT_CHUNK_SIZE)
Expand Down Expand Up @@ -1228,15 +1209,7 @@ pub fn append_file_text_chunked(path: String, data: String, chunk_size: Int) ->
return total

fn write_all_process_stdin(writer: ProcessStdin, data: String) -> Int!:
mut total := 0
mut remaining := data
while remaining.len() > 0:
wrote := writer.write(remaining)!
if wrote <= 0:
fail "write returned 0"
total = total + wrote
remaining = remaining_after_write(remaining, wrote)
return total
return write_all_process_stdin_bytes(writer, encode_utf8(data))

fn read_all_process_stdout(reader: ProcessStdout) -> String!:
return read_all_process_stdout_chunked(reader, DEFAULT_CHUNK_SIZE)
Expand Down Expand Up @@ -1275,15 +1248,7 @@ fn process_stdin_write_ctx(writer: ProcessStdin, ctx: Context, data: String) ->
return wrote.ok

fn write_all_process_stdin_ctx(writer: ProcessStdin, ctx: Context, data: String) -> Int!BlockingError:
mut total := 0
mut remaining := data
while remaining.len() > 0:
wrote := process_stdin_write_ctx(writer, ctx, remaining)!
if wrote <= 0:
fail blocking_failed("write returned 0")
total = total + wrote
remaining = remaining_after_write(remaining, wrote)
return total
return write_all_process_stdin_bytes_ctx(writer, ctx, encode_utf8(data))

fn process_stdout_read_ctx(reader: ProcessStdout, ctx: Context, max_bytes: Int) -> String!BlockingError:
task := spawn process_stdout_read_worker(reader, max_bytes)
Expand Down
3 changes: 2 additions & 1 deletion std/mysql.pith
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import std.bytes as bytes
import std.bits as bits
import std.hash as hash
import std.encoding as encoding
import std.net.tcp as tcp
import std.net.tls as tls
from std.io import TcpStream
from std.iter import Iterator
Expand Down Expand Up @@ -144,7 +145,7 @@ impl Conn:
fn send_raw(data: Bytes) -> Int!:
if self.secure:
return tls.conn_from_handle(self.tls_handle).write_all_bytes(data)!
return tcp_write_bytes(self.fd, data)!
return tcp.write_all_bytes(self.fd, data)!

fn send_packet(seq: Int, payload: Bytes) -> Int!:
out := bytes.buffer()
Expand Down
3 changes: 2 additions & 1 deletion std/net/http2/connection.pith
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import std.bytes as bytes
import std.binary as binary
import std.encoding as encoding
import std.net.url as url
import std.net.tcp as tcp
import std.net.tls as tls
import std.time as time

Expand Down Expand Up @@ -177,7 +178,7 @@ impl Connection:
fn send(data: Bytes) -> Int!:
if self.live:
if self.plaintext:
return tcp_write_bytes(self.tls_handle, data)!
return tcp.write_all_bytes(self.tls_handle, data)!
return tls.conn_from_handle(self.tls_handle).write_all_bytes(data)!
self.sent = bytes.concat(self.sent, data)
return data.len()
Expand Down
2 changes: 1 addition & 1 deletion std/net/http2/server.pith
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ impl ServerConn:
# is sent, exactly as the h2 client writes.
fn send(data: Bytes) -> Int!:
if self.transport == TRANSPORT_TCP:
return tcp_write_bytes(self.fd, data)!
return tcp.write_all_bytes(self.fd, data)!
if self.transport == TRANSPORT_TLS:
return tls.conn_from_handle(self.tls_handle).write_all_bytes(data)!
self.sent = bytes.concat(self.sent, data)
Expand Down
51 changes: 47 additions & 4 deletions std/net/tcp.pith
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
# TCP client and server functionality. All I/O is blocking — use spawn for concurrency.
# Connections are represented as plain Int file descriptors.
#
# from std.net.tcp import connect, read, write, close
import std.resilience as resilience
# from std.net.tcp import connect, read, write_all, close
#
# fd := connect("127.0.0.1", 8080)!
# write(fd, "hello")!
# write_all(fd, "hello")!
# data := read(fd, 1024)!
# close(fd)

import std.bytes as bytes
import std.resilience as resilience

# ===============================================================
# Client Operations
# ===============================================================
Expand Down Expand Up @@ -105,13 +108,47 @@ pub fn read(fd: Int, max_bytes: Int) -> String!:
return tcp_read(fd, max_bytes)

# Writes data to a connection.
# Returns the number of bytes written or fails on error.
#
# this is one write syscall, and it returns the number of bytes the kernel
# accepted — which may be fewer than were offered. a socket has a bounded send
# buffer, so a buffer larger than the space left in it is written in part and
# the rest is not written at all. the count is the whole result: ignoring it
# drops the tail silently, and the peer waits for bytes that will never come.
# reach for write_all() unless you have a reason to handle the count yourself.
#
# as with read(), fd must be a socket: a write to a descriptor that is not one
# would overwrite whatever file inherited the number, so it stops the process.
pub fn write(fd: Int, data: String) -> Int!:
return tcp_write(fd, data)

# Writes every byte of data to a connection, and returns how many that was.
#
# a short write is normal rather than an error — the send buffer filled — so
# this resumes from where the last one stopped and keeps going until the buffer
# is out. only a write that accepts nothing at all ends it, and that means the
# reader is gone.
#
# the loop runs on bytes, not on characters: a send buffer fills at whatever
# byte offset it fills at, which may be in the middle of a multi-byte character,
# and a String cannot be cut there.
pub fn write_all(fd: Int, data: String) -> Int!:
return write_all_bytes(fd, bytes.from_string_utf8(data))

# Writes every byte of data to a connection, and returns how many that was.
# The Bytes form of write_all(); see it for the contract.
pub fn write_all_bytes(fd: Int, data: Bytes) -> Int!:
total := data.len()
mut sent := 0
mut remaining := data
while sent < total:
wrote := tcp_write_bytes(fd, remaining)!
if wrote <= 0:
fail "tcp: connection closed after writing {sent} of {total} bytes"
sent = sent + wrote
if sent < total:
remaining = remaining.slice(wrote, remaining.len())
return sent

# Set read timeout in milliseconds (0 = no timeout).
# Connections default to 5 second timeout.
pub fn set_timeout(fd: Int, ms: Int):
Expand All @@ -129,5 +166,11 @@ pub fn close(fd: Int):
test "tcp wrappers report invalid descriptor errors":
assert(read(-1, 8).is_err)
assert(write(-1, "hello").is_err)
assert(write_all(-1, "hello").is_err)
assert(write_all_bytes(-1, bytes.from_string_utf8("hello")).is_err)
set_timeout(-1, 1)
close(-1)

test "write_all on an empty buffer writes nothing and does not fail":
assert_eq(write_all(-1, "") catch -1, 0)
assert_eq(write_all_bytes(-1, bytes.empty()) catch -1, 0)
5 changes: 3 additions & 2 deletions std/postgres.pith
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import std.hash as hash
import std.crypto.hmac as hmac
import std.crypto.kdf as kdf
import std.crypto.random as random
import std.net.tcp as tcp
import std.net.tls as tls
from std.io import TcpStream
from std.iter import Iterator
Expand Down Expand Up @@ -129,7 +130,7 @@ impl Conn:
fn send_raw(data: Bytes) -> Int!:
if self.secure:
return tls.conn_from_handle(self.tls_handle).write_all_bytes(data)!
return tcp_write_bytes(self.fd, data)!
return tcp.write_all_bytes(self.fd, data)!

# frame and send a message. a `type_byte` of 0 omits the type prefix, as the
# startup message requires.
Expand Down Expand Up @@ -198,7 +199,7 @@ pub fn connect_tls(host: String, port: Int, user: String, password: String, data
# until the socket is wrapped in a Conn (or handed to the tls session), no
# owner will close it, so every failure below closes the fd by hand.
request := bytes.concat(be32(8), be32(80877103))
sent := tcp_write_bytes(fd, request)
sent := tcp.write_all_bytes(fd, request)
if sent.is_err:
tcp_close(fd)
fail sent.err
Expand Down
Loading
Loading