Simple solution to read a byte with timeout
This is not really a project showcase, it’s a short utility snipped I came up with while trying to solve an issue I had and I felt that sharing it might be of use to someone.
I recently had to use nanomodbus, a modbus protocol library written in C. To use it, you have to implement essentially two functions: readSerial, writeSerial, which accept a timeout parameter. This is a pattern common enough in such libraries. Well, the naive implementation of using a reader does not work, as it does not accept a timeout parameter, and I found the alternative of spawning a separate task with io.concurrent() and starting a timer for each byte to be a bit overkill.
To solve this I reached for the Io.Operation api, which turned out to be surprisingly simple. I tested the below implementation on linux, but it should work on any other operating system.
const std = @import("std");
const Io = std.Io;
pub fn readByteTimeout(io: Io, file: Io.File, duration: Io.Duration) Io.OperateTimeoutError!u8 {
var ret: [1]u8 = undefined;
const duration_w_clock: Io.Clock.Duration = .{
.clock = .awake,
.raw = duration,
};
const operation: Io.Operation = .{ .file_read_streaming = .{
.file = file,
.data = &.{&ret},
} };
const result = try io.operateTimeout(operation, .{ .duration = duration_w_clock });
std.log.debug("{any}", .{result});
return ret[0];
}
pub fn writeByteTimeout(io: Io, file: Io.File, duration: Io.Duration, byte: u8) Io.OperateTimeoutError!void {
var tmp: [1]u8 = .{byte};
const duration_w_clock: Io.Clock.Duration = .{
.clock = .awake,
.raw = duration,
};
const operation: Io.Operation = .{ .file_write_streaming = .{
.file = file,
.data = &.{&tmp},
} };
const result = try io.operateTimeout(operation, .{ .duration = duration_w_clock });
std.log.debug("{any}", .{result});
}
test readByteTimeout {
if (comptime @import("builtin").os.tag != .linux) {
return error.SkipZigTest;
}
const io = std.testing.io;
var fd: [2]i32 = undefined;
_ = std.os.linux.pipe2(&fd, .{ .NONBLOCK = false });
defer _ = std.os.linux.close(fd[0]);
defer _ = std.os.linux.close(fd[1]);
const read_file: Io.File = .{ .handle = fd[0], .flags = .{ .nonblocking = false } };
const result = readByteTimeout(io, read_file, .fromMilliseconds(1));
try std.testing.expectError(error.Timeout, result);
}
test writeByteTimeout {
if (comptime @import("builtin").os.tag != .linux) {
return error.SkipZigTest;
}
const io = std.testing.io;
var fd: [2]i32 = undefined;
_ = std.os.linux.pipe2(&fd, .{ .NONBLOCK = false });
defer _ = std.os.linux.close(fd[0]);
defer _ = std.os.linux.close(fd[1]);
const write_file: Io.File = .{ .handle = fd[1], .flags = .{ .nonblocking = false } };
// this should succeed
try writeByteTimeout(io, write_file, .fromMilliseconds(100), 0xAB);
}
test "roundtrip" {
if (comptime @import("builtin").os.tag != .linux) {
return error.SkipZigTest;
}
const io = std.testing.io;
var fd: [2]i32 = undefined;
_ = std.os.linux.pipe2(&fd, .{ .NONBLOCK = false });
defer _ = std.os.linux.close(fd[0]);
defer _ = std.os.linux.close(fd[1]);
const read_file: Io.File = .{ .handle = fd[0], .flags = .{ .nonblocking = false } };
const write_file: Io.File = .{ .handle = fd[1], .flags = .{ .nonblocking = false } };
try writeByteTimeout(io, write_file, .fromMilliseconds(100), 0xAB);
const byte = try readByteTimeout(io, read_file, .fromMilliseconds(100));
try std.testing.expectEqual(@as(u8, 0xAB), byte);
}
Then in the nanomodbus interface implementation:
pub fn read_serial(buf: [*c]u8, count: u16, byte_timeout_ms: i32, arg: ?*anyopaque) callconv(.c) i32 {
const ctx: *NMBS = @ptrCast(@alignCast(arg));
var bytes_read: u16 = 0;
while (bytes_read < count) : (bytes_read += 1) {
buf[bytes_read] = util.readByteTimeout(ctx.io, ctx.tty, .fromMilliseconds(byte_timeout_ms)) catch |err| {
std.log.err("nmbs: read_serial: readByteTimeout: error: {t}", .{err});
break;
};
}
return @intCast(bytes_read);
}
I hope someone will find this to be useful, as I’ve found the Io.Operation documentation to be otherwise a bit lacking. If you know of a simpler way on how to do this then I’ll gladly hear about it, because I really feel like there has to be a better way to do this.