ベストプラクティス
商用展開
メモリ設定
ターゲット別推奨設定
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. スタックサイズ: 最深の関数呼び出し経路を分析
- ローカル変数の総量 + 関数呼び出しオーバーヘッド × ネスト深度
- 安全マージン: 計算値の1.5〜2倍
2. グローバル領域サイズ: グローバル変数の総量
- コンパイラ出力(
.lcbc)のヘッダーに記録 lcvm_var_dump_all()で実際の使用量を確認
3. ヒープサイズ: 動的確保の最大値
- 全
malloc()/realloc()の合計 - 安全マージン: 計算値の2倍(誤差対策)
初期化ベストプラクティス
パターン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);
}
利点:
- 全体リセットなしで部分的なメモリ解放が可能
- O(1)の解放時間
- フラグメンテーションの心配なし
デバッグ支援機能の活用
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種類のスタックを使用します:
- VM スタック (
vm->stack) - 監視あり、オーバーフロー検知 - C スタック - OS管理、VMは監視しない
- 型スタック (
vm->type_stack) - 内部型追跡
リスク: スクリプトの深い再帰でCスタックオーバーフローが検知されずに発生する可能性
対策1: コンパイル時の制限
common.hで最大パース深度を調整:
#ifndef LCVM_MAX_PARSE_DEPTH
#define LCVM_MAX_PARSE_DEPTH 100 // デフォルト: 100階層まで
#endif
推奨値:
- 汎用PC: 100〜200
- 組み込み(32KB RAM): 50〜100
- ローエンド(8KB RAM): 20〜50
対策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"
原因: 制限が厳しすぎる、またはスクリプトが非効率
解決方法:
- 閾値を上げる:
lcvm_set_watchdog_ms(200); - スクリプトを最適化(ループ内の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 ...
商用製品組み込み前チェックリスト
初期化
-
lcvm_init()またはlcvm_init_with_memory()を呼んでいる -
lcvm_compact_init_garea()でグローバル変数を初期化している -
FFI関数を
lcvm_register_function()で登録している -
lcvm_link_program()でリンクしている
メモリ
- スタック/グローバル/ヒープサイズが適切
-
アライメント対策(
vm_stack_cell_t使用)を実施 -
定期的な
lcvm_heap_reset()を実装
エラー処理
-
lcvm_run_compact()の戻り値をチェック -
エラー時に
lcvm_has_error()で詳細を取得 - フェイルセーフ動作(安全停止)を実装
安全性
- ウォッチドッグを適切に設定
- サンドボックスモード(必要な場合)を有効化
- ポインタアクセスの検証を実装(FFI関数内)
長時間稼働
- 定期的なリセット機構を実装
- メモリ使用量の監視を実装
- エラーログの記録機構を実装
テスト
- 最悪ケース(最大深度、最大メモリ)でテスト
- 長時間稼働テスト(24時間以上)
- エラー注入テスト(ゼロ除算、NULL参照等)