Давно присматривался к языку программирования Zig и наконец решил на нём что-то написать. Выбор пал на TCP/UDP echo сервер: задача не слишком сложная, но с интересными моментами, особенно при переходе на event loop. В статье поделюсь процессом и своими впечатлениями.

О себе: последние 7 лет на работе пишу бэкенды на Java/Kotlin, помимо C++ в университетские годы и пет проектов на Rust опыта с языками для системной разработки немного, но верхнеуровневое понимание проблем и подходов есть.
Про язык
Долго здесь останавливаться не будем, иначе к практике можно не продвинуться. Если вкратце: современный аналог C, памятью управляем руками (GC нет). Несколько случайных фактов:
Юридическое лицо за языком – Zig Software Foundation, настоящий 501(c)(3) non-profit, как у
OpenAIВикипедииЯзык новый, стабильной версии еще нет, поэтому экосистема бедная, но можно легко подключать библиотеки, написанные на C, а сам C при этом кросс-компилировать с помощью Zig
На Zig написан эмулятор терминала Ghostty (я его полюбил просто уже за то, что хоткеи для навигация по строке ввода на macOS работают из коробки) и крайне интересная база данных TigerBeetle (отдельная рекомендация – ютуб канал команды, кладезь полезной информации)
Настройка окружения
По идее для Visual Studio Code достаточно поставить официальный плагин и в нем выбрать необходимую версию языка. Но я пошел немного другим путем: скачал Zig с официального сайта (в моем случае версия 0.14.1), поставил языковой сервер, затем в настройках указанного выше плагина прописал пути к бинарям и добавил alias для терминала.

Проект
Создаем папку и инициализируем проект с помощью zig init
, на выходе получаем файлы:
src/main.zig – файл с main функцией, тут и будем запускать наш сервер
src/root.zig – использовали бы его, если бы делали библиотеку вместо executable, удаляем
build.zig – описывает процесс сборки, удаляем отсюда все про «root.zig»
build.zig.zon – содержит различную метаинформация про наш проект, а также список зависимостей, оставляем как есть
Радует, что не надо отдельно ставить никакие сборщики, скачали релиз языка и вперед.
TCP сервер
Алгоритм поднятия TCP сервера:
создать сокет
прикрепить его к адресу
ожидать входящих соединений
принять новое соединение
получить/отправить данные
закрыть соединение
закрыть сокет
Мы будем использовать модуль стандартной библиотеки std.posix
, как можно понять из названия, он использует POSIX API для работы с системой (OS-специфичное API можно найти в std.os
, например std.os.windows
), из интересного: некоторые из методов std.posix
работают и на Windows, так как подобная реализация оказалась проще, чем возврат ошибок компиляции.
Сначала смутил небольшой объем документации внутри модуля, но потом до меня дошло, что могу открыть какой-нибудь мануал по линуксу и использовать его в качестве референса, модуль ведь отражает POSIX API.
Создадим сокет (подсветки для Zig на хабре нет, поставил C++):
const std = @import("std");
const net = std.net;
const posix = std.posix;
pub fn main() !void {
// Адрес нашего сервера
const address = try net.Address.parseIp("127.0.0.1", 8086);
// Тип cокета
const tpe: u32 = posix.SOCK.STREAM;
// Протокол
const protocol = posix.IPPROTO.TCP;
// Создаем сокет
const listener = try posix.socket(address.any.family, tpe, protocol);
// В конце необходимо закрыть сокет
defer posix.close(listener);
}
!void
– аналог Result в Rust, справа от!
указываем тип возвращаемого значения, слева – тип ошибки (его компилятор может вывести самостоятельно)try
– в случае ошибки при вызове метода, прокидывает ее наверх (возвращает в качестве результата метода)defer
– выполняет код при выходе из метода, удобная штука: объявляем ресурс, который требует освобождения, и вместо того, чтобы держать в голове мысль про то, что его надо будет закрыть, сразу пишем соответствующую логику
Прикрепим к адресу и будем ожидать соединения:
pub fn main() !void {
...
// При рестарте сокет освобождается не сразу, разрешаем переиспользовать адрес
try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
// Прикрепляем сокет к адресу
try posix.bind(listener, &address.any, address.getOsSockLen());
// Длина очереди для ожидающий соединений
const backlog = 128;
// Ожидаем соединения
try posix.listen(listener, backlog);
std.debug.print("TCP server started on {}\n", .{address});
}
Так как активных соединений может быть несколько, для каждого будем запускать отдельный поток:
pub fn main() !void {
...
// Адрес клиента
var client_address: net.Address = undefined;
var client_address_len: posix.socklen_t = @sizeOf(net.Address);
while (true) {
// Принимаем новое соединение
const socket = try posix.accept(listener, &client_address.any, &client_address_len, 0);
// Закрываем его, если из-за ошибки не создали отдельный поток
errdefer posix.close(socket);
std.debug.print("TCP {} connected\n", .{client_address});
// Запускаем отдельный поток для каждого соединения
const spawn_config: std.Thread.SpawnConfig = .{};
const thread_function = handleTcpConnection;
const thread_function_args = .{ socket, client_address };
_ = try std.Thread.spawn(spawn_config, thread_function, thread_function_args);
}
}
в
posix.accept
мы передаем указатели на переменные в стеке, куда будет записано соответствующее значение (&
– взять адрес/указатель)@sizeOf
– возвращает количество байт для хранения типа (c@
начинаются специальные встроенные в язык функции)errdefer
– какdefer
, но вызывается только в случае возврата с ошибкой_
– игнорирует возвращаемое значение (Zig требует обрабатывать возвращаемые значения, а если не хочется, то нужно явно это сообщить).{}
– синтаксис создания объектов/структур
Реализуем handleTcpConnection
:
fn handleTcpConnection(socket: posix.socket_t, client_address: net.Address) !void {
// Закрыть соедидение в конце
defer posix.close(socket);
// Буфер для данных
var buf: [128]u8 = undefined;
while (true) {
// Читаем
const read_len = try posix.read(socket, &buf);
// 0 = EOF, выходим из цикла
if (read_len == 0) {
std.debug.print("{} disconnected\n", .{client_address});
break;
}
std.debug.print("TCP read {} bytes\n", .{read_len});
// Отправляем слайс буфера с данными обратно
// Слайс внутри: указатель на массив + длина
const write_len = try posix.write(socket, buf[0..read_len]);
std.debug.print("TCP write {} bytes\n", .{write_len});
}
}
Собираем через zig build
, запускаем бинарник в папке ./zig-oud/bin/
и тестируем:

UDP сервер
С UDP будет проще, так как тем нет соединений:
создать сокет
прикрепить к адресу
получать/отправлять данные
закрыть сокет
Напишем:
fn listenUdp(address: net.Address) !void {
const tpe: u32 = posix.SOCK.DGRAM;
const protocol = posix.IPPROTO.UDP;
const socket = try posix.socket(address.any.family, tpe, protocol);
defer posix.close(socket);
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(socket, &address.any, address.getOsSockLen());
std.debug.print("UDP server started on {}\n", .{address});
var buf: [128]u8 = undefined;
while (true) {
var client_addr: posix.sockaddr = undefined;
var client_addr_len: posix.socklen_t = @sizeOf(posix.sockaddr);
// Получаем UDP пакеты
const read_len = try posix.recvfrom(socket, &buf, 0, &client_addr, &client_addr_len);
std.debug.print("UDP read {} bytes\n", .{read_len});
// Отправляем обратно клиенту
const sendto_len = try posix.sendto(socket, buf[0..read_len], 0, &client_addr, client_addr_len);
std.debug.print("UDP write {} bytes\n", .{sendto_len});
}
}
Чтобы оба сервера работали одновременно, вынесем их в отдельные потоки:
pub fn main() !void {
const address = try net.Address.parseIp("127.0.0.1", 8086);
var tcpServer = try std.Thread.spawn(.{}, listenTcp, .{address});
var udpServer = try std.Thread.spawn(.{}, listenUdp, .{address});
tcpServer.join();
udpServer.join();
}
Код целиком
const std = @import("std");
const net = std.net;
const posix = std.posix;
pub fn main() !void {
const address = try net.Address.parseIp("127.0.0.1", 8086);
var tcpServer = try std.Thread.spawn(.{}, listenTcp, .{address});
var udpServer = try std.Thread.spawn(.{}, listenUdp, .{address});
tcpServer.join();
udpServer.join();
}
fn listenTcp(address: net.Address) !void {
const tpe: u32 = posix.SOCK.STREAM;
const protocol = posix.IPPROTO.TCP;
const listener = try posix.socket(address.any.family, tpe, protocol);
defer posix.close(listener);
try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(listener, &address.any, address.getOsSockLen());
const backlog = 128;
try posix.listen(listener, backlog);
std.debug.print("TCP server started on {}\n", .{address});
var client_address: net.Address = undefined;
var client_address_len: posix.socklen_t = @sizeOf(net.Address);
while (true) {
const socket = try posix.accept(listener, &client_address.any, &client_address_len, 0);
errdefer posix.close(socket);
std.debug.print("TCP {} connected\n", .{client_address});
_ = try std.Thread.spawn(.{}, handleTcpConnection, .{ socket, client_address });
}
}
fn handleTcpConnection(socket: posix.socket_t, client_address: net.Address) !void {
defer posix.close(socket);
var buf: [128]u8 = undefined;
while (true) {
const read_len = try posix.read(socket, &buf);
if (read_len == 0) {
std.debug.print("{} disconnected\n", .{client_address});
break;
}
std.debug.print("TCP read {} bytes\n", .{read_len});
const write_len = try posix.write(socket, buf[0..read_len]);
std.debug.print("TCP write {} bytes\n", .{write_len});
}
}
fn listenUdp(address: net.Address) !void {
const tpe: u32 = posix.SOCK.DGRAM;
const protocol = posix.IPPROTO.UDP;
const socket = try posix.socket(address.any.family, tpe, protocol);
defer posix.close(socket);
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(socket, &address.any, address.getOsSockLen());
std.debug.print("UDP server started on {}\n", .{address});
var buf: [128]u8 = undefined;
while (true) {
var client_addr: posix.sockaddr = undefined;
var client_addr_len: posix.socklen_t = @sizeOf(posix.sockaddr);
const read_len = try posix.recvfrom(socket, &buf, 0, &client_addr, &client_addr_len);
std.debug.print("UDP read {} bytes\n", .{read_len});
const sendto_len = try posix.sendto(socket, buf[0..read_len], 0, &client_addr, client_addr_len);
std.debug.print("UDP write {} bytes\n", .{sendto_len});
}
}
Проверим все вместе:

Сервер работает, но в нем есть некоторые недостатки, например:
сообщение клиента может не поместиться в буфер, который мы передаем при чтении
сообщение сервера может отправиться лишь частично за один вызов write/sendto (с текущим размером буфера для сообщений маловероятно, но все же)
под каждое TCP соединение мы создаем новый поток
Попробуем исправить последний пункт.
Event loop
Что если запускать оба сервера на event loop, который будет вызывать соответствующие callback'и при подключении, чтении, отправки и т.д. Так мы сможем в один поток обрабатывать весь трафик. Подключим библиотеку с event loop для асинхронного IO – libxev.
Устанавливаем необходимую версии zig fetch --save git+https://github.com/mitchellh/libxev#9f785d202ddccf7a625e799250579253977978b6
Обновляем build.zig:
...
const xev = b.dependency("libxev", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("xev", xev.module("xev"));
...
Теперь библиотека доступна в коде: const xev = @import("xev");
Обновим скелет нашей программы:
const std = @import("std");
const net = std.net;
const posix = std.posix;
const xev = @import("xev");
pub fn main() !void {
const address = try net.Address.parseIp("127.0.0.1", 8086);
// Создаем event loop
var loop = try xev.Loop.init(.{});
// Деинициализируем в конце
defer loop.deinit();
// Для регистрации колбэков нам понадобиться аллокатор
// Вызов GeneralPurposeAllocator возвращает тип, через {} создаем объект
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
// Деинициализируем в конце
defer _ = general_purpose_allocator.deinit();
// Получили "интерфейс"
const gpa = general_purpose_allocator.allocator();
// Поднять UDP сервер
// Создать accept callback и зарегистрировать его в event loop
// Поднять UDP сервер
// Создать recv callback и зарегистрировать его в event loop
// Итерируемся в цикле, пока не исполним все callback'и
try loop.run(.until_done);
}
Слово "интерфейс" в коде выше взято в кавычки, потому что в Zig нет интерфейсов, если посмотреть на реализацию метода allocator
, мы увидим следующее:

То есть мы руками создаем таблицу виртуальных методов.
Еще в Zig принято, что любой метод, который аллоцирует память, должен принимать аллокатор аргументом (никаких скрытых аллокаций!). Тем самым по сигнатуре мы можем понять, работает код с памятью или нет, из минусов – надо протаскивать аллокатор через цепочки вызовов. Мне подход нравится. В языке нет GC как в Java/Gо или borrow checker'а как в Rust, так что лучше явно обозначить места, где надо быть особо внимательным, чтобы не поймать undefined behaviour.
Вернемся к делу, поднимем UDP сервер и зарегистрируем callback в event loop:
pub fn main() !void {
...
// Поднять UDP сервер
const udp_socket = ...
// Создать recv callback и зарегистрировать его в event loop
// Аллоцируем память под объект
const udp_recv_data = try gpa.create(UdpRecvData);
// В конце освобождаем
defer gpa.destroy(udp_recv_data);
// Инициализируем callback
udp_recv_data.* = UdpRecvData{
.allocator = gpa,
.buffer = undefined,
.completion = .{
.op = .{
.recvfrom = .{
.fd = udp_socket,
.buffer = .{ .slice = &udp_recv_data.buffer }
},
},
.userdata = udp_recv_data,
.callback = udpRecvCallback
}
};
// Регистрируем в event loop
loop.add(&udp_recv_data.completion);
...
}
// Данные для callback'а
const UdpRecvData = struct {
// аллокатор, чтобы внутри callback'ов мы тоже могли управлять памятью
allocator: std.mem.Allocator,
// буфер для приема данных
buffer: [128]u8,
// абстракция библиотеки xev для работы с callback'ами
completion: xev.Completion,
};
// Callback, который xev вызовет при получении данных по сети
fn udpRecvCallback(
ud: ?*anyopaque,
loop: *xev.Loop,
comp: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
// Получаем данные внутри callback'а
const recv_data = @as(*UdpRecvData, @ptrCast(@alignCast(ud.?)));
...
}
.*
– синтаксис обращения к значению по указателюop
– tagged union, который содержит различные IO операцииuserdata
– поле для передачи кастомных данных в callback'и, при получении необходимо кастить к нужному типуcompletion
содержит указатель на родительский объект, можно обойтись без этого, но тогда придется делать две аллокации: под completion и под userdata.*
– указатель на объект соответствующего типа*anyopaque
– указатель на неизвестный тип данных?
– nullable тип
Полная версия callback'а:
fn udpRecvCallback(
ud: ?*anyopaque,
loop: *xev.Loop,
comp: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
const recv_data = @as(*UdpRecvData, @ptrCast(@alignCast(ud.?)));
const allocator = recv_data.allocator;
const read_len = result.recvfrom catch |e| {
std.debug.print("UDP read error {}\n", .{e});
// больше не вызывать callback
return .disarm;
};
std.debug.print("UDP read {} bytes\n", .{read_len});
const recvfrom = comp.op.recvfrom;
const socket = recvfrom.fd;
const client_address = net.Address.initPosix(@alignCast(&recvfrom.addr));
// Регистрируем callback на запись
const sendto_data = allocator.create(UdpSendtoData) catch |e| {
std.debug.print("UDP allocation error {}\n", .{e});
// больше не вызывать callback
return .disarm;
};
sendto_data.* = UdpSendtoData{
.allocator = allocator,
.buffer = undefined,
.completion = .{
.op = .{
.sendto = .{
.fd = socket,
.addr = client_address,
.buffer = .{
.slice = sendto_data.buffer[0..read_len],
},
},
},
.userdata = sendto_data,
.callback = udpSendtoCallback,
},
};
@memcpy(sendto_data.buffer[0..read_len], recvfrom.buffer.slice[0..read_len]);
loop.add(&sendto_data.completion);
// повторяем callback на чтение
return .rearm;
}
catch
– позволяет сразу обработать возвращаемую методом ошибку
Callback для ответа клиенту:
const UdpSendtoData = struct {
allocator: std.mem.Allocator,
buffer: [128]u8,
completion: xev.Completion,
};
fn udpSendtoCallback(
ud: ?*anyopaque,
_: *xev.Loop,
_: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
const sendto_data = @as(*UdpSendtoData, @ptrCast(@alignCast(ud.?)));
const allocator = sendto_data.allocator;
defer allocator.destroy(sendto_data);
const write_len = result.sendto catch |e| {
std.debug.print("UDP write error {}\n", .{e});
return .disarm;
};
std.debug.print("UDP write {} bytes\n", .{write_len});
return .disarm;
}
Похожая реализация будет и для TCP, правда, немного сложнее из-за управления соединениями, нужно быть внимательнее с аллокациями и закрытием ресурсов (segmentation fault я пару раз словил).
Полная версия для TCP + UDP
const std = @import("std");
const net = std.net;
const posix = std.posix;
const xev = @import("xev");
pub fn main() !void {
const address = try net.Address.parseIp("127.0.0.1", 8086);
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = general_purpose_allocator.deinit();
const gpa = general_purpose_allocator.allocator();
// start TCP server
const tcp_listener = try posix.socket(address.any.family, posix.SOCK.STREAM, posix.IPPROTO.TCP);
defer posix.close(tcp_listener);
try posix.setsockopt(tcp_listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(tcp_listener, &address.any, address.getOsSockLen());
const backlog = 128;
try posix.listen(tcp_listener, backlog);
std.debug.print("TCP server started on {}\n", .{address});
const tcp_accept_data = try gpa.create(TcpAcceptData);
defer gpa.destroy(tcp_accept_data);
tcp_accept_data.* = TcpAcceptData{
.allocator = gpa,
.completion = .{
.op = .{
.accept = .{ .socket = tcp_listener },
},
.userdata = tcp_accept_data,
.callback = tcpAcceptCallback,
},
};
loop.add(&tcp_accept_data.completion);
// start UDP server
const udp_socket = try posix.socket(address.any.family, posix.SOCK.DGRAM, posix.IPPROTO.UDP);
defer posix.close(udp_socket);
try posix.setsockopt(udp_socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(udp_socket, &address.any, address.getOsSockLen());
std.debug.print("UDP server started on {}\n", .{address});
const udp_recv_data = try gpa.create(UdpRecvData);
defer gpa.destroy(udp_recv_data);
udp_recv_data.* = UdpRecvData{
.allocator = gpa,
.buffer = undefined,
.completion = .{
.op = .{
.recvfrom = .{
.fd = udp_socket,
.buffer = .{
.slice = &udp_recv_data.buffer,
},
},
},
.userdata = udp_recv_data,
.callback = udpRecvCallback,
},
};
loop.add(&udp_recv_data.completion);
try loop.run(.until_done);
}
const TcpAcceptData = struct {
allocator: std.mem.Allocator,
completion: xev.Completion,
};
fn tcpAcceptCallback(
ud: ?*anyopaque,
loop: *xev.Loop,
comp: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
const accept_data = @as(*TcpAcceptData, @ptrCast(@alignCast(ud.?)));
const allocator = accept_data.allocator;
const socket = result.accept catch |err| {
std.log.err("TCP accept failed: {}", .{err});
return .disarm;
};
const client_address = net.Address.initPosix(@alignCast(&comp.op.accept.addr));
std.debug.print("TCP {} connected\n", .{client_address});
const read_data = allocator.create(TcpReadData) catch |e| {
std.debug.print("TCP allocation error {}\n", .{e});
posix.close(socket);
return .disarm;
};
read_data.* = TcpReadData{
.allocator = allocator,
.buffer = undefined,
.client_address = client_address,
.completion = .{
.op = .{
.read = .{
.fd = socket,
.buffer = .{
.slice = &read_data.buffer,
},
},
},
.callback = tcpReadCallback,
.userdata = read_data,
},
};
loop.add(&read_data.completion);
return .rearm;
}
const TcpReadData = struct {
allocator: std.mem.Allocator,
buffer: [128]u8,
client_address: net.Address,
completion: xev.Completion,
};
fn tcpReadCallback(
ud: ?*anyopaque,
loop: *xev.Loop,
comp: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
const read_data = @as(*TcpReadData, @ptrCast(@alignCast(ud.?)));
const read = comp.op.read;
const socket = read.fd;
const allocator = read_data.allocator;
const read_len = result.read catch {
std.debug.print("TCP {} disconnected\n", .{read_data.client_address});
posix.close(socket);
allocator.destroy(read_data);
return .disarm;
};
std.debug.print("TCP read {} bytes\n", .{read_len});
// schedule write
const write_data = allocator.create(TcpWriteData) catch |e| {
std.debug.print("TCP allocation error {}\n", .{e});
posix.close(socket);
allocator.destroy(read_data);
return .disarm;
};
write_data.* = TcpWriteData{
.allocator = allocator,
.buffer = undefined,
.completion = .{
.op = .{
.write = .{
.fd = socket,
.buffer = .{
.slice = write_data.buffer[0..read_len],
},
},
},
.userdata = write_data,
.callback = tcpWriteCallback,
},
};
@memcpy(write_data.buffer[0..read_len], read.buffer.slice[0..read_len]);
loop.add(&write_data.completion);
// schedule next read
return .rearm;
}
const TcpWriteData = struct {
allocator: std.mem.Allocator,
buffer: [128]u8,
completion: xev.Completion,
};
fn tcpWriteCallback(
ud: ?*anyopaque,
_: *xev.Loop,
_: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
const write_data = @as(*TcpWriteData, @ptrCast(@alignCast(ud.?)));
const allocator = write_data.allocator;
defer allocator.destroy(write_data);
const write_len = result.write catch |e| {
std.debug.print("TCP write error {}\n", .{e});
return .disarm;
};
std.debug.print("TCP write {} bytes\n", .{write_len});
return .disarm;
}
const UdpRecvData = struct {
allocator: std.mem.Allocator,
buffer: [128]u8,
completion: xev.Completion,
};
fn udpRecvCallback(
ud: ?*anyopaque,
loop: *xev.Loop,
comp: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
const recv_data = @as(*UdpRecvData, @ptrCast(@alignCast(ud.?)));
const allocator = recv_data.allocator;
const read_len = result.recvfrom catch |e| {
std.debug.print("UDP read error {}\n", .{e});
return .disarm;
};
std.debug.print("UDP read {} bytes\n", .{read_len});
const recvfrom = comp.op.recvfrom;
const socket = recvfrom.fd;
const client_address = net.Address.initPosix(@alignCast(&recvfrom.addr));
// schedule write
const sendto_data = allocator.create(UdpSendtoData) catch |e| {
std.debug.print("UDP allocation error {}\n", .{e});
return .disarm;
};
sendto_data.* = UdpSendtoData{
.allocator = allocator,
.buffer = undefined,
.completion = .{
.op = .{
.sendto = .{
.fd = socket,
.addr = client_address,
.buffer = .{
.slice = sendto_data.buffer[0..read_len],
},
},
},
.userdata = sendto_data,
.callback = udpSendtoCallback,
},
};
@memcpy(sendto_data.buffer[0..read_len], recvfrom.buffer.slice[0..read_len]);
loop.add(&sendto_data.completion);
// schedule next read
return .rearm;
}
const UdpSendtoData = struct {
allocator: std.mem.Allocator,
buffer: [128]u8,
completion: xev.Completion,
};
fn udpSendtoCallback(
ud: ?*anyopaque,
_: *xev.Loop,
_: *xev.Completion,
result: xev.Result,
) xev.CallbackAction {
const sendto_data = @as(*UdpSendtoData, @ptrCast(@alignCast(ud.?)));
const allocator = sendto_data.allocator;
defer allocator.destroy(sendto_data);
const write_len = result.sendto catch |e| {
std.debug.print("UDP write error {}\n", .{e});
return .disarm;
};
std.debug.print("UDP write {} bytes\n", .{write_len});
return .disarm;
}
Заключение
Подобного рода сервер – хороший вариант проекта для ознакомления с языком. Потрогали значительную часть конструкций, да и в системное API заглянули. Zig – простой с точки зрения синтаксиса низкоуровневый язык. Он дает ограниченный набор понятных инструментов, этакий «DSL для создания машинного кода».
Ссылки:
код проекта на github
материалы для обучения: на английском и на русском
популярные проекты на Zig: Ghostty, Bun, TigerBeetle (ютуб канал команды)
блог одного из разработчиков TigerBeetle
рекомендую также взглянуть на мета-программирование в Zig