The RPC (Remote Procedure Call) API provides a mechanism for making structured function calls over WebSocket connections using the Message API. It builds on top of the messaging layer to provide a request-response pattern with automatic message tagging, retry logic, and a modular server-side dispatch system. The RPC API has both a client-side component for making calls and a server-side component for servicing them.
The client-side RPC API is encapsulated in the vrtql_rpc structure. This structure wraps a WebSocket
connection (vws_cnx) and
provides facilities for making RPC calls, handling timeouts with automatic
retries, processing out-of-band messages, and reconnecting on connection
failure.
You create an RPC instance using vrtql_rpc_new(), passing in an established WebSocket
connection. The RPC instance does not take ownership of the connection —
you are responsible for freeing both the RPC instance and the connection
independently. The instance is configured with sensible defaults: five retry
attempts on timeout and a default out-of-band message handler that simply
discards unrecognized messages.
There are two ways to make RPC calls. The first is vrtql_rpc_exec(), which is the low-level call. It
takes a vrtql_msg request, sends
it to the server, and returns the raw reply message. The caller takes ownership
of the reply and must free it with vrtql_msg_free(). This
function automatically generates a random tag and sets it in the message's
routing map under the key "tag". It uses this tag to correlate
the response with the request. Any messages received that do not match the tag
are passed to the out_of_band callback handler.
The second method is vrtql_rpc_invoke(), which is a higher-level
convenience function. It calls vrtql_rpc_exec() internally but additionally
translates the response's "rc" (return code) and
"msg" (message) headers into the thread-local error state
(vws.e.code and vws.e.text). If the
response contains content, it is copied into the rpc->val
buffer. This function takes ownership of the request message — the caller
should not use or free the request after calling it. It returns
true if a response was received and false
otherwise.
Both functions handle connection failures gracefully. If the connection
drops during a send, the RPC instance will attempt to reconnect using the
vws_reconnect() function and, if a
reconnect callback is defined, will invoke it to allow the
caller to perform any post-reconnection setup (such as re-authentication). If
reconnection succeeds, the send is retried. If reconnection fails or the
connection drops during a receive, the error state is set with
VE_SOCKET along with VE_SEND or
VE_RECV to indicate where the failure occurred.
You can also generate random tags independently using vrtql_rpc_tag(), which produces a random alphanumeric
string of the specified length. The caller is responsible for freeing the
returned string.
The following example illustrates basic client-side RPC usage:
// Create a WebSocket connection and connect vws_cnx* cnx = vws_cnx_new(); vws_connect(cnx, "ws://localhost:8181/websocket"); // Create an RPC instance using the connection vrtql_rpc* rpc = vrtql_rpc_new(cnx); // Create a request message vrtql_msg* req = vrtql_msg_new(); vrtql_msg_set_header(req, "id", "session.login"); vrtql_msg_set_header(req, "username", "admin"); vrtql_msg_set_header(req, "password", "secret"); // Make the RPC call (low-level) vrtql_msg* reply = vrtql_rpc_exec(rpc, req); if (reply != NULL) { cstr rc = vrtql_msg_get_header(reply, "rc"); printf("Return code: %s\n", rc); vrtql_msg_free(reply); } // Cleanup vrtql_msg_free(req); vrtql_rpc_free(rpc); vws_disconnect(cnx); vws_cnx_free(cnx);
The server-side RPC API provides a modular dispatch system for organizing and servicing RPC calls. It is built around three key structures: modules, a system registry, and an execution environment.
An RPC module (vrtql_rpc_module) is a named collection of RPC
functions. You create a module using vrtql_rpc_module_new(), passing in the module name.
You then register individual RPC functions within the module using vrtql_rpc_module_set(). Each function is identified by
a name and must conform to the vrtql_rpc_call callback signature, which takes a
vrtql_rpc_env pointer and a
vrtql_msg pointer and returns a
reply message. You can look up registered functions using vrtql_rpc_module_get().
The RPC system (vrtql_rpc_system) is a registry that holds modules.
You create a system using vrtql_rpc_system_new() and register modules in it
using vrtql_rpc_system_set(). You can
retrieve modules by name using vrtql_rpc_system_get().
The execution environment (vrtql_rpc_env) provides context to RPC handlers
during execution. It contains a data pointer for user-defined
data and a module pointer that references the module
containing the invoked function. This environment is set up by the dispatch
system before calling the handler.
To service an incoming RPC call, you use vrtql_rpc_service(). This function takes the RPC
system, an environment, and the incoming request message. The request must
contain an "id" header in the format
"module.function" (for example,
"session.login"). The service function parses this identifier,
looks up the appropriate module and function, sets the environment, and invokes
the handler. It takes ownership of the request message and frees it after the
handler returns. The handler's reply message is returned to the caller.
To assist in writing RPC handlers, the vrtql_rpc_reply() convenience function creates a new
reply message, copies the request's tag into the reply for correlation, and sets
the reply format to match the request.
The following example illustrates setting up a server-side RPC system:
// Define RPC handler functions vrtql_msg* session_login(vrtql_rpc_env* e, vrtql_msg* req) { vrtql_msg* reply = vrtql_rpc_reply(req); vrtql_msg_set_header(reply, "rc", "0"); vrtql_msg_set_header(reply, "msg", "Login successful"); return reply; } vrtql_msg* session_logout(vrtql_rpc_env* e, vrtql_msg* req) { vrtql_msg* reply = vrtql_rpc_reply(req); vrtql_msg_set_header(reply, "rc", "0"); return reply; } // Create and populate a module vrtql_rpc_module* module = vrtql_rpc_module_new("session"); vrtql_rpc_module_set(module, "login", session_login); vrtql_rpc_module_set(module, "logout", session_logout); // Create the system and register the module vrtql_rpc_system* system = vrtql_rpc_system_new(); vrtql_rpc_system_set(system, module); // Service an incoming request (e.g. from a message server handler) vrtql_rpc_env env; env.data = NULL; vrtql_msg* req = vrtql_msg_new(); vrtql_msg_set_header(req, "id", "session.login"); vrtql_msg* reply = vrtql_rpc_service(system, &env, req); // reply now contains the handler's response // req has been freed by vrtql_rpc_service() if (reply != NULL) { vrtql_msg_free(reply); } // Cleanup vrtql_rpc_system_free(system);