Created
August 28, 2018 22:26
-
-
Save snej/26fec48724e21cf89fae0718c73c75b7 to your computer and use it in GitHub Desktop.
C++ implementation of C# / JavaScript style async calls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Async.hh | |
// | |
// Copyright © 2018 Couchbase. All rights reserved. | |
// | |
#pragma once | |
#include "RefCounted.hh" | |
#include <functional> | |
namespace litecore { namespace actor { | |
/* | |
Async<T> represents a result of type T that may not be available yet. This concept is | |
also referred to as a "future". You can create one by first creating an AsyncProvider<T>: | |
Async<int> getIntFromServer() { | |
Retained<AsyncProvider<int>> intProvider = new AsyncProvider<int>; | |
sendServerRequestFor(intProvider); | |
return intProvider->asyncValue(); | |
} | |
Then, when the result becomes available, you call the provider's setResult() method: | |
int result = valueReceivedFromServer(); | |
intProvider.setResult(result); | |
Async<T> has a `ready` method that tells whether the result is available, and a `result` | |
method that returns the result (or aborts if it's not available.) However, it has no way | |
to block and wait for the result. That's intentional: we don't want blocking. Instead, | |
the way you work with async results is within an _asynchronous function_. | |
An asynchronous function is a function that can resolve Async values synchronously, | |
without actually blocking. It always returns an Async result (or void). It looks like this: | |
Async<T> anAsyncFunction() { | |
BEGIN_ASYNC_RETURNING(T) | |
... | |
return t; | |
END_ASYNC() | |
} | |
If the function doesn't return a result, it looks like this: | |
void aVoidAsyncFunction() { | |
BEGIN_ASYNC(T) | |
... | |
END_ASYNC() | |
} | |
In between BEGIN and END you can "unwrap" Async values, such as those returned by other | |
asynchronous functions, by calling asyncCall(). The first parameter is the variable to | |
assign the result to, and the second is the expression returning the async result: | |
asyncCall(int n, someOtherAsyncFunction()); | |
`asyncCall()` has an odd effect on variable scope: it causes variables declared above it | |
(in the BEGIN...END block) to end their scope. This is because the flow of control is | |
likely to exit the function during evaluation of asyncCall, then return later. This means | |
that you cannot use a variable after an asyncCall: | |
int foo = ....; | |
asyncCall(int n, someOtherAsyncFunction()); | |
foo += n; // ERROR: 'foo' is not declared | |
If you want to use a variable across `asyncCall` scopes, you must declare it _before_ the | |
BEGIN_ASYNC: | |
int foo; | |
BEGIN_ASYNC_RETURNING(T) | |
... | |
foo = ....; | |
asyncCall(int n, someOtherAsyncFunction()); | |
foo += n; // OK! | |
You can also have functions that return an Async value without needing to be async. | |
For example, a function could return a value, and then later provide its result through | |
some other means, such as via a networking call. You do this by creating an AsyncProvider: | |
Async<int> getIntFromServer() { | |
Retained<AsyncProvider<int>> intProvider = new AsyncProvider<int>; | |
sendServerRequestFor(intProvider); | |
return intProvider->asyncValue(); | |
} | |
Later, when the response arrives, you call the provider's setResult() method: | |
int result = valueReceivedFromServer(); | |
intProvider.setResult(result); | |
*/ | |
#define BEGIN_ASYNC_RETURNING(T) \ | |
return _asyncBody<T>([=](AsyncState &_state) mutable -> T { \ | |
switch (_state._continueAt) { \ | |
case 0: { \ | |
#define BEGIN_ASYNC() \ | |
_asyncBodyVoid([=](AsyncState &_state) mutable -> void { \ | |
switch (_state._continueAt) { \ | |
case 0: { \ | |
#define asyncCall(VAR, CALL) \ | |
if (_state.mustWaitFor(CALL, __LINE__)) return {}; \ | |
} \ | |
case __LINE__: \ | |
{ \ | |
VAR = _state.asyncResult<decltype(CALL)::ResultType>() | |
#define END_ASYNC() \ | |
break; \ | |
} \ | |
default: /*fprintf(stderr,"Wrong label %d\n", _state._continueAt);*/ abort(); \ | |
} \ | |
}); | |
class AsyncBase; | |
class AsyncProviderBase; | |
template <class T> class Async; | |
template <class T> class AsyncProvider; | |
// base class of Async<T> | |
class AsyncBase { | |
public: | |
explicit AsyncBase(const fleece::Retained<AsyncProviderBase> &provider) | |
:_provider(provider) | |
{ } | |
inline bool ready() const; | |
protected: | |
fleece::Retained<AsyncProviderBase> _provider; | |
friend struct AsyncState; | |
}; | |
/** The state data passed to the lambda of an async function. */ | |
struct AsyncState { | |
fleece::Retained<AsyncProviderBase> _waitingOn; | |
int _continueAt {0}; | |
bool mustWaitFor(const AsyncBase &a, int lineNo) { | |
_waitingOn = a._provider; | |
_continueAt = lineNo; | |
return !a.ready(); | |
} | |
template <class T> | |
T asyncResult() { | |
T result = ((AsyncProvider<T>*)_waitingOn.get())->result(); | |
_waitingOn = nullptr; | |
return result; | |
} | |
}; | |
// base class of AsyncProvider<T> and AsyncVoidProvider. | |
class AsyncProviderBase : public fleece::RefCounted { | |
public: | |
bool ready() const {return _ready;} | |
void setObserver(AsyncProviderBase *p) { | |
assert(!_observer); | |
_observer = p; | |
} | |
void asyncGotResult(AsyncProviderBase *async) { | |
assert(async == _state._waitingOn); | |
next(); | |
} | |
protected: | |
virtual void next() =0; | |
AsyncState _state; | |
bool _ready {false}; | |
AsyncProviderBase* _observer {nullptr}; | |
}; | |
/** Special case of AsyncProvider for use in functions with no return value (void). */ | |
class AsyncVoidProvider : public AsyncProviderBase { | |
public: | |
explicit AsyncVoidProvider(std::function<void(AsyncState&)> body) | |
:_body(body) | |
{ | |
next(); | |
} | |
private: | |
void next() override { | |
_body(_state); | |
if (_state._waitingOn) | |
_state._waitingOn->setObserver(this); | |
} | |
std::function<void(AsyncState&)> _body; | |
}; | |
/** An asynchronously-provided result, seen from the producer side. */ | |
template <class T> | |
class AsyncProvider : public AsyncProviderBase { | |
public: | |
explicit AsyncProvider(std::function<T(AsyncState&)> body) | |
:_body(body) | |
{ | |
next(); | |
} | |
AsyncProvider() | |
{ } | |
Async<T> asyncValue() { | |
return Async<T>(this); | |
} | |
void setResult(const T &result) { | |
_result = result; | |
_gotResult(); | |
} | |
const T& result() const { | |
assert(_ready); | |
return _result; | |
} | |
private: | |
void next() override { | |
_result = _body(_state); | |
if (_state._waitingOn) { | |
_state._waitingOn->setObserver(this); | |
} else { | |
_gotResult(); | |
} | |
} | |
void _gotResult() { | |
_ready = true; | |
auto observer = _observer; | |
_observer = nullptr; | |
if (observer) | |
observer->asyncGotResult(this); | |
} | |
std::function<T(AsyncState&)> _body; | |
T _result {}; | |
}; | |
/** An asynchronously-provided result, seen from the client side. */ | |
template <class T> | |
class Async : public AsyncBase { | |
public: | |
Async(AsyncProvider<T> *provider) | |
:AsyncBase(provider) | |
{ } | |
Async(const fleece::Retained<AsyncProvider<T>> &provider) | |
:AsyncBase(provider) | |
{ } | |
bool ready() const { | |
return _provider->ready(); | |
} | |
const T& result() const { | |
return ((AsyncProvider<T>*)_provider.get())->result(); | |
} | |
using ResultType = T; | |
}; | |
/** Body of an async function: Creates an AsyncProvider from the lambda given, | |
then returns an Async that refers to that provider. */ | |
template <class T> | |
Async<T> _asyncBody(std::function<T(AsyncState&)> bodyFn) { | |
auto provider = new AsyncProvider<T>(bodyFn); | |
return Async<T>(provider); | |
} | |
/** Special case of _asyncBody for functions returning void. */ | |
static inline | |
void _asyncBodyVoid(std::function<void(AsyncState&)> bodyFn) { | |
(void) new AsyncVoidProvider(bodyFn); | |
} | |
bool AsyncBase::ready() const { | |
return _provider->ready(); | |
} | |
} } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment