# ESP32-P4 JIT System - Limitations and Technical Constraints This document describes the technical limitations, constraints, and safety considerations for the ESP32-P4 dynamic code loading system. --- ## Table of Contents - [Type System Limitations](#type-system-limitations) - [Alignment Requirements](#alignment-requirements) - [Thread Safety](#thread-safety) - [Memory Barriers](#memory-barriers) - [Return Value Constraints](#return-value-constraints) - [Workarounds and Best Practices](#workarounds-and-best-practices) --- ## Type System Limitations ### 64-bit Types Not Supported The wrapper generation system **does not support 64-bit types** in function parameters or return values. **Unsupported Types:** - `int64_t` / `uint64_t` - `double` - `long long` - Any 64-bit integer or floating-point type **Why This Limitation Exists:** The memory-mapped I/O system uses a fixed 4-byte stride for argument passing: ```c volatile int32_t *io = (volatile int32_t *)0x50000000; io[0] = arg0; // Slot 0 (4 bytes) io[1] = arg1; // Slot 1 (4 bytes) io[2] = arg2; // Slot 2 (4 bytes) ``` 64-bit types require 8 bytes and cannot fit in a single 4-byte slot. Attempting to pass 64-bit types will result in: - Data truncation - Memory corruption - Undefined behavior **Example - Will Fail:** ```c // THIS WILL NOT WORK double compute_double(int64_t a, double b) { return (double)a + b; } ``` **What Happens:** 1. Parser extracts signature successfully 2. Wrapper generator creates incorrect code 3. Runtime: upper 32 bits of values are lost 4. Function receives garbage data **Workaround:** Split 64-bit types into two 32-bit slots manually: ```c // Instead of int64_t, use two int32_t int32_t compute_split(int32_t a_low, int32_t a_high, int32_t b_low, int32_t b_high) { int64_t a = ((int64_t)a_high << 32) | (uint32_t)a_low; int64_t b = ((int64_t)b_high << 32) | (uint32_t)b_low; int64_t result = a + b; return (int32_t)result; // Return only low 32 bits } ``` On host side: ```c io[0] = (int32_t)(value & 0xFFFFFFFF); // Low 32 bits io[1] = (int32_t)((value >> 32) & 0xFFFFFFFF); // High 32 bits ``` --- ## Alignment Requirements ### 4-Byte Stride Limitation **Critical Constraint:** All arguments are stored in 4-byte aligned slots, regardless of their natural alignment requirements. **Memory Layout:** ``` Address Index Size Alignment ───────────────────────────────────────────── 0x50000000 [0] 4 4-byte aligned 0x50000004 [1] 4 4-byte aligned 0x50000008 [2] 4 4-byte aligned 0x5000000c [3] 4 4-byte aligned ``` ### RISC-V ABI Alignment Requirements Per RISC-V ABI specification (ilp32f): | Type | Size | Natural Alignment | |------------|---------|-------------------| | int8_t | 1 byte | 1-byte | | int16_t | 2 bytes | 2-byte | | int32_t | 4 bytes | 4-byte | | int64_t | 8 bytes | **8-byte** | | float | 4 bytes | 4-byte | | double | 8 bytes | **8-byte** | | pointers | 4 bytes | 4-byte | **Problem:** The I/O array provides only 4-byte alignment, but `double` and `int64_t` require 8-byte alignment. ### Why This Matters **Misaligned Access Consequences:** 1. **Undefined Behavior:** RISC-V specification does not guarantee correct results for misaligned loads/stores 2. **Performance Penalty:** Misaligned access may trap to software handler (10-100x slower) 3. **Potential Hardware Exception:** Some implementations raise alignment faults 4. **Data Corruption:** Bits may be read/written incorrectly **Example - Incorrect Code:** ```c // Generated by wrapper (WRONG for double) volatile int32_t *io = (volatile int32_t *)0x50000000; double value = *(double*)&io[1]; // io[1] is at 0x50000004 // 4-byte aligned, but double needs 8-byte ``` This violates RISC-V alignment rules and causes undefined behavior. ### Current System Behavior The system **does not validate alignment** and will generate incorrect code for types requiring >4-byte alignment. **Safe Types:** - `int8_t`, `int16_t`, `int32_t`, `uint8_t`, `uint16_t`, `uint32_t` - `float` (4-byte, naturally 4-byte aligned) - All pointer types (4-byte on RV32) **Unsafe Types:** - `int64_t`, `uint64_t` (require 8-byte alignment) - `double` (requires 8-byte alignment) --- ## Thread Safety ### Single-Threaded Constraint **CRITICAL:** The wrapper system is **NOT thread-safe** and must only be used in single-threaded contexts. ### Why Wrappers Are Not Thread-Safe **Shared Memory-Mapped I/O:** All dynamically loaded functions share the same I/O region: ```c // Every wrapper uses this SAME address volatile int32_t *io = (volatile int32_t *)0x50000000; ``` **Race Condition Example:** ```c // Thread 1 io[0] = 10; io[1] = 20; call_remote(); // Expects io[0]=10, io[1]=20 // Thread 2 (concurrent) io[0] = 50; // OVERWRITES Thread 1's data io[1] = 60; call_remote(); // Result: Thread 1 gets io[0]=50, io[1]=60 (WRONG) ``` ### Data Race Scenarios **1. Argument Corruption:** ``` Time Thread 1 Thread 2 ──────────────────────────────────────────── t0 io[0] = 10; t1 io[1] = 20; t2 io[0] = 50; ← Overwrites Thread 1 t3 call_remote(); (Thread 1 sees io[0]=50, WRONG) t4 io[1] = 60; t5 call_remote(); ``` **2. Return Value Corruption:** ```c // Both threads write return value to io[31] io[31] = result_thread1; io[31] = result_thread2; // Overwrites result_thread1 // Thread 1 reads io[31] and gets Thread 2's result ``` ### Allowed Usage Patterns **✓ Safe: Single-Threaded Sequential Calls** ```c void task1(void) { io[0] = 10; io[1] = 20; call_remote(); float result = *(float*)&io[31]; } void task2(void) { io[0] = 30; io[1] = 40; call_remote(); float result = *(float*)&io[31]; } // Safe if called sequentially: task1(); task2(); ``` **✗ Unsafe: Concurrent Calls from Multiple Threads** ```c // Thread 1 xTaskCreate(task1, "Task1", 2048, NULL, 5, NULL); // Thread 2 xTaskCreate(task2, "Task2", 2048, NULL, 5, NULL); // RACE CONDITION: Both access io[] simultaneously ``` ### Making Wrappers Thread-Safe (Manual) If multi-threaded access is required, you must implement external synchronization: **Option 1: Global Mutex** ```c static SemaphoreHandle_t io_mutex; void thread_safe_call(int32_t a, int32_t b) { xSemaphoreTake(io_mutex, portMAX_DELAY); io[0] = a; io[1] = b; call_remote(); float result = *(float*)&io[31]; xSemaphoreGive(io_mutex); } ``` **Option 2: Separate I/O Regions Per Thread** Modify config to allocate multiple I/O regions: ```yaml # Not supported by default, requires custom implementation wrapper: arg_base_thread0: 0x50000000 arg_base_thread1: 0x50000100 arg_base_thread2: 0x50000200 ``` **Option 3: Task Queue (Recommended)** ```c typedef struct { int32_t arg0; int32_t arg1; float *result; } call_request_t; QueueHandle_t call_queue; void worker_task(void *param) { call_request_t req; while (1) { xQueueReceive(call_queue, &req, portMAX_DELAY); io[0] = req.arg0; io[1] = req.arg1; call_remote(); *req.result = *(float*)&io[31]; } } ``` --- ## Memory Barriers ### Multi-Core Synchronization The ESP32-P4 has **dual RISC-V cores**. Without proper memory barriers, the wrapper system may exhibit race conditions on multi-core systems. ### Why Memory Barriers Are Needed **Problem: Volatile Is Not Enough** ```c volatile int32_t *io = (volatile int32_t *)0x50000000; io[0] = 10; // Core 0 writes io[1] = 20; // Core 0 writes call_remote(); // Core 1 may not see writes yet ``` `volatile` prevents compiler optimizations but **does not enforce memory ordering between cores**. ### RISC-V Memory Model RISC-V has a **relaxed memory model** (RVWMO - RISC-V Weak Memory Ordering): - Cores may observe memory operations in different orders - Store buffers may delay writes becoming visible - Loads may be reordered with other loads **Without Fences:** ``` Core 0 Core 1 ───────────────────────────────────────── io[0] = 10; io[1] = 20; // May read OLD values x = io[0]; // Could be 0 y = io[1]; // Could be 0 ``` ### Required Memory Barriers **Full Fence:** ```c __sync_synchronize(); // GCC builtin // or asm volatile("fence rw, rw"); // RISC-V fence instruction ``` **Recommended Wrapper Pattern:** ```c esp_err_t call_remote(void) { volatile int32_t *io = (volatile int32_t *)0x50000000; int32_t a = io[0]; int32_t b = io[1]; __sync_synchronize(); // ← BARRIER: Ensure reads complete float result = compute(a, b); __sync_synchronize(); // ← BARRIER: Ensure writes visible io[31] = *(int32_t*)&result; return ESP_OK; } ``` ### When Memory Barriers Are Needed **Scenario 1: Multi-Core Host ↔ Loaded Code** ``` Core 0 (Host) Core 1 (Loaded Code) ──────────────────────────────────────────────────── io[0] = 10; io[1] = 20; [NO BARRIER] call_remote(); // May see stale io[] values ``` **Fix:** Add barrier after writing arguments: ```c io[0] = 10; io[1] = 20; __sync_synchronize(); // ← Force visibility call_remote(); ``` **Scenario 2: Loaded Code ↔ Multi-Core Host** ``` Core 0 (Loaded Code) Core 1 (Host) ──────────────────────────────────────────────────── io[31] = result; [NO BARRIER] x = io[31]; // May see stale value ``` **Fix:** Add barrier before reading result: ```c call_remote(); __sync_synchronize(); // ← Ensure write is visible float result = *(float*)&io[31]; ``` ### ESP32-P4 Specific Notes **Cache Coherency:** ESP32-P4 has hardware cache coherency, but: - Store buffers still introduce delays - Non-cached memory may require explicit fences - DMA regions **always** require barriers **Recommendation:** Always use memory barriers when: 1. Multiple cores access shared I/O region 2. Communicating between cached and uncached regions 3. Using DMA with dynamically loaded code 4. Any cross-core synchronization --- ## Return Value Constraints ### Scalar Returns Only The wrapper system **only supports scalar return types**. **Supported Return Types:** - `void` - `int8_t`, `int16_t`, `int32_t` - `uint8_t`, `uint16_t`, `uint32_t` - `float` - Pointer types (e.g., `int32_t*`, `void*`) **Unsupported Return Types:** - `struct` or `union` (aggregate types) - Arrays (e.g., `int32_t[4]`) - `int64_t`, `uint64_t`, `double` (64-bit types) ### Why Aggregates Cannot Be Returned **RISC-V ABI for Aggregate Returns:** Per RISC-V calling convention, structs >8 bytes are returned via memory: ```c struct Vec3 { float x, y, z; }; // 12 bytes Vec3 get_vector(void); // Caller allocates space, passes pointer as hidden first argument // Callee writes to that pointer ``` The wrapper system cannot: 1. Allocate space for return struct 2. Pass hidden pointer to callee 3. Copy result from callee's buffer **Example - Will Not Work:** ```c typedef struct { float x, y, z; } Vec3; Vec3 get_position(void) { Vec3 v = {1.0f, 2.0f, 3.0f}; return v; // UNSUPPORTED } ``` **Error:** Parser will extract signature, but generated code is incorrect. ### Workaround for Aggregate Returns **Option 1: Return via Pointer Parameter** ```c // Instead of returning struct, pass output pointer void get_position(Vec3 *out) { out->x = 1.0f; out->y = 2.0f; out->z = 3.0f; } ``` **Option 2: Split into Multiple Calls** ```c float get_position_x(void) { return 1.0f; } float get_position_y(void) { return 2.0f; } float get_position_z(void) { return 3.0f; } ``` **Option 3: Pack into Single Scalar** For small structs, pack into `uint32_t`: ```c typedef struct { int16_t x, y; } Vec2i; // 4 bytes total uint32_t get_vec2i(void) { Vec2i v = {10, 20}; return *(uint32_t*)&v; // Pack both into uint32_t } // Caller unpacks: uint32_t packed = get_vec2i(); Vec2i v = *(Vec2i*)&packed; ``` --- ## Workarounds and Best Practices ### General Guidelines **1. Validate Function Signatures Before Wrapping** ```python # Check parameter types manually params = ['int32_t', 'float', 'int32_t*'] for p in params: if 'int64_t' in p or 'double' in p: raise ValueError(f"64-bit type not supported: {p}") ``` **2. Use Type Aliases Consistently** ```c // Define once, use everywhere typedef int int32_t; typedef unsigned int uint32_t; typedef float float32_t; ``` **3. Document Thread Safety Requirements** ```c /** * @brief Compute function * @warning NOT THREAD-SAFE: Must be called sequentially * @warning Requires external mutex if used in multi-threaded context */ int32_t compute(int32_t a, int32_t b); ``` **4. Add Runtime Assertions** ```c esp_err_t call_remote(void) { volatile int32_t *io = (volatile int32_t *)0x50000000; // Validate pointer alignment int32_t *ptr = (int32_t*)io[2]; assert(((uintptr_t)ptr & 0x3) == 0); // Must be 4-byte aligned // Rest of wrapper... } ``` **5. Use Static Analysis** Enable compiler warnings: ```yaml compiler: flags: - "-Wall" - "-Wextra" - "-Wcast-align" # Warn on alignment issues - "-Wstrict-aliasing" # Warn on type punning ``` ### Performance Considerations **Memory Barrier Overhead:** - Full fence: ~10-20 cycles - Impacts performance on critical paths - Only add where necessary **Alignment Penalty:** - Misaligned access: 10-100x slower if trapped - Keep all types 4-byte aligned **Cache Effects:** - Uncached I/O region: ~10x slower than cached - Consider using cached memory if bandwidth-limited --- ## Summary of Constraints | Constraint | Status | Workaround Available | |--------------------------|-----------------|----------------------| | 64-bit types | Not supported | Manual split | | Struct returns | Not supported | Pointer parameter | | Array returns | Not supported | Pointer parameter | | Thread safety | Not provided | External mutex | | Multi-core barriers | Manual required | Add __sync_synchronize() | | 8-byte alignment | Not guaranteed | Avoid double/int64_t | | Pointer validation | Not provided | Manual assertions | --- **Last Updated:** 2025-01-20