Remote Procedure Call
Now that you understand basic CMRX applications, let’s explore how applications can communicate with each other while maintaining memory isolation. This example shows how to use Remote Procedure Calls (RPCs) to enable secure inter-process communication.
What You’ll Learn
- How applications communicate across memory boundaries
- Creating RPC servers and clients
- Understanding objects and vtables in CMRX
- Implementing type-safe cross-process function calls
- Using shared memory for data exchange
Why RPC is Needed
Memory isolation is great for security and stability, but it creates a problem: how do applications share data or services? CMRX solves this with Remote Procedure Calls (RPCs).
Think of RPC as a way to “call a function in another application” while maintaining security. When you make an RPC call:
- The function runs in the target application’s memory space - it can access that application’s data
- Type safety is enforced at compile time - you can’t accidentally call functions with wrong parameters
- Security is maintained - only explicitly shared data is accessible
Basic RPC Call Structure
Here’s what an RPC call looks like:
int result = rpc_call(object, method_name, param1, param2);
object- The target object in another applicationmethod_name- The function to call on that objectparam1, param2- Parameters passed to the function
Understanding Objects and VTables
In CMRX, an object is a structure that contains:
- A vtable (virtual function table) - pointers to functions that operate on this object
- Data members - the actual data the functions work with
Think of it like a simple C++ class, but implemented in C.
Object Structure Rules
Every RPC-callable object must follow these rules:
- The vtable must be the first member of the structure
- The vtable member must be named
vtable - The vtable must be marked with the
VTABLEkeyword for security
Complete Example: Character Device
Let’s create a complete example with a circular buffer that acts like a character device. We’ll build this in three parts:
Part 1: Define the Interface (API)
First, define what functions our character device will support:
#include <cmrx/rpc/interface.h>
struct CharacterDevVtable {
// Read data from device
int (*read)(INSTANCE(this), char * buffer, unsigned char bufsize);
// Write data to device
int (*write)(INSTANCE(this), const char * buffer, unsigned char datasize);
// Close the device
void (*close)(INSTANCE(this));
};
What This Does:
- Defines a “contract” for any character device
INSTANCE(this)is a CMRX macro that provides type safety- Each function pointer represents an operation you can perform
Part 2: Create the RPC Server
Now let’s implement a circular buffer that uses this interface:
#include <cmrx/application.h>
#include <cmrx/rpc/implementation.h>
// Tell CMRX we're implementing the CharacterDevVtable interface
// for the CircularBuffer object
IMPLEMENTATION_OF(struct CircularBuffer, struct CharacterDevVtable);
// Define the object structure
struct CircularBuffer {
const struct CharacterDevVtable * vtable; // Must be first!
char buffer[256]; // Our data storage
unsigned char read_cursor;
unsigned char write_cursor;
bool empty;
bool open;
};
// Implementation of the read function
static int buffer_read(INSTANCE(this), char * buffer, unsigned char bufsize) {
if (!this->open) {
return -1; // Device closed
}
unsigned char cursor = 0;
while (bufsize--) {
if (this->read_cursor == this->write_cursor) {
this->empty = true;
break; // No more data
}
buffer[cursor++] = this->buffer[this->read_cursor++];
}
return cursor; // Return number of bytes read
}
// Implementation of the write function
static int buffer_write(INSTANCE(this), const char * buffer, unsigned char datasize) {
if (!this->open) {
return -1; // Device closed
}
unsigned char cursor = 0;
while (datasize--) {
if (this->read_cursor == this->write_cursor && this->empty == false) {
break; // Buffer full
} else {
this->empty = false;
}
this->buffer[this->write_cursor++] = buffer[cursor++];
}
return cursor; // Return number of bytes written
}
// Implementation of the close function
static void buffer_close(INSTANCE(this)) {
this->open = false;
}
// Create the vtable and mark it as legitimate with VTABLE keyword
VTABLE struct CharacterDevVtable circular_buffer_vtable = {
buffer_read,
buffer_write,
buffer_close
};
// Create the actual buffer instance
struct CircularBuffer buffer = {
&circular_buffer_vtable, // Point to our vtable
{ 0 }, // Initialize buffer to zeros
0, // Read cursor at start
0, // Write cursor at start
true, // Buffer is empty
true // Buffer is open
};
// Make this an RPC server application
OS_APPLICATION(buffer_server);
Key Points:
IMPLEMENTATION_OF()provides compile-time type checkingINSTANCE(this)gives you a properly typed pointer to the objectVTABLEkeyword marks the vtable as legitimate (security feature)- The object instance is globally accessible for RPC calls
Part 3: Create the RPC Client
Now let’s create a client that uses the buffer:
#include "circular_buffer.h" // Include the buffer declaration
#include <cmrx/ipc/rpc.h>
// Mark data as shared so RPC can access it
SHARED char mybuffer[16] = "Hello, RPC!";
void do_write_buffer(void * data)
{
// Make an RPC call to write data to the buffer
int written = rpc_call(buffer, write, mybuffer, sizeof(mybuffer));
if (written < 0) {
printf("Error writing buffer: %d\n", written);
} else {
printf("Written into buffer: %d bytes\n", written);
}
}
// Create the client application
OS_APPLICATION(buffer_client);
OS_THREAD_CREATE(buffer_client, do_write_buffer, NULL, 64);
Important Security Feature:
The SHARED keyword is crucial - it tells CMRX that this variable can be accessed during RPC calls. Without it, the RPC server couldn’t read your data, maintaining security.
How It All Works Together
-
Server Application (
buffer_server):- Contains the actual buffer data
- Implements the read/write/close functions
- Provides the RPC interface
-
Client Application (
buffer_client):- Calls functions on the server’s buffer
- Shares its data using the
SHAREDkeyword - Receives return values from RPC calls
-
CMRX Kernel:
- Routes RPC calls between applications
- Validates security (vtable legitimacy, shared data)
- Ensures type safety at compile time
Security and Type Safety Features
CMRX RPC provides several safety mechanisms:
- Compile-time Type Checking: The
rpc_call()macro verifies parameter types and counts - Vtable Validation: Only structures marked with
VTABLEcan be used as vtables - Shared Memory Control: Only
SHAREDvariables are accessible during RPC calls - Runtime Validation: The kernel checks that RPC objects are legitimate
Benefits of This Approach
- Modularity: Split your firmware into well-defined, isolated components
- Security: Applications can only access explicitly shared data
- Type Safety: Compile-time checking prevents many common errors
- Reusability: The same vtable interface can be implemented by different objects
- Maintainability: Clear separation between interface and implementation
Next Steps
Try experimenting with this example:
- Add more functions to the
CharacterDevVtableinterface - Create multiple clients that use the same buffer
- Implement different types of character devices using the same interface
- Add error handling and more sophisticated data structures
This RPC mechanism forms the foundation for building complex, multi-process embedded systems while maintaining security and reliability.