Getting Started
Quick Start
1. Write a Script
// hello.c
double get_time();
int main() {
double t = get_time();
printf("Time: %.2f\n", t);
return 0;
}2. Register FFI Functions
// host.c
#include "common.h"
#include "bytecode.h"
static double g_time = 0.0;
// FFI function implementation
static void ffi_get_time(LcvmState *vm) {
lcvm_push_double(vm, g_time);
}
int main() {
LcvmState vm;
compact_program_t prog;
// Initialize VM
lcvm_init(&vm);
// Register FFI function (with type signature)
lcvm_register_function_ex("get_time", ffi_get_time, 0, "");
// Compile script
system("lcvmc -oc hello.lcbc hello.c");
// Load and link
lcvm_compact_load("hello.lcbc", &prog, 0);
lcvm_link_program(&prog);
// Run
g_time = 123.45;
lcvm_run_compact(&vm, &prog, 0);
// Cleanup
lcvm_compact_free(&prog);
return 0;
}3. Compile and Run
The easiest path is the bundled makefile:
make host
./host
To build manually, link against lib/liblcvm.a (no need to drag in raw object files):
# Assumes you ran `make` at the project root so lib/liblcvm.a exists
gcc -o host host.c -I./src -L./lib -llcvm -lm
./host
Output:
Time: 123.45Initialization
Pattern A: Automatic Memory Allocation (Simple)
#include "common.h"
#include "bytecode.h"
int main() {
LcvmState vm;
compact_program_t prog;
/* 1. Initialize VM (default: 100KB stack + 100KB garea) */
lcvm_init(&vm);
/* 2. Load bytecode */
lcvm_compact_load("script.lcbc", &prog, 0);
/* 3. Register FFI functions (with type signature) */
lcvm_register_function_ex("my_func", ffi_my_func, 1, "i");
/* 4. Link FFI */
lcvm_link_program(&prog);
/* 5. Initialize global variables (IMPORTANT!) */
lcvm_compact_init_garea(&vm, &prog);
/* 6. Run */
lcvm_run_compact(&vm, &prog, 0);
/* 7. Cleanup */
lcvm_compact_free(&prog);
return 0;
}Pattern B: Embedded (Static Memory Management)
#include "common.h"
#include "bytecode.h"
/* Cortex-M0+ (32KB RAM) */
#define VM_STACK_SIZE (8 * 1024)
#define VM_GAREA_SIZE (8 * 1024)
#define VM_HEAP_SIZE (12 * 1024)
static char vm_stack[VM_STACK_SIZE];
static char vm_garea[VM_GAREA_SIZE];
static unsigned char vm_heap[VM_HEAP_SIZE];
int embedded_main() {
LcvmState vm;
compact_program_t prog;
/* 1. Initialize heap */
lcvm_heap_init(vm_heap, VM_HEAP_SIZE);
/* 2. Initialize VM with external buffers */
lcvm_init_with_memory(&vm, vm_stack, VM_STACK_SIZE,
vm_garea, VM_GAREA_SIZE);
/* 3. Load bytecode from Flash ROM */
extern const unsigned char script_lcbc[];
extern const int script_lcbc_size;
lcvm_compact_load_mem(script_lcbc, script_lcbc_size, &prog, 0);
/* 4. Register FFI & link */
lcvm_register_function_ex("gpio_write", ffi_gpio_write, 2, "ii");
lcvm_link_program(&prog);
/* 5. Initialize global variables */
lcvm_compact_init_garea(&vm, &prog);
/* 6. Run */
return lcvm_run_compact(&vm, &prog, 0);
}FFI Registration (Type Signatures)
Use lcvm_register_function_ex() to enable argument type checking in DEBUG builds:
// Basic registration
lcvm_register_function("func", ffi_func, 2);
// Registration with type signature
lcvm_register_function_ex("func", ffi_func, 2, "id");Signature Characters
| Character | Type |
|---|---|
i | int |
d | double |
f | float |
p | pointer |
s | string |
Usage Example
// Function taking int and double
static void ffi_add(LcvmState *vm) {
double b = lcvm_pop_double(vm);
int a = lcvm_pop_int(vm);
lcvm_push_double(vm, a + b);
}
lcvm_register_function_ex("add", ffi_add, 2, "id");
Type mismatch log output:
FFI add: arg 0 type mismatch (expected i, got pointer)Heap Watermark API
Ideal for temporary bulk memory allocation and batch release patterns:
// Record current heap position
size_t mark = lcvm_heap_mark();
// Allocate temporary memory
char *buf = lcvm_malloc(64 * 1024);
// ... processing ...
// Batch release to mark position (O(1), no fragmentation)
lcvm_heap_release(mark);Usage from Scripts
int main() {
int mark = heap_mark();
// Temporary processing...
heap_release(mark); // Batch release
return 0;
}Static Type Mode
Compile with -Os option for ~2x speedup:
# Normal mode
lcvmc -oc output.lcbc source.c
# Static type mode (faster)
lcvmc -Os -oc output.lcbc source.cLimitations
- Optimized for integer operations (normal mode recommended for floating-point)
- printf/sprintf supported; configure
LCVM_PRINTF_BUFSIZEfor embedded (default: 1024 bytes)
Debug Support
Watchpoint
Detects writes to the global variable area (garea). The watch range is specified by an offset and size into garea (size must be 1, 2, 4, or 8).
// Build with LCVM_WATCH_ENABLED=1
// Obtain garea offsets for player_hp / game_state
// (e.g. from the compiler's debug info)
int hp_offset = /* ... */;
int state_offset = /* ... */;
lcvm_watch_add(&vm, hp_offset, sizeof(int), "player_hp"); // Start watching
lcvm_watch_add(&vm, state_offset, sizeof(int), "game_state");
// ... execution ...
lcvm_watch_clear(&vm); // Stop watchingGDB Pretty Printer
(gdb) source tools/gdb_lcvm.py
(gdb) p vm
$1 = LcvmState { pc=42, sp=8, error=NONE, ... }Postmortem Tool
Writes a diagnostic binary into a buffer and returns the number of bytes written (buffer must be at least 24 bytes). The caller is responsible for persisting the data.
// Output dump on host side
unsigned char buf[256];
int len = lcvm_dump_diagnostic_binary(&vm, buf, sizeof(buf));
if (len > 0) {
FILE *fp = fopen("crash.bin", "wb");
if (fp) {
fwrite(buf, 1, len, fp);
fclose(fp);
}
}# Analyze on PC
python tools/lcvm_postmortem.py crash.binError Handling
Basic Pattern
int vm_execute_with_retry(LcvmState *vm, compact_program_t *prog, int max_retries) {
int ret, i;
for (i = 0; i < max_retries; i++) {
ret = lcvm_run_compact(vm, prog, 0);
if (ret == 0) {
return 0; // Success
}
// Get error information
if (lcvm_has_error(vm)) {
LcvmErrorCode code = lcvm_get_error_code(vm);
const char *msg = lcvm_get_error_message(vm);
int pc = lcvm_get_error_pc(vm);
int line = lcvm_get_error_line(vm);
fprintf(stderr, "VM Error: %s (code=%d) at PC=%d, line=%d\n",
msg, code, pc, line);
// Handle based on error type
switch (code) {
case LCVM_ERR_HEAP_EXHAUSTED:
// Reset and retry
lcvm_reset(vm);
lcvm_compact_init_garea(vm, prog);
continue;
default:
return -1;
}
}
}
return -1;
}Troubleshooting
Q1: Global variables not initialized
Symptom: Previous values persist after script re-execution
Cause: lcvm_reset() does not initialize global variables (by design)
Solution:
lcvm_reset(&vm);
lcvm_compact_init_garea(&vm, &prog); // Add thisQ2: Alignment error (Bus Error / HardFault)
Symptom: Crashes on ARM Cortex-M0
Cause: Direct casting instead of using vm_stack_cell_t
Solution: Use VM_STACK_CELL() macro
// Bad
int *p = (int *)&vm.stack[vm.sp];
// Good
VM_STACK_CELL(vm.stack, vm.sp)->i = 42;Q3: Heap exhaustion
Symptom: lcvm_malloc() returns NULL
Cause: Arena Allocator doesn't free memory
Solution:
// Full reset
lcvm_heap_reset();
// Or partial release (Heap Watermark)
size_t mark = lcvm_heap_mark();
// ... processing ...
lcvm_heap_release(mark);Q4: FFI function not found
Symptom: "Failed to link program"
Cause: Function not registered before lcvm_link_program()
Solution:
lcvm_register_function_ex("my_func", ffi_my_func, 1, "i"); // First
lcvm_link_program(&prog); // Then link
Production Deployment
Recommended Memory Settings
Cortex-M0+ (32KB RAM)
#define VM_STACK_SIZE (8 * 1024) // 8KB
#define VM_GAREA_SIZE (8 * 1024) // 8KB
#define VM_HEAP_SIZE (12 * 1024) // 12KB
// Remaining 4KB: C stack, OS, driversCortex-M3/M4 (64KB RAM)
#define VM_STACK_SIZE (16 * 1024) // 16KB
#define VM_GAREA_SIZE (16 * 1024) // 16KB
#define VM_HEAP_SIZE (24 * 1024) // 24KB
// Remaining 8KB: C stack, OS, driversESP32 (320KB RAM)
#define VM_STACK_SIZE (32 * 1024) // 32KB
#define VM_GAREA_SIZE (32 * 1024) // 32KB
#define VM_HEAP_SIZE (128 * 1024) // 128KB
// Remaining: WiFi/BLE, FreeRTOS
Performance Tuning
Watchdog Configuration
Prevent infinite loops and ensure timely execution:
Time-based watchdog:
lcvm_set_watchdog_ms(100); // Max 100ms execution time
Instruction count watchdog:
lcvm_set_watchdog_ic(100000); // Max 100,000 instructionsRecommended Settings
| Application Type | Time Limit | Instruction Limit |
|---|---|---|
| Real-time control (10ms cycle) | 8ms | 50,000 |
| Periodic task (1s cycle) | 800ms | 500,000 |
| Batch processing | 5000ms | 1,000,000+ |
| Development/Debug | 0 (disabled) | 0 (disabled) |
Long-Running Operation
For 24/7 operation, implement periodic maintenance:
void periodic_maintenance(LcvmState *vm, compact_program_t *prog) {
static int cycle_count = 0;
cycle_count++;
// Reset heap every 1000 cycles
if (cycle_count % 1000 == 0) {
lcvm_reset(vm);
lcvm_compact_init_garea(vm, prog);
lcvm_heap_reset();
printf("Heap reset at cycle %d\n", cycle_count);
}
}Sandbox Mode
Sandbox mode is enabled by default — pointer access is range-checked from the moment the VM is initialized. Disable it only for fully trusted bytecode:
lcvm_set_sandbox_mode(0); // disable (or set LCVM_SANDBOX=0)
While enabled (the default), it restricts pointer access to:
- VM stack (
vm->stack) - Global area (
vm->garea) - Heap regions only
Hardware access must go through validated FFI functions.
Advanced Topics
For more advanced topics including:
- Architecture deep dive
- FFI dispatch internals
- Memory layout optimization
- Hot-reload mechanism
Visit the Architecture page.
For production best practices:
- Error recovery patterns
- C stack depth management
- Safety-critical deployment
Visit the Best Practices page.