Simple Hello World RPC with Margo

The previous tutorial explained how to initialize a server and a client. This this tutoria, we will have the server register and RPC handler and the client send an RPC request to the server.

Server-side RPC handler

We will change the code of our server as follows.

server.c (show/hide)

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <margo.h>

static const int TOTAL_RPCS = 4;
static int num_rpcs = 0;

static void hello_world(hg_handle_t h);
DECLARE_MARGO_RPC_HANDLER(hello_world)

int main(int argc, char** argv)
{
    margo_instance_id mid = margo_init("tcp", MARGO_SERVER_MODE, 0, -1);
    assert(mid);

    hg_addr_t my_address;
    margo_addr_self(mid, &my_address);
    char addr_str[128];
    size_t addr_str_size = 128;
    margo_addr_to_string(mid, addr_str, &addr_str_size, my_address);
    margo_addr_free(mid,my_address);

    margo_set_log_level(mid, MARGO_LOG_INFO);
    margo_info(mid, "Server running at address %s", addr_str);

    hg_id_t rpc_id = MARGO_REGISTER(mid, "hello", void, void, hello_world);
    margo_registered_disable_response(mid, rpc_id, HG_TRUE);

    margo_wait_for_finalize(mid);

    return 0;
}

static void hello_world(hg_handle_t h)
{
    hg_return_t ret;

    margo_instance_id mid = margo_hg_handle_get_instance(h);

    margo_info(mid, "Hello World!");
    num_rpcs += 1;

    ret = margo_destroy(h);
    assert(ret == HG_SUCCESS);

    if(num_rpcs == TOTAL_RPCS) {
        margo_finalize(mid);
    }
}
DEFINE_MARGO_RPC_HANDLER(hello_world)

What changes is the following declaration of an RPC handler.

static void hello_world(hg_handle_t h);
DECLARE_MARGO_RPC_HANDLER(hello_world)

The first line declares the function that will be called upon receiving a “hello” RPC request. This function must take a hg_handle_t object as argument and does not return anything.

The second line declares hello_world as a RPC handler. DECLARE_MARGO_RPC_HANDLER is a macro that generates the code necessary for the RPC handler to be placed in an Argobots user-level thread (ULT).

The two lines that register the RPC handler in the Margo instance are the following.

hg_id_t rpc_id = MARGO_REGISTER(mid, "hello", void, void, hello_world);
margo_registered_disable_response(mid, rpc_id, HG_TRUE);

MARGO_REGISTER is a macro that registers the RPC handler. Its first argument is the Margo instance. The second is the name of the RPC. The third and fourth are the types of the RPC’s input and output, respectively. We will cover these in the next tutorial. For now, our hello_world RPC is not going to receive any argument and not return any value. The last parameter is the function we want to use as RPC handler.

The margo_registered_disable_response is used to indicate that this RPC handler does not send a response back to the client.

The rest of the program defines the hello_world function. From inside an RPC handler, we can access the Margo instance using margo_hg_handle_get_instance. This is the prefered method for better code organization, rather than declaring the Margo instance as a global variable.

The RPC handler must call margo_destroy on the hg_handle_t argument it is being passed, after we are done using it.

In this example, after receiving 4 requests, the RPC handler will call margo_finalize, which will make the main ES exit the call to margo_wait_for_finalize and terminate.

After the definition of the RPC handler, DEFINE_MARGO_RPC_HANDLER must be called for Margo to define additional wrapper functions.

Note

We will see at the end of this tutorial how to avoid using global variables (here TOTAL_RPCS and num_rpcs).

Calling the RPC from clients

The following code is the corresponding client.

client.c (show/hide)

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <margo.h>

int main(int argc, char** argv)
{
    if(argc != 2) {
        fprintf(stderr,"Usage: %s <server address>\n", argv[0]);
        exit(0);
    }

    hg_return_t ret;
    margo_instance_id mid = MARGO_INSTANCE_NULL;


    mid = margo_init("tcp",MARGO_CLIENT_MODE, 0, 0);
    assert(mid);

    hg_id_t hello_rpc_id = MARGO_REGISTER(mid, "hello", void, void, NULL);

    margo_registered_disable_response(mid, hello_rpc_id, HG_TRUE);

    hg_addr_t svr_addr;
    ret = margo_addr_lookup(mid, argv[1], &svr_addr);
    assert(ret == HG_SUCCESS);

    hg_handle_t handle;
    ret = margo_create(mid, svr_addr, hello_rpc_id, &handle);
    assert(ret == HG_SUCCESS);

    ret = margo_forward(handle, NULL);
    assert(ret == HG_SUCCESS);

    ret = margo_destroy(handle);
    assert(ret == HG_SUCCESS);

    ret = margo_addr_free(mid, svr_addr);
    assert(ret == HG_SUCCESS);

    margo_finalize(mid);

    return 0;
}

This client takes the server’s address as argument (copy-past the address printed by the server when calling the client). This string representation of the server’s address must be resolved into a hg_addr_t object. This is done by margo_addr_lookup.

Once resolved, the address can be used in a call to margo_create to create a hg_handle_t object. The hg_handle_t object represents an RPC request ready to be sent to the server.

margo_forward effectively sends the request to the server. We pass NULL as a second argument because the RPC does not take any input.

Because we have called margo_registered_disable_response, Margo knows that the client should not expect a response from the server, hence margo_forward will return immediately. We then destroy the handle using margo_destroy, free the hg_addr_t object using margo_addr_free, and finalize Margo.

Note

MARGO_REGISTER in clients is being passed NULL as last argument, since the actual RPC handler function is located in the server.

Attaching data to RPC handlers

Back to the server, we have used two global variables: TOTAL_RPCS and num_rpcs. Any good developer knows that global variables are evil and every use of a global variable kills a kitten somewhere. Fortunately, we can modify our program to get rid of global variables.

First we will declare a structure to encapsulate the server’s data.

typedef struct {
    int max_rpcs;
    int num_rpcs;
} server_data;

We can now initialize our server data as a local variable inside main, and attach it to our hello RPC handler, as follows.

server_data svr_data = {
        .max_rpcs = 4,
        .num_rpcs = 0
};
...
hg_id_t rpc_id = MARGO_REGISTER(mid, "hello", void, void, hello_world);
margo_registered_disable_response(mid, rpc_id, HG_TRUE);
margo_register_data(mid, rpc_id, &svr_data, NULL);

margo_register_data is the function to use to attach data to an RPC handler. It takes a Margo instance, the id of the registered RPC, a pointer to the data to register, and a pointer to a function to call to free that pointer. Since here our data is on the stack, we pass NULL as the last parameter.

Important

You need to make sure that the data attached to an RPC handler will not disappear before Margo is finalized. A common mistake consists of attaching a pointer to a piece of data that is on the stack within a function that then returns. In our example above, because main will block on margo_wait_for_finalize, we know main will return only after margo_finalize has been called.

In the hello_world RPC handler, we can now retrieve the attached data as follows.

const struct hg_info* info = margo_get_info(h);
server_data* svr_data = (server_data*)margo_registered_data(mid, info->id);

We can now replace the use of global variables by accessing the variables inside svr_data instead.

Important

If you have initialized Margo with multiple ES to server RPCs (last argument of margo_init strictly greater than 1), you will need to protect such attached data with a mutex or a read-write lock. For more information on such locking mechanisms, please refer to the Argobots tutorials.