ベストプラクティス

商用展開

メモリ設定

ターゲット別推奨設定

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
// 残り4KB: Cスタック、OS、ドライバ

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
// 残り8KB: Cスタック、OS、ドライバ

ESP32 (320KB RAM)

#define VM_STACK_SIZE   (32 * 1024)  // 32KB
#define VM_GAREA_SIZE   (32 * 1024)  // 32KB
#define VM_HEAP_SIZE    (128 * 1024) // 128KB
// 残り: WiFi/BLE、FreeRTOS等

メモリサイズの決定方法

1. スタックサイズ: 最深の関数呼び出し経路を分析

2. グローバル領域サイズ: グローバル変数の総量

3. ヒープサイズ: 動的確保の最大値


初期化ベストプラクティス

パターンA: 自動確保(簡易版)

#include "common.h"
#include "bytecode.h"

int main() {
    LcvmState vm;
    compact_program_t prog;

    // 1. VM初期化(デフォルト: 100KB stack + 100KB garea)
    lcvm_init(&vm);

    // 2. バイトコードロード
    lcvm_compact_load("script.lcbc", &prog, 0);

    // 3. FFI関数の登録
    lcvm_register_function("my_func", ffi_my_func, 1);

    // 4. FFIリンク
    lcvm_link_program(&prog);

    // 5. グローバル変数初期化(重要!)
    lcvm_compact_init_garea(&vm, &prog);

    // 6. 実行
    lcvm_run_compact(&vm, &prog, 0);

    // 7. 後処理
    lcvm_compact_free(&prog);
    return 0;
}

パターンB: 組み込み向け(静的メモリ管理)

/* 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. ヒープ初期化
    lcvm_heap_init(vm_heap, VM_HEAP_SIZE);

    // 2. VM初期化(外部バッファ使用)
    lcvm_init_with_memory(&vm, vm_stack, VM_STACK_SIZE,
                          vm_garea, VM_GAREA_SIZE);

    // 3. 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. FFI登録・リンク
    lcvm_register_function("gpio_write", ffi_gpio_write, 2);
    lcvm_link_program(&prog);

    // 5. グローバル変数初期化
    lcvm_compact_init_garea(&vm, &prog);

    // 6. 実行
    return lcvm_run_compact(&vm, &prog, 0);
}

リセットとリロード

完全なVMリセット

重要: lcvm_reset()グローバル変数を初期化しません。これは意図的な設計です(ホストがリセット後に独自の値を設定できるようにするため)。

void vm_full_reset(LcvmState *vm, compact_program_t *prog) {
    // 1. VM状態をリセット(sp, fp, ic をゼロに、スタック/garea をクリア)
    lcvm_reset(vm);

    // 2. グローバル変数を初期値に復元(明示的に呼び出す)
    lcvm_compact_init_garea(vm, prog);

    // 3. ヒープをリセット(必要な場合)
    lcvm_heap_reset();
}

ホットリロード(スクリプト再読み込み)

void vm_reload_script(LcvmState *vm, compact_program_t *old_prog,
                      const char *new_script_path) {
    compact_program_t new_prog;

    // 1. 古いプログラムを解放
    lcvm_compact_free(old_prog);

    // 2. 新しいバイトコードをロード
    if (lcvm_compact_load(new_script_path, &new_prog, 0) != 0) {
        fprintf(stderr, "Failed to load new script\n");
        return;
    }

    // 3. FFI再リンク
    lcvm_link_program(&new_prog);

    // 4. VM状態をリセット
    lcvm_reset(vm);

    // 5. グローバル変数を新しい初期値で初期化
    lcvm_compact_init_garea(vm, &new_prog);

    // 6. 新しいプログラムをコピー
    *old_prog = new_prog;
}

エラー処理

リトライパターンとエラー回復

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;  // 成功
        }

        // エラー情報を取得
        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);

            // エラー種別に応じた処理
            switch (code) {
                case LCVM_ERR_DIV_ZERO:
                case LCVM_ERR_NULL_PTR:
                    // スクリプトのバグ → リトライしても無駄
                    return -1;

                case LCVM_ERR_WATCHDOG_TIME:
                case LCVM_ERR_WATCHDOG_IC:
                    // タイムアウト → 閾値を上げるか、スクリプト最適化
                    fprintf(stderr, "Watchdog timeout, consider optimization\n");
                    return -1;

                case LCVM_ERR_HEAP_EXHAUSTED:
                    // ヒープ不足 → リセットして再実行
                    fprintf(stderr, "Heap exhausted, resetting...\n");
                    lcvm_reset(vm);
                    lcvm_compact_init_garea(vm, prog);
                    continue;  // リトライ

                default:
                    return -1;
            }
        }
    }

    fprintf(stderr, "VM execution failed after %d retries\n", max_retries);
    return -1;
}

セーフティクリティカルなエラー処理

/* ドローン制御の例 */
int drone_control_loop(LcvmState *vm, compact_program_t *prog) {
    int ret = lcvm_run_compact(vm, prog, 0);

    if (ret != 0 || lcvm_has_error(vm)) {
        // エラー発生 → 即座にフェイルセーフモードへ
        emergency_landing();
        log_error("VM error during flight control");
        return -1;
    }

    return 0;
}

長時間稼働時の運用

定期的なヒープリセット

Arena Allocatorはfree()しないため、ヒープは使い切りです。定期的にリセットします:

void periodic_maintenance(LcvmState *vm, compact_program_t *prog) {
    static int cycle_count = 0;

    cycle_count++;

    // 1000サイクルごとにヒープをリセット
    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);
    }
}

メモリリーク監視

ヒープが予期せず枯渇していないか監視:

void check_heap_usage(void) {
    // 大きなmallocが成功するかテスト
    void *test = lcvm_malloc(1024);
    if (test == NULL) {
        fprintf(stderr, "Warning: Heap exhausted\n");
        // リセット推奨
    } else {
        lcvm_free(test);  // Arenaでは No-op だが、一応呼ぶ
    }
}

Heap Watermark活用

一時的な大量メモリ確保と一括解放のパターンに最適:

void process_sensor_data(LcvmState *vm, compact_program_t *prog) {
    // 現在のヒープ位置を記録
    size_t mark = lcvm_heap_mark();

    // 一時バッファを確保
    char *buffer = lcvm_malloc(4096);
    int *sensor_array = lcvm_malloc(100 * sizeof(int));

    // センサーデータ処理
    read_sensors(sensor_array, 100);
    process_data(buffer, sensor_array);
    send_result(buffer);

    // マーク位置まで一括解放(O(1)、フラグメンテーションなし)
    lcvm_heap_release(mark);
}

利点:


デバッグ支援機能の活用

Watchpoint機能

グローバル変数の予期せぬ変更を検出:

// ビルド時: LCVM_WATCH_ENABLED=1

void setup_debugging(void) {
    // 監視対象を追加(最大8個)
    lcvm_watch_add("motor_speed");
    lcvm_watch_add("emergency_flag");
    lcvm_watch_add("sensor_state");
}

void run_with_monitoring(LcvmState *vm, compact_program_t *prog) {
    setup_debugging();

    lcvm_run_compact(vm, prog, 0);

    // 監視をクリア
    lcvm_watch_clear();
}

変数が書き換えられると自動でログ出力されます。

GDB Pretty Printer

組み込み環境でのデバッグに:

(gdb) source tools/gdb_lcvm.py
(gdb) p vm
$1 = LcvmState { pc=42, sp=8, fp=4, ic=12345, error=NONE }
(gdb) p vm->stack[0:8]
...

エラー時は詳細情報(コールスタック、レジスタ、オペコード)を表示。

Postmortemツール

クラッシュ発生時の診断ダンプ出力と解析:

void emergency_handler(LcvmState *vm) {
    if (lcvm_has_error(vm)) {
        // バイナリダンプを出力(Flash等に保存)
        lcvm_dump_diagnostic_binary(vm, "crash.bin");
    }
}

PC側で解析:

python tools/lcvm_postmortem.py crash.bin

Cスタック深度管理

問題の背景

Lambda C のVMは、以下の3種類のスタックを使用します:

  1. VM スタック (vm->stack) - 監視あり、オーバーフロー検知
  2. C スタック - OS管理、VMは監視しない
  3. 型スタック (vm->type_stack) - 内部型追跡

リスク: スクリプトの深い再帰でCスタックオーバーフローが検知されずに発生する可能性

対策1: コンパイル時の制限

common.hで最大パース深度を調整:

#ifndef LCVM_MAX_PARSE_DEPTH
#define LCVM_MAX_PARSE_DEPTH 100  // デフォルト: 100階層まで
#endif

推奨値:

対策2: 実行時の再帰深度監視

static int recursion_depth = 0;
#define MAX_RECURSION_DEPTH 50

void ffi_user_function(LcvmState *vm) {
    // 再帰深度チェック
    if (recursion_depth >= MAX_RECURSION_DEPTH) {
        lcvm_record_error_ex(vm, LCVM_ERR_STACK_OVERFLOW,
                             "C stack depth limit exceeded", 0);
        return;
    }

    recursion_depth++;

    // 処理
    int result = expensive_calculation();
    lcvm_push_v(&result, sizeof(int));

    recursion_depth--;
}

対策3: スクリプト内で再帰を避ける

悪い例(深い再帰):

int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);  // 指数的にスタック消費
}

良い例(反復):

int fib(int n) {
    int a = 0, b = 1, c, i;
    if (n == 0) return a;
    for (i = 2; i <= n; i++) {
        c = a + b;
        a = b;
        b = c;
    }
    return b;
}

ウォッチドッグのチューニング

初期の保守的設定

// 厳しめに開始し、実測値で調整
lcvm_set_watchdog_ms(100);   // 100ms制限
lcvm_set_watchdog_ic(100000); // 10万命令制限

// 実測後、実測値の1.5〜2倍を設定

アダプティブ調整

void adaptive_watchdog(LcvmState *vm, compact_program_t *prog) {
    clock_t start, end;
    long elapsed_ms;

    start = clock();
    lcvm_run_compact(vm, prog, 0);
    end = clock();

    elapsed_ms = (end - start) * 1000 / CLOCKS_PER_SEC;

    // 実測値の2倍を新しい閾値に
    lcvm_set_watchdog_ms(elapsed_ms * 2);
}

サンドボックスモード

サンドボックスの有効化

LcvmState vm;
lcvm_init(&vm);

// サンドボックスモードを有効化
lcvm_set_sandbox_mode(1);

// これ以降、ポインタアクセスは以下に制限される:
// - vm->stack
// - vm->garea
// - ヒープ領域

ハードウェアアクセスの許可(FFI経由)

/* スクリプト側 */
gpio_write(13, 1);  /* GPIO 13番ピンをHIGHに */

/* ホスト側(FFI実装) */
void ffi_gpio_write(LcvmState *vm) {
    int pin, value;
    lcvm_pop_v(&value, sizeof(int));
    lcvm_pop_v(&pin, sizeof(int));

    // ピン番号の妥当性チェック
    if (pin < 0 || pin >= NUM_GPIO_PINS) {
        lcvm_record_error(vm, "Invalid GPIO pin", 0);
        return;
    }

    // ハードウェアアクセス
    HAL_GPIO_WritePin(GPIO_PORT, pin, value);
}

トラブルシューティング

Q1: グローバル変数が初期化されない

症状: スクリプトを再実行すると、前回の値が残っている

原因: lcvm_reset() はグローバル変数を初期化しない(仕様)

解決方法:

lcvm_reset(&vm);
lcvm_compact_init_garea(&vm, &prog);  // これを追加

Q2: アライメントエラー(Bus Error / HardFault)

症状: ARM Cortex-M0 で実行時にクラッシュ

原因: vm_stack_cell_t を使わず、直接キャストしている

解決方法: VM_STACK_CELL() マクロを使用

// 悪い例
int *p = (int *)&vm.stack[vm.sp];

// 良い例
VM_STACK_CELL(vm.stack, vm.sp)->i = 42;

Q3: ヒープが枯渇する

症状: lcvm_malloc() が NULL を返す

原因: Arena Allocator は free() しない

解決方法:

lcvm_heap_reset();  // 定期的に呼び出す

Q4: ウォッチドッグが頻繁に発火

症状: "watchdog: time limit exceeded"

原因: 制限が厳しすぎる、またはスクリプトが非効率

解決方法:

  1. 閾値を上げる: lcvm_set_watchdog_ms(200);
  2. スクリプトを最適化(ループ内のmalloc削減等)

Q5: FFI関数が見つからない

症状: "Failed to link program"

原因: lcvm_link_program() の前に関数登録していない

解決方法:

lcvm_register_function("my_func", ffi_my_func, 1);  // 先にこれ
lcvm_link_program(&prog);  // その後にリンク

Q6: 浮動小数点の精度が合わない

症状: float版とdouble版で結果が異なる

原因: コンパイラとVMの精度設定が不一致

解決方法: コンパイル時に統一

# Float版でビルド
lcvmc -DLCVM_USE_FLOAT -oc script.lcbc script.c

# VM側もFloat版
gcc -DLCVM_USE_FLOAT -o lcvm lcvm.c ...

商用製品組み込み前チェックリスト

初期化

メモリ

エラー処理

安全性

長時間稼働

テスト