An open API service indexing awesome lists of open source software.

https://github.com/Thomvanoorschot/wire

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.
https://github.com/Thomvanoorschot/wire

event-loop libxev networking non-blocking tcp tcp-client tcp-server zig-package

Last synced: 7 months ago
JSON representation

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.

Awesome Lists containing this project

README

          


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:

```zig
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:

```zig
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.