diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f430fe3 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,20 @@ +name: Build and Deploy +on: [push] +permissions: + contents: write +jobs: + build-and-deploy: + concurrency: ci-${{ github.ref }} + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v4 + - name: Install and Build 🔧 + run: | + cd docs + npm install + npm run build + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: docs/dist diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..2a6deef --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +dist +node_modules +_lib +tsconfig.tsbuildinfo +tsconfig.*.tsbuildinfo +vocs.config.ts.timestamp-* +.vercel +.vocs diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..ae7aa80 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,17 @@ +{ + "name": "zig-aio", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vocs dev", + "build": "vocs build", + "preview": "vocs preview" + }, + "dependencies": { + "@types/react": "latest", + "react": "latest", + "react-dom": "latest", + "typescript": "latest", + "vocs": "latest" + } +} diff --git a/docs/pages/aio-dynamic.mdx b/docs/pages/aio-dynamic.mdx new file mode 100644 index 0000000..365d0a6 --- /dev/null +++ b/docs/pages/aio-dynamic.mdx @@ -0,0 +1,49 @@ +# AIO API + +## Dynamic IO + +In case the amount of IO operations isn't known ahead of time the dynamic api can be used. + +### Initializing Dynamic instance + +Creating a Dynamic instance requires an allocator and upper bound for non-completed operations. +The instance allocates only during the `init`, and frees the memory during `deinit`. +Same allocator must be used in `deinit` that was used in `init`. + +```zig +const max_operations = 32; +var work = try Dynamic.init(std.heap.page_allocator, max_operations); +defer work.deinit(std.heap.page_allocator); +``` + +### Queuing operations + +It is possible to queue either single or multiple operations just like with the immediate api. +The call to queue is atomic, if the call fails, none of the operations will be actually performed. + +```zig +// Multiple operations +try work.queue(.{ + aio.Read{...}, + aio.Write{...}, + aio.Fsync{...}, +}); + +// Single operation +try work.queue(aio.Timeout{...}); +``` + +### Completing operations + +It is possible to complete the operations either in blocking or non-blocking fashion. +The blocking mode will wait for at least one operation to complete. +The non-blocking always returns immediately even if no operations were completed. +The call to complete returns `aio.CompletionResult` containing the number of operations that were completed +and the number of errors that occured. + +```zig +// blocks until at least 1 operation completes +const res = try work.complete(.blocking); +// returns immediately +const res = try work.complete(.nonblocking); +``` diff --git a/docs/pages/aio-immediate.mdx b/docs/pages/aio-immediate.mdx new file mode 100644 index 0000000..b5ac3d8 --- /dev/null +++ b/docs/pages/aio-immediate.mdx @@ -0,0 +1,62 @@ +# AIO API + +## Immediate IO + +For immediate blocking IO, `zig-aio` provides the following functions in the `aio` module. + +### Perform a single operation + +Completes a single operation, the call blocks until it's complete. +Returns error of the operation if the operation failed. +Returns `void` if there was no error. + +```zig +try aio.single(aio.Write{.file = f, .buffer = "contents"}); +``` + +### Perform multiple operations + +`zig-aio` provides two methods for batching IO operations. + +#### Using multi + +Completes a list of operations immediately, blocks until complete +Returns `error.SomeOperationFailed` if any operation failed +Returns `void` if there were no errors. + +```zig +var my_buffer: [1024]u8 = undefined; +var my_len: usize = undefined; + +try aio.multi(.{ + aio.Write{.file = f, .buffer = "contents", .link_next = true}, + aio.Read{.file = f, .buffer = &my_buffer, .out_read = &my_len}, +}); +``` + +The `.link_next` field of operation can be used to link the operation to the next operation. +When linking operations, the next operation won't start until the previous operation is complete. + +#### Using batch + +Batch is similar to multi, but it will not return `error.SomeOperationFailed` in case any of the operations fail. +Instead batch returns `aio.CompletionResult` which contains the number of operations that was completed, and number of +errors that occured. To find out which operations failed, errors have to be stored somewhere by setting the `.out_error` +field of the operation. The batch call may still fail in implementation defined ways, such as running out of system resources. + +```zig +var my_buffer: [1024]u8 = undefined; +var my_len: usize = undefined; +var write_error: std.posix.WriteError = undefined; +var read_error: std.posix.ReadError = undefined; + +const res = try aio.batch(.{ + aio.Write{.file = f, .buffer = "contents", .out_error = &write_error, .link_next = true}, + aio.Read{.file = f, .buffer = &my_buffer, .out_error = &read_error, .out_read = &my_len}, +}); + +if (res.num_errors > 0) { + if (write_error != error.Success) @panic("write failed"); + if (read_error != error.Success) @panic("read failed"); +} +``` diff --git a/docs/pages/aio-operations.mdx b/docs/pages/aio-operations.mdx new file mode 100644 index 0000000..da9ff11 --- /dev/null +++ b/docs/pages/aio-operations.mdx @@ -0,0 +1,262 @@ +# AIO API + +## Operations + +A handful of IO operations are supported. + +### Common fields + +Every operation supports these common fields. + +```zig +const Counter = union(enum) { + inc: *u16, + dec: *u16, + nop: void, +}; + +out_id: ?*Id = null, +out_error: ?*(E || SharedError) = null, +counter: Counter = .nop, +link_next: bool = false, +``` + +If `out_id` is set, the id of the operation will be written into that address. +The `id` can then be used in future operations to refer to this operation. +If `out_error` is set, the error of the operation will be written into that address, in case the operation failed. +If there was no failure a `error.Success` will be store in that address. +`counter` can be used to set either decreasing or increasing counter. +When operation completes it will either decrease or increase the `u16` stored at the address. +`link_next` can be used to link the next operation into this operation. +When operations are linked, the next operation won't start until this operation has completed first. + +### Fsync + +Synchronizes the contents of a `file` onto the disk. + +```zig +pub const Fsync = Define(struct { + file: std.fs.File, +}, std.fs.File.SyncError); +``` + +### Read + +Reads a `file` into a `buffer` from a `offset`. +The amount of bytes read is stored in the location pointed by `out_read`. + +```zig +pub const Read = Define(struct { + file: std.fs.File, + buffer: []u8, + offset: u64 = 0, + out_read: *usize, +}, std.fs.File.ReadError); +``` + +### Write + +Writes contents of `buffer` from `offset` into a `file`. +The amount of bytes written is stored in the location pointed by `out_written`. + +```zig +pub const Write = Define(struct { + file: std.fs.File, + buffer: []const u8, + offset: u64 = 0, + out_written: ?*usize = null, +}, std.fs.File.WriteError); +``` + +### Accept + +See `man accept(2)` + +```zig +pub const Accept = Define(struct { + socket: std.posix.socket_t, + addr: ?*sockaddr = null, + inout_addrlen: ?*std.posix.socklen_t = null, + out_socket: *std.posix.socket_t, +}, std.posix.AcceptError); +``` + +### Connect + +See `man connect(2)` + +```zig +pub const Connect = Define(struct { + socket: std.posix.socket_t, + addr: *const sockaddr, + addrlen: std.posix.socklen_t, +}, std.posix.ConnectError); +``` + +### Recv + +See `man recv(2)` + +```zig +pub const Recv = Define(struct { + socket: std.posix.socket_t, + buffer: []u8, + out_read: *usize, +}, std.posix.RecvFromError); +``` + +### Send + +See `man send(2)` + +```zig +pub const Send = Define(struct { + socket: std.posix.socket_t, + buffer: []const u8, + out_written: ?*usize = null, +}, std.posix.SendError); +``` + +### OpenAt + +Opens `path` relative to a `dir`, opening is customized by `flags`. +The opened file is stored into the location pointed by `out_file`. + +```zig +pub const OpenAt = Define(struct { + dir: std.fs.Dir, + path: [*:0]const u8, + flags: std.fs.File.OpenFlags, + out_file: *std.fs.File, +}, std.fs.File.OpenError); +``` + +### Close + +Closes a `file`. + +```zig +pub const Close = Define(struct { + file: std.fs.File, +}, error{}); +``` + +### Timeout + +Starts a timeout. Once the timeout expires the operation completes. +The timeout uses a monotnic clock source. + +```zig +pub const Timeout = Define(struct { + ts: struct { sec: i64 = 0, nsec: i64 = 0 }, +}, error{}); +``` + +### TimeoutRemove + +Cancel existing timeout referenced by `id`. + +```zig +pub const TimeoutRemove = Define(struct { + id: Id, +}, error{ InProgress, NotFound }); +``` + +### LinkTimeout + +Timeout linked to a operation. +The operation before must have set `link_next` to `true`. +If the operation finishes before the timeout, then the timeout will be canceled. +If the timeout finishes before the operation, then the operation will be canceled. + +```zig +pub const LinkTimeout = Define(struct { + ts: struct { sec: i64 = 0, nsec: i64 = 0 }, + out_expired: ?*bool = null, +}, error{InProgress}); +``` + +### Cancel + +Cancel existing operation referenced by `id`. + +```zig +pub const Cancel = Define(struct { + id: Id, +}, error{ InProgress, NotFound }); +``` + +### RenameAt + +Rename a `old_path` relative to `old_dir` into `new_path` relative to `new_dir`. + +```zig +pub const RenameAt = Define(struct { + old_dir: std.fs.Dir, + old_path: [*:0]const u8, + new_dir: std.fs.Dir, + new_path: [*:0]const u8, +}, std.fs.Dir.RenameError); +``` + +### UnlinkAt + +Delete a file or directory locating in `path` relative to `dir`. + +```zig +pub const UnlinkAt = Define(struct { + dir: std.fs.Dir, + path: [*:0]const u8, +}, std.posix.UnlinkatError); +``` + +### MkDirAt + +Create directory relative to `dir` at `path`. +The `mode` parameter can specify the mode of the directory on supporting operating systems. + +```zig +pub const MkDirAt = Define(struct { + dir: std.fs.Dir, + path: [*:0]const u8, + mode: u32 = std.fs.Dir.default_mode, +}, std.fs.Dir.MakeError); +``` + +### SymlinkAt + +Create a symlink relative to `dir` at `link_path` linking to the `target`. + +```zig +pub const SymlinkAt = Define(struct { + dir: std.fs.Dir, + target: [*:0]const u8, + link_path: [*:0]const u8, +}, std.posix.SymLinkError); +``` + +### Socket + +See `man socket(2)` + +```zig +pub const Socket = Define(struct { + /// std.posix.AF + domain: u32, + /// std.posix.SOCK + flags: u32, + /// std.posix.IPPROTO + protocol: u32, + out_socket: *std.posix.socket_t, +}, std.posix.SocketError); +``` + +### CloseSocket + +Closes a `socket`. + +```zig +pub const CloseSocket = Define(struct { + socket: std.posix.socket_t, +}, error{}); +``` diff --git a/docs/pages/coro-context-switches.mdx b/docs/pages/coro-context-switches.mdx new file mode 100644 index 0000000..9d933cc --- /dev/null +++ b/docs/pages/coro-context-switches.mdx @@ -0,0 +1,23 @@ +# CORO API + +:::warning + +This part of the API is likely to change. + +::: + + +## Context switches + +To yield running task to the caller use the following. + +```zig +coro.yield(); +``` + +To continue running the task from where it left, use the following. +This can also be used to cancel any IO operations. + +```zig +coro.wakeup(); +``` diff --git a/docs/pages/coro-io.mdx b/docs/pages/coro-io.mdx new file mode 100644 index 0000000..639a463 --- /dev/null +++ b/docs/pages/coro-io.mdx @@ -0,0 +1,27 @@ +# CORO API + +## IO + +Inside a task it is possible to use the IO functions inside the `coro.io` namespace to perform cooperative +IO with the `Scheduler`. When calling a `coro.io` operation from a task, the task setups some internal state, +queues the IO operations for `Scheduler` and then yields, allowing other code to run in the program. + +All the IO operations are merged into one `aio.Dynamic` instance for completition during the next scheduler tick. +While this may not be beneficial on all backends, the io_uring backend allows the kernel to execute all +the yielding tasks IO operations with a single syscall. + +### Performing io operations + +Performing operations is similar to `aio` module. The api is the same, but instead use the `coro.io` namespace. +Below is a full example of simple server / client program using the `coro` api. + +```zig +// [!include ~/../examples/coro.zig] +``` + +### Cancellations + +Use `aio.Cancel` operation to cancel the currently running operations in a task. +The `out_error` of such operation will then be set as `error.OperationCanceled`. + +Alternatively it's possible to call `scheduler.wakeup(task);` which also cancels all currently running io on that task. diff --git a/docs/pages/coro-scheduler.mdx b/docs/pages/coro-scheduler.mdx new file mode 100644 index 0000000..04b320e --- /dev/null +++ b/docs/pages/coro-scheduler.mdx @@ -0,0 +1,67 @@ +# CORO API + +## Scheduler + +To do a non-blocking IO while still maintaining the imperative blocking style of coding. +We need a coroutines and a scheduler that schedules the context switches of said coroutines. +In this guide we refer to the coroutines as tasks. + +### Instanting a Scheduler + +Scheduler requires an `allocator` and optional `InitOptions`. +The scheduler stores the `allocator` and uses it for managed task creation and destruction. + +```zig +var scheduler = try coro.Scheduler.init(gpa.allocator(), .{}); +defer scheduler.deinit(); +``` + +### Spawning tasks + +A new task can be spawned by specifying `entrypoint`, which must be a function with either `void` or `!void` return type. +If the function is `!void` and it returns error, a stacktrace of the error is dumped, similarly to how `std.Thread` api works. +Supply arguments to the `entrypoint` by providing a tuple as the second parameter. +For third parameter, spawn takes a optional `SpawnOptions`, which can be used to specify the stack size of the Task, or +provide a pre-allocated unmanaged task. + +When task is spawned, the `entrypoint` is immediately called and the code in the `entrypoint` runs until the task either +yields or performs a IO operation using one of the `coro.io` namespace functions. + +```zig +var task = try scheduler.spawn(entrypoint, .{ 1, "args" }, .{}); +``` + +### Reaping tasks + +Following removes a task, frees its memory and cancels all potential running IO operations. + +```zig +scheduler.reap(task); +``` + +Alternatively reap all the tasks using the following. + +```zig +scheduler.reapAll(); +``` + +Call to `deinit` also reaps all tasks. + +### Running + +The scheduler can process tasks and io one step a time with the tick method. +By running tick the scheduler will reap tasks that returned (dead tasks) and context switch back to the +tasks in case they completed their IO operations. + +```zig +// if there are pending io operations, blocks until at least one completes +try scheduler.tick(.blocking); +// returns immediately regardless of the current io state +try scheduler.tick(.nonblocking); +``` + +To run the scheduler until all tasks have returned aka died, then use the following. + +```zig +try scheduler.run(); +``` diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx new file mode 100644 index 0000000..245989a --- /dev/null +++ b/docs/pages/index.mdx @@ -0,0 +1,21 @@ +--- +title: 'zig-aio: io_uring like asynchronous API and coroutine powered IO tasks for zig' +--- + +# Overview + +zig-aio provides io_uring like asynchronous API and coroutine powered IO tasks for zig + +```zig +// [!include ~/../examples/aio_static.zig] +``` + +## Features + +- Blocking and asynchronous API +- Atomic operations +- Parallel execution +- Cancellation +- Timeouts +- Comes with a runtime and scheduler for coroutines +- Tightly tied into io_uring diff --git a/docs/pages/integration.mdx b/docs/pages/integration.mdx new file mode 100644 index 0000000..96b80d0 --- /dev/null +++ b/docs/pages/integration.mdx @@ -0,0 +1,34 @@ +# Integrating zig-aio + +## Zig Package Manager + +### Fetching and updating the zig-aio dependency + +Run the following command in zig project root directory. + +```sh +zig fetch --save git+https://github.com/Cloudef/zig-aio.git +``` + +### Using zig-aio modules in zig project + +In `build.zig` file add the following for whichever modules `zig-aio` is required. + +```zig +// get the "zig-aio" dependency from "build.zig.zon" +const zig_aio = b.dependency("zig-aio", .{}); +// for exe, lib, tests, etc. +exe.root_module.addImport("aio", zig_aio.module("aio")); +// for coroutines api +exe.root_module.addImport("coro", zig_aio.module("coro")); +``` + +### Using zig-aio in zig code + +It's possible to import the modules like this. + +```zig +const aio = @import("aio"); +const coro = @import("coro"); +``` + diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000..d2636aa --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/docs/vocs.config.tsx b/docs/vocs.config.tsx new file mode 100644 index 0000000..b513b51 --- /dev/null +++ b/docs/vocs.config.tsx @@ -0,0 +1,81 @@ +import { defineConfig } from 'vocs' +import plainText from 'vite-plugin-virtual-plain-text'; + +export default defineConfig({ + title: 'zig-aio', + titleTemplate: '%s - zig-aio', + description: 'IO-uring like asynchronous API and coroutine powered IO tasks for zig', + editLink: { + pattern: 'https://github.com/Cloudef/zig-aio/edit/master/docs/pages/:path', + text: 'Suggest changes to this page', + }, + rootDir: '.', + socials: [ + { + icon: "github", + link: "https://github.com/Cloudef/zig-aio", + }, + ], + head: ( + <> + + + + ), + topNav: [ + { text: "Docs", link: "/" }, + { + text: "Examples", + link: "https://github.com/Cloudef/zig-aio/tree/master/examples", + }, + ], + sidebar: [ + { + text: 'Getting Started', + items: [{text: 'Overview', link: '/'}], + }, + { + text: 'Integration', + items: [{text: 'Integrating zig-aio', link: '/integration'}], + }, + { + text: 'AIO API', + collapsed: false, + items: [ + { + text: 'Operations', + link: '/aio-operations', + }, + { + text: 'Immediate', + link: '/aio-immediate', + }, + { + text: 'Dynamic', + link: '/aio-dynamic', + }, + ], + }, + { + text: 'CORO API', + collapsed: false, + items: [ + { + text: 'Scheduler', + link: '/coro-scheduler', + }, + { + text: 'IO', + link: '/coro-io', + }, + { + text: 'Context switches', + link: '/coro-context-switches', + }, + ], + }, + ], +})