From d3811e70be433ee8473674911dda00feed65998f Mon Sep 17 00:00:00 2001 From: Adrian Gjerstad Date: Thu, 26 Mar 2026 11:02:18 -0400 Subject: [PATCH 1/3] Add basic Fiber implementation Current implementation is not covered by tests, and is not suitable for submission to the mainline. The implemented Fiber class acts as a higher-level wrapper around ConfigureStack and SwitchStack, supporting functions that work closer to how a developer familiar with writing code that runs in fibers would expect it. --- fibers/BUILD | 11 ++++ fibers/fiber.cc | 107 +++++++++++++++++++++++++++++++++++++ fibers/fiber.h | 139 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 fibers/fiber.cc create mode 100644 fibers/fiber.h diff --git a/fibers/BUILD b/fibers/BUILD index d6e0430..75da64e 100644 --- a/fibers/BUILD +++ b/fibers/BUILD @@ -22,6 +22,17 @@ load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") +cc_library( + name = "fiber", + srcs = ["fiber.cc"], + hdrs = ["fiber.h"], + deps = [ + ":stackarena", + ":stackswitch", + ], + visibility = ["//visibility:public"], +) + cc_library( name = "stackarena", srcs = select({ diff --git a/fibers/fiber.cc b/fibers/fiber.cc new file mode 100644 index 0000000..f57c094 --- /dev/null +++ b/fibers/fiber.cc @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2026 Adrian Gjerstad. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ----------------------------------------------------------------------------- +// loom/fibers/fiber.cc +// ----------------------------------------------------------------------------- +// +// Implementation details of loom::Fiber. +// + +#include "fibers/fiber.h" + +#include "fibers/stackarena.h" +#include "fibers/stackswitch.h" + +namespace loom { + +namespace { + +thread_local Fiber* current_fiber = nullptr; + +} + +Fiber::~Fiber() { + if (arena_ == nullptr) { + return; + } + + state_ = Fiber::State::kDead; + stack_ = nullptr; + task_ = nullptr; + sp_ = nullptr; + yield_sp_ = nullptr; + arena_->Release(stack_); +} + +void Fiber::Reap(Fiber* fiber) { + if (fiber->state_ != Fiber::State::kSuspended && + fiber->state_ != Fiber::State::kDead) { + // Do not reap a fiber that is already running. + return; + } + + fiber->~Fiber(); +} + +Fiber* Fiber::GetCurrentFiber() { + return current_fiber; +} + +void Fiber::Jump() { + // Configure this thread's environment for running this fiber + current_fiber = this; + state_ = Fiber::State::kRunning; + + // Perform the context switch + loom::SwitchStack(sp_, &yield_sp_); + + // Fiber has yielded back, reset the environment + if (state_ == Fiber::State::kRunning) { + // We only want to update this state if the fiber still has work to do. If + // the fiber returns, state will already be marked kDead by the time we get + // here. + state_ = Fiber::State::kSuspended; + } + current_fiber = nullptr; +} + +void Fiber::YieldBack() { + loom::SwitchStack(yield_sp_, &sp_); +} + +Fiber::Fiber(StackArena* arena, void* stack) : arena_(arena), stack_(stack), + state_(Fiber::State::kSuspended) { + sp_ = loom::ConfigureStack(stack_, arena_->stack_size(), Fiber::EntryPoint, + this); + yield_sp_ = nullptr; +} + +void Fiber::EntryPoint(void* fiber_ptr) { + auto* fiber = static_cast(fiber_ptr); + + // Run task + fiber->task_(); + + // Prevent returning from this function at all costs. + while (true) { + fiber->state_ = Fiber::State::kDead; + fiber->YieldBack(); + } +} + +} + diff --git a/fibers/fiber.h b/fibers/fiber.h new file mode 100644 index 0000000..bb06ff7 --- /dev/null +++ b/fibers/fiber.h @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2026 Adrian Gjerstad. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ----------------------------------------------------------------------------- +// loom/fibers/fiber.h +// ----------------------------------------------------------------------------- +// +// Any good fiber-driven application needs a concept of fibers to operate. That +// is what this file is for. It defines a Fiber object. +// + +#ifndef LOOM_FIBERS_FIBER_H_ +#define LOOM_FIBERS_FIBER_H_ + +#include + +#include "fibers/stackarena.h" + +namespace loom { + +// loom::Fiber +// +// A single unit of execution within Loom, cooperatively scheduled with many +// thousands of other fibers at any given time. These are good to use for any +// tasks that are primarily I/O bound, such as socket handling. +class Fiber { + public: + // Copying is not allowed. We want to avoid having a fiber running twice + // simultaneously on the same stack. + Fiber(const Fiber& other) = delete; + Fiber& operator=(const Fiber& other) = delete; + + // Not moveable. Fibers must only be allocated at the top of the stack they + // control. + Fiber(Fiber&& other) = delete; + Fiber& operator=(Fiber&& other) = delete; + + // A list of different states that a Fiber can be in. + enum class State { + kUnknown = 0, + kRunning, // Fiber is actively running + kSuspended, // Fiber is not actively running but can be ;) + kDead, // Fiber has no more code to execute + }; + + // Virtual to allow for mocks and fakes. + virtual ~Fiber(); + + // Creates a new loom::Fiber, getting a stack from the given StackArena and + // configuring it so that it is ready to run. Takes a variadic argument list + // for ease of use. Fiber is allocated on the stack itself, so you must + // remember to `Reap()` it. + // + // Fails if the stack could not be allocated. + template + static absl::StatusOr Create(StackArena* arena, F&& entry, + Args&&... args) { + // Allocate a stack + auto stack_or = arena->Lease(); + if (!stack_or.ok()) { + return stack_or.status(); + } + + // Place the Fiber object itself at the top of the stack. + void* stack = stack_or.value(); + + void* fiber_ptr = reinterpret_cast( + (reinterpret_cast(stack) + arena->stack_size() + - sizeof(Fiber)) & ~(0xF)); + Fiber* fiber = new(fiber_ptr) Fiber(arena, stack); + + fiber->task_ = [f = std::forward(entry), + tup = std::make_tuple(std::forward(args)...)]() + mutable { + std::apply(f, tup); + }; + + return fiber; + } + + // Cleans up a finished loom::Fiber. Destructor is not called automatically + // because of where the Fiber is allocated. Use of the fiber after a call to + // this function is equivalent to use-after-free. + static void Reap(Fiber* fiber); + + // Obtains the pointer to the currently running Fiber. Undefined behavior if + // this function is called outside of a Fiber. + static Fiber* GetCurrentFiber(); + + // Suspends execution of the currently-executing thread/fiber and jumps into + // this fiber for execution. Execution returns to the caller once the fiber + // yields. MUST NOT be called from within this fiber, but can be called from + // within other fibers. + virtual void Jump(); + + // Yields execution of the current Fiber, back to whoever called into it last + // via Jump(). MUST ONLY be called from within this fiber. + // + // The proper call to this function within a fiber looks like this: + // loom::Fiber::GetCurrentFiber()->YieldBack(); + virtual void YieldBack(); + + private: + // Initializes a Fiber and configures the given stack for that fiber's needs. + Fiber(StackArena* arena, void* stack); + + // The entry point that is actually provided to loom::ConfigureStack + static void EntryPoint(void* fiber_ptr); + + StackArena* arena_; + void* stack_; + std::function task_; + + // Stack pointers. sp_ is the stack pointer of this fiber after a yield. + // yield_sp_ is the stack pointer in the stack that we need to jump back into + // upon this fiber yielding. + void* sp_; + void* yield_sp_; + + State state_; +}; + +} + +#endif // LOOM_FIBERS_FIBER_H_ + From 598e162fef53c633640ff4c9f9a971f9ff5c28f3 Mon Sep 17 00:00:00 2001 From: Adrian Gjerstad Date: Thu, 26 Mar 2026 11:27:27 -0400 Subject: [PATCH 2/3] Add state() getter to Fiber State is an important piece of information to have for testing and for writing an actual scheduler. --- fibers/fiber.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fibers/fiber.h b/fibers/fiber.h index bb06ff7..2a3926e 100644 --- a/fibers/fiber.h +++ b/fibers/fiber.h @@ -113,6 +113,9 @@ class Fiber { // loom::Fiber::GetCurrentFiber()->YieldBack(); virtual void YieldBack(); + // Gets the current state of the fiber + State state() const { return state_; } + private: // Initializes a Fiber and configures the given stack for that fiber's needs. Fiber(StackArena* arena, void* stack); From 5b2dd440959b459f0ad97e7aa1a944d5adc76651 Mon Sep 17 00:00:00 2001 From: Adrian Gjerstad Date: Thu, 26 Mar 2026 13:19:03 -0400 Subject: [PATCH 3/3] Add unit tests for fibers The tests use a fake stack arena that simply allocates memory on the heap and checks for memory leaks at the end of the test. All tests pass at this time. Fibers are functioning as they should at this stage. --- fibers/BUILD | 13 +++ fibers/fiber.cc | 17 +++- fibers/fiber.h | 9 +- fibers/fiber_test.cc | 214 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 9 deletions(-) create mode 100644 fibers/fiber_test.cc diff --git a/fibers/BUILD b/fibers/BUILD index 75da64e..e754181 100644 --- a/fibers/BUILD +++ b/fibers/BUILD @@ -58,6 +58,19 @@ cc_library( visibility = ["//visibility:public"], ) +cc_test( + name = "fiber_test", + srcs = ["fiber_test.cc"], + deps = [ + ":fiber", + ":stackarena", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/status:status_matchers", + "@googletest//:gtest", + ], +) + cc_test( name = "stackarena_test", srcs = ["stackarena_test.cc"], diff --git a/fibers/fiber.cc b/fibers/fiber.cc index f57c094..69016a9 100644 --- a/fibers/fiber.cc +++ b/fibers/fiber.cc @@ -26,6 +26,8 @@ #include "fibers/stackarena.h" #include "fibers/stackswitch.h" +#include + namespace loom { namespace { @@ -39,12 +41,12 @@ Fiber::~Fiber() { return; } - state_ = Fiber::State::kDead; + arena_ = nullptr; stack_ = nullptr; + state_ = Fiber::State::kDead; task_ = nullptr; sp_ = nullptr; yield_sp_ = nullptr; - arena_->Release(stack_); } void Fiber::Reap(Fiber* fiber) { @@ -54,7 +56,14 @@ void Fiber::Reap(Fiber* fiber) { return; } + auto* arena = fiber->arena_; + auto* stack = fiber->stack_; + + std::cerr << "vptr for fiber = " << *(void**)fiber << std::endl; + fiber->~Fiber(); + + arena->Release(stack); } Fiber* Fiber::GetCurrentFiber() { @@ -85,8 +94,8 @@ void Fiber::YieldBack() { Fiber::Fiber(StackArena* arena, void* stack) : arena_(arena), stack_(stack), state_(Fiber::State::kSuspended) { - sp_ = loom::ConfigureStack(stack_, arena_->stack_size(), Fiber::EntryPoint, - this); + sp_ = loom::ConfigureStack(stack_, arena_->stack_size() - sizeof(Fiber), + Fiber::EntryPoint, this); yield_sp_ = nullptr; } diff --git a/fibers/fiber.h b/fibers/fiber.h index 2a3926e..950e567 100644 --- a/fibers/fiber.h +++ b/fibers/fiber.h @@ -82,7 +82,7 @@ class Fiber { - sizeof(Fiber)) & ~(0xF)); Fiber* fiber = new(fiber_ptr) Fiber(arena, stack); - fiber->task_ = [f = std::forward(entry), + fiber->task_ = [f = std::forward(entry), tup = std::make_tuple(std::forward(args)...)]() mutable { std::apply(f, tup); @@ -96,14 +96,13 @@ class Fiber { // this function is equivalent to use-after-free. static void Reap(Fiber* fiber); - // Obtains the pointer to the currently running Fiber. Undefined behavior if - // this function is called outside of a Fiber. + // Obtains the pointer to the currently running Fiber. Returns nullptr if + // called outside of a fiber. static Fiber* GetCurrentFiber(); // Suspends execution of the currently-executing thread/fiber and jumps into // this fiber for execution. Execution returns to the caller once the fiber - // yields. MUST NOT be called from within this fiber, but can be called from - // within other fibers. + // yields. MUST NOT be called from within a fiber. virtual void Jump(); // Yields execution of the current Fiber, back to whoever called into it last diff --git a/fibers/fiber_test.cc b/fibers/fiber_test.cc new file mode 100644 index 0000000..36dd090 --- /dev/null +++ b/fibers/fiber_test.cc @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2026 Adrian Gjerstad. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ----------------------------------------------------------------------------- +// loom/fibers/fiber_test.cc +// ----------------------------------------------------------------------------- +// +// Unit test cases for loom::Fiber. +// + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/status/status_matchers.h" +#include + +#include "fibers/fiber.h" +#include "fibers/stackarena.h" + +#include + +namespace loom { + +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::Not; +using absl::StatusCode; + +// A stack arena that just malloc's and free's stacks that it gives out. +class FakeStackArena : public StackArena { + public: + explicit FakeStackArena(size_t stack_size) : StackArena(stack_size) {} + + absl::StatusOr Lease() override { + // Typical machines rely on an alignment of 16 bytes at most + void* stack = std::aligned_alloc(16, stack_size()); + if (stack == nullptr) { + return absl::ResourceExhaustedError("could not allocate test stack"); + } + + ++outstanding_stacks_; + return stack; + } + + void Release(void* stack) override { + --outstanding_stacks_; + std::free(stack); + } + + int outstanding_stacks() const { return outstanding_stacks_; } + + private: + int outstanding_stacks_ = 0; +}; + +class FiberTest : public ::testing::Test { + protected: + void TearDown() override { + // Catch memory leaks. + // IF YOUR TEST FAILED AT THIS ASSERT, NOT ALL STACKS ARE BEING RELEASED. + ASSERT_EQ(arena_.outstanding_stacks(), 0); + } + + FakeStackArena arena_ = FakeStackArena(64 * 1024); +}; + +// A Fiber should be creatable and in a suspended state when it is first +// created. +TEST_F(FiberTest, CreatesSuccessfullyAndHasSuspendedState) { + auto fiber_or = Fiber::Create(&arena_, []() {}); + ASSERT_THAT(fiber_or, IsOk()); + Fiber* fiber = fiber_or.value(); + + // There should be one stack allocated + EXPECT_EQ(arena_.outstanding_stacks(), 1); + + // Fiber should have state "kSuspended" when it is first created and is not + // being executed. + EXPECT_EQ(fiber->state(), Fiber::State::kSuspended); + + // Cleanup + Fiber::Reap(fiber); +} + +// A Fiber must be able to accept an entry point function with any number of +// arguments, and accept a variadic set of arguments to forward to that +// function. +TEST_F(FiberTest, PerfectlyForwardsVariadicArguments) { + bool executed = false; + int captured_int = 0; + std::string captured_str = ""; + + auto entry = [&](int i, std::string s) { + executed = true; + captured_int = i; + captured_str = s; + // Returning here causes the fiber to switch back as dead. + }; + + auto fiber_or = Fiber::Create(&arena_, entry, 42, "Fibers are awesome!"); + ASSERT_THAT(fiber_or, IsOk()); + Fiber* fiber = fiber_or.value(); + + // Allow the fiber to run. + fiber->Jump(); + + // Confirm that the variadic arguments were passed successfully. + EXPECT_TRUE(executed); + EXPECT_EQ(captured_int, 42); + EXPECT_EQ(captured_str, "Fibers are awesome!"); + + // Confirm that the fiber was marked as dead. + EXPECT_EQ(fiber->state(), Fiber::State::kDead); + + // Cleanup + Fiber::Reap(fiber); +} + +// A Fiber must accurately track its execution state at any given time. +TEST_F(FiberTest, AccuratelyTracksStateOverLifecycle) { + auto entry = []() { + EXPECT_EQ(Fiber::GetCurrentFiber()->state(), Fiber::State::kRunning); + Fiber::GetCurrentFiber()->YieldBack(); + EXPECT_EQ(Fiber::GetCurrentFiber()->state(), Fiber::State::kRunning); + }; + + auto fiber_or = Fiber::Create(&arena_, entry); + ASSERT_THAT(fiber_or, IsOk()); + Fiber* fiber = fiber_or.value(); + + // Should be suspended at creation + EXPECT_EQ(fiber->state(), Fiber::State::kSuspended); + + fiber->Jump(); + + // Should be back to suspended + EXPECT_EQ(fiber->state(), Fiber::State::kSuspended); + + // Jump back in one time to finish the fiber + fiber->Jump(); + + // Confirm that the fiber was marked as dead. + EXPECT_EQ(fiber->state(), Fiber::State::kDead); + + // Cleanup + Fiber::Reap(fiber); +} + +// GetCurrentFiber() should always return an accurate pointer to the currently +// executing fiber, and should return NULL when not in a fiber. +TEST_F(FiberTest, GetCurrentFiberReturnsTheCorrectPointer) { + Fiber* captured_ptr = nullptr; + + auto entry = [&]() { + captured_ptr = Fiber::GetCurrentFiber(); + }; + + // Create two fibers + auto fiber_a_or = Fiber::Create(&arena_, entry); + ASSERT_THAT(fiber_a_or, IsOk()); + Fiber* fiber_a = fiber_a_or.value(); + + auto fiber_b_or = Fiber::Create(&arena_, entry); + ASSERT_THAT(fiber_b_or, IsOk()); + Fiber* fiber_b = fiber_b_or.value(); + + // Make sure it is null to begin with + EXPECT_EQ(Fiber::GetCurrentFiber(), nullptr); + + // Make sure fiber_a is reading the current fiber correctly. + fiber_a->Jump(); + EXPECT_EQ(captured_ptr, fiber_a); + + // Should now be nullptr again + EXPECT_EQ(Fiber::GetCurrentFiber(), nullptr); + + // Same check for fiber_b + fiber_b->Jump(); + EXPECT_EQ(captured_ptr, fiber_b); + + // One last time should be nullptr + EXPECT_EQ(Fiber::GetCurrentFiber(), nullptr); + + // Cleanup + Fiber::Reap(fiber_a); + Fiber::Reap(fiber_b); +} + +} + +} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +