Skip to content

Instantly share code, notes, and snippets.

@jlucaso1
Last active March 28, 2025 01:37
Show Gist options
  • Select an option

  • Save jlucaso1/23a6878d532427f7e28c91d3062662b1 to your computer and use it in GitHub Desktop.

Select an option

Save jlucaso1/23a6878d532427f7e28c91d3062662b1 to your computer and use it in GitHub Desktop.
Zig 0.14.0 summary std libraries

Zig Standard Library std.crypto Summary

This document summarizes the std.crypto module in Zig's standard library, focusing on common cryptographic operations and how to use them. The library provides a wide range of cryptographic primitives and high-level APIs designed for security and ease of use.

Core Concepts

  • Hashing: Creating fixed-size digests of data (e.g., for integrity checks). See `crypto.hash`.
  • Authentication (MAC): Creating tags to verify data integrity and authenticity using a secret key. See `crypto.auth`.
  • Password Hashing: Securely hashing passwords for storage, designed to be slow and resource-intensive. See `crypto.pwhash`.
  • Encryption (AEAD): Encrypting data while also providing integrity and authenticity verification (Authenticated Encryption with Associated Data). Preferred over raw stream/block ciphers. See `crypto.aead`.
  • Signatures: Proving data origin and integrity using public-key cryptography. See `crypto.sign`.
  • Key Exchange (DH): Securely establishing a shared secret between parties over an insecure channel. See `crypto.dh`.
  • Key Derivation (KDF): Deriving cryptographic keys from a master secret or password. See `crypto.kdf`.
  • Key Encapsulation (KEM): Public-key mechanism to securely transport a symmetric key. See `crypto.kem`.
  • Randomness: Secure random number generation via `crypto.random`.
  • Utilities: Secure memory zeroing (`crypto.secureZero`) and constant-time operations (`crypto.timing_safe`).
  • High-Level API: Simplified NaCl/libsodium-inspired APIs. See `crypto.nacl`.

Hashing (`crypto.hash`)

Computes a fixed-size digest (hash) of input data. Useful for integrity checks, data identification, etc.

  • Common Algorithms:
    • `crypto.hash.Sha256`, `crypto.hash.Sha384`, `crypto.hash.Sha512`: SHA-2 family.
    • `crypto.hash.Sha3_256`, `crypto.hash.Sha3_512`: SHA-3 family.
    • `crypto.hash.Blake3`: Modern, very fast hash function.
    • `crypto.hash.Md5`, `crypto.hash.Sha1`: Deprecated and insecure for cryptographic use.
  • Usage Patterns:
    1. One-shot: Easiest for complete data via the `hash` function.
    2. Incremental: Useful for streaming data via `init`, `update`, and `final` methods.
  • Example (One-shot SHA-256):
      const crypto = @import("std").crypto;
      const message = "hello world";
      var digest: [crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
      crypto.hash.sha2.Sha256.hash(message, &digest, .{});
  • Example (Incremental Blake3):
    const crypto = @import("std").crypto;
    const message_part1 = "hello ";
    const message_part2 = "world";
    var hasher = crypto.hash.Blake3.init(.{}); // options can include a key for keyed hashing
    hasher.update(message_part1);
    hasher.update(message_part2);
    var digest: [crypto.hash.Blake3.digest_length]u8 = undefined;
    hasher.final(&digest);

Authentication / MAC (`crypto.auth`)

Creates a Message Authentication Code (tag) using a secret key to verify data integrity and authenticity.

  • Common Algorithms:
    • `crypto.auth.hmac.sha2.HmacSha256`: HMAC using SHA-256. Widely used.
    • `crypto.auth.siphash`: Fast MAC, often used for hash tables (requires key).
    • `crypto.auth.aegis`: High-performance MACs.
    • `crypto.onetimeauth.Poly1305`: Fast one-time MAC (key must never be reused). Often used with stream ciphers in AEAD modes.
  • Usage: Similar `init`/`update`/`final` or one-shot (`mac`) patterns as hashing, but requires a secret `key`.
  • Key Constants: `key_length`, `mac_length` (tag length).
  • Verification: Compare the computed tag with the received tag using `crypto.timing_safe.eql`. Failure implies tampering or wrong key. Related error: `crypto.errors.AuthenticationError`.
  • Example (HMAC-SHA256):
    const crypto = @import("std").crypto;
    const message = "authenticate me";
    var key: [crypto.auth.hmac.sha2.HmacSha256.key_length]u8 = undefined;
    // Fill 'key' with a secure random key
    crypto.random.bytes(&key);
    
    var tag: [crypto.auth.hmac.sha2.HmacSha256.mac_length]u8 = undefined;
    
    // One-shot
    crypto.auth.hmac.sha2.HmacSha256.create(&tag, message, &key);

Password Hashing (`crypto.pwhash`)

Securely hashes passwords for storage. Designed to be slow and memory/CPU intensive to resist brute-force attacks.

  • Algorithms:
    • `crypto.pwhash.argon2`: Modern, recommended algorithm (Argon2id, Argon2i, Argon2d modes). Requires an `Allocator`.
    • `crypto.pwhash.bcrypt`: Widely used, robust algorithm.
    • `crypto.pwhash.scrypt`: Memory-hard algorithm. Requires an `Allocator`.
  • Key Functions:
    • `strHash`: Computes the hash and encodes it into a standard string format (e.g., PHC format) including algorithm, parameters, salt, and hash.
    • `strVerify`: Verifies a password against a previously generated hash string.
  • Parameters: Algorithms require parameters like rounds/iterations (`time_cost`), memory cost (`memory_kib`), parallelism (`parallelism`), and a unique random `salt` per password. Use predefined constants (e.g., `argon2.Params.owasp_2id`) or tune carefully.
  • Error: `crypto.errors.PasswordVerificationError` on mismatch during `strVerify`.
  • Example (Argon2id):
    const crypto = @import("std").crypto;
    const std = @import("std");
    const allocator = std.heap.page_allocator;
    
    const password = "correct horse battery staple";
    
    var output_buf: [200]u8 = undefined;
    const params = crypto.pwhash.argon2.Params.owasp_2id;
    const hash_options = crypto.pwhash.argon2.HashOptions{
        .allocator = allocator,
        .params = params,
        .mode = .argon2id,
    };
    const hash_str = try crypto.pwhash.argon2.strHash(password, hash_options, &output_buf);
    std.debug.print("Hash string: {s}\n", .{hash_str}); // Store this string
    
    // Verification
    const stored_hash_str = hash_str; // Retrieve stored hash string
    const verify_options = crypto.pwhash.argon2.VerifyOptions{ .allocator = allocator };
    try crypto.pwhash.argon2.strVerify(stored_hash_str, password, verify_options);
    std.debug.print("Password verified!\n", .{});
    
    // Example with wrong password (will error)
    const wrong_password = "wrong password";
    _ = try crypto.pwhash.argon2.strVerify(stored_hash_str, wrong_password, verify_options);

Authenticated Encryption (`crypto.aead`)

Encrypts data and provides integrity/authenticity. This is generally preferred over using stream/block ciphers directly.

  • Common Algorithms:
    • `crypto.aead.chacha_poly.ChaCha20Poly1305`: ChaCha20 stream cipher + Poly1305 MAC. Widely used, good performance. `XChaCha20Poly1305` supports larger nonces.
    • `crypto.aead.aes_gcm.Aes128Gcm`, `crypto.aead.aes_gcm.Aes256Gcm`: AES in GCM mode. Hardware accelerated on many platforms.
    • `crypto.aead.aegis`: High-performance AEAD algorithms.
  • Key Parameters:
    • `key`: Secret key (size: `key_length`).
    • `nonce`: Number used once per key (size: `nonce_length`). Critical for security. Must be unique for each encryption with the same key.
    • `plaintext`: Data to encrypt.
    • `ciphertext`: Encrypted data output.
    • `associated_data` (AD): Optional data that is authenticated but not encrypted (e.g., headers).
    • `tag`: Authentication tag output (during encryption) or input (during decryption) (size: `tag_length`).
  • Key Functions: `encrypt`, `decrypt`.
  • Error: `crypto.errors.AuthenticationError` if decryption fails (tag mismatch or ciphertext corruption).
  • Example (ChaCha20Poly1305):
    const crypto = @import("std").crypto;
    const aead = crypto.aead.chacha_poly.ChaCha20Poly1305;
    
    var key: [aead.key_length]u8 = undefined;
    crypto.random.bytes(&key);
    var nonce: [aead.nonce_length]u8 = undefined;
    crypto.random.bytes(&nonce); // Nonce MUST be unique per key
    
    const plaintext = "secret message";
    const associated_data = "metadata";
    var ciphertext: [plaintext.len]u8 = undefined;
    var tag: [aead.tag_length]u8 = undefined;
    
    // Encryption
    aead.encrypt(&ciphertext, &tag, plaintext, associated_data, nonce, key);
    // Transmit ciphertext, tag, nonce, associated_data
    
    // Decryption
    var decrypted_plaintext: [plaintext.len]u8 = undefined;
    aead.decrypt(&decrypted_plaintext, &ciphertext, tag, associated_data, nonce, key) catch |err| {
        // If err == error.AuthenticationFailed, the data is invalid/tampered!
        return err;
    };
    // 'decrypted_plaintext' matches 'plaintext' if successful

Digital Signatures (`crypto.sign`)

Verify data integrity and origin using public-key cryptography. A private key is used to sign, and the corresponding public key is used to verify.

  • Algorithms:
    • `crypto.sign.Ed25519`: Modern, fast signature scheme based on Edwards curves. Recommended.
    • `crypto.sign.ecdsa`: ECDSA implementation (e.g., `EcdsaP256Sha256`).
  • Key Concepts:
    • `KeyPair`: Holds both `public_key` and `secret_key`.
    • `SecretKey`: The private key used for signing.
    • `PublicKey`: The public key used for verification.
    • `Signature`: The signature data produced.
  • Key Functions:
    • `KeyPair.generate()`: Create a new random key pair.
    • `KeyPair.generateDeterministic(seed)`: Create a key pair from a seed.
    • `keypair.sign(message, noise)`: Create a signature. `noise` can be `null` for deterministic signatures or random data for resilience against some attacks.
    • `signature.verify(message, public_key)`: Verify a signature.
  • Error: `crypto.errors.SignatureVerificationError` if verification fails.
  • Example (Ed25519):
    const crypto = @import("std").crypto;
    const Ed25519 = crypto.sign.Ed25519;
    
    // Key Generation
    const key_pair = Ed25519.KeyPair.generate();
    const public_key = key_pair.public_key; // Share this public key
    
    // Signing
    const message = "message to sign";
    const signature = try key_pair.sign(message, null); // Deterministic signature
    
    // Verification (by someone with the public key)
    try signature.verify(message, public_key);
    
    // Verification with wrong message (will error)
    const wrong_message = "different message";
    _ = try signature.verify(wrong_message, public_key);

Key Exchange (`crypto.dh`)

Allows two parties to establish a shared secret over an insecure channel using public-key cryptography.

  • Algorithm:
    • `crypto.dh.X25519`: Based on Curve25519. Modern and fast.
  • Key Concepts: Each party generates an X25519 `KeyPair`. They exchange public keys (`public_key`). Each party computes the shared secret using their own `secret_key` and the other party's `public_key`.
  • Key Function: `crypto.dh.X25519.scalarmult(my_secret_key, their_public_key)` computes the shared secret.
  • Output: The raw output of `scalarmult` should typically be hashed or used as input to a KDF (like HKDF) before being used as a symmetric key.
  • Example (X25519):
    const std = @import("std");
    const crypto = std.crypto;
    const X25519 = crypto.dh.X25519;
    
    // Alice generates keys
    const alice_key_pair = X25519.KeyPair.generate();
    
    // Bob generates keys
    const bob_key_pair = X25519.KeyPair.generate();
    
    // They exchange public keys (alice_key_pair.public_key, bob_key_pair.public_key)
    
    // Alice computes shared secret
    const alice_shared_secret = try X25519.scalarmult(alice_key_pair.secret_key, bob_key_pair.public_key);
    
    // Bob computes shared secret
    const bob_shared_secret = try X25519.scalarmult(bob_key_pair.secret_key, alice_key_pair.public_key);
    
    // alice_shared_secret and bob_shared_secret will be identical
    std.debug.assert(std.mem.eql(u8, &alice_shared_secret, &bob_shared_secret));
    
    // Use the shared secret (e.g., derive keys using HKDF)
    var derived_key: [32]u8 = undefined;
    crypto.kdf.hkdf.HkdfSha256.expand(&derived_key, "context", alice_shared_secret);

Key Derivation (`crypto.kdf`)

Derives one or more cryptographic keys from a master secret or password.

  • Algorithm:
    • `crypto.kdf.hkdf`: HKDF (HMAC-based KDF). Standardized and widely used. Requires a `Hmac` type (e.g., `HmacSha256`).
  • Key Function: `Hkdf(HmacType).kdf(input_key_material, salt, info, output_key_buffer)`
    • `input_key_material`: The master secret (IKM).
    • `salt`: Optional non-secret random value. Recommended.
    • `info`: Optional context/application-specific string.
    • `output_key_buffer`: Buffer to store derived key(s). Length determines output size.
  • Example (HKDF-SHA256):
    const crypto = @import("std").crypto;
    const HkdfSha256 = crypto.kdf.hkdf.HkdfSha256;
    
    const ikm = "initial keying material"; // e.g., output from DH or a master secret
    const salt = "random salt"; // Can be empty, but recommended
    const info = "my application context"; // Context helps separate keys
    
    // Step 1: Extract to get the PRK
    const prk = HkdfSha256.extract(salt, ikm);
    
    // Step 2: Expand to get the derived key
    var derived_key1: [32]u8 = undefined;
    HkdfSha256.expand(&derived_key1, info, prk);
    
    // For longer key
    var longer_derived_key: [64]u8 = undefined;
    HkdfSha256.expand(&longer_derived_key, info, prk);

High-Level API (`crypto.nacl`)

Provides simplified APIs inspired by NaCl/libsodium for common cryptographic tasks, combining primitives.

  • `crypto.nacl.SecretBox`: Symmetric authenticated encryption (like AEAD, uses XSalsa20Poly1305). Requires a shared secret key.
    • `seal(ciphertext, plaintext, nonce, key)`
    • `open(plaintext, ciphertext, nonce, key)`
  • `crypto.nacl.Box`: Public-key authenticated encryption (uses X25519 + XSalsa20Poly1305). Requires sender's secret key and recipient's public key.
    • `seal(ciphertext, plaintext, nonce, recipient_public_key, sender_secret_key)`
    • `open(plaintext, ciphertext, nonce, sender_public_key, recipient_secret_key)`
  • `crypto.nacl.SealedBox`: Anonymous public-key authenticated encryption. Only recipient's public key is needed to encrypt; sender identity is not revealed/authenticated by the primitive itself.
    • `seal(ciphertext, plaintext, recipient_public_key)`
    • `open(plaintext, ciphertext, recipient_keypair)`

Other Notable Areas

  • `crypto.ecc`: Low-level elliptic curve arithmetic (Curve25519, Edwards25519, P-256, Secp256k1). Used internally by `sign`, `dh`. Generally not used directly unless implementing custom protocols.
  • `crypto.stream`: Stream ciphers like `ChaCha20`, `Salsa20`. AEAD modes are strongly preferred as stream ciphers alone provide no integrity or authentication.
  • `crypto.random`: Provides access to the system's cryptographically secure random number generator (interface compatible with `std.rand.Random`). Use `crypto.random.bytes(slice)` to fill a slice with random bytes.
  • `crypto.secureZero(T, slice)`: Zeros memory securely, preventing compiler optimizations that might remove the zeroing operation (important for secrets).
  • `crypto.timing_safe`: Functions for constant-time comparison (`eql`, `compare`), addition (`add`), subtraction (`sub`). Crucial for comparing secret data like MAC tags or passwords without leaking timing information.
  • `crypto.asn1`: ASN.1 (especially DER) encoding/decoding, used primarily for X.509 certificates and keys.
  • `crypto.Certificate`: Parsing and verification of X.509 certificates. Includes `Bundle` for managing trusted Certificate Authorities (CAs).
  • `crypto.tls`: Underlying types and logic for implementing TLS clients/servers (complex). `Client` provides a high-level stream interface.
  • `crypto.errors`: Defines common error sets like `AuthenticationError`, `SignatureVerificationError`, `PasswordVerificationError`, `EncodingError`.


Zig Standard Library std.net Summary

This document summarizes the std.net module in Zig's standard library. It provides cross-platform abstractions for networking tasks, primarily focusing on TCP/IP (v4 and v6) and Unix domain sockets (where available).

Core Structures

Address (extern union)

  • Represents a network address, capable of holding various address types.
  • Key fields: `in` (`Ip4Address`), `in6` (`Ip6Address`), `un` (`posix.sockaddr.un` for Unix sockets, if has_unix_sockets is true), `any` (raw `posix.sockaddr`).
  • Used to specify addresses for connecting, listening, or representing remote endpoints. Created via parsing functions or initializers like `initIp4`/`initIp6`.

Ip4Address / Ip6Address (extern struct)

  • Represent specific IPv4 and IPv6 socket addresses, including IP and port.
  • Contain the underlying platform-specific address structures (e.g., `posix.sockaddr.in`).
  • Can be created via parsing (e.g., `Address.parseIp`) or initialization (e.g., `Address.initIp4`).

Stream (struct)

  • Represents an active, connected network stream (like a TCP connection or a connected Unix socket).
  • Holds the underlying socket handle (`handle: posix.socket_t`).
  • Provides methods for reading (`read`, `readAll`, etc.), writing (`write`, `writeAll`, etc.), and closing (`close`).
  • Obtained via client connection functions (e.g., `tcpConnectToHost`) or server accept (`Server.accept`).
  • Provides `reader()` and `writer()` methods returning `std.io.Reader` and `std.io.Writer` interfaces.

Server (struct)

  • Represents a listening network server socket.
  • Fields: `listen_address` (`Address`), `stream` (`std.net.Stream` representing the listening socket itself).
  • Created by calling `Address.listen()`.
  • Primary method: `accept()` to wait for and accept incoming connections.
  • Must be cleaned up with `deinit()`.

AddressList (struct)

  • Holds a list of resolved `Address` objects, typically the result of a DNS lookup via `getAddressList`.
  • Fields: `addrs` (`[]Address`), `canon_name` (`?[]u8`).
  • Managed by an `std.mem.Allocator` (specifically `ArenaAllocator` internally).
  • Must be cleaned up with `deinit()` to free the allocator's memory.

ListenOptions (struct)

  • Configuration for `Address.listen()`.
  • Fields: `kernel_backlog` (connection queue size), `reuse_address`, `reuse_port` (socket option flags), `force_nonblocking`.

Connection (struct)

  • Returned by `Server.accept()`.
  • Represents an accepted client connection.
  • Fields: `stream` (`std.net.Stream` for I/O), `address` (`Address` of the connected client).

Key Functions by Purpose

Address Creation and Parsing

  • `Address.initIp4(addr: [4]u8, port: u16) Address`: Creates an IPv4 `Address`.
  • `Address.initIp6(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) Address`: Creates an IPv6 `Address`.
  • `Address.initUnix(path: []const u8) !Address`: Creates a Unix domain socket `Address` (if supported).
  • `Address.parseIp(name: []const u8, port: u16) !Address`: Parses an IP address string (v4 or v6) into an `Address`. Use `resolveIp` for better IPv6 link-local handling.
  • `Address.parseIp4(buf: []const u8, port: u16) IPv4ParseError!Address`: Parses only IPv4 strings.
  • `Address.parseIp6(buf: []const u8, port: u16) IPv6ParseError!Address`: Parses only IPv6 strings (numeric scope ID).
  • `Address.resolveIp(name: []const u8, port: u16) !Address`: Resolves an IP string (v4/v6), handling IPv6 scope IDs/interface names. Recommended over `parseIp` for user input.
  • `Address.resolveIp6(buf: []const u8, port: u16) IPv6ResolveError!Address`: Resolves IPv6 strings, handling interface names for scope IDs.

DNS Resolution and Hostname Handling

  • `getAddressList(allocator: mem.Allocator, name: []const u8, port: u16) GetAddressListError!*AddressList`: Performs DNS lookup for a hostname and port, returning a list of possible addresses. Result must be `deinit()`ed.
  • `isValidHostName(hostname: []const u8) bool`: Checks if a string has valid hostname syntax.

Client Connections

  • `tcpConnectToHost(allocator: mem.Allocator, name: []const u8, port: u16) TcpConnectToHostError!Stream`: Common Case. Resolves hostname (name) via DNS, tries connecting to the resolved addresses, and returns a `Stream` on success. Uses the provided allocator temporarily for resolution.
  • `tcpConnectToAddress(address: Address) TcpConnectToAddressError!Stream`: Connects directly to a pre-defined `Address` (IPv4 or IPv6). Returns a `Stream`.
  • `connectUnixSocket(path: []const u8) !Stream`: Connects to a Unix domain socket at the given `path`. Returns a `Stream`. Only available if has_unix_sockets is true.

Server Operations

  • `Address.listen(options: ListenOptions) ListenError!Server`: Creates a listening `Server` bound to the `Address`.
  • `Server.accept() AcceptError!Connection`: Blocks until a client connects, then returns a `Connection` containing the client's `Stream` and `Address`.
  • `Server.deinit()`: Closes the listening socket and cleans up the `Server` struct.

Stream Input/Output

  • `Stream.read(buffer: []u8) ReadError!usize`: Reads up to `buffer.len` bytes. Returns bytes read. Can return 0 without error (e.g., on non-blocking sockets).
  • `Stream.readAll(buffer: []u8) ReadError!void`: Reads exactly `buffer.len` bytes, unless EOF (`error.EndOfStream`) is reached first.
  • `Stream.readAtLeast(buffer: []u8, len: usize) ReadError!usize`: Reads at least `len` bytes, unless EOF is reached first. Returns bytes read.
  • `Stream.write(buffer: []const u8) WriteError!usize`: Writes up to `buffer.len` bytes. Returns bytes written.
  • `Stream.writeAll(bytes: []const u8) WriteError!void`: Writes exactly `bytes.len` bytes. Loops internally until all bytes are written or an error occurs.
  • `Stream.readv(...)`, `Stream.writev(...)`, `Stream.writevAll(...)`: Scatter/gather I/O using `iovec` arrays.
  • `Stream.close()`: Closes the connection/stream.
  • `Stream.reader() -> std.io.Reader`, `Stream.writer() -> std.io.Writer`: Get standard I/O interfaces.

Address Utilities

  • `Address.getPort() u16`: Gets the port from an IP `Address` (asserts it's IP).
  • `Address.setPort(port: u16) void`: Sets the port on an IP `Address` (asserts it's IP).
  • `Address.format(self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void`: Formats the address to a stream for printing.
  • `Address.eql(a: Address, b: Address) bool`: Checks if two addresses are equal.
  • `Address.getOsSockLen() posix.socklen_t`: Gets the size needed for OS socket calls.

Error Sets

  • Functions return errors combined into specific sets (e.g., `TcpConnectToHostError`, `ReadError`, `WriteError`, `ListenError`, `AcceptError`, `GetAddressListError`).
  • Common errors include: `error.ConnectionRefused`, `error.NetworkUnreachable`, `error.HostUnreachable`, `error.AccessDenied`, `error.AddressInUse`, `error.OutOfMemory`, `error.Unexpected` (OS error), `error.EndOfStream`, `error.ConnectionResetByPeer`, `error.BlockingOperationWouldBlock` (or `error.WouldBlock`).

Usage Examples

TCP Client

const std = @import("std");
const net = std.net;
const mem = std.mem;
const print = std.debug.print;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const host = "example.org";
    const port = 80;

    print("Connecting to {s}:{d}...\n", .{ host, port });
    // Resolve DNS and connect
    var stream = try net.tcpConnectToHost(allocator, host, port);
    defer stream.close();
    print("Connected!\n", .{});

    // Send an HTTP GET request
    const request = "GET / HTTP/1.1\r\nHost: example.org\r\nConnection: close\r\n\r\n";
    try stream.writer().writeAll(request); // Using writer interface
    print("Sent request.\n", .{});

    // Read the response
    var buffer: [1024]u8 = undefined;
    while (true) {
       const bytes_read = try stream.reader().read(&buffer);
        if (bytes_read == 0) break; // EOF or non-blocking read empty

        // Process the received data (just print it here)
        print("{s}", .{buffer[0..bytes_read]});
    }
    print("\nConnection closed by peer.\n", .{});
}

TCP Server

const std = @import("std");
const net = std.net;
const print = std.debug.print;

pub fn main() !void {
    // Parse listen address (listen on all IPv4 interfaces, port 8080)
    var listen_address = try net.Address.parseIp4("0.0.0.0", 8080);

    // Start listening
    var server = try listen_address.listen(.{ .reuse_address = true });
    defer server.deinit(); // Ensure server socket is closed

    print("Server listening on {}\n", .{server.listen_address});

    while (true) {
        // Accept a new connection
        var connection = try server.accept();
        // Note: In a real server, you'd likely spawn a thread or use async I/O here
        // to handle multiple clients concurrently. This example handles one at a time.
        defer connection.stream.close(); // Close client connection when done

        print("Accepted connection from {}\n", .{connection.address});

        // Simple echo handling
        var buffer: [1024]u8 = undefined;
        while (true) {
            const bytes_read = try connection.stream.read(&buffer);
            if (bytes_read == 0) break; // Should generally be caught by EndOfStream

            // Echo back the received data using the writer interface
            try connection.stream.writer().writeAll(buffer[0..bytes_read]);
        }
        print("Connection closed from {}\n", .{connection.address});
    }
}


Zig Standard Library std.posix Summary

This document summarizes the std.posix module in Zig, which provides a low-level API layer mirroring POSIX (and sometimes related, like Linux or Windows) system calls.

Overall Purpose

  • Provides a cross-platform (primarily targeting POSIX-compliant systems like Linux, macOS, BSDs, and also WASI, with partial Windows support) interface to operating system functionalities.
  • Lower-level and less abstract than modules like std.fs or std.process. Often maps directly to libc functions or syscalls.
  • Use this when you need fine-grained control, access to specific OS features not exposed by higher-level APIs, or maximum performance by avoiding higher-level abstractions.
  • Be aware that portability might be limited for non-standard features, and OS-specific code might still be needed. Check function documentation for platform support.

Key Concepts & Patterns

Direct Syscall/Libc Mapping

Functions generally correspond closely to their C counterparts (e.g., `posix.open` maps to `open(2)`). Names and parameters often match the underlying syscall.

File Descriptors (fd_t)

Many operations revolve around file descriptors (`posix.fd_t`), which are opaque handles (usually integers) representing open files, sockets, pipes, etc. Constants `posix.STDIN_FILENO`, `posix.STDOUT_FILENO`, `posix.STDERR_FILENO` are predefined.

Error Handling

  • Functions typically return `Error!ReturnType` (e.g., `OpenError!fd_t`).
  • Each function usually has a specific ErrorSet (e.g., `OpenError`, `BindError`) containing possible OS errors (mapped from errno).
  • `error.Unexpected` indicates an OS error code not explicitly mapped by the Zig standard library for that function.
  • Use `try` or `catch` for error handling. Typed errors are preferred over checking errno.

Path Handling & Suffixes

  • Paths are typically passed as `[]const u8` (slices). Encoding depends on the platform (often UTF-8, but can be opaque bytes on Unix, WTF-8 on Windows).
  • Z Suffix: Functions ending in Z (e.g., `openZ`, `accessZ`) expect null-terminated C strings (`[*:0]const u8`).
  • W Suffix: Functions ending in W (e.g., `mkdirW`, `renameW`) are Windows-specific and expect WTF-16 Little Endian encoded paths (`[]const u16` or `[*:0]const u16`). Check function docs for encoding details.
  • at Suffix: Functions ending in at (e.g., `openat`, `fstatat`) perform path-based operations relative to a directory file descriptor (`dirfd`), avoiding races associated with the process's current working directory. Use `posix.AT.FDCWD` for the CWD.

Flags and Constants

Operations often take flags defined as constants within `posix` (or nested structs/enums like `posix.O`, `posix.PROT`, `posix.MAP`). These map directly to POSIX constants (e.g., `posix.O.RDONLY`).

Function Categories

File System I/O & Operations

Opening/Closing

  • `open(path, flags, mode)` / `openZ`: Open/create a file. Returns `fd_t`.
  • `openat(dirfd, path, flags, mode)` / `openatZ`: Open/create relative to `dirfd`.
  • `close(fd)`: Close a file descriptor.

Reading/Writing

  • `read(fd, buf)`: Read bytes into buffer. Returns bytes read (`usize`).
  • `write(fd, buf)`: Write bytes from buffer. Returns bytes written (`usize`).
  • `pread(fd, buf, offset)`: Read at specific offset.
  • `pwrite(fd, buf, offset)`: Write at specific offset.
  • `readv(fd, iov)` / `writev(fd, iov)`: Scatter/gather read/write using `posix.iovec`.

Seeking

  • `lseek(fd, offset, whence)`: General seek function (use `posix.SEEK.SET`, `posix.SEEK.CUR`, `posix.SEEK.END` for `whence`). Returns new offset.
  • Convenience wrappers: `lseek_SET`, `lseek_CUR`, `lseek_END`, `lseek_CUR_get`.

Metadata

  • `fstat(fd)`: Get status (metadata, `posix.Stat`) of an open file.
  • `fstatat(dirfd, path, flags)` / `fstatatZ`: Get status of a file by path relative to `dirfd` (flags like `posix.AT.SYMLINK_NOFOLLOW`).
  • `access(path, mode)` / `accessZ`: Check permissions (`posix.R_OK`, `W_OK`, `X_OK`, `F_OK`).
  • `faccessat(dirfd, path, mode, flags)` / `faccessatZ`: Check permissions relative to `dirfd`.

Modification

  • `ftruncate(fd, length)`: Change file size.
  • `fchmod(fd, mode)`: Change permissions (mode bits) of an open file.
  • `fchmodat(dirfd, path, mode, flags)`: Change permissions by path relative to `dirfd`.
  • `fchown(fd, owner, group)`: Change ownership (UID/GID) of an open file.
  • `fsync(fd)` / `fdatasync(fd)`: Flush changes to disk (data+metadata / data only).
  • `sync()` / `syncfs(fd)`: Flush all filesystem buffers / buffers for the filesystem containing fd.

Directory/Link Management

  • `mkdir(path, mode)` / `mkdirZ` / `mkdirW`: Create directory.
  • `mkdirat(dirfd, path, mode)` / `mkdiratZ` / `mkdiratW`: Create directory relative to `dirfd`.
  • `rmdir(path)` / `rmdirZ` / `rmdirW`: Remove empty directory.
  • `unlink(path)` / `unlinkZ` / `unlinkW`: Remove file link (name).
  • `unlinkat(dirfd, path, flags)` / `unlinkatZ` / `unlinkatW`: Remove file/directory (if `AT.REMOVEDIR`) relative to `dirfd`.
  • `rename(oldpath, newpath)` / `renameZ` / `renameW`: Rename/move file.
  • `renameat(olddirfd, oldpath, newdirfd, newpath)` / `renameatZ` / `renameatW`: Rename/move relative to directory fds.
  • `link(oldpath, newpath)` / `linkZ`: Create hard link.
  • `linkat(olddirfd, oldpath, newdirfd, newpath, flags)` / `linkatZ`: Create hard link relative to directory fds.
  • `symlink(target, linkpath)` / `symlinkZ`: Create symbolic link.
  • `symlinkat(target, newdirfd, linkpath)` / `symlinkatZ`: Create symbolic link relative to `newdirfd`.
  • `readlink(path, buf)` / `readlinkZ` / `readlinkW`: Read value of symbolic link. Returns length of link path.
  • `readlinkat(dirfd, path, buf)` / `readlinkatZ` / `readlinkatW`: Read symlink relative to `dirfd`.

Working Directory

  • `chdir(path)` / `chdirZ` / `chdirW`: Change current working directory.
  • `fchdir(dirfd)`: Change CWD to directory represented by `dirfd`.
  • `getcwd(buf)`: Get current working directory path. Returns slice of buf.
// Example: Read from a file using posix
const std = @import("std");
const posix = std.posix;

pub fn main() !void {
    const fd = try posix.open("my_file.txt", .{}, 0);
    defer posix.close(fd);

    var buf: [1024]u8 = undefined;
    const bytes_read = try posix.read(fd, &buf);
    std.debug.print("Read {d} bytes: {s}\n", .{bytes_read, buf[0..bytes_read]});
}

Process Management

Creation/Execution

  • `fork()`: Create a child process (Unix-like). Returns pid_t (0 in child, child's PID in parent).
  • `execveZ(path, argv, envp)`: Replace current process image with a new one (no PATH search). argv and envp are null-terminated arrays of C strings.
  • `execvpeZ(file, argv, envp)`: Replace process image, searching PATH for file.
  • (Windows uses separate CreateProcess functions, often via std.process).

Termination/Waiting

  • `exit(status)`: Terminate process normally (never returns).
  • `abort()`: Terminate process abnormally (often raises `SIGABRT`).
  • `waitpid(pid, flags)` / `wait4(pid, flags, rusage)`: Wait for child process state changes. Returns info like PID and exit status. pid can be specific PID, -1 (any child), 0 (any child in process group), etc. Flags like `WNOHANG`.

Control

  • `kill(pid, sig)`: Send a signal (`posix.SIG.*`) to a process or process group.
  • `raise(sig)`: Send a signal to the calling thread/process.
  • `getpid()`: Get current process ID (`pid_t`).
  • `getppid()`: Get parent process ID.
  • `setpgid(pid, pgid)`: Set process group ID.
  • `getuid()`/`geteuid()`/`setuid()`/`seteuid()`: User ID management (`uid_t`).
  • `getgid()`/`getegid()`/`setgid()`/`setegid()`: Group ID management (`gid_t`).

Resource Limits & Usage

  • `getrlimit(resource)` / `setrlimit(resource, limits)`: Get/Set resource limits (`posix.RLIMIT.*`, uses `posix.rlimit` struct).
  • `getrusage(who)`: Get resource usage statistics (`posix.rusage` struct, `who` is e.g. `RUSAGE.SELF`).
// Example: Simple fork/exec (Conceptual, needs robust error handling)
const std = @import("std");
const posix = std.posix;

pub fn main() !void {
    const pid = try posix.fork();
    if (pid == 0) {
        // Child process
        const argv = [_:null]?[*:0]const u8{ "ls", "-l" };
        const envp = [_:null]?[*:0]const u8{null}; // Inherit environment
        switch (posix.execvpeZ("ls", &argv, &envp)) {
            else => unreachable,
        }
    } else {
        const res = posix.waitpid(pid, 0);
        // Extract the actual exit status using WEXITSTATUS if the process exited normally
        const status = if (posix.W.IFEXITED(res.status))
            posix.W.EXITSTATUS(res.status)
        else
            res.status;

        std.debug.print("Child {d} exited with status {d}\n", .{ res.pid, status });
    }
}

Networking (Sockets)

Note: std.net provides higher-level abstractions over these.

Creation/Setup

  • `socket(domain, type, protocol)`: Create a socket (e.g., `posix.AF.INET`, `posix.SOCK.STREAM`). Returns `fd_t`.
  • `bind(sockfd, addr, addrlen)`: Assign address (`posix.sockaddr`) to socket.
  • `listen(sockfd, backlog)`: Mark socket to accept connections.
  • `accept(sockfd, addr, addrlen, flags)` / `accept4`: Accept incoming connection. Fills addr and returns new client `fd_t`.
  • `connect(sockfd, addr, addrlen)`: Initiate connection on a socket.

Data Transfer

  • `send(sockfd, buf, flags)` / `sendto(sockfd, buf, flags, dest_addr, addrlen)` / `sendmsg(sockfd, msghdr, flags)`: Send data.
  • `recv(sockfd, buf, flags)` / `recvfrom(sockfd, buf, flags, src_addr, addrlen)` / `recvmsg(sockfd, msghdr, flags)`: Receive data.

Control/Options

  • `shutdown(sockfd, how)`: Disable send/receive on socket (`posix.SHUT.RD`, `WR`, `RDWR`).
  • `getsockopt(sockfd, level, optname, optval_buf)` / `setsockopt(sockfd, level, optname, optval_buf)`: Get/Set socket options (e.g., `posix.SOL.SOCKET`, `posix.SO.REUSEADDR`).
  • `getsockname(sockfd, addr, addrlen)`: Get local socket address.
  • `getpeername(sockfd, addr, addrlen)`: Get remote peer address.

Host Info

  • `gethostname(buf)`: Get standard hostname.
  • (DNS/Address resolution often uses higher-level functions like getaddrinfo or std.net.getAddressList).
// Import the standard library namespace
const std = @import("std");
// Import the expect function for writing tests from the standard testing module
const expect = std.testing.expect;
// Import the networking module from the standard library
const net = std.net;
// Import the operating system specific functions module (like socket operations)
const os = std.os;

// Test block to verify the basic functionality of Socket.init
test "create a socket" {
    // Attempt to initialize a Socket for localhost (127.0.0.1) on port 3000.
    // 'try' will propagate any error returned by Socket.init.
    const socket = try Socket.init("127.0.0.1", 3000);
    // Ensure that the underlying socket descriptor created within the Socket struct
    // is of the correct type, std.posix.socket_t (which is typically an integer file descriptor).
    // 'try' is used here because 'expect' can potentially fail (though unlikely in this specific type check).
    try expect(@TypeOf(socket.socket) == std.posix.socket_t);

    // Note: The socket created here is not closed because it goes out of scope
    // when the test finishes. In a real application, you'd need explicit cleanup.
    // However, test cleanup is often handled by the OS or test runner implicitly.
}

// Define a struct named 'Socket' to encapsulate socket-related data and operations.
const Socket = struct {
    // Stores the parsed network address (IP and port) associated with this socket.
    address: std.net.Address,
    // Stores the low-level operating system socket descriptor (e.g., a file descriptor).
    // This is the handle used for OS-level socket operations.
    socket: std.posix.socket_t,

    // Public function (method) to initialize a new Socket instance.
    // Takes an IP address string (IPv4) and a port number.
    // Returns '!Socket', meaning it returns either a Socket instance or an error.
    fn init(ip: []const u8, port: u16) !Socket {
        // Parse the string IP address and port into a std.net.Address structure.
        // This validates the IP format and combines it with the port.
        // 'try' propagates errors from parsing (e.g., invalid IP format).
        const parsed_address = try std.net.Address.parseIp4(ip, port);

        // Create a low-level POSIX socket using the operating system's socket call.
        // - std.posix.AF.INET: Specifies the address family (IPv4).
        // - std.posix.SOCK.DGRAM: Specifies the socket type (UDP - datagram based, connectionless).
        // - 0: Specifies the protocol (0 usually means the default protocol for the type, which is UDP here).
        // 'try' propagates errors from the OS (e.g., insufficient permissions, resource limits).
        const sock = try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0);

        // 'errdefer' schedules code to run ONLY if an error occurs *after* this line
        // but *before* the function successfully returns. If socket creation succeeds
        // but a subsequent operation in this function were to fail (none here, but imagine more steps),
        // this ensures the created socket `sock` is closed, preventing resource leaks.
        errdefer os.closeSocket(sock);

        // If both parsing and socket creation succeed, return a new Socket struct instance,
        // populated with the parsed address and the created OS socket descriptor.
        return Socket{ .address = parsed_address, .socket = sock };
    }

    // Method to bind the socket to the address specified during initialization.
    // Takes a mutable pointer 'self' to the Socket instance.
    // Returns '!void', meaning it returns void on success or an error.
    // Binding is necessary on the server/receiving side to listen on a specific IP/port.
    fn bind(self: *Socket) !void {
        // Call the operating system's bind function.
        // - self.socket: The socket descriptor to bind.
        // - &self.address.any: A pointer to the low-level OS socket address structure
        //   (like sockaddr_in for IPv4). std.net.Address provides access via the '.any' field.
        // - self.address.getOsSockLen(): The size of the specific OS socket address structure.
        // 'try' propagates errors from the OS (e.g., address already in use, invalid address).
        try os.bind(self.socket, &self.address.any, self.address.getOsSockLen());
    }

    // Method to continuously listen for incoming UDP datagrams on the bound socket.
    // Takes a mutable pointer 'self' to the Socket instance.
    // Returns '!void', meaning it returns void or an error (though the loop is infinite).
    // Note: For UDP, "listen" really means "start receiving". There's no connection setup like TCP.
    fn listen(self: *Socket) !void {
        // Declare a buffer on the stack to hold incoming data. Size 1024 bytes.
        // Initialized to 'undefined' as recvfrom will fill it.
        var buffer: [1024]u8 = undefined;

        // Loop indefinitely to continuously receive data.
        while (true) {
            // Wait to receive data from the socket using the OS's recvfrom call.
            // - self.socket: The socket descriptor to receive from.
            // - buffer[0..]: A slice covering the entire buffer where data will be stored.
            // - 0: Flags for the receive operation (0 means no special flags).
            // - null: Pointer to store the sender's address (ignored in this example).
            // - null: Pointer to store the sender's address length (ignored in this example).
            // 'try' propagates errors from the OS (e.g., network issues).
            // 'received_bytes' will hold the number of bytes actually received.
            const received_bytes = try std.os.recvfrom(self.socket, buffer[0..], 0, null, null);

            // Print the number of bytes received and the received data (as a string)
            // to the standard error output (using std.debug.print).
            // We use a slice `buffer[0..received_bytes]` to only print the valid data received,
            // not the potentially uninitialized parts of the buffer.
            std.debug.print("Received {d} bytes: {s}\n", .{ received_bytes, buffer[0..received_bytes] });
        }
    }
};

Memory Management

  • `mmap(addr_hint, len, prot, flags, fd, offset)`: Map files or devices into memory (`posix.PROT.*`, `posix.MAP.*`).
  • `munmap(addr, len)`: Unmap memory.
  • `mprotect(addr, len, prot)`: Change memory protection.
  • `mremap(...)`: Remap/resize virtual memory area (Linux-specific).
  • `madvise(addr, len, advice)`: Give advice about memory usage (`posix.MADV.*`).
  • `memfd_create(name, flags)`: Create an anonymous file in RAM (Linux-specific). Returns `fd_t`.

Time

  • `clock_gettime(clock_id)` / `clock_getres(clock_id)`: Get time/resolution (`posix.timespec`) from specific clocks (e.g., `posix.CLOCK.MONOTONIC`, `REALTIME`).
  • `nanosleep(req_seconds, req_nanos)`: Suspend execution for an interval.
  • `gettimeofday()`: Get wall-clock time (`posix.timeval`, legacy).
  • `timerfd_create(clockid, flags)` / `timerfd_settime(...)` / `timerfd_gettime(...)`: File descriptor based timers (Linux-specific).

Terminal/TTY

  • `isatty(fd)`: Check if fd refers to a terminal. Returns bool.
  • `tcgetattr(fd)` / `tcsetattr(fd, action, termios_p)`: Get/Set terminal attributes (`posix.termios` struct).
  • `tcgetpgrp(fd)` / `tcsetpgrp(fd, pgrp)`: Get/Set terminal foreground process group.
  • `ioctl(fd, request, ...)`: Device-specific control (e.g., getting window size `posix.TIOCGWINSZ` with `posix.winsize` struct).

Signals

  • `sigaction(signum, act, oldact)`: Examine/change signal action (install signal handler using `posix.Sigaction` struct).
  • `sigprocmask(how, set, oldset)`: Examine/change blocked signals (`posix.sigset_t`) for the calling thread.
  • `sigaltstack(ss, old_ss)`: Set/get alternate signal stack.
  • (See also `kill`, `raise` in Process Management).

Event Notification/Polling

  • `poll(fds, timeout_ms)`: Wait for events on multiple file descriptors (`posix.pollfd` array).
  • `ppoll(fds, timeout, sigmask)`: Like `poll`, but with atomic signal mask change.
  • `epoll_create1(flags)` / `epoll_ctl(epfd, op, fd, event)` / `epoll_wait(epfd, events, maxevents, timeout)`: Edge-triggered I/O event notification (Linux-specific).
  • `kqueue()` / `kevent(kq, changelist, eventlist, timeout)`: Generic event notification (BSD/macOS).
  • `inotify_init1(flags)` / `inotify_add_watch(infd, path, mask)` / `inotify_rm_watch(infd, wd)`: Filesystem event notification (Linux-specific).
  • `signalfd(...)`: Read signals as events from a file descriptor (Linux-specific).

System Information/Control

  • `uname()`: Get system name and information (`posix.utsname` struct).
  • `getenv(name)` / `getenvZ`: Get environment variable value (returns `?[]const u8`). Note: Not thread-safe in all libc implementations.
  • `sysctl(...)` / `sysctlbynameZ(...)`: Get/Set system information (BSD/macOS).
  • `prctl(...)`: Process control operations (Linux-specific).

Important Types & Constants

Types

`fd_t`, `pid_t`, `gid_t`, `uid_t`, `mode_t`, `off_t`, `size_t`, `ssize_t`, `timespec`, `timeval`, `sockaddr`, `sockaddr_in`, `sockaddr_in6`, `socklen_t`, `iovec`, `Stat`, `rusage`, `rlimit`, `sigset_t`, `Sigaction`, `termios`, `pollfd`, `utsname`, etc.

Constants

Found within `posix` namespace or nested structs/enums:

  • File Flags: `posix.O.*` (e.g., `O.RDONLY`, `O.RDWR`, `O.CREAT`, `O.CLOEXEC`, `O.NONBLOCK`)
  • Memory Protection: `posix.PROT.*` (e.g., `PROT.READ`, `PROT.WRITE`, `PROT.EXEC`)
  • Memory Map Flags: `posix.MAP.*` (e.g., `MAP.SHARED`, `MAP.PRIVATE`, `MAP.FIXED`)
  • Seek Modes: `posix.SEEK.*` ( `SEEK.SET`, `SEEK.CUR`, `SEEK.END`)
  • Access Modes: `posix.F_OK`, `posix.R_OK`, `posix.W_OK`, `posix.X_OK`
  • Socket Domains: `posix.AF.*` (e.g., `AF.INET`, `AF.INET6`, `AF.UNIX`)
  • Socket Types: `posix.SOCK.*` (e.g., `SOCK.STREAM`, `SOCK.DGRAM`, `SOCK.CLOEXEC`)
  • Socket Options: `posix.SOL.*`, `posix.SO.*`, `posix.IPPROTO.*`, `posix.TCP.*`
  • Signal Numbers: `posix.SIG.*` (e.g., `SIG.INT`, `SIG.TERM`, `SIG.KILL`, `SIG.CHLD`)
  • at Flags: `posix.AT.*` (e.g., `AT.FDCWD`, `AT.SYMLINK_NOFOLLOW`, `AT.REMOVEDIR`)
  • Standard FDs: `STDIN_FILENO`, `STDOUT_FILENO`, `STDERR_FILENO`
  • Error Codes (for reference): `posix.E.*` (e.g., `E.INVAL`, `E.PERM`, `E.NOENT`, `E.AGAIN`/`E.WOULDBLOCK`) - typically consumed via typed errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment