mirror of
https://github.com/latentPrion/libspinscale.git
synced 2026-06-23 19:48:32 +00:00
Add readme file
This commit is contained in:
@@ -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<typename T>
|
||||||
|
using BodyPostingPromise =
|
||||||
|
sscl::co::TaggedPostingPromise<T, BodyThreadTag>;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
using BodyInvoker =
|
||||||
|
sscl::co::ViralPostingInvoker<BodyPostingPromise, T>;
|
||||||
|
```
|
||||||
|
|
||||||
|
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<void> 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<T>` and
|
||||||
|
`ExplicitPostTarget` allow the caller to supply the destination `io_context`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
sscl::co::DynamicViralPostingInvoker<void>
|
||||||
|
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<T, ThreadTag>`.
|
||||||
|
- Runtime-selected posting uses `DynamicViralPostingInvoker<T>`.
|
||||||
|
- 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.
|
||||||
Reference in New Issue
Block a user