I would also stick to making the RPC inheritly async and then emulating sync behaviour on top with an await functionality.
Code:
int gracht_client_invoke(gracht_client_t*, struct gracht_message_context*, struct gracht_message*);
int gracht_client_await(gracht_client_t*, struct gracht_message_context*);
int gracht_client_await_multiple(gracht_client_t*, struct gracht_message_context**, int, unsigned int);
int gracht_client_status(gracht_client_t*, struct gracht_message_context*, struct gracht_param*);
This is the interface of my protocol client, one can invoke purely async RPC calls, and if the message is synchronous you can await the result. The invoke method is never called directly, but rather by the protocol functions that are generated by my protocol generator. If you need to await an RPC you need to provide a message context where the result can be stored. It's a temporary storage thats required to hold the response.
An example of calling a synchronous method through an async API:
Code:
struct vali_link_message msg = VALI_MSG_INIT_HANDLE(GetProcessService());
status = svc_process_report_crash(GetGrachtClient(), &msg.base, *GetInternalProcessId(), Context, sizeof(Context_t), Signal->signal);
gracht_client_await(GetGrachtClient(), &msg.base);
svc_process_report_crash_result(GetGrachtClient(), &msg.base, &osStatus);