アーキテクチャ
VM アーキテクチャ概要
Lambda C は、組み込みシステムに最適化されたコンパクトな3層アーキテクチャを採用しています。
システム構成
1. ホストアプリケーション (C)
- FFI関数の登録
- VM初期化
- スクリプト実行ループ
- ハードウェアインターフェース
2. Lambda C VM (Compact)
- バイトコードローダー&リンカー
- 4バイト命令インタープリタ
- FFIディスパッチテーブル(64スロット)
- グローバルメモリマネージャー
3. スクリプト層 (C言語サブセット)
- ユーザーロジック
- アルゴリズム実装
- ホスト関数へのFFI呼び出し
4バイト命令フォーマット
なぜ4バイトなのか?
Lambda Cの4バイト命令フォーマットは、厳しいサイズと性能制約を持つ組み込みシステム向けに最適化されています。
これにより実現されること:
- 超コンパクトなバイトコード - 典型的なプログラムは5〜10KBに収まり、Flash/EEPROM展開が可能
- 命令ごとに1回のフェッチ - キャッシュフレンドリーな設計で一貫した性能
- 高い命令密度 - 約1000命令がわずか4KBのROMに収まる
- 予測可能なメモリフットプリント - リソース制約デバイスでの容量計画が容易
命令エンコーディング
各命令は4バイトにパックされます:
[31:24] オペコード (8ビット) - 命令種別
[23:16] オペランド1 (8ビット) - レジスタ/即値
[15:8] オペランド2 (8ビット) - レジスタ/即値
[7:0] オペランド3 (8ビット) - レジスタ/即値
例: 整数加算
ADD r1, r2, r3 → [0x01][r1][r2][r3]
例: FFI呼び出し
CALL func_id, argc → [0x20][func_id][argc][0x00]比較: バイトコード密度
| プログラム | Lambda C (4バイト) | Lua 5.4 | MicroPython |
|---|---|---|---|
| Fibonacci (25回) | ~200バイト | ~450バイト | ~800バイト |
| 1225命令 | ~5 KB | ~12 KB | ~20 KB |
メモリアーキテクチャ
3スタックモデル
Lambda Cは安全性と効率のため、3つの独立したスタックを使用します:
1. VMスタック (vm->stack)
用途: スクリプト変数と一時値
構造:
typedef union {
int i;
double d;
void *p;
char bytes[8];
} vm_stack_cell_t;
特性:
sp(スタックポインタ)で管理- オーバーフロー検知機能内蔵
- ARM互換性のための8バイトアラインメントセル
2. Cスタック
用途: VM内部の関数呼び出し、Intrinsic関数
特性:
- OS管理(VMは監視しない)
- リスク: 深い再帰でオーバーフロー可能
- 軽減策: パース深度制限、再帰カウンター
3. 型スタック (vm->type_stack)
用途: Compact VMのランタイム型追跡
特性:
- VMスタックと並行
- int/double/pointer型を追跡
- 型安全なFFIマーシャリングを実現
メモリレイアウト例 (Cortex-M0+, 32KB RAM)
0x20000000 ┌─────────────────────┐
│ Cスタック (4KB) │ OS、割り込み、VM呼び出し
0x20001000 ├─────────────────────┤
│ VMスタック (8KB) │ スクリプト変数
0x20003000 ├─────────────────────┤
│ グローバル領域 (8KB)│ グローバル変数
0x20005000 ├─────────────────────┤
│ ヒープ (12KB) │ malloc/realloc
0x20008000 └─────────────────────┘
FFI (Foreign Function Interface)
ロード時リンク
Lambda Cは関数シンボルをロード時に一度だけ解決し、実行時はO(1)の定数時間ディスパッチを実現します。これにより、繰り返しのシンボル検索を排除し、予測可能なFFI呼び出しオーバーヘッドを提供します。
リンクプロセス
ステップ1: 登録(ホスト側)
lcvm_register_function("get_time", ffi_get_time, 0);
lcvm_register_function("gpio_write", ffi_gpio_write, 2);
ステップ2: バイトコードロード
lcvm_compact_load("script.lcbc", &prog, 0);
ステップ3: リンク(シンボル→ID変換)
lcvm_link_program(&prog); // "get_time" → 関数ID 0
ステップ4: 実行時ディスパッチ
CALL 0, 0 → ffi_get_timeへ直接ジャンプ(ルックアップ不要!)FFIディスパッチ性能
| 操作 | 時間 | 機構 |
|---|---|---|
| Lambda C | ~100ns | O(1) テーブルルックアップ |
| Lua 5.4 | ~200ns | ハッシュテーブルルックアップ |
| MicroPython | ~500ns | 文字列比較 |
型マーシャリング
Lambda CはC型とVM型を自動的に変換します:
スクリプトからホストへ(FFI呼び出し)
// スクリプト
gpio_write(13, 1);
// ホストFFI実装
void ffi_gpio_write(LcvmState *vm) {
int value, pin;
lcvm_pop_v(&value, sizeof(int)); // 逆順でポップ
lcvm_pop_v(&pin, sizeof(int));
HAL_GPIO_WritePin(GPIO_PORT, pin, value);
}
ホストからスクリプトへ(戻り値)
// ホストFFI
void ffi_get_time(LcvmState *vm) {
double t = system_time_ms() / 1000.0;
lcvm_push_double(vm, t); // スクリプトへ返す
}
// スクリプト
double t = get_time();
サンドボックスモード
セキュリティ分離
サンドボックスモードは、メモリ破壊や不正なハードウェアアクセスを防ぐため、ポインタアクセスを制限します。
許可されるメモリ領域
サンドボックス有効時:
lcvm_set_sandbox_mode(1);
許可:
- VMスタック (
vm->stack) - グローバル領域 (
vm->garea) - ヒープ(
lcvm_malloc()で確保した領域)
禁止:
- OSメモリ
- MMIO(Memory-Mapped I/O)レジスタ
- 他のスレッドのメモリ
ハードウェアアクセスパターン
// 誤り: 直接ハードウェアアクセス(サンドボックスでブロック)
int *GPIO_REGISTER = (int *)0x40020000;
*GPIO_REGISTER = 1; // 実行時エラー!
// 正しい: FFI経由
gpio_write(13, 1); // 検証済みFFI関数を呼び出しサンドボックス利用シーン
- 信頼できないスクリプト: ユーザーアップロードコードの安全実行
- マルチテナント: 異なるソースのスクリプトを分離
- セーフティクリティカル: ハードウェア損傷の偶発的防止
ホットリロード機構
ゼロダウンタイム更新
Lambda Cは、VMを再起動したり状態を失うことなく、スクリプトをリロードできます。
ホットリロードシーケンス
void vm_hot_reload(LcvmState *vm, compact_program_t *old_prog,
const char *new_script_path) {
compact_program_t new_prog;
// 1. 新しいバイトコードをロード
lcvm_compact_load(new_script_path, &new_prog, 0);
// 2. FFIを再リンク
lcvm_link_program(&new_prog);
// 3. VM状態をリセット
lcvm_reset(vm);
lcvm_compact_init_garea(vm, &new_prog);
// 4. 古いプログラムを解放し、新しいものにスワップ
lcvm_compact_free(old_prog);
*old_prog = new_prog;
// 5. 実行を継続
lcvm_run_compact(vm, old_prog, 0);
}利用例: ゲーム開発
while (game_running) {
if (key_pressed('R')) {
// スクリプトを再コンパイル
system("lcvmc -oc gameplay.lcbc gameplay.c");
// ホットリロード
vm_hot_reload(&vm, &prog, "gameplay.lcbc");
}
// ゲームロジックを実行
lcvm_run_compact(&vm, &prog, 0);
render_frame();
}
ウォッチドッグシステム
二重保護
Lambda Cは2つのウォッチドッグ機構を提供します:
1. 時間ベースウォッチドッグ
無限ループによるシステムハングを防止:
lcvm_set_watchdog_ms(100); // 最大100ms実行時間
例:
// 無限ループのスクリプト
while (1) {
// ... 処理 ...
}
// 100ms後にLCVM_ERR_WATCHDOG_TIMEでVMが中断2. 命令数ベースウォッチドッグ
CPU速度に依存しない保護:
lcvm_set_watchdog_ic(100000); // 最大100,000命令
例:
for (i = 0; i < 1000000; i++) {
// ... 処理 ...
}
// 100,000命令後にVMが中断推奨設定
| アプリケーション種別 | 時間制限 | 命令数制限 |
|---|---|---|
| リアルタイム制御(10msサイクル) | 8ms | 50,000 |
| 定期タスク(1秒サイクル) | 800ms | 500,000 |
| バッチ処理 | 5000ms | 1,000,000+ |
| 開発/デバッグ | 0(無効) | 0(無効) |
エラー回復
グレースフルデグラデーション
Lambda Cは非局所的エラー処理のためsetjmp/longjmpを使用します:
int lcvm_run_compact(LcvmState *vm, compact_program_t *prog, int trace) {
if (setjmp(vm->error_jmp) != 0) {
// エラー発生 - 安全に復帰
return -1;
}
// 通常実行
// ...
}エラー処理パターン
int ret = lcvm_run_compact(&vm, &prog, 0);
if (ret != 0) {
// 詳細なエラー情報を取得
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, "Error: %s at PC=%d, line=%d\n", msg, pc, line);
// 是正措置
switch (code) {
case LCVM_ERR_DIV_ZERO:
// ログ記録とリセット
break;
case LCVM_ERR_HEAP_EXHAUSTED:
lcvm_heap_reset();
break;
}
}
静的型モード
約2倍の高速化
-Os オプションでコンパイルすると、スタック上の型情報(12バイト/エントリ)を省略し、実行速度を大幅に向上:
lcvmc -Os -oc output.lcbc source.cベンチマーク(fib(25))
| モード | 実行時間 |
|---|---|
| 通常モード | 27ms |
| 静的型モード | 13ms |
技術的背景
通常モードでは、VMスタック上の各値に対して型情報を追跡します。静的型モードでは、コンパイル時に型が確定する整数演算プログラムにおいて、この型追跡をスキップします。
制限事項:
- 整数演算に特化(浮動小数点は通常モード推奨)
- intrinsic関数(printf等)は現時点で未対応
関数ポインタ対応
コールバックパターン
Lambda CはCompact VMで関数ポインタを完全サポート:
typedef int (*operation_t)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int apply(operation_t op, int x, int y) {
return op(x, y);
}
int main() {
int r1 = apply(add, 10, 5); // 15
int r2 = apply(sub, 10, 5); // 5
return 0;
}状態遷移テーブル
関数ポインタ配列を使った効率的な状態管理:
typedef void (*state_handler_t)();
void state_idle() { /* ... */ }
void state_run() { /* ... */ }
void state_stop() { /* ... */ }
state_handler_t handlers[3] = { state_idle, state_run, state_stop };
void execute_state(int state) {
handlers[state]();
}
性能特性
ベンチマーク結果
Fibonacci(25) 計算
- 通常モード: 27ms
- 静的型モード: 13ms(約2倍高速化)
- 総反復回数: 121,393回
- テストプラットフォーム: AMD Radeon 780M
命令スループット
- 約120万命令/秒
- 異なるワークロードでも一貫した性能
メモリ効率
典型的な実行時メモリ使用量
- 最小RAM: 基本的なアプリケーションで8〜16KB
- バイトコード密度: 命令あたり4バイト
- サンプルプログラム(1225命令): バイトコードサイズ約5KB
スケーラビリティ
- ローエンドMCU: 8KB RAMで動作(Cortex-M0+)
- ミッドレンジ: 16〜32KB RAMで快適なマージン
- ハイエンド: 64KB以上のRAMで大規模アプリケーション対応
設計判断
なぜ4バイトなのか?
検討した代替案:
- 1バイトオペコード: 制限が厳しすぎる(最大256オペコード)
- 8バイト命令: 空間の無駄、キャッシュ非効率
- 可変長: 複雑なデコーダ、サイズ予測不可
4バイトの利点:
- 単一の32ビットフェッチに収まる
- 3つのオペランドに十分な空間
- ARM/RISC-Vでアラインメント済み
- 予測可能なバイトコードサイズ
なぜArena Allocatorなのか?
設計上free()なし:
- フラグメンテーションを排除
- O(1)アロケーション
- 決定論的な性能
- 定期リセットシナリオ(制御ループ)に適合
トレードオフ: lcvm_heap_reset()を定期的に呼び出す必要がある
なぜC言語サブセットなのか?
Lua/Pythonに対する利点:
- 組み込み開発者にとってゼロ学習コスト
- 自然なFFI統合(C型→C型)
- 予測可能な性能(GCポーズなし)
- Cからのアルゴリズムポーティングが容易
オーケストレーション用途に最適化: Lambda Cは意図的にサブセットとして設計されており、汎用言語ではありません。組み込みオーケストレーションにおいて、シンプルさは強みです:
- 予測可能な制御フロー - メタプログラミングや動的な驚きがない
- 決定論的な性能 - 見たままが実行される
- 検証が容易 - シンプルな言語はコードレビューとテストが容易
- 目的に集中 - ハードウェアの協調制御に特化、複雑なデータ変換は不要