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.
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:
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 functionIn CMRX, an object is a structure that contains:
Think of it like a simple C++ class, but implemented in C.
Every RPC-callable object must follow these rules:
vtableVTABLE keyword for securityLet’s create a complete example with a circular buffer that acts like a character device. We’ll build this in three parts:
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:
INSTANCE(this) is a CMRX macro that provides type safetyNow 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 objectVTABLE keyword marks the vtable as legitimate (security feature)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.
Server Application (buffer_server):
Client Application (buffer_client):
SHARED keywordCMRX Kernel:
CMRX RPC provides several safety mechanisms:
rpc_call() macro verifies parameter types and countsVTABLE can be used as vtablesSHARED variables are accessible during RPC callsTry experimenting with this example:
CharacterDevVtable interfaceThis RPC mechanism forms the foundation for building complex, multi-process embedded systems while maintaining security and reliability.