Skip to content

Getting Started

Qifan Lu edited this page Sep 18, 2017 · 3 revisions

Getting Started

This tutorial will help you get started with the u-RPC framework step by step.

Create Endpoints

The first step to use the u-RPC framework is to create caller and callee endpoints, and that can be done with the URPC class for the Python API and urpc_init() for the C API.

For the callee endpoint, suppose we have a function send_func that takes bytes data and sends it to the caller endpoint. We create the callee endpoint by calling the constructor:

# Callee endpoint
callee = URPC(send_callback=send_func)

When we receive any u-RPC message data from the callee endpoint, we call URPC.recv_callback() with the data we received.

For the caller endpoint the process is similar for the Python API, except send_func sends data to the callee endpoint and URPC.recv_callback() should be called with data coming from the callee endpoint.

For the C API, the caller endpoint is invoked with urpc_init():

//Caller endpoint
urpc_t caller;

//Initialize caller endpoint
urpc_init(
    &caller,
    //Function table size
    //(Currently not implemented)
    16,
    //Send buffer size
    96,
    //Temporary buffer size
    32,
    //Send function and closure data
    NULL,
    send_func,
    //Capacity of callback table
    8
);

Similar to the Python API, here send_func sends data to the callee endpoint.

Add Functions to Endpoint

For the callee endpoint, we then add functions to the endpoint to make it available for others to call. Let's add a test function that increases the only argument by 1:

def test(arg_types, args):
    # Make sure the only argument is uint16_t
    assert len(arg_types)==1 and arg_types[0]==urpc.U16
    # Increase argument by 1
    args[0] += 1
    # Return results with corresponding type information
    return [urpc.U16], args

# Add test function to endpoint
callee.add_func(
    func=test,
    name="test"
)

For each u-RPC function, it receives the types and the values of the arguments, and it returns the types and the values of the results.

For functions with fixed signature, we can pass the signature of the function to the framework and let it check the types of the arguments for us:

def test(x):
    return x+1

# Use "urpc_wrap()"
callee.add_func(
    func=urpc.urpc_wrap([urpc.U16], [urpc.U16], test),
    name="test_1"
)

# Pass arguments and results types when adding function
callee.add_func(
    func=test,
    name="test_2",
    arg_types=[urpc.U16],
    ret_types=[urpc.U16]
)

Or if you only want to attach u-RPC signature information to the function without modifying it:

@urpc.urpc_sig([urpc.U16], [urpc.U16])
def test(x):
    return x+1

# The function isn't changed
assert test(1)==2

callee.add_func(
    func=test,
    name="test_3"
)

High-level u-RPC types

Apart from the basic types, the u-RPC framework also supports high-level data types. Each high-level data type has an underlying basic data type and describes how to serialize and deserialize data from its underlying type.

To create a high-level u-RPC type, we inherit from URPCType, implements loads() and dumps() for serialization and deserialization, and declare its underlying type:

# Simplified u-RPC string type
class StringType(urpc.URPCType):
    # Constructor
    def __init__(self, encoding="utf-8"):
        # Encoding
        self.encoding = encoding
    # Deserializer
    def loads(self, data):
        return text_type(data, encoding=self.encoding)
    # Serializer
    def dumps(self, value):
        return bytearray(value.encode(self.encoding))
    # Underlying type
    underlying_type = URPC_TYPE_VARY

The above code shows how the StringType works. It can be used to represent a string on the callee endpoint.

To use high-level types, just replace basic types with high-level types or their instances in the function signature:

@urpc.urpc_sig([urpc.StringType], [urpc.U16])
def str_len(string):
    return len(string)

Query Remote Function Handle

For the caller endpoint, we need to know the handle of the remote function before we can call them. While we can be certain of the function handles if we add functions in order, a better approach would be querying the function handle by function name.

For the Python API we call URPC.query() to query corresponding function handle:

# Error happened
def cb(e, handle):
    # Error happened
    if e:
        print("Error happened: %s" % e)
    # RPC succeeded
    else:
        test_handle = handle
        print("Handle for function test: %d" % handle)

test_handle = None
# Query function handle
caller.query("test", cb)

For the C API we call urpc_get_func():

//Handle of test function
urpc_func_t test_handle;

//Query function handle callback
WIO_CALLBACK(cb) {
    //Error happened
    if (status)
        printf("Error happened: %d\n", status);
    //Query succeeded
    else {
        //Function handle
        test_handle = *((urpc_func_t*)result);

        printf("Handle for function test: %d", test_handle);
    }

    return WIO_OK;
}
//Query function handle
urpc_get_func(
    &caller,
    "test",
    NULL,
    cb
);

For both Python and C API, we query the function handle on the endpoint with the function name. When the query succeeds the callback will be invoked with the function handle, or else it will receive an error code describing the error happened.

Call Remote Function

Now that all the preparation work is done, we can start calling our test function from the caller endpoint. Below is the demo code for the Python API URPC.call():

def cb(e, result):
    # Error happened
    if e:
        print("Error happened: %s" % e)
    # RPC succeeded
    else:
        assert result[0]==2
        print("RPC call succeeded!")

# Do RPC
caller.call(test_handle, [urpc.U16], [1], cb)

For the C API, we do remote procedure call with urpc_call():

//RPC callback function
WIO_CALLBACK(cb) {
    //Error happened
    if (status)
        printf("Error happened: %d\n", status);
    //RPC succeeded
    else {
        //RPC results
        void** rpc_results = (void**)result;
        //The first return value
        //(rpc_results[0] points to the signature of return values)
        uint16_t* ret = (uint16_t*)rpc_results[1];

        if (*ret==2)
            printf("RPC call succeeded!\n");
    }

    return WIO_OK;
}
//Argument
uint16_t arg = 1;
//Do RPC
urpc_call(
    &caller,
    test_handle,
    URPC_SIG(1, URPC_TYPE_U16),
    URPC_ARG(&arg),
    NULL,
    cb
);

For both Python API and C API, we do u-RPC call on the endpoint with function handle, types of arguments, arguments and a callback. When the callback is invoked we can check if the call succeeds or not, and if it succeeds we can then get the call result and move on.