diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d3a226c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + strategy: + fail-fast: false + # fiber only supports x86_64 (System V ABI and Windows). GitHub's macOS + # runners are ARM64 and the context switch is x86_64-only, so macOS is + # intentionally omitted from the matrix. + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + - name: Build + run: zig build + + - name: Test + run: zig build test diff --git a/CHANGELOG.md b/CHANGELOG.md index 7db0fe3..b2fcb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `examples/basic.zig` and `examples/scheduler.zig`, runnable via + `zig build examples`. +- GitHub Actions CI building and testing on Linux and Windows (x86_64). + +### Removed + +- `src/main.zig`; the demo now lives under `examples/`. + ## [0.1.0] - 2026-07-22 Initial release. diff --git a/README.md b/README.md index a4ac775..134b927 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # fiber +[![CI](https://github.com/itsakeyfut/fiber/actions/workflows/ci.yml/badge.svg)](https://github.com/itsakeyfut/fiber/actions/workflows/ci.yml) [![Zig](https://img.shields.io/badge/zig-0.16.0-orange.svg)](https://ziglang.org/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -78,10 +79,11 @@ pub fn main() !void { } ``` -A runnable version lives in [`src/main.zig`](src/main.zig): +Runnable, expanded versions live in [`examples/`](examples/) — a single-fiber +walkthrough and a small cooperative scheduler over several fibers: ```sh -zig build run +zig build examples ``` ## API diff --git a/build.zig b/build.zig index b18540f..3d6a6d7 100644 --- a/build.zig +++ b/build.zig @@ -10,23 +10,30 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - const exe = b.addExecutable(.{ - .name = "fiber-demo", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "fiber", .module = fiber_mod }, - }, - }), - }); - b.installArtifact(exe); + // Examples: `zig build examples` builds and runs each program in examples/. + const example_step = b.step("examples", "Build and run the examples"); + const examples = [_][]const u8{ "basic", "scheduler" }; + for (examples) |name| { + const exe = b.addExecutable(.{ + .name = name, + .root_module = b.createModule(.{ + .root_source_file = b.path(b.fmt("examples/{s}.zig", .{name})), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "fiber", .module = fiber_mod }, + }, + }), + }); + b.installArtifact(exe); - const run_exe = b.addRunArtifact(exe); - const run_step = b.step("run", "Run the demo"); - run_step.dependOn(&run_exe.step); + const run_example = b.addRunArtifact(exe); + run_example.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_example.addArgs(args); + example_step.dependOn(&run_example.step); + } + // Tests: `zig build test`. const tests = b.addTest(.{ .root_module = fiber_mod }); const run_tests = b.addRunArtifact(tests); const test_step = b.step("test", "Run unit tests"); diff --git a/build.zig.zon b/build.zig.zon index ab98895..ed12767 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -74,6 +74,7 @@ "build.zig", "build.zig.zon", "src", + "examples", "LICENSE", "README.md", "CHANGELOG.md", diff --git a/examples/basic.zig b/examples/basic.zig new file mode 100644 index 0000000..e4aede2 --- /dev/null +++ b/examples/basic.zig @@ -0,0 +1,37 @@ +//! Basic fiber usage. +//! +//! Create a single fiber, resume it repeatedly, and watch how it suspends at +//! each `yield` and continues from exactly where it left off the next time it +//! is resumed. Run with `zig build examples`. + +const std = @import("std"); +const fiber = @import("fiber"); +const Fiber = fiber.Fiber; + +fn counter(_: *Fiber) void { + var i: usize = 0; + while (i < 3) : (i += 1) { + std.debug.print(" fiber: tick {d}\n", .{i}); + Fiber.yield(); // freeze here; control returns to the caller + } + std.debug.print(" fiber: finished\n", .{}); +} + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + + const f = try Fiber.create(gpa, &counter); + defer f.destroy(); + + std.debug.print("main: created fiber (state={s})\n", .{@tagName(f.state)}); + + var resumes: usize = 0; + while (f.state != .done) { + resumes += 1; + std.debug.print("main: resume #{d}\n", .{resumes}); + f.resumeFiber(); + std.debug.print("main: back in main (state={s})\n", .{@tagName(f.state)}); + } + + std.debug.print("main: fiber done after {d} resumes\n", .{resumes}); +} diff --git a/examples/scheduler.zig b/examples/scheduler.zig new file mode 100644 index 0000000..6448ff9 --- /dev/null +++ b/examples/scheduler.zig @@ -0,0 +1,65 @@ +//! A tiny cooperative scheduler. +//! +//! Several fibers share a single OS thread. Each runs until it voluntarily +//! yields, and the scheduler round-robins over them until every fiber finishes. +//! Their output interleaves — this is the core of a cooperative task system, +//! the kind of thing a game engine drives once per frame. Run with +//! `zig build examples`. + +const std = @import("std"); +const fiber = @import("fiber"); +const Fiber = fiber.Fiber; + +/// Build a worker entry point that logs `steps` progress lines under `name`, +/// yielding after each one. Each fiber needs its own entry function, so we +/// generate one per task at comptime. +fn makeWorker(comptime name: []const u8, comptime steps: usize) *const fn (*Fiber) void { + return &struct { + fn run(_: *Fiber) void { + var i: usize = 0; + while (i < steps) : (i += 1) { + std.debug.print(" [{s}] step {d}/{d}\n", .{ name, i + 1, steps }); + Fiber.yield(); + } + std.debug.print(" [{s}] finished\n", .{name}); + } + }.run; +} + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + + const entries = [_]*const fn (*Fiber) void{ + makeWorker("A", 3), + makeWorker("B", 5), + makeWorker("C", 2), + }; + + var fibers: [entries.len]*Fiber = undefined; + var created: usize = 0; + errdefer for (fibers[0..created]) |f| f.destroy(); + for (&fibers, entries) |*slot, entry| { + slot.* = try Fiber.create(gpa, entry); + created += 1; + } + defer for (fibers) |f| f.destroy(); + + std.debug.print("scheduler: running {d} fibers\n", .{fibers.len}); + + // Round-robin: each round gives every still-running fiber one turn, until + // they have all reached `.done`. + var round: usize = 0; + var remaining: usize = fibers.len; + while (remaining > 0) { + round += 1; + std.debug.print("-- round {d} --\n", .{round}); + remaining = 0; + for (fibers) |f| { + if (f.state == .done) continue; + f.resumeFiber(); + if (f.state != .done) remaining += 1; + } + } + + std.debug.print("scheduler: all fibers finished in {d} rounds\n", .{round}); +} diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index e7a6f62..0000000 --- a/src/main.zig +++ /dev/null @@ -1,26 +0,0 @@ -const std = @import("std"); -const fiber = @import("fiber"); -const Fiber = fiber.Fiber; - -fn worker(_: *Fiber) void { - var i: usize = 0; - while (i < 3) : (i += 1) { - std.debug.print(" fiber: {d}\n", .{i}); - Fiber.yield(); // Yield control back to the main fiber - } - std.debug.print(" fiber: finished\n", .{}); -} - -pub fn main() !void { - const allocator = std.heap.page_allocator; - - const f = try Fiber.create(allocator, &worker); - defer f.destroy(); - - std.debug.print("main: start\n", .{}); - while (f.state != .done) { - std.debug.print("main: resuming fiber\n", .{}); - f.resumeFiber(); - } - std.debug.print("main: done\n", .{}); -}