Skip to content

Wire is a networking library written in Zig, designed to simplify building non-blocking TCP-based client-server applications. It integrates with the xev event loop for asynchronous I/O operations and is able to run non-blocking on a single thread.

License

Notifications You must be signed in to change notification settings

Thomvanoorschot/wire

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Project Logo

Wire: A TCP Networking Library for Zig

Overview

Wire is a networking library written in Zig, designed to simplify building TCP-based client-server applications. It leverages Zig's capabilities for memory safety and performance, and integrates with the xev event loop for asynchronous I/O operations able to run on a single thread.

Goals

  • Provide a straightforward API for TCP client and server creation.
  • Implement a simple message framing protocol for clear data exchange.
  • Enable asynchronous, non-blocking network communication.
  • Offer a callback-based mechanism for handling network events.
  • Serve as a practical example of Zig for network programming.

Architecture

Wire is built around two main components:

  • Client: Manages a connection to a TCP server. It handles connecting, sending data, and receiving framed messages.
  • Server: Listens for incoming TCP connections and manages multiple ClientConnection instances.
  • ClientConnection: Represents a connection from a client to the server, handling reading and writing of framed data.
  • Frame / FrameHeader: Defines the structure for messages, where each message is prefixed with a header indicating its type and the length of its payload. This allows for structured communication between client and server.

Features

  • Asynchronous Operations: Utilizes xev for non-blocking network I/O.
  • Message Framing: Implements a basic framing protocol (message type + payload length) to delineate messages over TCP streams.
  • Callback-Driven: Uses callbacks to notify application code of events such as new connections, incoming data, and disconnections.
  • Memory Management: Leverages Zig's allocators for explicit memory control.
  • Client and Server Abstractions: Provides easy-to-use Client and Server types.

Learning Outcomes

This project can provide insights into:

  • Network programming in Zig.
  • Working with event loops (specifically xev).
  • Implementing basic network protocols (framing).
  • Zig's error handling and memory management in a networking context.
  • Callback-based event handling.

Getting Started

(Instructions for integrating and using the Wire library will be added as development progresses.)

Usage Example

Here's a basic example of how to use the Client to connect to a server and handle messages:

const std = @import("std");
const xev = @import("xev"); // Assuming xev is available
const wire = @import("wire");

// 1. Define the message types your application will use.
//    This enum will be used by the client to dispatch messages to the correct callbacks.
pub const MessageTypes = enum {
    myMessageA,
    myMessageB,
};

// 2. Define callback functions for each message type.
//    These functions will be called when a message of the corresponding type is received.
fn handleMyMessageA(context: ?*anyopaque, payload: []const u8) anyerror!void {
    // 'context' is the optional context pointer provided during client initialization.
    // 'payload' is the raw byte slice of the message.
    // Process payload for myMessageA
    std.debug.print("Received myMessageA: {s}\n", .{payload});
    _ = context; // Avoid unused variable warning if context is not used
}

const MyCustomStruct = struct {
    const Self = @This();

    fn handleMyMessageB(context: ?*anyopaque, payload: []const u8) anyerror!void {
        // Process payload for myMessageB
        const self = @as(*Self, @ptrCast(@alignCast(context)));
        std.debug.print("Received myMessageB: {s}\n", .{payload});
        _ = context;
    }
}

var customStruct = MyCustomStruct{};

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

    var loop = try xev.Loop.init(.{});
    defer loop.deinit();

    
    // 3. Initialize the Client.
    var client = try wire.Client(MessageTypes).init(
        allocator,
        &loop,
        .{ // ClientOptions
            .server_addr = try std.net.Address.parseIp4("127.0.0.1", 8080), // Target server address
            // .keep_alive = false, // Optional: defaults to false
        },
        .{ // Callbacks for each MessageType
            .myMessageA = .{
                .context = null,
                .cb = handleMyMessageA
            },
            .myMessageB = .{
                .context = @ptrCast(&customStruct),
                .cb = handleMyMessageB
            },
        },
    );

    // 4. Connect to the server.
    //    The connection happens asynchronously.
    //    You might want a connection callback in a real application to know when it's established.
    client.connect();

    // 5. Start reading messages from the server.
    //    This tells the client to begin listening for incoming framed messages.
    client.startReading();

    // Run the event loop to process network events.
    try loop.run();
}

This example demonstrates:

  • Defining MessageTypes.
  • Creating callback functions for these types.
  • Initializing the wire.Client with server address, options, and callbacks.
  • Connecting the client using client.connect().
  • Initiating message reading with client.startReading().
  • Running the xev event loop.

Server Usage Example

Here's a basic example of how to use the Server to accept connections and handle client messages:

const std = @import("std");
const xev = @import("xev");
const wire = @import("wire");

// 1. Define message types (must be the same as the client's MessageTypes).
pub const MessageTypes = enum {
    myMessageA,
    myMessageB,
};

// Forward declaration for ConnectionContext if needed for callbacks
const ConnectionContext = struct {
    allocator: std.mem.Allocator,
    client_conn: *wire.ClientConnection,
};

// 2. Implement the callback for when a new client is accepted.
fn serverAcceptCallback(
    server_context: ?*anyopaque, // Context provided during Server.init
    loop: *xev.Loop,
    accept_completion: *xev.Completion,
    client_conn: *wire.ClientConnection,
) xev.CallbackAction {
    _ = loop;
    _ = accept_completion;

    const allocator = @as(*std.mem.Allocator, @ptrCast(@alignCast(server_context))).?;

    std.debug.print("Server: Client connected (fd: {d})\n", .{client_conn.socket.fd});

    // 3. For each connection, set up context and start reading.
    const conn_ctx = allocator.create(ConnectionContext) catch |err| {
        std.debug.print("Server: Failed to allocate context for connection: {any}\n", .{err});
        // client_conn.close(null); // Close if context allocation fails
        return .rearm; // Continue accepting other connections
    };
    conn_ctx.* = .{
        .allocator = allocator,
        .client_conn = client_conn,
    };

    // Set a callback for when this specific client connection closes
    client_conn.setCloseCallback(@ptrCast(conn_ctx), clientCloseCallback);

    // Start reading messages from this client
    client_conn.read(@ptrCast(conn_ctx), clientReadCallback);

    // Server should continue to accept new connections
    return .rearm;
}

// 4. Implement the callback for reading data from a client.
fn clientReadCallback(
    context: ?*anyopaque,
    payload: []const u8,
) void {
    const conn_ctx = @as(*ConnectionContext, @ptrCast(@alignCast(context))).?;
    std.debug.print("Server: Received from client (fd: {d}): {s}\n", .{conn_ctx.client_conn.socket.fd, payload});

    // Example: Echo the message back or send a different response
    const response_payload = "Server acknowledges your message!" catch unreachable; // Using a string literal
    conn_ctx.client_conn.write(
        MessageTypes, // The enum type
        .myMessageA, // The specific message type from the enum
        response_payload,
    ) catch |err| {
        std.debug.print("Server: Failed to write to client (fd: {d}): {any}\n", .{conn_ctx.client_conn.socket.fd, err});
        // The connection might be closed by the write error handler in ClientConnection
    };
}

// 5. Implement the callback for when a client connection is closed.
fn clientCloseCallback(context: ?*anyopaque) anyerror!void {
    const conn_ctx = @as(*ConnectionContext, @ptrCast(@alignCast(context))).?;
    std.debug.print("Server: Client disconnected (fd: {d})\n", .{conn_ctx.client_conn.socket.fd});
    // Deinitialize/free the ConnectionContext
    conn_ctx.allocator.destroy(conn_ctx);
}

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

    var loop = try xev.Loop.init(.{});
    defer loop.deinit();

    // 6. Initialize the Server.
    var server = try wire.Server.init(
        allocator,
        &loop,
        .{ // ServerOptions
            .address = try std.net.Address.parseIp4("127.0.0.1", 8080),
            .max_connections = 10,
        },
        @ptrCast(&allocator), // Pass allocator as server context for acceptCallback
        serverAcceptCallback,
    );
    defer server.deinit(); // Ensure server resources are cleaned up

    // 7. Start accepting connections.
    //    This will continuously listen for new clients in the background via the event loop.
    server.accept();
    std.debug.print("Server listening on 127.0.0.1:8080\n", .{});

    // Run the event loop to process network events.
    try loop.run();
}

This server example shows:

  • Initializing wire.Server with an address and an acceptCallback.
  • The acceptCallback is invoked for each new client.
  • Inside acceptCallback, client_conn.read() is called with a readCallback to process incoming data from that specific client.
  • client_conn.write() is used to send framed messages back to the client.
  • client_conn.setCloseCallback() is used to register a function to be called when the client disconnects, allowing for resource cleanup.

Project Status

🚧 Early Development – The core client and server components, along with message framing, are implemented. Further development will focus on robustness, features, and examples.

About

Wire is a networking library written in Zig, designed to simplify building non-blocking TCP-based client-server applications. It integrates with the xev event loop for asynchronous I/O operations and is able to run non-blocking on a single thread.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages