FTN12: FutoIn Async API Version: 1.16 Date: 2026-10-04 Copyright: 2014-2026 FutoIn Project (http://futoin.org) Authors: Andrey Galkin
asi.newInstance() to inherit catch_trace,
unhandled_error, and cancel_handler. Handlers may need to check the
referenced object now.State.State accessors definition and asi.state()
shortcuts.State interface for better compatibility with ABI.stateVariable ABI.asi.copyFrom() idea;State interface convention;Mutex and ThrottleLimiter primitiveThis interface was initially born as a secondary option for the FTN6 Executor
concept. However, it quickly became clear that async, reactor, proactor, light
threads, etc. should be the base of scalable high performance server
implementations and light clients, even though it is sometimes more difficult
for understanding and/or debugging. Later that has been confirmed multiple times
with all sorts of modern async functionality, including its iconic async/await
pattern. The traditional synchronous program flow becomes an add-on on top of an
asynchronous base for legacy code and/or overly complex logic. Academic and
practical research in this direction was started in the field of cooperative
multitasking back in the XX century.
Program flow is split into non-blocking execution steps, represented with execution callback functions. Processing Unit (e.g. CPU) halting/ spinning/switching-to-another-task is seen as a blocking action in program flow. Execution of such fragments is partially ordered.
Any step must not call any blocking functions, except for synchronization with guaranteed minimal period of lock acquisition. Note: under minimal period, it is assumed that any acquired lock is immediately released after action with O(1) complexity and no delay caused by programmatic suspension/locking of the executing task.
Every step is executed sequentially. A successful result of any step becomes the input for the following step.
Each step can have its own error handler. An error handler is called if
AsyncSteps.error() is called within step execution or any of its sub-steps.
A typical behavior of such handler is to override the error and to continue, or
to make cleanup actions and complete the job with the error still pending.
Each step can have its own sequence of sub-steps. Sub-steps can be added only during that step's execution. Sub-step sequence is executed after current step execution is finished.
If there are any sub-steps added, then the current step must not call
AsyncSteps.success() or AsyncSteps.error(). Otherwise, InternalError
is raised.
It is possible to create a special "parallel" sub-step and add independent sub-steps to it. Execution of each parallel sub-step starts in an interleaved way. The parallel step completes with success when all sub-steps complete successfully. If an error is raised in any sub-step of the parallel step, then all other sub-steps are canceled.
Out-of-order cancellation of execution can occur by a timeout, execution control engine decision (e.g. Invoker disconnect), or a failure in the sibling parallel steps. Each step can install a custom on-cancel handler to free up resources and/or cancel external jobs. After cancellation, it must be safe to destroy the AsyncSteps object.
AsyncSteps must be used in Executor request processing. The same [root] AsyncSteps object must be used for all asynchronous tasks within a given request processing session.
AsyncSteps may be used by the FTN7 Invoker implementation.
AsyncSteps may support derived classes in an implementation-defined way. A typical use case: functionality extension (e.g. request processing API). However, this is discouraged in later implementations.
For performance reasons, it may be more efficient to initialize AsyncSteps with many business logic steps via platform-specific AsyncSteps cloning/duplicating, but this idea does not have much practical use and is now deprecated.
When an AsyncSteps (or derived) object is created, all steps are added
sequentially in Level 0 through the add(), parallel(), or loop API. Note:
each parallel() is seen as a step.
After AsyncSteps execution is initiated, each step of Level 0 is executed. All sub-steps are added in Level n+1. Example:
add() -> Level 0 #1
add() -> Level 1 #1
add() -> Level 2 #1
parallel().add() -> Level 2 #2
add() -> Level 2 #3
parallel().add() -> Level 1 #2
add() -> Level 1 #3
parallel() -> Level 0 #2
add() -> Level 0 #3
Execution cannot continue to the next step of the current Level until all sub-steps of nested levels are executed.
The execution sequence would be:
Level 0 add #1
Level 1 add #1
Level 2 add #1
Level 2 parallel #2
Level 2 add #3
Level 1 parallel #2
Level 1 add #3
Level 0 parallel #2
Level 0 add #3
Due to nonlinear programming, classic try/catch blocks are converted into execute-onerror pairs. Each added step may have its custom error handler. If such error handler is not specified, then control is passed to a lower Level error handler. If none is defined, execution is aborted.
Example:
add( -> Level 0
func( asi ){
print( "Level 0 func" )
add( -> Level 1
func( asi ){
print( "Level 1 func" )
asi.error( "myerror" )
},
onerror( asi, error ){
print( "Level 1 onerror: " + error )
asi.error( "newerror" )
}
)
},
onerror( asi, error ){
print( "Level 0 onerror: " + error )
asi.success( "Prm" )
}
)
add( -> Level 0
func( asi, param ){
print( "Level 0 func2: " + param )
asi.success()
}
)
The output would be:
Level 0 func
Level 1 func
Level 1 onerror: myerror
Level 0 onerror: newerror
Level 0 func2: Prm
In a synchronous way, it would look like:
variable = null
try
{
print( "Level 0 func" )
try
{
print( "Level 1 func" )
throw "myerror"
}
catch ( error )
{
print( "Level 1 onerror: " + error )
throw "newerror"
}
}
catch( error )
{
print( "Level 0 onerror: " + error )
variable = "Prm"
}
print( "Level 0 func2: " + variable )
Very often, an error handler creates an alternative complex program path which
requires its own async operation. Therefore, such error handler must accept
asi.add() as an implicit asi.success() override.
If steps are added inside an error handler, they must remain on the same async stack level while the error handler itself gets removed.
Example:
add( -> Level 0
func( asi ){
print( "Level 0 func" )
add( -> Level 1
func( asi ){
print( "Level 1 func" )
asi.error( "first" )
},
onerror( asi, error ){
print( "Level 1 onerror: " + error )
asi.add( -> Level 2
func() {
print( "Level 2 func" )
asi.error( "second" );
},
onerror( asi, error ) {
print( "Level 2 onerror: " + error )
}
)
}
)
},
onerror( asi, error ){
print( "Level 0 onerror: " + error )
}
)
The output would be:
Level 0 func
Level 1 func
Level 1 onerror: first
Level 2 func
Level 2 onerror: second
Level 0 onerror: second
Note: the "Level 1 onerror" is not executed a second time!
Very often, execution of a step cannot continue without waiting for some
external event, like input from a network or disk. It is forbidden to block
execution while waiting for such events. As a solution, there are special
setTimeout() and setCancel() API methods.
Example:
add(
func( asi ){
socket.read( function( data ){
asi.success( data )
} )
asi.setCancel( function(){
socket.cancel_read()
} )
asi.setTimeout( 30_000 ) // 30 seconds
},
onerror( asi, error ){
if ( error == timeout ) {
print( "Timeout" )
}
else
{
print( "Read Error" )
}
}
)
The definition of parallel steps aborts execution if any of the parallel steps fails. To avoid spending excessive time and resources on other steps, there is a concept of canceling sibling steps execution similar to the timeout above.
Example:
asi.parallel()
.add(
func( asi ){
asi.setCancel( function(){ ... } )
// do parallel job #1
asi.state()->result1 = ...;
}
)
.add(
func( asi ){
asi.setCancel( function(){ ... } )
// do parallel job #2
asi.state()->result2 = ...;
}
)
.add(
func( asi ){
asi.error( "Some Error" )
}
)
asi.add(
func( asi ){
print( asi.state()->result1 + asi.state->result2 )
asi.success()
}
)
This is a deprecated concept which can be used only in certain niche optimizations. Therefore, it may not be supported in all implementations.
In long-running applications, the same business logic may be reused multiple times during execution. This applies only to same-level sub-steps.
In a REST API server example, complex business logic can be defined only once and stored in a kind of AsyncSteps object repository. On each request, a reference object from the repository would be copied for actual processing with minimal overhead for sub-steps initialization.
However, there would be little-to-no performance difference in sub-step definition unless its callback function is also created at initialization time rather than parent step execution time (the default concept). So, it should be possible to predefine those as well and copy them during step execution. Copying steps must also involve copying state variables that are not set in the target state.
Example:
AsyncSteps req_repo_common;
req_repo_common.add(func( asi ){
asi.add( func( asi ){ ... } );
asi.copyFrom( asi.state().business_logic );
asi.add( func( asi ){ ... } );
});
AsyncSteps req_repo_buslog1;
req_repo_buslog1
.add(func( asi ){ ... })
.add(func( asi ){ ... });
AsyncSteps actual_exec = copy req_repo_common;
actual_exec.state().business_logic = req_repo_buslog1;
actual_exec.execute();
However, this approach only makes sense for deep performance optimizations.
asi.success()If there are no sub-steps added, no timeout set, and no cancel handler set, then
an implicit asi.success() call is assumed to simplify code and increase
efficiency of step execution. However, such an approach does not apply to
error handlers!
asi.add(func( asi ){
doSomeStuff( asi );
})
asi.waitExternal() Shortcut for Empty Cancellation HandlerAs in many cases it is required to wait for an external event without any
additional conditions, the general approach appeared to be adding an empty
cancel handler. To avoid that, an explicit asi.waitExternal() API is
provided.
Predefined state variables:
Implementation may replace state variables with a specific State object API.
The error code is not always descriptive enough, especially if it can be
generated in multiple places. As a convention, the special error_info state
field should hold descriptive information about the last error. Therefore,
asi.error() is extended with an optional parameter error_info.
The last_exception state variable may hold the last exception object caught,
if feasible to implement. It should be populated with thrown FutoIn error
objects as well.
Almost always, program flow is nonlinear and loops are required.
Basic principles of async loops:
asi.loop( func( asi ){
call_some_library( asi );
asi.add( func( asi, result ){
if ( !result )
{
// exit loop
asi.break();
}
} );
} )
Inner loops and identifiers:
// start loop
asi.loop(
func( asi ){
asi.loop( func( asi ){
call_some_library( asi );
asi.add( func( asi, result ){
if ( !result )
{
// exit loop
asi.continue( "OUTER" );
}
asi.success( result );
} );
} );
asi.add( func( asi, result ){
// use it somehow
asi.success();
} );
},
"OUTER"
)
Loop n times:
asi.repeat( 3, func( asi, i ){
print( 'Iteration: ' + i )
} )
Traverse through a list:
asi.forEach(
[ 'apple', 'banana' ],
func( asi, index, v ){
print( index + ". " + v )
}
)
Traverse through a map:
asi.forEach(
[ 'apple', 'banana' ],
func( asi, k, v ){
print( k + " = " + v )
}
)
Normal loop termination is performed either by a loop condition (e.g.
asi.forEach(), asi.repeat()) or by a asi.break() call. Normal
termination is seen as an implicit asi.success() call.
Abnormal termination is possible through asi.error(), including timeout, or
external asi.cancel(). Abnormal termination is seen as an asi.error()
call.
A good library calling convention has never been provided, and each use case in the field is unique. The following recommendations are provided, beginning with FTN12 v1.15.
asi.successStep() to return any variables.asi.setCancel(), asi.setTimeout(), and asi.waitExternal must only be
called in the sub-steps added by the library function;as.add()
internally for any operation with side effects to ensure correct order of
operations.Library calls inside current project boundaries may deviate from such guidelines for operational efficiency.
If any API identifiers clash with reserved words or contain illegal symbols, implementation-defined name mangling is allowed, prioritized according to the following guidelines.
Predefined alternative method names, if default matches language-specific reserved keywords:
Although AsyncSteps are designed for single-thread operation, synchronization between executing instances is still necessary.
As with any multi-threaded application, multistep cases may also require synchronization to ensure that no more than N steps enter the same critical section spanning several fragments (steps) of the asynchronous flow.
Implemented as Mutex class.
For general stability reasons and protection against self-DoS, it may be required to limit the number of steps allowed to enter a critical section within a given time period.
Implemented as Throttle class.
A special asi.sync(obj, step, err_handler) shortcut API is available to
synchronize against any object supporting the synchronization protocol with
pattern obj.sync(asi, step, err_handler).
A synchronization object is allowed to add its own steps and is responsible for adding requested sub-steps under protection of the provided synchronization. Synchronization objects must correctly handle canceled execution and possible errors.
Incoming success parameters must be passed to the critical section step. Resulting success parameters must be forwarded to following steps as if there were no critical section logic.
All synchronization implementations must either allow multiple re-entrances of
the same AsyncSteps instance or properly detect and raise an error on such an
event. This requirement part does not apply to Throttle behavior.
All implementations must correctly detect parallel flows within the scope of a single AsyncSteps instance and treat each as a separate flow. No parallel step should inherit the lock state of its parent step.
Deadlock detection is optional and not strictly required.
It may be required to limit the maximum number of pending AsyncSteps flows. If the overall queue limit is reached, new entries must receive the predefined "DefenseRejected" error.
Request processing stability requires limiting both simultaneous connections
and request rate. Therefore, a special synchronization primitive Limiter
wrapping Mutex and Throttle is introduced to impose combined limits.
Sometimes, returning a value after inner steps complete is required. It leads to code like:
value = 123;
asi.add( subStep );
asi.add( ( asi ) => asi.success( value ) );
To optimize and clean up code, the previously deprecated asi.successStep() is
re-introduced. Example:
value = 123;
asi.add( subStep );
asi.successStep( value );
As Promise and await patterns become increasingly popular in modern
technologies, AsyncSteps should support them through asi.await(future_or_promise)
calls where feasible to implement.
Implementation details are specific to particular technologies. However, the following guidelines should be used:
future_or_promise is cancellable, asi.setCancel() must be used.asi.waitExternal() should be used.asi.error().asi.success().For most GC-based technologies, step closures can use objects allocated in outer steps without issues. However, object lifetime management is important for technologies like ISO C++.
A special Pointer stack(size) execution API is provided. The raw version acts
like regular heap allocation, but allocated memory is automatically freed once
the step is destroyed.
If another lifetime is required, implementation-specific shared pointers should be used.
Technology-specific implementations should provide a template or generic overload to better integrate with specific type systems and features. Example:
// Prototype
template<typename T, typename... Args>
T& stack(Args&&... args);
// to be used like
asi.stack<T>();
asi.stack<T>(SomeCtorParam);
Originally, this feature was a non-spec optimization introduced to the JS reference implementation to aid performance by improving data locality in CPU caches.
Each AsyncSteps instance is expected to run a burst of steps until it requires a wait for an external event. The default recommended burst is 100 sub-steps. The burst can be reduced if it takes too long (e.g. 1 millisecond).
A non-core helper asi.relinquish() should be provided as a shortcut for an
empty sub-step, forcing an external wait with immediate completion from the
event-loop, effectively moving the current instance to the end of the queue.
void execute_callback( AsyncSteps asi[, previous_success_args] ):asi.success() call, if any;asi.success() or
asi.error();asi.setTimeout() and/or
set a cancel handler through asi.setCancel();asi.error( InternalError );asi.state() for global current job state data.void error_callback( AsyncSteps asi, error ):asi.error() call;asi.success() - continue execution from the next step after return,asi.error() - change error string,asi.error( InternalError ).asi.state() for global current job state data.void cancel_callback( AsyncSteps asi ):interface ISyncvoid sync( AsyncSteps asi, execute_callback[, error_callback] ):interface Stateinterface CatchTrace for callback(asi, exception) on any caught
exception;interface UnhandledError for callback(asi, error) on any unhandled
FutoIn error in steps;ValueRef get<V>(key) - get-accessor for dynamic items, type cast;ValueRef set<V>(key, value)- set-accessor for dynamic items,
type cast;ValueRef set_default<V>(key, value)- set for dynamic items if not
present, type cast;error_info - get/set last error_info property;set_error_info() / error_info() accessor as applicable;last_exception() - get last caught exception;catch_trace - get/set last CatchTrace property;set_catch_trace(cb) / get_catch_trace() accessor as
applicable;unhandled_error - get/set last UnhandledError property;set_unhandled_error(cb) / get_unhandled_error()
accessor as applicable;cancel_handler - get/set last CancelCallback property;set_cancel_handler(cb) / get_cancel_handler() accessor
as applicable;mem_pool - get associated memory pool property, if applicable;mem_pool() accessor as applicable.It is assumed that all functions in this section are part of the single AsyncSteps interface. However, they are grouped by semantic scope of use. Such design violates certain best practices, but it is done intentionally.
This API can be used in any context.
AsyncSteps add( execute_callback func[, error_callback onerror] ):AsyncSteps object accessor for easy chaining.AsyncSteps parallel( [error_callback onerror] ):add()ed sub-steps are executed in parallel in the same thread,success() does not allow any arguments - use either state() or
stack() to pass results.State state():null to identify invalid state of the AsyncSteps
object;ValueRef state(key) - alias for state().get(key);ValueRef state(key, defaultValue) - alias for
state().set_default(key, value).AsyncSteps copyFrom( AsyncSteps other ):AsyncSteps sync(ISync obj, execute_callback func[, error_callback onerror] ):obj.AsyncSteps successStep( [result_arg, ...] ):as.add( (as) => as.success( result_arg, ... ) ).AsyncSteps await( future_or_promise[, error_callback onerror] ):AsyncSteps newInstance():catch_trace, unhandled_error, and
cancel_handler handlers.mem pool.boolean cast():state() notes.FutoInAsyncSteps cast():binary() notes.FutoInAsyncSteps binary():AsyncSteps wrap(FutoInAsyncSteps):wrap() is
being called.This API can be used only inside execute_callback context. success()
and error() can be used in error_callback context as well.
void success( [result_arg, ...] )execute_callback;AsyncSteps stack during external
event waiting;void error( name [, error_info] ):FutoIn.Error exception immediately;onerror( async_iface, name ) after returning to execution engine;error_info is assigned to error_info state field.void errorNoThrow( name [, error_info] ):asi.error() which does not throw;
user must return from executing function without relying on
exceptions.void setTimeout( timeout_ms ):Timeout error is raised.call operator overloading:asi.success().void setCancel( cancel_callback oncancel ):void waitExternal():void relinquish():Pointer stack(size[, destroy_cb]):This API can be used only on Root AsyncSteps objects.
void execute() - must be called only once after root object steps are
configured.void cancel() - may be called on root object to asynchronously cancel
execution.AsyncSteps execution.asi.error().Promise promise() - must be called only once after root object steps
are configured.execute() into a native Promise object.This API can be used only inside execute_callback.
void loop( func, [, label] ):asi.break() is called;func( asi ) - loop body;label - optional label to use for asi.break() and asi.continue()
in inner loops.void forEach( map|list, func [, label] ):map or list element, call func( asi, key, value );func( asi, key, value ) - loop body;label - optional label to use for asi.break() and asi.continue()
in inner loops.void repeat( count, func [, label] ):func(asi, i) count times;count - how many times to call func;func( asi, i ) - loop body, i - current iteration starting
from 0;label - optional label to use for asi.break() and asi.continue()
in inner loops.void break( [label] ):AsyncSteps
internally;label - unwinds nested loops until label named loop is exited.error_info.void breakNoThrow( [label] ):break() that does not throw; user must return from
step immediately.void continue( [label] ):AsyncSteps
internally;label - breaks nested loops until label named loop is found.error_info.void continueNoThrow( [label] ):continue() that does not throw; user must return from
step immediately.Mutex classISync interface.c-tor(unsigned integer max=1, unsigned integer max_queue=null):max_queue - optionally limit queue length.Throttle classISync interface.c-tor(unsigned integer max, unsigned integer period_ms=1000, unsigned integer max_queue=null):period_ms - time period in milliseconds;max_queue - optionally limit queue length.Limiter classISync interface.c-tor(options):options.concurrent=1 - maximum number of concurrent flows;options.max_queue=0 - maximum number of queued flows;options.rate=1 - maximum number of critical section entries
in given period;options.period_ms=1000 - time period in milliseconds;options.burst=0 - maximum number of queued flows for rate
limiting.There is a strong assumption that AsyncSteps instances are executed in partial order by a common instance of event loop, with historical name AsyncTool.
There is an assumption that AsyncTool will be extended with Input/Output event support to act as a true reactor, but it may not always be possible.
AsyncTool was not defined in previous versions of specification because its interface is technology-specific, even though it always existed. Below is only a general suggestion.
Handle immediate( func ):func() - general callback.Handle deferred( delay, func ):delay - typically time period in milliseconds;func() - general callback.bool is_same_thread():void cancel( handle ):Handle object interface.bool is_valid( handle ):Handle object interface.To achieve initial goal of FutoIn project - universal cross-technology interface, a certain minimal binary interface has to be defined to be passed as an ordinary memory pointer for first parameter of callback functions, so any technology-specific solution could wrap that as necessary and allow mixing asynchronous step fragments written in different languages like C, C++, C#, ECMAScript, Java, Lua, Ruby, Rust, and others in scope of single asynchronous thread.
As base idea, Java Native Interface approach is taken, where pointer to abstract plain structure is passed. First field of such structure is pointer to table of plain C functions; each API function also assumes getting pointer to structure as first parameter. C++ virtual table works similarly.
Plain ISO C is supported one way or another in almost every technology to create bindings and other glue functionality. Therefore, it is used to describe binary interface with assumption of only standard platform-defined paddings and pointer sizes while all API callbacks use standard platform-defined calling convention.
There are limitations as it is problematic to guarantee type safety without
significant overhead, so binary interface user must be more aware of what is
being done. State access is split into two API functions operating over abstract
void pointers.
Binary data interface is used to pass execute_callback arguments between
technologies. Directly supported are:
For efficiency reasons, complex types like vectors may be stored both in agnostic C format and as technology-specific object instances. Therefore, binary value holding object supports cleanup callbacks to properly destroy such objects even from C or Assembly code.
typedef struct FutoInBinaryValue_ FutoInBinaryValue;
typedef struct FutoInType_ FutoInType;
typedef uint8_t FutoInTypeFlags;
enum
{
FTN_TYPE_CUSTOM_OBJECT = 0x01,
FTN_TYPE_STRING = 0x02,
FTN_TYPE_STRING16 = 0x03,
FTN_TYPE_STRING32 = 0x04,
FTN_TYPE_BOOL = 0x05,
FTN_TYPE_INT8 = 0x06,
FTN_TYPE_INT16 = 0x07,
FTN_TYPE_INT32 = 0x08,
FTN_TYPE_INT64 = 0x09,
FTN_TYPE_UINT8 = 0x0A,
FTN_TYPE_UINT16 = 0x0B,
FTN_TYPE_UINT32 = 0x0C,
FTN_TYPE_UINT64 = 0x0D,
FTN_TYPE_FLOAT = 0x0E,
FTN_TYPE_DOUBLE = 0x0F,
FTN_BASE_TYPE_MASK = 0x0F,
// --
FTN_TYPE_ARRAY = 0x10,
FTN_COMPLEX_TYPE_MASK = 0xF0,
};
struct FutoInType_
{
const FutoInTypeFlags flags;
void (*const cleanup)(FutoInBinaryValue* v);
// NOTE: extendable by implementation
};
struct FutoInBinaryValue_
{
const FutoInType* type;
union
{
const void* p;
const char* cstr;
const char16_t* cstr16;
const char32_t* cstr32;
bool b;
int8_t i8;
int16_t i16;
int32_t i32;
int64_t i64;
uint8_t u8;
uint16_t u16;
uint32_t u32;
uint64_t u64;
float f;
double d;
};
void* custom_data;
uint32_t length;
};
static inline void futoin_reset_binval(FutoInBinaryValue* v)
{
auto tp = v->type;
if (tp) {
auto* f = tp->cleanup;
if (f) {
f(v);
}
}
v->type = 0;
v->u64 = 0;
v->custom_data = 0;
v->length = 0;
}
Binary interface has a maximum limit of 4 custom arguments according to industry best practices. Therefore, argument object is collection of 4 binary value holders.
Binary interface is inspired by typical C++ vtable and Java Native Interface
specifications. It is assumed that pointer to agnostic FutoInAsyncSteps
structure is passed instead of technology-specific interface object. Such
structure has first field of pointer to function table. Each function receives
same pointer to structure as first argument. There may be additional
implementation-defined fields. Therefore, business logic code must not assume it
knows actual size of such structure.
Unlike most traditional cases, ISO C11 does not support exceptions and that imposes restrictions and duties for business logic. For example, raising errors requires returning from handler function manually.
Meaning of functions is same, except additional data and similar arguments may
be added to bind dynamic data to callbacks in user-defined ways.
Function table is also extended with AsyncTool interface for convenience.
The stateVariable() ABI primary purpose is to provide storage for
technology-native value types, which breaks general interoperability guarantees,
unless such variables get intentionally mapped as plain ISO C11 types.
By convention, special variable keys correspond to execution-related
functionality, and act as read-only. Their allocation and cleanup handlers must
be null pointers. Special key "error_info" must be accessible as const char*;
"last_exception" must be accessible as void * mapped to C++ const std::exception_ptr*.
typedef struct FutoInAsyncStepsAPI_ FutoInAsyncStepsAPI;
typedef struct FutoInAsyncSteps_ FutoInAsyncSteps;
typedef struct FutoInSyncAPI_ FutoInSyncAPI;
typedef struct FutoInSync_ FutoInSync;
typedef struct FutoInArgs_ FutoInArgs;
typedef struct FutoInHandle_ FutoInHandle;
struct FutoInArgs_
{
union
{
struct
{
FutoInBinaryValue arg0;
FutoInBinaryValue arg1;
FutoInBinaryValue arg2;
FutoInBinaryValue arg3;
};
FutoInBinaryValue args[4];
};
};
struct FutoInHandle_
{
void* data1;
void* data2;
ptrdiff_t data3;
};
typedef void (*FutoInAsyncSteps_execute_callback)(
FutoInAsyncSteps* bsi, void* data, const FutoInArgs* args);
typedef void (*FutoInAsyncSteps_error_callback)(
FutoInAsyncSteps* bsi, void* data, const char* code);
typedef void (*FutoInAsyncSteps_cancel_callback)(
FutoInAsyncSteps* bsi, void* data);
struct FutoInAsyncStepsAPI_
{
union
{
struct
{
// Index 0
void (*add)(
FutoInAsyncSteps* bsi,
void* data,
FutoInAsyncSteps_execute_callback f,
FutoInAsyncSteps_error_callback eh);
// Index 1
FutoInAsyncSteps* (*parallel)(
FutoInAsyncSteps* bsi,
void* data,
FutoInAsyncSteps_error_callback eh);
// Index 2
void* (*stateVariable)(
FutoInAsyncSteps* bsi,
void* data,
const char* name,
void* (*allocate)(void* data),
void (*cleanup)(void* data, void* value));
// Index 3
void* (*stack)(
FutoInAsyncSteps* bsi,
size_t data_size,
void (*cleanup)(void* value));
// Index 4
void (*success)(FutoInAsyncSteps* bsi, FutoInArgs* args);
// Index 5
void (*handle_error)(
FutoInAsyncSteps* bsi, const char* code, const char* info);
// Index 6
void (*setTimeout)(FutoInAsyncSteps* bsi, uint32_t timeout_ms);
// Index 7
void (*setCancel)(
FutoInAsyncSteps* bsi,
void* data,
FutoInAsyncSteps_cancel_callback ch);
// Index 8
void (*waitExternal)(FutoInAsyncSteps* bsi);
// Index 9
void (*loop)(
FutoInAsyncSteps* bsi,
void* data,
void (*f)(FutoInAsyncSteps* bsi, void* data),
const char* label);
// Index 10
void (*repeat)(
FutoInAsyncSteps* bsi,
void* data,
size_t count,
void (*f)(FutoInAsyncSteps* bsi, void* data, size_t i),
const char* label);
// Index 11
void (*breakLoop)(FutoInAsyncSteps* bsi, const char* label);
// Index 12
void (*continueLoop)(FutoInAsyncSteps* bsi, const char* label);
// Index 13
void (*execute)(
FutoInAsyncSteps* bsi,
void* data,
FutoInAsyncSteps_error_callback unhandled_error);
// Index 14
void (*cancel)(FutoInAsyncSteps* bsi);
// Index 15
void (*addSync)(
FutoInAsyncSteps* bsi,
FutoInSync* sync,
void* data,
FutoInAsyncSteps_execute_callback f,
FutoInAsyncSteps_error_callback eh);
// Index 16
ptrdiff_t (*rootId)(FutoInAsyncSteps* bsi);
// Index 17
int (*isValid)(FutoInAsyncSteps* bsi);
// Index 18
FutoInAsyncSteps* (*newInstance)(FutoInAsyncSteps* bsi);
// Index 19
void (*free)(FutoInAsyncSteps* bsi);
// Index 20
FutoInHandle (*sched_immediate)(
FutoInAsyncSteps* bsi, void* data, void (*cb)(void* data));
// Index 21
FutoInHandle (*sched_deferred)(
FutoInAsyncSteps* bsi,
uint32_t delay_ms,
void* data,
void (*cb)(void* data));
// Index 22
void (*sched_cancel)(FutoInAsyncSteps* bsi, FutoInHandle* handle);
// Index 23
int (*sched_is_valid)(FutoInAsyncSteps* bsi, FutoInHandle* handle);
// Index 24
int (*is_same_thread)(FutoInAsyncSteps* bsi);
};
void* funcs[25];
};
// NOTE: extendable by implementation
};
struct FutoInAsyncSteps_
{
#ifdef __cplusplus
FutoInAsyncSteps_(const FutoInAsyncStepsAPI* api) noexcept : api(api) {}
#endif
const FutoInAsyncStepsAPI* const api;
// NOTE: extendable by implementation
};
Synchronization object interface is defined separately from AsyncSteps interface, as it is quite possible AsyncSteps may be implemented in one technology while synchronization object is implemented in an absolutely different one.
struct FutoInSyncAPI_
{
union
{
struct
{
// Index 0
void (*lock)(FutoInAsyncSteps* bsi, FutoInSync* sync);
// Index 1
void (*unlock)(FutoInAsyncSteps* bsi, FutoInSync* sync);
};
void* funcs[2];
};
// NOTE: extendable by implementation
};
struct FutoInSync_
{
#ifdef __cplusplus
FutoInSync_() noexcept : api(nullptr) {}
#endif
const FutoInSyncAPI* const api;
// NOTE: extendable by implementation
};
In pseudo-code.
AsyncStepsImpl asi;
asi.add(
function( inner_as ){
if ( something )
inner_as.success( 1, 2 )
else
inner_as.error( NotImplemented )
},
function( inner_as, error ){
externalError( error );
}
).add(
function( inner_as, res1, res2 ){
externalSuccess( res1, res2 );
},
)
AsyncStepsImpl asi;
asi.add(
function( inner_as ){
inner_as.add(
function( inner2_as ){
if ( something )
inner2_as.success( 1 )
else
inner2_as.error( NotImplemented )
},
function( inner2_as, error )
{
log( "Spotted error " + error )
// continue with higher level error handlers
}
)
inner_as.add(
function( inner2_as, res1 ){
inner2_as.success( res1, 2 )
}
)
},
function( inner_as, error ){
externalError( error );
}
).add(
function( inner_as, res1, res2 ){
externalSuccess( res1, res2 );
},
)
parallel() Steps and state()AsyncStepsImpl asi;
asi.add(
function( inner_as ){
inner_as.parallel().add(
function( inner2_as ){
inner2_as.state().parallel_1 = 1;
},
function( inner2_as, error )
{
log( "Spotted error " + error )
// continue with higher level error handlers
}
).add(
function( inner2_as ){
inner2_as.state().parallel_2 = 2;
},
function( inner2_as, error )
{
inner2_as.state().parallel_2 = 0;
// ignore error
}
)
},
function( inner_as, error ){
externalError( error );
}
).add(
function( inner_as, res1, res2 ){
externalSuccess(
inner_as.state().parallel_1,
inner_as.state().parallel_2
);
},
)
AsyncStepsImpl asi;
asi.add(
function( asi ){
asi.repeat( 3, function( asi, i ) {
print i;
} );
asi.forEach( [ 1, 3, 3 ], function( asi, k, v ) {
print k "=" v;
} );
asi.forEach( asi.state(), function( asi, k, v ) {
print k "=" v;
} );
},
)
AsyncStepsImpl asi;
asi.add(
function( asi ){
asi.waitExternal();
callSomeExternal( function(err) {
if (err)
{
asi.error(err);
}
else
{
asi.success();
}
} );
},
)
AsyncStepsImpl asi;
MutexImpl mutex(10);
asi.sync(
mutex,
function( asi ){
// critical section with regular AsyncSteps
},
)
=END OF SPEC=