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.45

Initialization

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

CharacterType
iint
ddouble
ffloat
ppointer
sstring

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.c

Limitations

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 watching

GDB 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.bin

Error 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 this

Q2: 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

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, drivers

Cortex-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, drivers

ESP32 (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 instructions
Application TypeTime LimitInstruction Limit
Real-time control (10ms cycle)8ms50,000
Periodic task (1s cycle)800ms500,000
Batch processing5000ms1,000,000+
Development/Debug0 (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:

Hardware access must go through validated FFI functions.


Advanced Topics

For more advanced topics including:

Visit the Architecture page.

For production best practices:

Visit the Best Practices page.