Async API

Many of our core API functions are asynchronous, meaning they return promptly, perform the bulk of the work in the background and notify you when done. This is because these functions might involve network calls or other asynchronous operations and thus take a long time (in computer terms) to complete. This architecture also serves as the basis for the future evolution of the plugin implementation, which should have at most minimal influence on the public API.

C++

In our C++ API async functions have signature like this with the last parameter being a callback function named OnComplete:

void GetPropertyI(int NodeId, const FString& Property, TUniqueFunction<void(int Value, EI3DHErrorCode ErrorCode)> OnComplete);

The semantics are as follows:

  • The function might be called from any thread and will return promptly.

  • The OnComplete callback will be called on the game thread once the operation is completed.

  • You should check the error code before accessing the “return value”.

Lifetime Considerations

Because the hub connector is an actor, it is difficult to control its lifetime over the async API calls. Therefore, each hub connector has an API object, which is managed via shared pointers. By capturing a shared pointer in a lambda, you can keep the API object alive over the async API call. You can also use it to get back to the hub connector in the callback. See the following example:

TSharedPtr<FI3DHConnectorAPI> API = HubConnector->GetAPI();
API->GetSelectionAPI()->ClearSelection([API](EI3DHErrorCode ErrorCode)
{
	// Make further API calls.
    API->GetSelectionAPI()->AddToSelection(...);

    // ...OR...

	if (AI3DHConnector* Connector = API->GetConnector())
	{
		// Do something with Connector...
	}
});

Note

The accessor functions for the individual API objects (GetSelectionAPI(), etc.) return raw pointers. These should not be stored across scope boundaries. For example, storing the selection API in a local variable to make multiple AddToSelection() calls is fine, but storing it in a member variable or capturing it in a lambda should be avoided. Store the shared pointer to the connector API object instead and access the selection API via GetSelectionAPI() when needed.

Futures

A callback-based function can be converted into a future using the following pattern:

TPromise<int> Promise;
TFuture<int> Future = Promise.GetFuture();
HubConnector->GetAPI()->GetInstanceGraphAPI()->GetPropertyI(NodeId, Property, [Promise = MoveTemp(Promise)](int Value, EI3DHErrorCode ErrorCode)
{
	if (ErrorCode == EI3DHErrorCode::Success)
		Promise.SetValue(Value);
	else
		Promise.SetValue(-1);
});
// Use Future...

Keep in mind not to block the game thread waiting for the future, as this will result in a deadlock, because the callback needs to run on the game thread to fulfill the promise.

Blueprint

In Blueprint async functions are exposed as “Async Blueprint Nodes”. The usual limitations of Async Nodes apply, including that they are only available in event graphs and macros, but not in Blueprint functions.