diff --git a/README.md b/README.md new file mode 100644 index 0000000..d47b6c9 --- /dev/null +++ b/README.md @@ -0,0 +1,233 @@ +# libspinscale + +libspinscale is a C++ coroutine and asynchronous component runtime for +thread-affine systems: applications where work is not just asynchronous, but +must run on specific long-lived component threads with explicit lifecycle, +posting, cancellation, and orchestration rules. + +It is built around a simple premise: in some systems, the important question is +not only "when does this async operation complete?", but also "which component +thread owns this work, where does it resume, what locks does the coroutine +chain hold, and how does a whole subsystem start or stop as one unit?" + +libspinscale targets that class of problems directly. + +## What It Is For + +libspinscale is meant for applications that look more like a small distributed +runtime inside one process than a collection of independent async tasks. It is a +fit when the program has: + +- Dedicated component threads with their own `boost::asio::io_context`. +- Thread-affine components that must run operations on their owning thread. +- Startup, pause, resume, and shutdown protocols across many threads. +- Coroutine orchestration that needs structured fan-out and settlement scanning. +- Cross-thread post-to and post-back behavior that must be explicit and + predictable. +- Coroutine-aware locking where deadlock detection must understand the caller + coroutine chain. + +Typical examples include simulation runtimes, test harnesses, embedded control +planes, game/server subsystems, robotics-style component graphs, multi-thread +workflow engines, and other applications where "run this on the right thread" is +part of the correctness contract. + +## What Makes It Different + +Most C++ coroutine libraries focus on awaitable primitives, task types, event +loops, generators, or composable async algorithms. libspinscale focuses on +thread-affine component orchestration. We'll compare libspinscale to contemporaneous C++ coro libraries below to differentiate it: + +Boost.Asio gives excellent networking and executor primitives, but it does not +define an application-level component model. libspinscale uses Asio +`io_context`s as posting threads and layers a stricter coroutine model on top: +component threads, post-to/post-back promises, lifecycle invokers, and +structured group settlement. + +Folly coroutines provide production-grade task abstractions and executor +integration, especially for large service stacks. libspinscale is narrower: it +optimizes for explicit component ownership and deterministic thread handoff +rather than general-purpose async service composition. + +cppcoro provides foundational coroutine types such as tasks, generators, async +manual reset events, and related primitives. libspinscale is less of a primitive +toolkit and more of a runtime pattern for systems with named threads, component +lifetimes, and cross-thread orchestration. + +In short: libspinscale is not trying to replace Boost.Asio, Folly, or cppcoro. +It is trying to solve the coordination problem that appears above them when an +application has a fixed topology of cooperating component threads. + +## Core Ideas + +### Component Threads + +`ComponentThread` owns a `boost::asio::io_context` and represents a named +execution thread. `PuppeteerThread` and `PuppetThread` build a lifecycle model +on top of that thread: + +- A puppeteer coordinates the application. +- Puppet threads host component work. +- Lifecycle operations such as JOLT, start, pause, resume, and exit are exposed + as awaitable operations. + +`PuppetApplication` orchestrates a set of puppet threads and exposes lifecycle +batch operations: + +```cpp +co_await app.joltAllPuppetThreadsCReq(); +co_await app.startAllPuppetThreadsCReq(); +co_await app.exitAllPuppetThreadsCReq(); +``` + +These operations are coroutine-native. Callers that are already in a coroutine +can `co_await` them directly. + +### Posting Promises + +Posting coroutines are tied to a target thread. A tagged posting promise posts the +callee coroutine to `ThreadTag::io_context()` at initial suspend and posts back +to the caller's `io_context` at completion. + +```cpp +struct BodyThreadTag +{ + static boost::asio::io_context &io_context(); +}; + +template +using BodyPostingPromise = + sscl::co::TaggedPostingPromise; + +template +using BodyInvoker = + sscl::co::ViralPostingInvoker; +``` + +This makes thread ownership visible in the coroutine return type. A component +operation can say, at the type level, that it runs on the body thread and resumes +its caller correctly when done. + +### Viral And Non-Viral Invokers + +libspinscale distinguishes coroutine-to-coroutine orchestration from +non-coroutine entry points. + +Viral invokers are awaitable and are used inside coroutine call chains: + +```cpp +BodyInvoker BodyComponent::initializeCReq() +{ + co_return; +} + +co_await body.initializeCReq(); +``` + +Non-viral invokers are for top-level boundaries where ordinary code starts a +coroutine and supplies a completion callback: + +```cpp +auto invoker = component.initializeFromHookCReq(exceptionPtr, [] { + // completion callback +}); +``` + +This distinction is intentional. Coroutine orchestration should use `co_await`. +Callback-style completion should stay at the outer boundary. + +### Dynamic Post Targets + +Most posting coroutines use a compile-time `ThreadTag`. When the target thread is +known only at runtime, `DynamicViralPostingInvoker` and +`ExplicitPostTarget` allow the caller to supply the destination `io_context`: + +```cpp +sscl::co::DynamicViralPostingInvoker +runOnSelectedThread(sscl::co::ExplicitPostTarget target) +{ + co_return; +} + +co_await runOnSelectedThread(sscl::co::ExplicitPostTarget{thread.getIoContext()}); +``` + +The post-back side still returns to the caller's thread. + +### Group Settlement + +`co::Group` provides structured fan-out over heterogeneous invokers. Members can +be added, awaited as a group, and inspected by settlement status: + +```cpp +sscl::co::Group group; + +auto bodyInit = body.initializeCReq(); +auto worldInit = world.initializeCReq(); +auto legInit = leg.initializeCReq(); + +group.add(bodyInit); +group.add(worldInit); +group.add(legInit); + +co_await group.getAwaitAllSettlementsInvoker(); +group.checkForAndReThrowGroupExceptions(); +``` + +Settlements record whether a member completed or threw. The original invoker can +be recovered from a descriptor when a caller needs typed return values. + +### Coroutine-Aware Locking + +`co::CoQutex` is a coroutine-aware mutual exclusion primitive. It tracks +acquired locks through the coroutine promise chain, which lets it detect +dangerous re-acquisition patterns across nested coroutine calls instead of only +within one stack frame. + +```cpp +auto releaseHandle = co_await qutex.getAcquireInvocationAndSuspensionPolicy(); +``` + +When a coroutine cannot acquire the qutex, it suspends and is resumed through +the correct caller `io_context`. + +## Design Biases + +libspinscale intentionally favors: + +- Explicit thread ownership over invisible executor selection. +- Member coroutine APIs over free-function workaround layers. +- `co_await` orchestration inside coroutine code. +- Callback completion only at non-coroutine boundaries. +- Structured group settlement over ad hoc counters and flags. +- Type-visible posting behavior over ambient global schedulers. + +These choices make the library opinionated. That is the point. It is designed +for systems where implicit scheduling is a source of bugs. + +## Non-Goals + +libspinscale is not a general replacement for: + +- Boost.Asio networking and executor facilities. +- Folly's broad async service infrastructure. +- cppcoro's foundational coroutine primitive set. +- Standard library coroutine machinery. + +It also is not trying to hide C++ coroutine mechanics. Promise types, invokers, +and suspension behavior are part of the public design because the target +applications need control over where work runs and where completion resumes. + +## Status + +The API is still evolving. The current direction is centered on coroutine-native +component orchestration: + +- `boost::asio::io_context` is the thread event-loop primitive. +- Posting coroutines use `TaggedPostingPromise`. +- Runtime-selected posting uses `DynamicViralPostingInvoker`. +- Component lifecycle batches are viral non-posting coroutines. +- `co::Group` is the primary structured fan-out/fan-in primitive. + +Expect breaking changes when they simplify the ownership, lifecycle, or +post-to/post-back model.