Skip to content

API design

cryptocode edited this page Dec 3, 2018 · 57 revisions

NOTE: replaced by IPC proposal

Overview

This document proposes a C API for Nano that will cover everything the current RPC protocol does, with the intention of replacing the current RPC implementation in terms of this API (and possibly a REST interface)

The point of the C API is to provide a secure, fast and portable way to embed a Nano node into a hosting application.

The end product for users are static and dynamic libraries, and a single C11 header file, hopefully with lots of language bindings later on.

Sessions

The hosting application first establishes a session. All API calls takes a nano_session argument (an opaque pointer), allowing the api implementation to keep track of resources. Passing this explicitly enable clients to have multiple sessions going at the same time.

A session can potentially be created in a number of ways, but typically through starting a node daemon:

nano_session* session = nano_node_daemon (data_path);

(To make this possible, rai::daemon has been refactored to be non-blocking.)

The session is closed by calling nano_session_close (session). This releases all resources related to the session.

Evolvability

An important consideration is making the api easy to evolve.

This proposal claims that a good C api only expose functions and opaque pointers, to avoid ABI/FFI issues. The only way to evolve is thus to add more functions, and this often results in dubious naming schemes and complex semantics.

It's tempting to expose functions like this:

struct* nano_pending_result 
     = nano_accounts_pending (session, 
                             (char**) account_list, 
                             block_count, 
                             threshold);

This approach has the following problems:

  • The API is "fixed in plastic". What if we want to remove, change or add parameters in the future?
  • Using structs in the API is opening a can of worms, since struct layouts may change, there are ABI issues due to compiler differences, and it's very easy to leak abstractions.
  • Hard to map types like char** in many FFI libraries

Flexibility through queries and accessors

This proposal tries to be as flexible as the JSON-based RPC api, at the cost of some type safety. Most C api clients will be dynamically typed languages. For statically typed languages, it makes sense to generate or write idiomatic type safe wrappers.

The main idea is the concept of queries and generic accessors.

Just as with the JSON interface, you first need to build a query. Then you execute the query, and then you read the result.

This produces a fairly low-level API, on top of which language bindings can build "porcelain" APIs.

Let's look at an example, namely getting pending account blocks.

First we create a query.

nano_value* q = nano_query_accounts_pending (session);

Next we use a generic function to add accounts to the query. It's important to note that this function works for any query accepting accounts. If not supported, the API will respond with the nano_invalid_field error code.

nano_add_accounts (session, q, "xrb_a12fed...");
nano_add_accounts (session, q, "xrb_bffec0...");

Let's set the optional threshold value as well:

nano_set_threshold (session, q, "1000000000000000000000000");

Now that our query is built, let's execute it to get the result:

nano_value* res = nano_execute (session, q);

And, just as with the query, we can now use generic accessors to pull out the data.

nano_blocks* blocks = nano_get_blocks (session);
// ... more accessor calls here to get the data
nano_release (session, blocks);

Again, this function works for any query returning blocks. As with the query, an attempt to call a non-supported accessor yields the nano_invalid_field error code.

Finally, we release any resources allocated by the query and result objects.

nano_release (session, q);
nano_release (session, res);

To implement this efficiently, the C++ side utilize mixin classes for generic accessors, and the command pattern for the execution.

Adding query options to the API thus never breaks existing clients, as long as the queries are designed to be backwards compatible.

Async

The nano_execute_async version will return a future/promise.

nano_future* future = nano_execute_async (session, q);
...
nano_value* val = nano_get (future);
...

Resource management

Memory/resource management is simple from the C side of the API:

  • Any function that returns a result may allocate resources, which is freed through a call to nano_release. This is implemented polymorphically, and thus works for any resource.

  • The implementation keeps track of mappings of opaque pointers to RAII-managed objects.

  • As a safe guard, any unreleased resources will be released when the session is closed.

Error handling

All API calls initially resets the per-session error flag, and all api calls may set it.

Two functions are available for returning the last error, as well as a string representation of the error.

This helps produce a uniform api, since no function returns error codes, but rather results.

nano_value* res = nano_execute (session, q);
if (nano_last_error (session))
{
    log (nano_last_error_string (session));
}

Or as a short-hand if you're only interested in the string:

if (const char* err = nano_last_error_string (session))
{
    log (err);
}

Implementation details

The implementation switches to RAII based C++ internally. The opaque pointers are converted to their polymorphic counterpart.

For instance, nano_add_accounts gets an opaque nano_query, which is dynamically cast to query_accounts, a mix-in class that holds a list of accounts. This mix-in can be used in any query that expects accounts.

The implementation for adding accounts to any query is thus as simple as:

void nano_add_accounts (nano_session * session_a, struct nano_value * query_a, const char* account_a)
{
    query_accounts* q = from_opaque<query_accounts> (query_a);
    q->accounts.push_back (account_a);
}

This is how you define a query using mixins:

/** Implements the opaque nano_accounts_pending_query */
struct accounts_pending_query 
    : public query_accounts, public query_blocks, public query_threshold
{
    virtual result* execute ()
    {
        //  ...implement logic, create result...
	
        return nullptr;
    }
};

So when you call:

nano_value* res = nano_execute (session, my_pending_query);

...the implementation polymorphically calls accounts_pending_query#execute.

Which explains why we can have a single nano_execute API to cover all queries.

File layout

  • Everything sits in rai/api
  • A public api.h C11 header for consumers
  • A private.hpp which is shared between API implementation files
  • One file per category of API functions, such as accounts.cpp, blocks.cpp, etc. This division corresponds to the categorization of the RPC queries. Better than a single 5000+ line file.

References

http://wezfurlong.org/blog/2006/dec/coding-for-coders-api-and-abi-considerations-in-an-evolving-code-base/

https://docs.python.org/3/library/ctypes.html