Custom Adapters
When to Use
You have an async operation (HTTP fetch, DNS lookup, thread pool work) and want to bridge it into the Promise system with automatic cancellation when the Promise is destroyed.
Adapter Contract
An Adapter is a class with:
- Constructor: receives
PromiseResolver<T>&&+ user args. Starts the async operation. - Destructor: cancels the async operation (if still in-flight).
- Async callback: calls
resolver.resolve(value)when done. Safe to call from any thread.
class MyAdapter {
PromiseResolver<int> m_resolver;
public:
MyAdapter(PromiseResolver<int>&& r, const char* url)
: m_resolver(std::move(r)) {
// Start async fetch...
// When done: m_resolver.resolve(response_code);
}
~MyAdapter() {
// Cancel the fetch if still in-flight
}
};
adapt<T, Adapter>(args...)
#include <xpp/promise_adapter.h>
auto p = xpp::adapt<int, MyAdapter>(url);
// MyAdapter is constructed with (PromiseResolver<int>&&, url)
// When the fetch completes, MyAdapter calls resolver.resolve(code)
int code = p.await();
How It Works
AdapterPromiseNode<T, Adapter>
├─ Arc<ResolveState<T>> m_state ← strong ref (keeps state alive)
├─ Adapter m_adapter ← owns the async operation
│ └─ PromiseResolver<T> ← weak ref (ArcWeak to state)
│
├─ poll(waker): ← generic, same for all adapters
│ if resolved → return value
│ else register waker → re-check → return None
│
└─ ~dtor: ~Adapter() → ~Arc() ← cancel op, then drop strong ref
ResolveState — Shared via Arc/ArcWeak
struct ResolveState<T> {
Option<T> value;
AtomicPromiseWaker waker;
std::atomic<bool> resolved{false};
};
- AdapterPromiseNode holds
Arc<ResolveState<T>>(strong) — keeps state alive. - PromiseResolver holds
ArcWeak<ResolveState<T>>(weak) —resolve()callsupgrade(). - When node is destroyed → strong count → 0 →
upgrade()returnsNone→resolve()silently drops.
Lifecycle Safety
Promise destroyed (e.g., race loser):
1. ~AdapterPromiseNode()
2. ~Adapter() → cancel async operation (e.g., xTimerStop)
3. ~Arc<State>() → strong count = 0
4. Async callback fires later → resolver.resolve(v)
5. ArcWeak::upgrade() → None (strong = 0) → silently drop
6. No UAF.
TimerAdapter — Built-in Adapter
TimerAdapter replaces the old TimerPromiseNode. It's a thin adapter (~25 lines) that owns an xTimer handle:
class TimerAdapter {
xTimer m_handle;
std::atomic<bool> m_fired{false};
PromiseResolver<void> m_resolver;
public:
TimerAdapter(PromiseResolver<void>&& r, uint64_t ms)
: m_resolver(std::move(r)) {
m_handle = xTimerStart(
[](void* a) {
auto* self = static_cast<TimerAdapter*>(a);
self->m_fired.store(true, release);
self->m_resolver.resolve();
},
this,
[](void* a) { // on_cancel (loop destroy)
auto* self = static_cast<TimerAdapter*>(a);
self->m_handle = nullptr;
self->m_fired.store(true, release);
},
ms, 0);
}
~TimerAdapter() {
if (!m_fired.load(acquire) && m_handle)
xTimerStop(m_handle);
}
};
Used internally by after(ms):
Promise<void> after(uint64_t ms) {
return adapt<void, TimerAdapter>(ms);
}
async<T>() — Manual Resolve
async uses ManualResolveNode<T> (same poll_state logic, no Adapter):
auto [p, r] = xpp::async<int>();
// p is backed by ManualResolveNode<int>
// r is PromiseResolver<int> (ArcWeak to shared state)
r.resolve(42);
p.await(); // 42
Writing a Custom Cross-Thread Adapter
The built-in WorkAdapter covers the common case. For custom adapters that
need more control (e.g., specific task group, progress reporting):
class MyFetchAdapter {
struct Ctx { PromiseResolver<Response> resolver; std::string url; };
Ctx* m_ctx;
xWork m_work;
public:
MyFetchAdapter(PromiseResolver<Response>&& r, const std::string& url)
: m_ctx(new Ctx{std::move(r), url}) {
m_work = xWorkSubmit(
nullptr,
[](void* a) -> void* {
auto* ctx = static_cast<Ctx*>(a);
Response resp = do_fetch(ctx->url);
ctx->resolver.resolve(std::move(resp)); // cross-thread, safe
return nullptr;
},
[](void* a, void*) { delete static_cast<Ctx*>(a); },
[](void* a, void*) { delete static_cast<Ctx*>(a); },
m_ctx);
}
~MyFetchAdapter() { if (m_work) xWorkCancel(m_work); }
};
auto p = xpp::adapt<Response, MyFetchAdapter>(url);