├── Fiber.zig ├── LICENSE ├── README.md ├── impls ├── aarch64.zig └── ucontext.zig └── tests.zig /Fiber.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const Fiber = @import("Fiber.zig"); 5 | 6 | /// There is no reason to do this other than 7 | /// a lack of arch support - please don't use this 8 | const use_ucontext = std.meta.globalOption("oatz_use_ucontext", bool) orelse false; 9 | 10 | const impl = if (use_ucontext) @import("impls/ucontext.zig") else switch (builtin.cpu.arch) { 11 | .aarch64, .aarch64_be, .aarch64_32 => @import("impls/aarch64.zig"), 12 | else => @compileError("Not supported; please open an issue or see oatz_use_context"), 13 | }; 14 | 15 | pub const Error = impl.Error; 16 | 17 | allocator: std.mem.Allocator, 18 | context: impl.Context, 19 | 20 | fn resolveFnType(comptime T: type) type { 21 | return switch (@typeInfo(T)) { 22 | .Fn => T, 23 | .Pointer => |ptr| { 24 | if (ptr.size != .One or @typeInfo(ptr.child) != .Fn) 25 | return resolveFnType(void); 26 | 27 | return resolveFnType(ptr.child); 28 | }, 29 | else => @compileError("Fiber `func` must be a function or single-pointer to a function!"), 30 | }; 31 | } 32 | 33 | pub fn create( 34 | allocator: std.mem.Allocator, 35 | stack_size: usize, 36 | func: anytype, 37 | args: anytype, 38 | ) Error!*Fiber { 39 | const T = @TypeOf(func); 40 | if (@typeInfo(T) == .Fn) return create(allocator, stack_size, &func, args); 41 | 42 | const Fn = resolveFnType(T); 43 | const funcInfo = @typeInfo(Fn).Fn; 44 | 45 | if (funcInfo.return_type == null or 46 | comptime for (funcInfo.params) |param| 47 | (if (param.type == null) 48 | break true) 49 | else 50 | false) @compileError("Fiber `func` cannot be generic!"); 51 | 52 | if (funcInfo.return_type.? != void) @compileError("Fibers must not return anything (" ++ @typeName(funcInfo.return_type.?) ++ " != void)"); 53 | 54 | var fiber = try allocator.create(Fiber); 55 | fiber.allocator = allocator; 56 | try impl.init(Fn, fiber, stack_size, func, args); 57 | 58 | return fiber; 59 | } 60 | 61 | pub fn destroy(fiber: *Fiber) void { 62 | impl.deinit(fiber.allocator, &fiber.context); 63 | fiber.allocator.destroy(fiber); 64 | } 65 | 66 | // NOTE: NEVER MODIFY THIS PLEASE USER 67 | pub threadlocal var current: ?*Fiber = null; 68 | 69 | pub fn switchTo(fiber: *Fiber) Error!void { 70 | const s = current; 71 | current = fiber; 72 | 73 | try impl.switchTo(fiber.allocator, &fiber.context); 74 | 75 | current = s; 76 | } 77 | 78 | pub fn yield(fiber: *Fiber) void { 79 | impl.yield(fiber.allocator, &fiber.context); 80 | } 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Auguste Rame 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oatz 2 | 3 | Light fiber(s). 4 | 5 | Very WIP. 6 | -------------------------------------------------------------------------------- /impls/aarch64.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Fiber = @import("../Fiber.zig"); 3 | 4 | comptime { 5 | asm ( 6 | \\.global _oatz_arm64_switchTo 7 | \\_oatz_arm64_switchTo: 8 | \\ stp d15, d14, [sp, #-22*8]! 9 | \\ stp d13, d12, [sp, #2*8] 10 | \\ stp d11, d10, [sp, #4*8] 11 | \\ stp d9, d8, [sp, #6*8] 12 | \\ stp x30, x29, [sp, #8*8] 13 | \\ stp x28, x27, [sp, #10*8] 14 | \\ stp x26, x25, [sp, #12*8] 15 | \\ stp x24, x23, [sp, #14*8] 16 | \\ stp x22, x21, [sp, #16*8] 17 | \\ stp x20, x19, [sp, #18*8] 18 | \\ stp x0, x1, [sp, #20*8] 19 | \\ 20 | \\ mov x19, sp 21 | \\ str x19, [x0, #0] 22 | \\ mov x19, #1 23 | \\ str x19, [x0, #8] 24 | \\ 25 | \\ mov x19, #0 26 | \\ str x19, [x1, #8] 27 | \\ ldr x19, [x1, #0] 28 | \\ mov sp, x19 29 | \\ 30 | \\ ldp x0, x1, [sp, #20*8] 31 | \\ ldp x20, x19, [sp, #18*8] 32 | \\ ldp x22, x21, [sp, #16*8] 33 | \\ ldp x24, x23, [sp, #14*8] 34 | \\ ldp x26, x25, [sp, #12*8] 35 | \\ ldp x28, x27, [sp, #10*8] 36 | \\ ldp x30, x29, [sp, #8*8] 37 | \\ ldp d9, d8, [sp, #6*8] 38 | \\ ldp d11, d10, [sp, #4*8] 39 | \\ ldp d13, d12, [sp, #2*8] 40 | \\ ldp d15, d14, [sp], #22*8 41 | \\ 42 | \\ mov x16, x30 43 | \\ mov x30, #0 44 | \\ br x16 45 | ); 46 | } 47 | 48 | pub const Error = error{ 49 | Unexpected, 50 | OutOfMemory, 51 | NotResumable, 52 | }; 53 | 54 | pub const Context = struct { 55 | stack: []const usize, 56 | caller_ctx: InternalContext, 57 | fiber_ctx: InternalContext, 58 | 59 | func_and_args: *anyopaque, 60 | }; 61 | 62 | pub const InternalContext = extern struct { 63 | stack_top: *anyopaque, 64 | resumable: c_long, 65 | registers: [20]usize = undefined, 66 | }; 67 | 68 | pub extern fn oatz_arm64_switchTo(current_context: *InternalContext, new_context: *InternalContext) void; 69 | 70 | pub fn init( 71 | comptime Fn: type, 72 | fiber: *Fiber, 73 | stack_size: usize, 74 | func: anytype, 75 | args: anytype, 76 | ) Error!void { 77 | const allocator = fiber.allocator; 78 | 79 | const Args = @TypeOf(args); 80 | const WrappedCaller = struct { 81 | func: *const Fn, 82 | args: Args, 83 | 84 | fn wrappedFunc(fib: *Fiber) void { 85 | const wc = @ptrCast(*@This(), @alignCast(@alignOf(@This()), fib.context.func_and_args)); 86 | const wcd = wc.*; 87 | 88 | fib.allocator.destroy(wc); 89 | @call(.auto, wcd.func, wcd.args); 90 | 91 | fib.yield(); 92 | } 93 | }; 94 | 95 | var wrapped_caller = try allocator.create(WrappedCaller); 96 | wrapped_caller.* = .{ 97 | .func = func, 98 | .args = args, 99 | }; 100 | 101 | var stack = try allocator.allocWithOptions(usize, @divTrunc(stack_size, @sizeOf(usize)), 16, null); 102 | 103 | var caller_ctx: InternalContext = undefined; 104 | var fiber_ctx = InternalContext{ 105 | .stack_top = stack.ptr + stack.len - 22, 106 | .resumable = 1, 107 | }; 108 | 109 | stack[stack.len - 2] = @ptrToInt(fiber); 110 | stack[stack.len - 14] = @ptrToInt(&WrappedCaller.wrappedFunc); 111 | 112 | fiber.context = .{ 113 | .stack = stack, 114 | .caller_ctx = caller_ctx, 115 | .fiber_ctx = fiber_ctx, 116 | 117 | .func_and_args = wrapped_caller, 118 | }; 119 | } 120 | 121 | pub fn deinit(allocator: std.mem.Allocator, ctx: *Context) void { 122 | allocator.free(ctx.stack); 123 | } 124 | 125 | pub fn switchTo(allocator: std.mem.Allocator, ctx: *Context) Error!void { 126 | _ = allocator; 127 | if (ctx.fiber_ctx.resumable == 0) 128 | return error.NotResumable; 129 | oatz_arm64_switchTo(&ctx.caller_ctx, &ctx.fiber_ctx); 130 | } 131 | 132 | pub fn yield(allocator: std.mem.Allocator, ctx: *Context) void { 133 | _ = allocator; 134 | if (ctx.caller_ctx.resumable == 0) 135 | @panic("Caller not resumable!"); 136 | oatz_arm64_switchTo(&ctx.fiber_ctx, &ctx.caller_ctx); 137 | } 138 | -------------------------------------------------------------------------------- /impls/ucontext.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Fiber = @import("../Fiber.zig"); 3 | 4 | const c = @cImport({ 5 | @cDefine("_XOPEN_SOURCE", ""); 6 | @cInclude("ucontext.h"); 7 | }); 8 | 9 | const ucontext_t = c.ucontext_t; 10 | extern "c" fn getcontext(ucp: *ucontext_t) callconv(.C) c_int; 11 | extern "c" fn makecontext(ucp: *ucontext_t, func: *const anyopaque, argc: c_int, ...) callconv(.C) void; 12 | extern "c" fn swapcontext(oucp: *ucontext_t, ucp: *ucontext_t) callconv(.C) c_int; 13 | 14 | pub const Error = error{ 15 | Unexpected, 16 | OutOfMemory, 17 | }; 18 | 19 | pub const Context = struct { 20 | oucp: *ucontext_t, 21 | ucp: *ucontext_t, 22 | 23 | args: *anyopaque, 24 | free: *const fn (allocator: std.mem.Allocator, *anyopaque) void, 25 | 26 | stack: []const u8, 27 | }; 28 | 29 | pub fn init( 30 | comptime Fn: type, 31 | fiber: *Fiber, 32 | stack_size: usize, 33 | func: anytype, 34 | args: anytype, 35 | ) Error!void { 36 | const allocator = fiber.allocator; 37 | 38 | var ucp = try allocator.create(ucontext_t); 39 | var oucp = try allocator.create(ucontext_t); 40 | 41 | var gc_err = std.c.getErrno(getcontext(ucp)); 42 | if (gc_err != .SUCCESS) { 43 | return std.os.unexpectedErrno(gc_err); 44 | } 45 | 46 | var stack = try allocator.alloc(u8, stack_size); 47 | 48 | ucp.uc_stack.ss_sp = stack.ptr; 49 | ucp.uc_stack.ss_size = stack.len; 50 | ucp.uc_stack.ss_flags = 0; 51 | ucp.uc_link = oucp; 52 | 53 | const Args = std.meta.ArgsTuple(Fn); 54 | 55 | var allocated_args = try allocator.create(Args); 56 | allocated_args.* = args; 57 | 58 | // We have to use a variety of awful hacks here because libc sux 59 | // (main issue is that it only supports 32-bit values) 60 | 61 | // TODO: Support 32-bit systems 62 | const run = struct { 63 | fn r(func_0: u32, func_1: u32, args_0: u32, args_1: u32) callconv(.C) void { 64 | var func_01 = @ptrCast(*const Fn, @alignCast(@alignOf(Fn), @intToPtr(*const anyopaque, @bitCast(usize, [2]u32{ func_0, func_1 })))); 65 | var args_01 = @intToPtr(*const Args, @bitCast(usize, [2]u32{ args_0, args_1 })); 66 | 67 | @call(.auto, func_01, args_01.*); 68 | } 69 | }.r; 70 | 71 | const us = @typeInfo(usize).Int.bits; 72 | var func_01 = @bitCast([us / 32]u32, @ptrToInt(@ptrCast(*const anyopaque, func))); 73 | var args_01 = @bitCast([us / 32]u32, @ptrToInt(allocated_args)); 74 | 75 | makecontext(ucp, &run, 4, func_01[0], func_01[1], args_01[0], args_01[1]); 76 | 77 | fiber.context = .{ 78 | .oucp = oucp, 79 | .ucp = ucp, 80 | .stack = stack, 81 | .args = allocated_args, 82 | .free = &struct { 83 | fn free(all: std.mem.Allocator, op: *anyopaque) void { 84 | all.destroy(@ptrCast(*Args, @alignCast(@alignOf(Args), op))); 85 | } 86 | }.free, 87 | }; 88 | } 89 | 90 | pub fn deinit(allocator: std.mem.Allocator, ctx: *Context) void { 91 | allocator.destroy(ctx.oucp); 92 | allocator.destroy(ctx.ucp); 93 | ctx.free(allocator, ctx.args); 94 | allocator.free(ctx.stack); 95 | } 96 | 97 | pub fn switchTo(allocator: std.mem.Allocator, ctx: *Context) Error!void { 98 | _ = allocator; 99 | var sc_err = std.c.getErrno(swapcontext(ctx.oucp, ctx.ucp)); 100 | if (sc_err != .SUCCESS) { 101 | return std.os.unexpectedErrno(sc_err); 102 | } 103 | } 104 | 105 | pub fn yield(allocator: std.mem.Allocator, ctx: *Context) void { 106 | _ = allocator; 107 | var sc_err = std.c.getErrno(swapcontext(ctx.ucp, ctx.oucp)); 108 | if (sc_err != .SUCCESS) { 109 | std.debug.panic("Could not resume caller; {any}", .{sc_err}); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Fiber = @import("Fiber.zig"); 3 | 4 | fn hello(value: *usize, other_fiber: *Fiber) void { 5 | std.testing.expectEqual(value.*, @as(usize, 5)) catch @panic("Fail"); 6 | other_fiber.switchTo() catch @panic("switchTo failed!"); 7 | std.testing.expectEqual(value.*, @as(usize, 10)) catch @panic("Fail"); 8 | Fiber.current.?.yield(); 9 | std.testing.expectEqual(value.*, @as(usize, 17)) catch @panic("Fail"); 10 | other_fiber.switchTo() catch @panic("switchTo failed!"); 11 | other_fiber.switchTo() catch @panic("switchTo failed!"); 12 | other_fiber.switchTo() catch @panic("switchTo failed!"); 13 | } 14 | 15 | fn hello2(value: *usize) void { 16 | value.* += 5; 17 | Fiber.current.?.yield(); 18 | Fiber.current.?.yield(); 19 | Fiber.current.?.yield(); 20 | } 21 | 22 | test { 23 | const allocator = std.testing.allocator; 24 | 25 | var val: usize = 0; 26 | 27 | var fiber_2 = try Fiber.create(allocator, 16_384, hello2, .{&val}); 28 | defer fiber_2.destroy(); 29 | 30 | var fiber = try Fiber.create(allocator, 16_384, hello, .{ &val, fiber_2 }); 31 | defer fiber.destroy(); 32 | 33 | val = 5; 34 | try fiber.switchTo(); 35 | val += 7; 36 | try fiber.switchTo(); 37 | } 38 | --------------------------------------------------------------------------------