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.