Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Keleusma クックブック

ナビゲーション: ガイド | ドキュメントルート

レシピは、Keleusma をより大きなシステムへ組み込むための、実際に動作するパターンです。各レシピは、それが解決する問題、それが尊重する制約、および最小限の動作する例を示します。レシピは、そのパターンを実運用規模でインスタンス化しているバンドル済みの例へリンクしています。リンク先の節は、より深く読み進めるための場所です。

索引

レシピ使うとき
Text を扱うホストまたはスクリプトが文字列を扱う必要があります。
モジュールからアリーナを自動サイズ設定するホストが、ハードコードされた容量ではなく、正確な WCMU 境界に基づくアリーナのサイズ設定を望んでいます。
データローダーパターンホストが、スクリプト側での編集から恩恵を受ける読み取り専用の構成データを必要としています。
狭いランタイムの型エイリアスホストが 64 ビット未満のネイティブランタイム(16 ビットまたは 8 ビットの符号付きワード)を対象としています。
署名付きバイトコードの配布ホストが、信頼できないチャネル越しにコンパイル済みモジュールを配信し、発信元の真正性を必要としています。
実測コストモデルによる較正済み WCETホストが、名目上の相対的な重みではなく、デプロイ対象における実際の CPU サイクル数での WCET 推定を望んでいます。

Text を扱う

問題

ホストまたはスクリプトが文字列を扱う必要があります。名前、ログメッセージ、エラーレポート、構成値、外部世界からの識別子などです。Keleusma は文字列処理における付加価値ではありませんが、実際のアプリケーションは境界において何らかの文字列処理を日常的に必要とします。

解決

2 つのルールがあります。

その一。静的なテキストには文字列リテラルを使用してください。 ソースレベルの文字列リテラルは Value::StaticStr へコンパイルされ、バイトコードの読み取り専用定数プールに配置されます。これらは不変で固定サイズのハンドルであり、関数の引数、戻り値、および yield のペイロードに使用可能です。スクリプトのサーフェス型は Text です。ランタイムは、内部的に静的と動的の区別を保持します。

fn label() -> Text {
    "ready"
}

2. リテラル以外のテキスト操作にはRust関数を登録する。 V0.2.0はバンドルされたconcatto_stringslicelengthユーティリティネイティブをf-string補間と並んで引退させました。スクリプト側の文字列合成はホスト登録の関数を通じて流れます。フォーマット、分割、正規表現、Unicode操作、エンコーディング変換が必要なホストはRust関数を登録し、スクリプトはuseを通じてそれをインポートします。

#![allow(unused)]
fn main() {
vm.register_fn("text::upper", |s: String| -> String { s.to_uppercase() });
vm.register_fn("text::trim",  |s: String| -> String { s.trim().to_string() });
}
use text::upper
use text::trim

fn greet(name: Text) -> Text {
    text::upper(text::trim(name))
}

ホスト生成の動的文字列は、Value::KStr(アリーナハンドル)としてアリーナヒープに住みます。スタックとローカル束縛で許容されますが、yield境界を跨ぐことはできません。検証器は、アリーナ常駐のKStrをホスト-VM境界を跨いで運ぼうとするプログラムを却下します。

なぜこれがRTOSや組み込みターゲットで動くか

静的文字列は読み取り専用データセクションに住み、割り当てコストがありません。静的文字列を通じて名前やログラベルを返すスクリプトはアリーナをゼロ消費します。ホスト生成の動的文字列は、ホストがregister_verified_native(name, fn, wcet, wcmu_bytes)を通じて計上するアリーナヒープを消費します。検証器が呼び出しごとのWCMUを反復予算に折り込みます。文字列作業が無制限に成長する経路はありません。固定サイズの静的文字列ハンドルを通るか、検証器境界付きのヒープ割り当てに計上されるか、そもそもコンパイルされないかです。

相互参照

  • FAQ.md, Stringsがサーフェスの注意点と静的文字列エスケープ表を扱います。
  • TYPE_SYSTEM.md、テキスト型 が型システムの仕様です。
  • ローグ例の bestiary スクリプトがモンスター名をこのパターンで返します。

モジュールからアリーナを自動サイズする

問題

すべてのKeleusma Vmはアリーナを必要とします。ホストが容量を選びます。小さすぎると検証器がVmError::VerifyErrorVm::newでモジュールを却下し、大きすぎるとホストは必要としないメモリを無駄にします。組み込みターゲットは特に厳密なサイジングを欲しがります。ヒープがまったく無いかもしれない(アリーナは.bss内の静的[u8; N]バッファから動きます)からです。

解決

keleusma::vm::auto_arena_capacity_for(&module, native_wcmu)を使って、VM構築前にコンパイル済みモジュールから最小必要容量を計算します。関数はモジュールのStreamチャンクを歩き、各チャンクのスタックとヒープのWCMUを合計し、最大の合計を返します。結果は、提供されたネイティブ計上のもとでモジュールを許容する最小の容量です。

#![allow(unused)]
fn main() {
use keleusma::vm::{auto_arena_capacity_for, Vm};
use keleusma::Arena;

let cap = auto_arena_capacity_for(&module, &[])?;
let arena = Arena::with_capacity(cap);
let vm = Vm::new(module, &arena)?;
}

2番目の引数はネイティブごとのヒープ割り当て計上のスライスです。アリーナから割り当てるネイティブが無いときは空のスライスを渡します。ヒープ割り当てネイティブをホストが登録している場合は適切なu32値を渡します。

#![allow(unused)]
fn main() {
// ホスト登録のテキストまたはバッファネイティブを使うスクリプト。
// ホストの呼び出しごとの計上は、モジュールの`native_names`テーブルと
// 同じ順序でスライスを流れる。
let native_wcmu = &[upper_wcmu, trim_wcmu];
let cap = auto_arena_capacity_for(&module, native_wcmu)?;
}

どのアリーナサイズオプションをいつ使うか

ライブラリは3つのパターンを提供しています。

オプション使うとき
Arena::with_capacity(DEFAULT_ARENA_CAPACITY)ホスト型開発と素早いプロトタイピング。寛大なデフォルト容量が許容できる。
auto_arena_capacity_for最小の正しい容量が欲しいプロダクションホスト、特に多くのVMを動かすときやホストメモリが厳しいとき。
Arena::from_static_bufferヒープなしのベアメタルターゲット。ホストが.bss内の固定サイズバッファを所有して、そのポインタをアリーナに渡す。

自動サイズオプションは静的バッファオプションと組み合わせられます。容量をコンパイル時に計算(モジュールがinclude_bytes!を通じてconst読み込み可能な場合)するか、ビルド時に計算(ホストを1度走らせて値を表示)して、それからそのサイズで静的バッファを宣言します。

失敗モード

選んだ容量がモジュールのWCMUより下回る場合、Vm::newがコードが走る前にVmError::VerifyErrorを返します。これは実行時ではなく構築時に検出されるので、失敗はランの途中ではなく前面で観測可能です。

相互参照

  • EMBEDDING.md, Arena Sizingが組み込みガイドリファレンスです。
  • バンドルされたexamples/wcmu_basic.rsが完全な自動サイズパターンを最初から最後まで示します。

データローダーパターン

問題

ホストが設定データのテーブルを必要とします。データは構造的に均質(エントリごとに固定形状のレコード)で設計者調整可能(ゲームバランス、ルックアップテーブル、コンテンツ)です。テーブルをRustソースに保存することは、設計者がチューニングしなおすためにホストを再ビルドしなければならないことを意味します。スクリプトファイルに保存することは、設計者が.kelファイルを編集して実行時に再読み込みできることを意味します。

Keleusmaは現在、レコード配列のモジュールスコープconst宣言、インライン文字列テーブル、成長可能な構造の実行時割り当てをサポートしません。下のパターンはそれらの制約の中で動きます。

解決

テーブルを3つの部分を持つKeleusmaスクリプトとしてエンコードします。

  1. データセグメントをスクリプト側で宣言し、レコードの出力列ごとに1つのフィールドを持ちます。データセグメントはホスト-スクリプトI/O構造体です。
  2. 多頭ディスパッチャをエントリごとに1つの頭で持ちます。各頭はエントリごとの定数をデータセグメントに書き込みます。
  3. ローダー関数がインデックス(負のインデックス慣習を含む)を解決し、ディスパッチャに連鎖します。

ホストは起動時にエントリごとに1度スクリプトを走らせ、各呼び出しのあとにデータセグメントを読み、結果を通常のRustコンテナ(Vec<T>HashMap<K, T>など)にキャッシュします。キャッシュが温まったあとは、実行時の読み取りはRustキャッシュを通ります。スクリプトはホストが再読み込みを欲しがるときにのみ再び触られます。

このパターンは実行時のホットリロードを許容します。テーブルがスクリプト形式だからです。スクリプトを再コンパイルし、ローダーを再実行し、キャッシュをアトミックに置き換えるホストは、再起動なしでデータを差し替えられます。起動時に1度キャッシュするホストも恩恵を受けます。テーブルがRustソースから設計者が編集できるファイルに移動するからです。

3つの構成テクニック

パターンは個別に知られている3つのテクニックを構成しますが、それらが組み合わさることがうまくいきます。

定数テーブルをエンコードする多頭ディスパッチ。 Keleusmaは整数パターンのパラメータを持つ多頭関数定義を許容しています。エントリごとに1頭で、各本体がエントリのフィールドを代入することは、機能的に定数配列と等価です。エンコードは検証器に優しいです。すべての本体が直線コードだからです。Prologファクトと、Erlang/Elixirのパターンマッチングが近い類似物です。

ホスト-スクリプトI/O構造体としてのデータセグメント。 データセグメントは通常、loop mainスクリプトが再開を跨いで状態を保持する場所です。1ショットの純粋関数のために、出力構造体としてそれを再利用することは、get_dataset_dataがすでにホスト境界の一部だから動きます。スクリプトは関数の引数を通じて入力を読み、state.field = ...代入を通じて出力を書きます。

負のインデックスサイズ発見。 ローダーは負のインデックスをcount + n(Pythonシーケンス慣習)に解決します。fn main(-1)を呼ぶと、最後のエントリのフィールドが、count - 1に等しいidスロットを含めて書かれます。ホストはidスロットを読んでテーブルサイズを1呼び出しで学び、そこから自分のキャッシュをサイズし、並列のホスト側定数に対してその値をアサートします。これはRustソースに数を埋め込むことを避けます。

最小例

3色のテーブル、それぞれに赤、緑、青のチャネルがあります。

// colours.kel
data state {
    id: Word,
    r: Word, g: Word, b: Word,
}

fn main(n: Word) -> Word {
    let count = 3;
    let i = if n < 0 { count + n } else { n };
    fill(i);
    0
}

fn fill(0) -> Word { state.id = 0; state.r = 255; state.g =   0; state.b =   0; 0 }  // 赤
fn fill(1) -> Word { state.id = 1; state.r =   0; state.g = 255; state.b =   0; 0 }  // 緑
fn fill(2) -> Word { state.id = 2; state.r =   0; state.g =   0; state.b = 255; 0 }  // 青
fn fill(_n: Word) -> Word { 0 }

ホスト側で、キャッシュをスクリプトから発見します。

#![allow(unused)]
fn main() {
use std::sync::OnceLock;

pub struct Colour { pub r: u8, pub g: u8, pub b: u8 }

static COLOURS: OnceLock<Vec<Colour>> = OnceLock::new();

pub fn colours() -> &'static [Colour] {
    COLOURS.get().expect("colours not loaded")
}

fn load_colours(vm: &mut Vm) -> Result<(), Box<dyn std::error::Error>> {
    // The host lends a zeroed shared buffer; the script writes its fields into
    // it and the host reads them back out (B28 item 2).
    let mut shared = vec![0u8; vm.shared_data_bytes()];
    // Discover the count by calling with -1.
    vm.call_with_shared(&mut shared, &[Value::Int(-1)])?;
    let count = read_int(vm, &shared, 0)? as usize + 1;
    let mut table = Vec::with_capacity(count);
    for i in 0..count {
        vm.call_with_shared(&mut shared, &[Value::Int(i as i64)])?;
        table.push(Colour {
            r: read_int(vm, &shared, 1)? as u8,
            g: read_int(vm, &shared, 2)? as u8,
            b: read_int(vm, &shared, 3)? as u8,
        });
    }
    let _ = COLOURS.set(table);
    Ok(())
}

fn read_int(vm: &Vm, shared: &[u8], slot: usize) -> Result<i64, Box<dyn std::error::Error>> {
    match vm.get_shared(shared, slot)? {
        Value::Int(n) => Ok(n),
        other => Err(format!("expected Int at slot {}, got {:?}", slot, other).into()),
    }
}
}

バリエーション

1つのスクリプトの中の複数のテーブル。 2つのテーブルが同じデータセグメント形状を共有する場合、先頭のtable引数でディスパッチします。fn main(table, tier)tableに基づいて2つのテーブルごとの内部関数のうち1つにディスパッチします。各テーブルは-1を通じて独立に発見可能です。

fn main(table: Word, tier: Word) -> Word {
    let count = 20;
    let i = if tier < 0 { count + tier } else { tier };
    if table == 0 { weapon(i); }
    else { if table == 1 { armor(i); } };
    0
}

fn weapon(0) -> Word { ... }
fn armor(0) -> Word { ... }

連鎖したディスパッチャ。 出力フィールドが他のものから派生する場合、ローダーで2つのディスパッチャを連鎖します。最初のディスパッチャがキーイングフィールドを設定し、2番目がデータセグメントからそれを読んで派生フィールドを設定します。ホストは1回の呼び出しから完全に埋まったエントリを受け取ります。

fn main(n: Word) -> Word {
    let count = 100;
    let i = if n < 0 { count + n } else { n };
    fill(i);
    corpse_fill(state.shape);
    0
}

戻り値を通じた名前。 Keleusmaのデータセグメントは現在、ソースで文字列フィールドを受け付けません。エントリに名前がある場合、それをTextを返す3つ目の多頭ディスパッチャとしてエンコードし、fn mainの最後の式として呼びます。ホストは戻り値として文字列を受け取り、データセグメントは数値フィールドを運びます。ホストは起動時に1度返された静的文字列をリークして、&'static strを取得できます。

いつ使うか

パターンが合うのは次のすべてが成り立つときです。

  • テーブルが約10エントリより多い。それより下では、スクリプトのオーバーヘッドが節約を超えます。
  • 各エントリが整数または列挙型の序数の小さな構造体。文字列、奇妙な精度の浮動小数点、可変サイズのペイロードは回避策が必要です。
  • データが、ホストの再ビルド無しに設計者が編集できることから恩恵を受ける。Rust作者だけがテーブルに触るなら、Rustに残します。
  • 初期実装が一度だけキャッシュするとしても、実行時のホットリロードが望ましい。パターンが経路を開いておきます。

いつ使わないか

  • データがすでにRustで密(エントリごとに1行、エントリごとの構造体ボイラープレートなし)。移行はストレージを圧縮することなくスクリプト読み込みオーバーヘッドを追加します。
  • データがライフサイクルフック(コンストラクタ、ドロップ)を持つ。Keleusmaはそれらを運べません。Rustに残します。
  • データがスクリプトが表現できない型でキーされる。文字列、特定の精度要件を持つ浮動小数点、複合キーはすべてパターンを適合外に押し出します。

このリポジトリでの例

ローグ例は bestiary と装備のテーブルにこのパターンを使います。ROGUE.md, bestiaryスクリプトを読むを参照してください。


狭いランタイム型エイリアス

問題

ホストがサブ64ビットネイティブランタイムを対象にします。16ビットマイクロコントローラ、レトロクラスの8ビットマシン、32ビット組み込みコア。デフォルトのVm<'a, 'arena>GenericVm<'a, 'arena, i64, u64, f64>です。16ビットネイティブターゲット上で64ビット値を運ぶことはメモリを無駄にし、ハードウェアがネイティブにサポートしていないマシンオペランドに対してソフトウェア算術を強制します。ホストはランタイムのワード、アドレス、フロート幅をターゲットに合わせたいのです。

解決

Vmの形は、バイトコードヘッダのword_bits_log2addr_bits_log2float_bits_log2宣言幅をミラーする3つのトレイトパラメータでジェネリックです。GenericVm<W, A, F>をホストの選んだ幅で直接インスタンス化し、人間工学的な呼び出し場所のために型エイリアスを定義します。

#![allow(unused)]
fn main() {
use keleusma::vm::GenericVm;

// 16ビット符号付きワード、16ビット符号なしアドレス、32ビットフロート。
type NarrowVm<'a, 'arena> = GenericVm<'a, 'arena, i16, u16, f32>;

// 8ビット符号付きワード、16ビット符号なしアドレス、32ビットフロート
// (6502クラスのレトロターゲット。将来のオペコードのためにフロートを保持)。
type RetroVm<'a, 'arena> = GenericVm<'a, 'arena, i8, u16, f32>;
}

狭いターゲット用のバイトコードはcompile_with_targetを通じて生成されます。embedded_16プリセットは浮動小数点オペコードを却下します。フロートが狭い幅で欲しい場合はカスタムTargetを使います。

#![allow(unused)]
fn main() {
use keleusma::Arena;
use keleusma::compiler::compile_with_target;
use keleusma::lexer::tokenize;
use keleusma::parser::parse;
use keleusma::target::Target;

let module = {
    let tokens = tokenize(src).expect("lex");
    let program = parse(&tokens).expect("parse");
    compile_with_target(&program, &Target::embedded_16()).expect("compile")
};

let arena = Arena::with_capacity(4096);
let mut vm: NarrowVm<'_, '_> = NarrowVm::new(module, &arena).expect("verify");
}

ホスト関数はRustの自然な型を話す

マーシャル層(KeleusmaTypeIntoNativeFnIntoFallibleNativeFn)は(W, F)に対してパラメトリックで、i64f64bool()Option<T>、固定配列、タプル(アリティ2から5)に対して普遍的実装を持ちます。KeleusmaType<W, F> for i64の普遍実装はWord::to_i64Word::from_i64_wrapを通じて橋渡しします。KeleusmaType<W, F> for f64の普遍実装はFloat::to_f64Float::from_f64を通じて橋渡しします。

ホスト作者は、スクリプトの狭いワードとフロート型に関わらず、クロージャのシグネチャにi64f64を書きます。ランタイムが境界で切り捨てます。

#![allow(unused)]
fn main() {
vm.register_fn("host::triple", |x: i64| -> i64 { x * 3 });
}

NarrowVmでは、スクリプト側のi16引数がホストクロージャ用にi64に広げられ、i64の戻り値はWord::from_i64_wrapを通じてi16に切り捨てられます。ネイティブ幅のRust型が欲しいホスト(i16を直接受け取って広げを避けるクロージャ本体)は、自分のクレートで自分のKeleusmaType<i16, f32> for i16実装を追加できます。

標準ライブラリバンドルは狭いランタイムで動く

3つのstddslバンドルはLibrary<W, A, F>を普遍的に実装していて、許容可能などのランタイム形にも登録します。MathAudioは内部のクロージャをf64で運びます。Ff32のランタイムでは、すべてのクロージャの引数と戻り値はマーシャル境界でFloat::from_f64Float::to_f64を通り、中間値と定数を狭めます。狭めは数学的に定義されていて、静かです。Shellは浮動小数点サーフェスを持たないので、精度の含みなくFを量化します。

#![allow(unused)]
fn main() {
let mut vm: NarrowVm<'_, '_> = NarrowVm::new(module, &arena).expect("verify");
vm.register_library(keleusma::stddsl::Math);
vm.register_library(keleusma::stddsl::Audio);
}

完全なf64精度が必要なプログラムは、静かな狭めに頼るのではなくFf64のランタイムを宣言すべきです。狭いフロートランタイムは、ターゲットのFPUが単精度で、スクリプトが余分な仮数を必要としないときの適切な選択です。

ワード幅算術の規律

狭いランタイム上のスクリプト側算術は、64ビットではなくランタイムのワード境界でラップします。Wordトレイトのwrapping_addwrapping_subwrapping_mulwrapping_divwrapping_remwrapping_negメソッドが、すべての算術ディスパッチサイトを駆動します。NarrowVm上では30_000 + 10_00040_000ではなく-25_536を生みます。より広い算術に依存するプログラムは、より広いワードを宣言するか、自然なRust型を取る登録されたネイティブを通じてホスト側で操作を実行すべきです。

相互参照

  • examples/narrow_runtime.rsが作業デモです。
  • tests/narrow_vm.rsがパターンをピンする統合テストです。
  • docs/decisions/BACKLOG.md、B16 がパラメトリック形状に関する設計上の根拠を記録しています。
  • WordAddressFloatトレイトはsrc/word.rssrc/address.rssrc/float.rsに住みます。カスタム実装は許容されます。バンドルされた実装は標準の幅をカバーします。

署名付きバイトコードの配信

問題

ホストアプリケーションがコンパイル済みバイトコードを信頼されないソースから(通信リンク、ディスク、コンテンツ配信チャネルから)読み込み、許可された署名者によって生成されたものでないモジュールを拒否する必要があります。脅威モデルは多関係者です。1つ以上の既知のマザーシップ識別がモジュールに署名することを信頼され、その他すべては却下されます。

解決

3つのステップ。Cargo機能のsignaturesはデフォルトでオフです。プロデューサとコンシューマの両方でオンにします。

1. キーペアを生成する。 keleusma keygen --seed seed.bin --public pub.binサブコマンドが32バイトのEd25519シードと一致する公開鍵を別々のファイルに書きます。Unixではシードファイルはモード0o600で、既存のファイルは上書きされません。シードを署名システム上で保管された長命の秘密として扱います。公開鍵は自由に配布可能で、コンシューマが信頼するものです。

2. 要求を宣言して署名する。 ソーススクリプトは、エントリ関数のsigned修飾子で署名要求を宣言します。

signed loop main(input: Word) -> Word {
    let next = yield input * 2;
    next
}

keleusma compile script.kel --signing-key seed.bin -o script.kel.binが署名付きバイトコードを生成します。コンパイラはフレーミングヘッダにFLAG_REQUIRES_SIGNATUREを発行し、署名者はEd25519署名を追加します。

3. コンシューマで検証する。 ホストは受け入れる公開鍵で信頼マトリクスを populate し、署名対応エントリポイントを通じて読み込みます。

#![allow(unused)]
fn main() {
let key = ed25519_dalek::VerifyingKey::from_bytes(&public_key_bytes)?;
let mut vm = Vm::load_signed_bytes(&signed_bytes, &arena, &[key])?;
}

ホットスワップ配信(マザーシップ/ドーターシップパターン)では、ホストは署名なしベースラインからVMを構築し、信頼マトリクスを登録し、通信リンク上で署名付きの更新を受け入れます。

#![allow(unused)]
fn main() {
let mut vm = Vm::new(baseline_module, &arena)?;
vm.register_verifying_key(signer_key);
// 後で、ワイヤ上で更新を受け取ったあと:
vm.replace_module_from_bytes(&update_bytes, initial_data)?;
}

署名は新しいバイトコードがデコードされる前に検証されます。無効な署名はスワップを却下し、現在のモジュールが走り続けます。

なぜこれが組み込みターゲットで動くか

検証経路はno_std + alloc下でed25519-dalekを使います。examples/rtosデモは--features keleusma-signaturesでビルドし、スケジューラループに入る前にブートで組み込みの署名付きフィクスチャを検証します。Cortex-M33上の600 MHzでのEd25519検証はミリ秒のオーダーで走ります。コストはモジュール読み込みまたはホットスワップごとに払われ、yieldごとではありません。

相互参照


測定コストモデルでのキャリブレートされたWCET

問題

ホストはデプロイメントターゲット用の実際のCPUサイクルでのWCET(ワーストケース実行時間)推定値を欲しがります。バンドルされたNOMINAL_COST_MODELが出荷する相対重み推定値ではありません。名目モデルはデータの移動に1、算術に2、除算に3、複合構築に5、関数呼び出しに10を割り当てます。これらの比率は同じホスト上でプログラムを互いに順序付けるのに役立ちますが、絶対スケールは特定のハードウェアのパイプライン化CPUサイクルではありません。

デプロイメントターゲットの本物のWCET解析は、測定された数字を欲しがります。Cortex-M55の800 MHz上のデータ移動はVMディスパッチごとに数百サイクル走ります。Apple M1 MaxのPコア上の同じワークロードは数十サイクル走ります。2つは別の単位で、WCETを壁時計時間に変換したいホストは正しいほうを必要とします。

解決

keleusma-benchワークスペースメンバーがホストCPU上のオペコードごとのパイプライン化サイクルを測定し、MEASURED_COST_MODEL定数を定義するRustソース断片を発行します。ホストクレートが断片をインクルードし、消費するWCET APIの_with_cost_modelヴァリアントに定数を渡します。

1. 断片を入手する。 クレートはkeleusma-bench/measured_cost_models/で開発ホストとSTM32N6570-DK用の事前生成された断片を出荷します。他のホストには再生成します。

# ホストベンチ(走っているCPU用の断片を書く)
cargo run --release --bin keleusma-bench -- \
    --cpu-hz 3500000000 \
    --output keleusma-bench/measured_cost_models/<target-triple>.rs

# Embedded bench (captures defmt RTT, parses to a fragment)
cd examples/rtos
cargo run --release --bin bench_n6 --target thumbv8m.main-none-eabihf \
    --no-default-features --features stm32n6570dk-platform \
    2>&1 | tee /tmp/bench.log
cd -
cargo run --release -p keleusma-bench -- \
    --from-log /tmp/bench.log \
    --output keleusma-bench/measured_cost_models/thumbv8m_main_none_eabihf.rs

--cpu-hzはサイクル計算のためのホストのCPUクロックを宣言します。デフォルトはkeleusma-bench/src/counter.rs(DEFAULT_ASSUMED_CPU_HZ)に文書化されています。ターゲットごとのキャプチャワークフローはkeleusma-bench/measured_cost_models/README.mdを参照してください。

2. 断片をホストクレートにインクルードする。 断片はMEASURED_COST_MODELとバッキングのmeasured_op_cycles関数を宣言する平のRustソースです。

#![allow(unused)]
fn main() {
include!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/path/to/keleusma-bench/measured_cost_models/<fragment>.rs"
));
}

複数の断片はcfg(target_arch = ...)でゲートすれば、きれいに同居します。

#![allow(unused)]
fn main() {
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
include!(concat!(env!("CARGO_MANIFEST_DIR"),
    "/keleusma-bench/measured_cost_models/aarch64_apple_darwin.rs"));

#[cfg(target_arch = "arm")]
include!(concat!(env!("CARGO_MANIFEST_DIR"),
    "/keleusma-bench/measured_cost_models/thumbv8m_main_none_eabihf.rs"));
}

3. モデルを検証器に渡す。 WCET APIの_with_cost_modelヴァリアントは&CostModelを受け取ります。

#![allow(unused)]
fn main() {
use keleusma::verify::wcet_stream_iteration_with_cost_model;

for chunk in module.chunks.iter() {
    if chunk.block_type != BlockType::Stream { continue; }
    let cycles = wcet_stream_iteration_with_cost_model(chunk, &MEASURED_COST_MODEL)?;
    println!("Chunk `{}`: {} CPU cycles per iteration", chunk.name, cycles);
}
}

読み込み時のリソース境界検証には、verify::verify_resource_bounds_with_cost_modelが同じ&CostModelを取り、WCETとWCMUの両方の経路に通します。

なぜこれが大切か

バンドルされたNOMINAL_COST_MODELは相対的にのみであることに正直です。「反復ごとに何マイクロ秒か」に答えられません。測定モデルはベンチのキャリブレーション仮定(CPUクロック、温まったキャッシュ、予測された分岐)を条件として、答えられます。WCET境界がスケジューラの判断を伝えるデプロイメントでは、この変換が土台のステップです。

失敗モード

  • 断片がデプロイメントターゲットと違うホストで測定された。 サイクル数は実行するCPUのキャリブレーションではなく開発ホストの推定値です。断片のヘッダがホストと仮定されたCPUクロックを記録します。使う前に両方を確認します。
  • ホストCPUの実際のクロックがベンチの仮定と違う。 実際のクロックに設定された--cpu-hz <Hz>で再ベンチするか、--cpu-hzkeleusma-bench --from-logに渡してキャプチャ後に文書化された値を訂正します。
  • ベンチがYieldまたはCallを孤立で測定しない。 両方のカテゴリはスケールされた名目値にフォールバックします。スケールされた値はWCETには保守的ですが、外挿であって測定ではありません。これは断片のヘッダに文書化されています。

相互参照