Futures in Nabto Edge

Nabto Edge uses Futures to manage return values and completion of asynchronous API-functions; a future resolves once such function has completed. This guide explains the concepts behind futures and goes through practical examples showing how to use futures in Nabto Edge.

Futures are introduced to unify the way return values and completion of asynchronous functions are handled and to minimize the number of specialized functions required in the APIs: Instead of having an asynchronous and synchronous version of all functions, the API instead provides a single version returning a future: For asynchronous behavior, a callback can then be configured on the future - for synchronous behavior, the future provides a wait function.

In addition to futures, asynchronous functions that are expected to be invoked recurringly introduces the concept of listeners. Asynchronously listening for incoming CoAP requests for a specific endpoint is an example of such a function. A listener is a mechanism for keeping state behind the Nabto Edge APIs.

Futures and listeners are used in both the Nabto Edge Embedded SDK and the low level Nabto Edge Client SDK. Since futures and listeners works the same way in both SDKs this guide will mainly focus on the Embedded SDK, however, the concepts are transferable to the Client SDK. The high level wrappers that wrap the Nabto Edge Client SDK abstract away the futures concept - ie, futures can only be used directly in client applications if the low level Nabto Edge Client SDK is used directly.

Futures

Futures are owned by the user and must be allocated before use. The user is also responsible for freeing the future when it is no longer needed. The life cycle of a future is as follows:

  1. Allocate future
  2. Invoke asynchronous function
  3. Determine how to resolve future, either:
    • set callback function
    • call blocking wait for future
    • poll the future’s status
  4. Access results in resolved future, repeat from 2 as desired
  5. Free future

1. Allocation

First the future is allocated, for example:

NabtoDeviceFuture* future = nabto_device_future_new(device);

2. Passing to async function

The future can then be passed to an asynchronous function, for example:

nabto_device_close(device, future);

The asynchronous function will clear the underlying data structure before preparing it for the particular function. This means a future must never be reused in another function until the future has been resolved. This also means step 3 must never be performed before this step, e.g. you are not allowed to set a completion callback before invoking the asynchronous function.

3. Configuring how to resolve the future

When the asynchronous execution has been started, the caller must choose 1 of 3 ways to resolve the future as outlined below.

Regardless of the way the future is resolved, an error code is provided with the status of the executed task. If the status is OK the task was successful, and any output variables provided when invoking the asynchronous function has been set to appropriate values.

3.1 Set a callback to be invoked when future is resolved

Setting a callback allows the current thread to keep performing other tasks while the asynchronous task is executed, for example:

nabto_device_future_set_callback(future, &my_callback, &my_context);

When the task is completed, the callback will be invoked by the Nabto core. The Nabto core is running in its own thread responsible for this invocation. This has two important implications:

  • A callback function must never make blocking function calls as this will block the Nabto core.
  • The callback function must be implemented with concurrency between the Nabto core thread and the main thread in mind. That is, even though the application appears to be single threaded, two threads are working concurrently. Also note in this context that you cannot use blocking locks as this would violate the above constraint about not blocking the Nabto core.

3.2 Blocking wait for future to be resolved

Blocking wait is invoked like this:

nabto_device_future_wait(future)

The blocking wait function can be timed (wait for max n milliseconds) or not (wait indefinitely). If timed wait returns the FUTURE_NOT_RESOLVED status, the deadline was reached before the future resolved.

3.3 Poll status of the future

The status of the future can be polled like this:

NabtoDeviceError status = nabto_device_future_ready(future);

Once the returned status is no longer FUTURE_NOT_RESOLVED, the future has been resolved.

4. Accessing results in resolved future

Once the future has been resolved, the results (written to output variables of the asynchronous function) can be processed and the future can now be reused for new asynchronous function invocations.

5. Freeing the future

When the future is no longer needed, it must be freed:

nabto_device_future_free(future);

Listeners

Listeners are used to handle recurring asynchronous events (eg. incoming CoAP requests). The main purpose of the listener is to queue incoming events if they cannot be handled immediately (eg. a new CoAP request arrives while still handling the previous request).

The listener works with a future, also allocated by the user (as outlined in the previous section). The future’s role is to interface with the user’s application, for instance to invoke user specified callbacks: The listener listens for and enqueues Nabto API events and resolves the associated future for each such event, repeating until the listener is stopped.

If no future is associated with a listener, events are enqueued until a future is associated.

Similar to futures, listeners are owned by the user, ie they must be allocated and deallocated by the user. The life cycle of a listener is as follows:

  1. Allocate listener
  2. Initialize listener for a specific purpose
  3. Register future to be resolved when a listener event has occured
  4. Handle event and repeat from 3 as desired
  5. Stop listener
  6. Free listener

1. Allocation

First the listener is allocated, for example:

NabtoDeviceListener listener = nabto_device_listener_new(device);

2. Initialize listener

The listener is initialized for a specific purpose, for instance to listen for incoming CoAP requests:

nabto_device_coap_init_listener(device, listener, method, segments);

Once initialized a listener should never be used for another purpose. That is, if a listener was initialized for a CoAP endpoint, it cannot be reinitialized for streaming, or even for another CoAP endpoint (method/path combination).

After initialization, the listener now enqueue any events until a future is registered.

3. Register future

Register a future to be resolved when an event occurs in the listener (or for an already enqueued event), for instance register a future to be resolved when a CoAP listener has a new request ready:

nabto_device_listener_new_coap_request(listener, future, &request);

or when a stream listener has a newly accepted stream ready:

nabto_device_listener_new_stream(listener, future, &stream)

This provides the listener with a future to resolve when a new event occurs. A listener can only have 1 associated future at the same time.

4. Repeat

Once an associated future has resolved, the user code handles the event. Step 3 can then be repeated to wait for the next event.

5. Stopping listener

When a listener is no longer needed, it must be stopped:

nabto_device_listener_stop(listener)

This will cause the listener to abort all queued events, clean up internal state, and resolve the associated future (if any).

6. Freeing listener

When a listener is stopped it can be freed:

nabto_device_listener_free(listener);

Listener and future - annotated example

The following pseudo code outlines the relationship between listener and future:

listener = nabto_device_listener_new()
nabto_device_stream_init_listener(device, listener, 42)

// now the listener is initialized and new incoming stream requests will be added to
// the listener's queue by the Nabto core

future = nabto_device_future_new()
nabto_device_listener_new_stream(listener, future, &stream) // [1]

// the listener is now associated with a future to be resolved on an incoming streaming

nabto_device_future_wait(future)

// the future is now resolved and the stream reference specified at [1] is set
//
// the listener is no longer associated with a future, so new incoming streams will be
// enqueued - the listener is associated with the future again:

nabto_device_listener_new_stream(listener, future, &stream)

// the stream reference is about to be overwritten by the Nabto core

nabto_device_future_wait(future)

// ... the stream reference is now overwritten by the Nabto core

Hello World example

To show the usage of futures, this full example implements a Nabto Edge Embedded device accepting CoAP requests on a single endpoint. To keep the focus on futures and listeners, this example will not include any user authentication, meaning anyone with an appropiate client will be able to connect to the device.

Since futures and listeners are conceptually equal in the Embedded SDK and the Client SDK, a detailed embedded implementation is shown whereas the accompanying client implementation uses the C++-wrapper where the details of futures and listeners are abstracted away.

Device

The device starts by configuring and starting the Nabto Embedded SDK core. When the core is running, a listener is created and initialized to listen for CoAP requests for the /hello-world endpoint.

Then the listener is queried for the next CoAP request using a future. When the future resolves, the request is handled. To show the different ways of handling futures, the listener is queried 3 times after which the listener and device is closed down and cleaned up.

Client

As client, use the simple CoAP client. The client simply opens a connection to the device, and sends CoAP request.