Skip to content

Instantly share code, notes, and snippets.

@ikouchiha47
Last active November 12, 2024 11:09
Show Gist options
  • Save ikouchiha47/f7e4c3b2dac0f1371b5ae92f948fe78f to your computer and use it in GitHub Desktop.
Save ikouchiha47/f7e4c3b2dac0f1371b5ae92f948fe78f to your computer and use it in GitHub Desktop.
WIP fiber
// Implements:
// fibers as lightweight concurrency control
//
// Source:
// https://cyp.sh/blog/coroutines-in-c
// https://agraphicsguynotes.com/posts/fiber_in_cpp_understanding_the_basics/
// https://github.com/SuperAuguste/oatz/blob/main/impls/aarch64.zig
// https://raw.githubusercontent.com/wiki/hjl-tools/x86-psABI/x86-64-psABI-1.0.pdf
const std = @import("std");
const builtin = @import("builtin");
const sctx = @import("stackcontent.zig");
const assert = std.debug.assert;
pub const log_level: std.log.Level = .debug;
pub const Error = error{ StackTooSmall, StackTooLarge, OutOfMemory };
threadlocal var tls_state: ?*State = null;
const InitialStackSize = 2 * 1024; // 2 KB
const MaxStackSize = 1 * 1024 * 1024; // 1 MB
pub const Fiber = struct {
arena: std.heap.ArenaAllocator,
pub fn init(allocator: std.mem.Allocator, user_data: usize, comptime func: anytype, args: anytype) Error!*Fiber {
const stack_size = InitialStackSize; // 2 KB stack
const stack = try allocator.alloc(u8, stack_size);
std.debug.print("backend {}", .{builtin.zig_backend});
const Args = @TypeOf(args);
const state = try State.init(stack, user_data, @sizeOf(Args), struct {
fn entry() callconv(.C) noreturn {
const state = tls_state orelse unreachable;
// Call the functions with the args.
const args_ptr: *align(1) Args = @ptrFromInt(@intFromPtr(state) - @sizeOf(Args));
@call(.auto, func, args_ptr.*);
// if (state.stack_start + 128 > state.stack_end) {
// std.debug.print("growing out of stack space", .{});
// }
__zig_fiber_stack_swap(&state.stack_ctx, &state.caller_ctx);
unreachable;
}
}.entry);
const args_ptr: *align(1) Args = @ptrFromInt(@intFromPtr(state) - @sizeOf(Args));
args_ptr.* = args;
return @ptrCast(state);
}
fn grow_stack(state: *State, new_stack_size: usize) Error!*[]u8 {
const allocator = std.heap.page_allocator;
const new_stack = try allocator.alloc(u8, new_stack_size);
const old_stack_start = state.stack_ptr;
const old_stack_size = state.stack_end - old_stack_start;
std.mem.copy(u8, new_stack, old_stack_start, old_stack_size);
return new_stack;
}
pub inline fn current() ?*Fiber {
return @ptrCast(tls_state);
}
pub fn getStack(fiber: *Fiber) []u8 {
const state: *State = @ptrCast(@alignCast(fiber));
const offset: u8 = @truncate(state.offset);
const stack_end = @intFromPtr(state) + offset;
const stack_base = stack_end - (state.offset >> @bitSizeOf(u8));
const addrs: [*]u8 = @ptrCast(stack_base);
return addrs[0..(stack_end - stack_base)];
}
pub fn getUserDataPtr(fiber: *Fiber) *usize {
const state: *State = @ptrCast(@alignCast(fiber));
return &state.user_data;
}
pub fn switchTo(fiber: *Fiber) void {
const state: *State = @ptrCast(@alignCast(fiber));
const old_state = tls_state;
assert(old_state != state);
tls_state = state;
defer tls_state = old_state;
std.debug.print("Switching stack \n", .{});
__zig_fiber_stack_swap(&state.caller_ctx, &state.stack_ctx);
}
/// Switches the current thread's execution back to the most recent switchTo() called on the currently running fiber.
/// Calling yield from outside a fiber context (`current() == null`) is illegal behavior.
/// Once execution is yielded back, switchTo() on the (now previous) current fiber can be called again
/// to continue the fiber from this yield point.
pub fn yield() void {
const state = tls_state orelse unreachable;
std.debug.print("state {}", .{state});
__zig_fiber_stack_swap(&state.stack_ctx, &state.caller_ctx);
}
};
const State = extern struct {
caller_ctx: *anyopaque,
stack_ctx: *anyopaque,
user_data: usize,
offset: usize,
stack_start: ?*u8,
stack_size: usize,
// Each fiber context has a stack associated
// Fiber's can be suspended, which means we
// need to be able to store the store in memory
//
// [---- Stack ----]
// [(start)---Stack----(end)----(rest)]
// [(start)---Stack----(end)----(state_size)--(args size)]
//
// The Stack grows downward, so end is at top, start is bottom
fn init(stack: []u8, user_data: usize, args_size: usize, entry: *const fn () callconv(.C) noreturn) Error!*State {
const stack_start = @intFromPtr(stack.ptr);
const stack_end = @intFromPtr(stack.ptr + stack.len);
if (stack.len > (std.math.maxInt(usize) >> @bitSizeOf(u8))) {
return Error.StackTooLarge;
}
var sp = stack_end - @sizeOf(State);
sp = std.mem.alignBackward(usize, sp, @alignOf(State));
if (sp < stack_start) {
return Error.StackTooSmall;
}
const state: *State = @ptrFromInt(sp);
const end = stack_end - sp;
sp = sp - args_size;
if (sp < stack_start) {
return Error.StackTooSmall;
}
sp = std.mem.alignBackward(usize, sp, 16);
if (sp < stack_start) {
return Error.StackTooSmall;
}
sp = sp - (@sizeOf(usize) * sctx.StackContext.word_count);
assert(std.mem.isAligned(sp, @alignOf(usize)));
if (sp < stack_start) {
return Error.StackTooSmall;
}
const entry_ptr: [*]@TypeOf(entry) = @ptrFromInt(sp);
entry_ptr[sctx.StackContext.entry_offset] = entry;
const stack_pointer: *u8 = &stack.ptr[0];
state.* = .{
.caller_ctx = undefined,
.stack_ctx = @ptrFromInt(sp),
.user_data = user_data,
.stack_start = stack_pointer,
.stack_size = stack.len,
.offset = (stack.len << @bitSizeOf(u8) | end),
};
return state;
}
};
extern fn __zig_fiber_stack_swap(
noalias current_context_ptr: **anyopaque,
noalias new_context_ptr: **anyopaque,
) void;
test "test fiber initialization" {
var val: usize = 0;
const user_data = 42;
const args = .{&val};
const func = test_fiber_func;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const fiber = try Fiber.init(allocator, user_data, func, args);
const user_data_ptr = fiber.getUserDataPtr();
try std.testing.expect(user_data_ptr.* == 42);
const state: *State = @ptrCast(@alignCast(fiber));
try std.testing.expect(state.stack_start != null);
try std.testing.expect(state.stack_size == InitialStackSize);
}
test "test fiber switching" {
var arena1 = std.heap.ArenaAllocator.init(std.testing.allocator);
var arena2 = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena1.deinit();
defer arena2.deinit();
const allocator1 = arena1.allocator();
const allocator2 = arena2.allocator();
var value1: usize = 69;
var value2: usize = 69;
const fiber1 = try Fiber.init(allocator1, 0, fiber_func1, .{&value1});
const fiber2 = try Fiber.init(allocator2, 0, fiber_func1, .{&value2});
const state1: *State = @ptrCast(@alignCast(fiber1));
const state2: *State = @ptrCast(@alignCast(fiber2));
std.log.warn("cpu arch {}, tag {}, sc {}\n", .{ builtin.cpu.arch, builtin.os.tag, sctx.StackContext });
try std.testing.expect(state1.stack_start != state2.stack_start);
fiber2.switchTo();
// Fiber.switchTo(fiber2);
// Fiber.switchTo(fiber1);
// try std.testing.expect(state1.stack_start != null);
// try std.testing.expect(state2.stack_start != null);
//
try std.testing.expect(value1 == 41);
try std.testing.expect(value2 == 101);
}
fn fiber_func1(value: *usize) void {
// std.debug.print("Inside fiber_func1 {}\n", .{args});
//
// const typed_args: [*]usize = @ptrCast(args);
// const value_ptr = &typed_args[0];
//
// std.debug.print("typed_args {}\n", .{value_ptr});
//
// value_ptr.* = 42;
// // Fiber.yield();
// std.log.err("Resuming fiber_func1\n", .{});
std.debug.print("fiber_func1 running with args: {}\n", .{value});
Fiber.yield();
}
fn fiber_func2(args: anytype) void {
// std.debug.print("Inside fiber_func2 {}\n", .{args});
//
// const typed_args: [*]usize = @ptrCast(args);
// const value_ptr = typed_args[0];
//
// std.debug.print("typed_args {}\n", .{value_ptr});
//
// // value_ptr = 102;
// // Fiber.yield();
//
// std.log.err("Resuming fiber_func2\n", .{});
std.debug.print("fiber_func2 running with args: {}\n", .{args});
Fiber.yield();
}
fn test_fiber_func(args: anytype) void {
std.debug.print("args {}", .{args});
}
^^/d/zsp >>> zig test src/fiber.zig 13:29:06
backend builtin.CompilerBackend.stage2_llvmbackend builtin.CompilerBackend.stage2_llvmbackend builtin.CompilerBackend.stage2_llvm[default] (warn): cpu arch Target.Cpu.Arch.x86_64, tag Target.Os.Tag.linux, sc stackcontent.Intel_SysV
Switching stack
error: the following test command crashed:
/home/darksied/dev/zsp/.zig-cache/o/a5045fda8002fa1d3485e48ce58c5867/test
^^/d/zsp >>> zig build 16:36:32
^^/d/zsp >>> gdb zig-out/bin/zsp 16:36:59
GNU gdb (Gentoo 14.2 vanilla) 14.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.gentoo.org/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from zig-out/bin/zsp...
(gdb) run
Starting program: /home/darksied/dev/zsp/zig-out/bin/zsp
backend builtin.CompilerBackend.stage2_llvmbackend builtin.CompilerBackend.stage2_llvm
Switching stack
Program received signal SIGSEGV, Segmentation fault.
0x0000000001077744 in io.GenericWriter(fs.File,error{AccessDenied,Unexpected,DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer},(function 'write')).print ()
at /home/darksied/.asdf/installs/zig/0.13.0/lib/std/io.zig:324
324 return @errorCast(self.any().print(format, args));
(gdb) bt
#0 0x0000000001077744 in io.GenericWriter(fs.File,error{AccessDenied,Unexpected,DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer},(function 'write')).print ()
at /home/darksied/.asdf/installs/zig/0.13.0/lib/std/io.zig:324
#1 debug.print__anon_8427 (args=...) at /home/darksied/.asdf/installs/zig/0.13.0/lib/std/debug.zig:97
#2 0x0000000001068c49 in main.fiber_func1 (value=0x7fffffffd870) at main.zig:35
#3 0x000000000103a64d in fiber.Fiber.init__anon_3042__struct_3551.entry () at fiber.zig:42
#4 0xaaaaaaaaaaaaaaaa in ?? ()
#5 0x00007fffffffd870 in ?? ()
#6 0x00007fffffffd738 in ?? ()
#7 0x00007ffff7ff7798 in ?? ()
#8 0x0000000000000000 in ?? ()
(gdb) info registers
rax 0x7ffff7ff7740 140737354102592
rbx 0xaaaaaaaaaaaaaaaa -6148914691236517206
rcx 0x2 2
rdx 0x7ffff7ff7780 140737354102656
rsi 0x7ffff7ff7710 140737354102544
rdi 0x7ffff7ff7714 140737354102548
rbp 0x7ffff7ff7768 0x7ffff7ff7768
rsp 0x7ffff7ff75b8 0x7ffff7ff75b8
r8 0x12 18
r9 0x7ffff7ff77e8 140737354102760
r10 0x7ffff7ff7798 140737354102680
r11 0x216 534
r12 0xaaaaaaaaaaaaaaaa -6148914691236517206
r13 0xaaaaaaaaaaaaaaaa -6148914691236517206
r14 0xaaaaaaaaaaaaaaaa -6148914691236517206
r15 0xaaaaaaaaaaaaaaaa -6148914691236517206
rip 0x1077744 0x1077744 <debug.print__anon_8427+228>
eflags 0x10202 [ IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
fs_base 0x10dd018 17682456
gs_base 0x0 0
const builtin = @import("builtin");
const std = @import("std");
pub const StackContext = switch (builtin.cpu.arch) {
.aarch64 => @compileError("cpu architecture not supported"),
.x86_64 => switch (builtin.os.tag) {
.windows => @compileError("cpu architecture not supported"),
else => Intel_SysV,
},
else => @compileError("cpu architecture not supported"),
};
const Intel_Microsoft = struct {
pub const word_count = 31;
pub const entry_offset = word_count - 1;
comptime {
asm (
\\.global __zig_fiber_stack_swap
\\__zig_fiber_stack_swap:
\\ pushq %gs:0x10
\\ pushq %gs:0x08
\\
\\ pushq %rbx
\\ pushq %rbp
\\ pushq %rdi
\\ pushq %rsi
\\ pushq %r12
\\ pushq %r13
\\ pushq %r14
\\ pushq %r15
\\
\\ TODO:
\\
\\ popq %r15
\\ popq %r14
\\ popq %r13
\\ popq %r12
\\ popq %rsi
\\ popq %rdi
\\ popq %rbp
\\ popq %rbx
\\
\\ popq %gs:0x08
\\ popq %gs:0x10
\\
\\ retq
);
}
};
const Intel_SysV = struct {
pub const word_count = 7;
pub const entry_offset = word_count - 1;
comptime {
asm (
\\.global __zig_fiber_stack_swap
\\__zig_fiber_stack_swap:
\\ pushq %rbx
\\ pushq %rbp
\\ pushq %r12
\\ pushq %r13
\\ pushq %r14
\\ pushq %r15
\\
\\ movq %rsp, (%rdi)
\\ movq (%rsi), %rsp
\\
\\ popq %r15
\\ popq %r14
\\ popq %r13
\\ popq %r12
\\ popq %rbp
\\ popq %rbx
\\
\\ retq
);
}
};
const Arm_64 = struct {
pub const word_count = 20;
pub const entry_offset = 0;
comptime {
asm (
\\.global ___zig_fiber_stack_swap
\\___zig_fiber_stack_swap:
\\ ret
);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment