Features Documentation Examples Repository Blog Contact

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:

  1. The function runs in the target application’s memory space - it can access that application’s data
  2. Type safety is enforced at compile time - you can’t accidentally call functions with wrong parameters
  3. 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 application
  • method_name - The function to call on that object
  • param1, 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:

  1. The vtable must be the first member of the structure
  2. The vtable member must be named vtable
  3. The vtable must be marked with the VTABLE keyword 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 checking
  • INSTANCE(this) gives you a properly typed pointer to the object
  • VTABLE keyword 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

  1. Server Application (buffer_server):

    • Contains the actual buffer data
    • Implements the read/write/close functions
    • Provides the RPC interface
  2. Client Application (buffer_client):

    • Calls functions on the server’s buffer
    • Shares its data using the SHARED keyword
    • Receives return values from RPC calls
  3. 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:

  1. Compile-time Type Checking: The rpc_call() macro verifies parameter types and counts
  2. Vtable Validation: Only structures marked with VTABLE can be used as vtables
  3. Shared Memory Control: Only SHARED variables are accessible during RPC calls
  4. 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 CharacterDevVtable interface
  • 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.

64kB of protected memory ought to be enough for everyone.