Lindenii Project Forge
Login

hare-aio

Asynchronous I/O event loops for Hare

Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!

Commit info
ID
5ad8541d347b92d64b73ab4bcb35ff6939f3d76a
Author
Runxi Yu <me@runxiyu.org>
Author date
Thu, 02 Oct 2025 04:42:10 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Thu, 02 Oct 2025 04:49:26 +0800
Actions
Initialize aio::
use linux::io_uring;

// Flags for when an I/O operation is completed.
export type completion_flags = io_uring::cqe_flags;
use errors;

// There are no available submission queue entries on the submission queue.
// You should probably create a larger loop.
export type full = !void;

// Buffer pool use was configured for a [[submission]], but there are no buffers
// available.
export type nobuffers = !void;

// All errors which may be returned by this module.
export type error = !(errors::error | nobuffers | full);
use errors;
use linux::io_uring;
use io;

// An asynchronous event loop.
export type loop = struct {
	// The underlying io_uring for this event loop.
	io_uring: io_uring::io_uring,
};

// Creates a new event loop. The user must pass the return value to [[finish]]
// to free associated resources when done using the loop.
//
// The optional "entries" parameter controls how many submission/completion
// queue entries the submission/completion queues could hold. Most applications
// should not need to configure this parameter.
export fn loop_new(entries: u32 = 4096) (loop | error) = {
	let params = io_uring::ring_params {
		...
	};

	let ring = io_uring::ring_init(entries, &params)!; // TODO

	return loop {
		io_uring = ring,
	};
};

// Frees resources associated with an event loop. Must only be called once per
// event loop object. Invalidates all buffers and other objects associated with
// the event loop.
export fn loop_finish(loop: *loop) void = {
	io_uring::ring_exit(&loop.io_uring);
};

// Submit the queued I/O asynchronously. Returns the number of submissions
// accepted by the kernel.
export fn loop_submit(loop: *loop) (uint | error) = {
	match (io_uring::ring_submit(&loop.io_uring)) {
	case let n: uint =>
		return n;
	case let e: errors::error =>
		return e;
	};
};

// Dispatches the event loop, waiting for new events and calling their callbacks
// as appropriate.
export fn loop_run(loop: *loop) void = { // TODO: return type?
	for (true) {
		let cqe = match(io_uring::cqe_wait(&loop.io_uring)) {
		case errors::interrupted => continue;
		case errors::busy => abort("NODROP issues... handle later");
		case errors::exists => abort("thread submitting work is invalid but we arent doing threads?");
		case let c: *io_uring::cqe => yield c;
		};

		let ctx = match (io_uring::cqe_get_data(cqe)) {
		case null => abort("context must not be null");
		case let o: *opaque => yield o;
		};

		let ctx = ctx: *op_ctx;

		match (*ctx) {
		case let c: op_read_ctx =>
			let res = match (io_uring::cqe_result(cqe)) {
			case let e: errors::error => yield e: io::error;
			case let n: int =>
				yield switch (n) {
				case 0 => yield io::EOF;
				case => yield n: size;
				};
			};
			c.callback(loop, c.file, res, cqe.flags: completion_flags, c.user);
		case void =>
			abort("my cat walked through the door");
		};

		free(ctx);
		io_uring::cqe_seen(loop, cqe);
	};
};
// An operation context.
//
// Because we need to retreive the file descriptor, user pointer, callbacks, and
// other information related to each SQE when they come back as CQEs, we must
// encapsulate these in our own "context" structs. But each operation's context
// is different yet could not be identified without looking into user data
// itself, and we also need a way to distinguish between different types
// of events; so here's a tagged union thereof.
type op_ctx = (op_read_ctx | void); // TODO: replace void with actual other ctx's
use io;
use linux::io_uring;

// The callback type for [[op_read]].
export type op_read_cb = fn(loop: *loop, file: io::file, r: (size | io::EOF | io::error), flags: completion_flags, user: nullable *opaque) void;

// An I/O request context for [[op_read]].
type op_read_ctx = struct {
	callback: *op_read_cb,
	user: nullable *opaque,
	file: io::file,
};

// Prepares an I/O read request.
export fn op_read(
	loop: *loop,
	file: io::file,
	buf: []u8,
	offs: u64,
	callback: *op_read_cb,
	user: nullable *opaque,
	flags: submission_flags
) (*submission | full | nomem) = {
	let sqe = match (io_uring::ring_get_sqe(&loop.io_uring)) {
	case null =>
		return full;
	case let s: *io_uring::sqe =>
		yield s;
	};
	io_uring::op_read(sqe, file: int, *(&buf: **opaque), len(buf), offs, flags: io_uring::sqe_flags);
	let ctx: op_ctx = op_read_ctx {
		callback = callback,
		user = user,
		file = file,
	};
	io_uring::sqe_set_data(sqe, alloc(ctx)?);
	return sqe: *submission;
};
use linux::io_uring;

// An SQE.
export type submission = io_uring::sqe;

// An SQE's lfags
export type submission_flags = io_uring::sqe_flags;
use aio;
use fmt;
use io;
use os;

let buf32: [32]u8 = [0...];

let buf: []u8 = buf32[..];

let ofs: size = 0;

export fn main() void = {
	let loop = aio::loop_new()!;

	let file = os::open("main.ha")!;

	aio::op_read(&loop, file, buf, ofs, &on_read, null, aio::submission_flags::NONE)!;
	aio::loop_submit(&loop)!;

	aio::loop_run(&loop);

	// loop_finish(&loop);
};

fn on_read(
	loop: *aio::loop,
	file: io::file,
	r: (size | io::EOF | io::error),
	flags: aio::completion_flags,
	user: nullable *opaque,
) void = {
	match (r) {
	case io::EOF => abort("EOF");
	case io::error => abort(io::strerror(r as io::error));
	case size => void;
	};

	io::write(os::stdout, buf[.. r as size])!;
	ofs += r as size;
	aio::op_read(loop, file, buf, ofs, &on_read, null, aio::submission_flags::NONE)!; // TODO: use multishot instead? or does that not work for files
	aio::loop_submit(loop)!;
};