diff --git a/fibers/BUILD b/fibers/BUILD index d6e0430..e754181 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({ @@ -47,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 new file mode 100644 index 0000000..69016a9 --- /dev/null +++ b/fibers/fiber.cc @@ -0,0 +1,116 @@ +// 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" + +#include + +namespace loom { + +namespace { + +thread_local Fiber* current_fiber = nullptr; + +} + +Fiber::~Fiber() { + if (arena_ == nullptr) { + return; + } + + arena_ = nullptr; + stack_ = nullptr; + state_ = Fiber::State::kDead; + task_ = nullptr; + sp_ = nullptr; + yield_sp_ = nullptr; +} + +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; + } + + 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() { + 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() - sizeof(Fiber), + 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..950e567 --- /dev/null +++ b/fibers/fiber.h @@ -0,0 +1,141 @@ +// 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. 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 a fiber. + 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(); + + // 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); + + // 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_ + 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(); +} +