Skip to content

Integration ja

github-actions[bot] edited this page Sep 27, 2026 · 4 revisions

C++ Integration

🌐 English · 日本語

目次

Mana Integration

このセクションでは、Mana Compiler と Mana VM を C++ アプリケーションやゲームエンジンへ組み込む方法を説明します。

全体像

Mana source
   |
   v
mana::Compile()
   |
   v
CompileResult::mProgramImage
   |
   +--> mana::ProgramImage で内容を照会
   |
   v
mana::VM::LoadProgram()
   |
   +--> Native Functions でホスト機能と接続
   |
   v
VM::Run()

Compiler へソースを供給する経路は SourceResolver で差し替えられ、コンパイル時の問題は Diagnostic、実行時の問題は Trace / ScriptError / FatalError などでホスト側へ通知できます。

読む順番

  1. 組み込み概要
  2. Compiler
  3. VM
  4. Program Image
  5. Native Functions
  6. SourceResolver
  7. Diagnostics
  8. Error Handling

目的別に探す

やりたいこと ページ
Mana をゲームへ組み込む全体像を知る 組み込み概要
C++ から Mana ソースをコンパイルする Compiler
Program Image を実行する VM
Actor / Action 一覧を実行せず取得する Program Image
Mana からゲーム側 C++ を呼ぶ Native Functions
エディタやアセットからソースを供給する SourceResolver
コンパイルエラーをIDEやCIへ表示する Diagnostics
実行時エラーや内部Faultを扱う Error Handling

対象読者

Language Reference が Mana スクリプトを書く人向けなのに対し、Integration は Mana をゲームやツールへ組み込む C++ 開発者向けです。

Mana の言語仕様そのものを調べたい場合は Language Reference を参照してください。

組み込み概要

Mana は、コンパイラと VM をライブラリとして C++ アプリケーションへ組み込めます。

最小構成

#include "compiler/Compiler.h"
#include "runner/Mana.h"

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

const mana::CompileResult result = mana::Compile(options);
if (!result.mSucceeded)
    return;

auto image = std::make_shared<std::vector<uint8_t>>(result.mProgramImage);
auto vm = std::make_shared<mana::VM>();
vm->LoadProgram(std::shared_ptr<const void>(image, image->data()));

while (vm->Run())
{
}

組み込み側は大きく3段階に分かれます。

  1. mana::Compile() でソースを Program Image へ変換する
  2. 必要なら mana::ProgramImage で Actor / Action を事前確認する
  3. mana::VM へ Program Image をロードして実行する

Compiler と VM を分離する

Mana Compiler はファイルを書き出すことを前提にしていません。Compile() は CompileResult を返し、Program Image は std::vector<uint8_t> として取得できます。

そのため、エディタ内コンパイル、アセットビルド、サーバー側ビルドなどにも組み込めます。

VM はコンパイル済み Program Image を受け取って実行します。実行環境に Compiler を含めず、ビルド済み Program Image だけを配布する構成も可能です。

ソースをファイル以外から供給する

CompileOptions::mSourceResolver を差し替えると、ファイルシステム以外からソースを供給できます。

例えば次のような用途があります。

  • エディタの未保存バッファ
  • ゲームエンジンのアセットシステム
  • パッケージ内の仮想ファイル
  • テスト用のメモリ上ソース

詳しくは後続の SourceResolver ページで扱います。

エラーの扱い

コンパイルエラーは CompileResult::mDiagnostics に格納されます。

Compile() は内部例外を捕捉し、C++ 呼び出し側へ例外を越境させない設計です。一方、VM 実行時の問題は Trace や実行状態として扱われます。

スレッドについて

現行 Compile() はコンパイラ内部にグローバル状態を持つため、複数スレッドから同時実行できません。

複数アセットを並列ビルドする場合でも、Mana Compiler の呼び出し部分は直列化してください。

関連項目

Compiler

Mana Compiler を C++ から利用する入口は mana::Compile() です。

基本形

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

mana::CompileResult result = mana::Compile(options);

Compile() はファイルを書き出さず、生成物を CompileResult に格納して返します。

CompileOptions

主な設定は次の通りです。

メンバー 内容
mSourceFilename エントリとなるソースファイル
mForcedIncludeFiles ソースより先に読み込むファイル群。CLI の -I 相当
mGenerateDump シンボル表・構文木・中間コードのダンプを生成
mGeneratePublicTypeDecl C++ 型宣言ヘッダーを生成
mSourceResolver ソース供給方法を差し替える
mDiagnosticHandler 診断発生時のコールバック

mSourceResolver を省略した場合は、標準のファイルシステムから読み込みます。

CompileResult

メンバー 内容
mSucceeded エラーがなければ true
mProgramImage 生成された Program Image
mPublicTypeDecl C++ 公開型宣言
mDump デバッグ用 Markdown ダンプ
mDiagnostics 発生した全診断

コンパイル失敗時、mProgramImage は空になります。

診断を即時表示する

options.mDiagnosticHandler = [](const mana::Diagnostic& diagnostic)
{
    std::cerr << diagnostic.ToString() << '\n';
};

ハンドラを指定しても、診断は CompileResult::mDiagnostics にも保持されます。

現行実装では、診断ハンドラ自身が例外を送出した場合も Compile() の外へ例外を越境させないよう処理されています。

強制インクルード

options.mForcedIncludeFiles.push_back("common.mn");
options.mForcedIncludeFiles.push_back("platform.mn");

先に追加したファイルほど先に読み込まれます。

公開型宣言を生成する

options.mGeneratePublicTypeDecl = true;
const mana::CompileResult result = mana::Compile(options);

if (result.mSucceeded)
{
    const std::string& header = result.mPublicTypeDecl;
}

CLI の -t と同じ生成機能を、ファイル出力せず文字列として取得できます。

ダンプを生成する

options.mGenerateDump = true;

成功・失敗の解析やコンパイラ開発用に、Markdown形式のダンプを mDump から取得できます。

例外境界

Compile() は内部で発生した例外を捕捉し、致命的な診断へ変換する設計です。通常、ホスト側は mSucceeded と mDiagnostics を確認すればよく、Mana内部例外を前提にした制御を組む必要はありません。

スレッド安全性

現行コンパイラはグローバル状態を持つため、Compile() を複数スレッドから同時に呼び出すことはできません。

関連項目

VM

mana::VM は、Compiler が生成した Program Image を読み込み、Actor / Action を実行する実行環境です。

Program Image をロードする

メモリ上の Program Image をロードできます。

auto image = std::make_shared<std::vector<uint8_t>>(result.mProgramImage);
auto vm = std::make_shared<mana::VM>();

vm->LoadProgram(std::shared_ptr<const void>(image, image->data()));

ファイルとして保存済みなら、パスからロードすることもできます。

vm->LoadProgram("game.mx");

実行する

while (vm->Run())
{
}

Run() はVMの処理が継続している間 true を返します。

ゲームループへ組み込む場合は、ホスト側の更新単位に合わせて Run() を呼び出す構成にできます。

Actor を検索する

auto actor = vm->FindActor("Game::NPC::Guide");

Actor名は namespace を含む完全名で扱えます。

C++ から Request する

VM全体へ名前指定で Request を送れます。

vm->Request(10, "Game::NPC::Guide", "talk", nullptr);

また、Actor を取得してから直接呼び出す経路もあります。

Actor の生成

既存Actorを複製するAPIと、Phantomから生成するAPIがあります。

auto clone = vm->CloneActor(actor, "GuideClone");
auto enemy = vm->CreateActorFromPhantom("EnemyTemplate", "Enemy01");

Phantom は Program Image ロード時には通常Actorとして生成されず、CreateActorFromPhantom() で明示的に生成します。

Native Function を登録する

vm->RegisterFunction("nativeAdd", &OnNativeAdd);

Mana側で native 宣言した関数名と一致する名前を登録します。

Programロード時の起動

現行VMはプログラムロード後、通常Actorに対して init を最高優先度(2147483647)、main を Priority 0 で Request します。

init : Priority 2147483647
main : Priority 0

このため、init は main より高いPriorityで初期化処理を実行できます。

秒単位の待機と時間の更新

FunctionInitialize(*vm) で組み込み関数を登録すると、Mana から native void delay(float seconds); を宣言して秒単位で待機できます。以前のフレーム数を受け取る delay とは互換性がありません。

ホストが時間を管理する場合は vm->Run(deltaSeconds) に有限かつ非負の経過秒数を渡します。Run(0.0) は命令を実行しますが時間を進めません。初回に Run(0.0) を呼ぶと、その時点から待機を開始できます。指定時間は呼び出しの冒頭で一度だけ加算されます。

引数なしの Run() は単調時計で前回の更新からの経過時間を計測します。ポーズや倍速を制御する場合は、引数付きの呼び出しに統一してください。GetElapsedSeconds() は VM の累積秒数、GetDeltaTime() は直前の更新の経過秒数を返します。ロードと Restart() で時間はリセットされます。Restart() 後の Action 起動には別途 Request が必要です。

待機期限は Action の割り込みごとに保持され、割り込み中も時間は進みます。期限に達した後、次にその Action が実行可能になった時点で再開します。

状態の確認

主な状態確認APIには次があります。

  • IsRunning()
  • GetFrameCounter()
  • GetDeltaTime()
  • IsFrameChanged()

ゲームエンジンとの連携では、VMを独立したアプリケーションとしてではなく、ホスト側更新ループの一部として扱うことを想定できます。

関連項目

Program Image

mana::ProgramImage は、コンパイル済み Program Image を読み込み、含まれている Actor / Action / Phantom を実行前に調べるためのクラスです。

mana::VM が「実行する」ためのAPIなのに対し、ProgramImage は「中身を調べる」ためのAPIです。

ロードする

auto bytes = std::make_shared<std::vector<uint8_t>>(result.mProgramImage);

mana::ProgramImage image;
const bool loaded = image.LoadProgram(
    std::shared_ptr<const void>(bytes, bytes->data()),
    bytes->size());

ロード結果は bool で返ります。

if (!loaded)
{
    std::cerr << image.GetLastError() << '\n';
}

IsLoaded() でも状態を確認できます。

Actor一覧を取得する

for (std::string_view name : image.GetActorNames())
{
    std::cout << name << '\n';
}

特定Actorの存在確認もできます。

if (image.HasActor("Game::NPC::Guide"))
{
}

Actor の Action を調べる

for (std::string_view action :
     image.GetActorActionNames("Game::NPC::Guide"))
{
    std::cout << action << '\n';
}
if (image.HasActorAction("Game::NPC::Guide", "talk"))
{
}

ゲーム側から名前でRequestする前の検証や、エディタUIの候補一覧生成に利用できます。

Phantom を調べる

Phantomについても存在確認とAction一覧の取得ができます。

if (image.HasPhantom("EnemyTemplate"))
{
    const auto actions =
        image.GetPhantomActionNames("EnemyTemplate");
}
image.HasPhantomAction("EnemyTemplate", "damage");

Program Image の寿命

ProgramImage はロードしたバイト列を shared_ptr<const void> として保持します。

Actor名やAction名の戻り値は std::string_view なので、これらのビューは Program Image が保持するデータの寿命と関係します。取得した string_view を、元の ProgramImage より長く保持しないようにしてください。

用途

ProgramImage は特に次の用途に向いています。

  • エディタでActor一覧を表示する
  • Action名をコンボボックスから選択する
  • C++側の設定に指定されたActor / Action名を事前検証する
  • Phantomの生成候補を列挙する
  • Program Imageを実行せず検査する

VMとの違い

ProgramImage VM
Program Imageを読む する する
Actor / Action一覧を調べる 主用途 一部検索APIあり
Actionを実行する しない する
Requestする しない する
Native Functionを登録する しない する

関連項目

Native Functions

Native Function は、Mana スクリプトからゲームやツール側の C++ 処理を呼び出すための境界です。

Mana 側では native を使って関数を宣言し、C++ 側では同じ名前の関数を mana::VM に登録します。

最小構成

Mana 側で次のように宣言します。

native int add(int a, int b);

actor Main
{
    action main()
    {
        int result = add(10, 20);
        print("%d\n", result);
    }
}

C++ 側では、外部関数を登録します。

void Add(const std::shared_ptr<mana::Actor>& actor, void*)
{
    const int32_t a = actor->GetParameterInteger(0);
    const int32_t b = actor->GetParameterInteger(1);
    actor->SetReturnInteger(a + b);
}

std::shared_ptr<mana::VM> vm = std::make_shared<mana::VM>();
vm->RegisterFunction("add", &Add);

登録した VM に Program Image を読み込み、通常どおり Run() すると、Mana の add() 呼び出しから C++ の Add() が実行されます。

外部関数の型

現行 VM の外部関数型は次の形式です。

using ExternalFunctionType =
    std::function<void(const std::shared_ptr<mana::Actor>& actor,
                       void* structPointer)>;

第1引数の actor は、その native 関数を実行している Actor です。

第2引数の structPointer は Struct の native メソッドを呼び出した場合に、その Struct インスタンスを参照するために使用されます。

引数を取得する

Native Function の引数は、実行中 Actor から取得します。

代表的な API は次の通りです。

actor->GetParameterInteger(index);
actor->GetParameterFloat(index);
actor->GetParameterString(index);
actor->GetParameterActor(index);
actor->GetParameterPointer(index);
actor->GetParameterAddress(index);

引数数は次で取得できます。

const int32_t count = actor->GetArgumentCount();

Mana 側の宣言と C++ 側で読み取る型・順番は、組み込み側の契約として一致させてください。

戻り値を返す

戻り値がある native 関数では SetReturn*() を使用します。

actor->SetReturnInteger(value);
actor->SetReturnFloat(value);
actor->SetReturnString(text);
actor->SetReturnActor(otherActor);
actor->SetReturnPointer(pointer);
actor->SetReturnData(data, size);

例えば Mana 側が

native float getSpeed();

なら、C++ 側では次のように値を設定します。

void GetSpeed(const std::shared_ptr<mana::Actor>& actor, void*)
{
    actor->SetReturnFloat(3.5f);
}

Struct の native メソッド

Mana では Struct のメンバーとして native 関数を宣言できます。

struct Position
{
    float x;
    float y;

    native void normalize();
}

この場合、C++ 側で解決される外部名は Struct名::メソッド名 です。

vm->RegisterFunction("Position::normalize", &NormalizePosition);

コールバックの第2引数 structPointer には対象 Struct のアドレスが渡されます。

void NormalizePosition(const std::shared_ptr<mana::Actor>&,
                       void* structPointer)
{
    // structPointer をホスト側で対応するレイアウトとして扱う
}

Struct のメモリレイアウトを C++ 側で直接扱う場合は、Mana 側の型定義との一致を厳密に管理してください。

C++ メンバー関数を登録する

VM::RegisterMemberFunction() を使うと、C++ オブジェクトのメンバー関数をラッパーなしで登録できます。

class GameBridge
{
public:
    void PlaySound(const std::shared_ptr<mana::Actor>& actor, void* structPointer)
    {
        // ゲーム側処理
    }
};

auto bridge = std::make_shared<GameBridge>();
vm->RegisterMemberFunction("playSound", bridge, &GameBridge::PlaySound);

現行 API には生ポインタ、shared_ptr、weak_ptr を利用するオーバーロードがあります。

shared_ptr を渡すオーバーロードは内部で弱参照として保持します。登録先オブジェクトが先に破棄された場合、呼び出し時にエラーが Trace へ出力されます。

登録名を一致させる

VM は実行時に文字列名で外部関数を検索します。

Mana 側の宣言名と RegisterFunction() の登録名が一致していない場合、VM は外部関数が見つからないことを Error Trace として報告します。

組み込み時には、Program Image をロードして実行する前に必要な Native Function をすべて登録しておくことを推奨します。

設計上の役割

Native Function は、Mana 自身へレンダリング、物理、サウンド、アセット管理などを持ち込むための仕組みではありません。

それらをホストアプリケーション側へ残したまま、Mana から必要な操作だけを公開する境界として設計すると、スクリプトとゲームエンジンの責務を分離しやすくなります。

関連項目

SourceResolver

mana::SourceResolver は、Mana Compiler へソースコードを供給するためのインターフェースです。

通常はファイルシステムから .mn を読み込みますが、エディタの未保存バッファ、アセットデータベース、パッケージ内データ、ネットワーク上の仮想ファイルなどからソースを供給することもできます。

Compiler との関係

CompileOptions::mSourceResolver に独自 Resolver を指定します。

mana::CompileOptions options;
options.mSourceFilename = "main.mn";
options.mSourceResolver = resolver;

mana::CompileResult result = mana::Compile(options);

mSourceResolver を省略した場合は、既定の mana::FileSourceResolver が使用されます。

インターフェース

独自 Resolver では、次の2関数を実装します。

class SourceResolver
{
public:
    virtual std::string Resolve(
        std::string_view from,
        std::string_view filename) const = 0;

    virtual bool Read(
        std::string_view path,
        std::string& outText) const = 0;
};

役割は明確に分かれています。

  • Resolve() : 参照元と指定名から、ソースを識別する位置を決める
  • Read() : 解決済み位置から実際のソース文字列を取得する

FileSourceResolver

標準の FileSourceResolver はファイルシステムから読み込みます。

project/
├─ main.mn
└─ actor/
   └─ npc.mn

main.mn が次のように書かれている場合、

import "actor/npc.mn";

相対パスは main.mn のあるディレクトリを基準に解決されます。

さらに actor/npc.mn から別ファイルを読み込む場合は、今度は npc.mn の場所が基準になります。

最初のソースだけは現在の作業ディレクトリを基準にします。

メモリ上のソースを供給する

例えばエディタの未保存内容を直接コンパイルしたい場合は、次のような Resolver を作れます。

class MemorySourceResolver final : public mana::SourceResolver
{
public:
    std::map<std::string, std::string, std::less<>> files;

    std::string Resolve(
        std::string_view,
        std::string_view filename) const override
    {
        return std::string(filename);
    }

    bool Read(
        std::string_view path,
        std::string& outText) const override
    {
        const auto it = files.find(path);
        if (it == files.end())
            return false;

        outText = it->second;
        return true;
    }
};

利用側は次のようになります。

auto resolver = std::make_shared<MemorySourceResolver>();
resolver->files["main.mn"] = R"(
actor Main
{
    action main()
    {
        print("Hello\n");
    }
}
)";

mana::CompileOptions options;
options.mSourceFilename = "main.mn";
options.mSourceResolver = resolver;

const mana::CompileResult result = mana::Compile(options);

import とパスの一意性

import は、同じ解決済みパスのソースを二度読み込まない仕組みです。

そのため独自 SourceResolver では、同じソースを表す指定に対して可能な限り同じ解決済み文字列を返すことが重要です。

例えば次の2つが同じデータを表すのに、

scripts/npc.mn
scripts/./npc.mn

異なる解決結果のまま返すと、Compiler からは別ソースとして見える可能性があります。

アセットIDや正規化済み仮想パスなど、安定した識別子へ正規化する設計を推奨します。

読み込み失敗

Resolve() が空文字列を返した場合、Compiler は指定位置を解決できなかったものとして診断します。

Resolve() が位置を返しても Read() が false を返した場合は、その解決済み位置を開けなかったものとして診断されます。

エディタ統合では、診断表示に使いやすい論理パスを Resolve() の結果として返しておくと、どの仮想ファイルで問題が起きたかを利用者へ示しやすくなります。

改行コード

Read() が返す文字列の改行コードを Resolver 側で統一する必要はありません。

Compiler の Lexer が読み込み後に LF へ正規化します。

-I 相当の強制読み込み

CompileOptions::mForcedIncludeFiles に指定したソースも、同じ SourceResolver を通して読み込まれます。

options.mForcedIncludeFiles.push_back("common.mn");

CLI の -I common.mn に相当します。

用途例

SourceResolver を差し替えることで、次のような統合が可能です。

  • ゲームエディタ上の未保存 Mana スクリプトをそのままコンパイルする
  • Unreal Engine などのアセットからソースを供給する
  • zip / pak などのパッケージ内から読む
  • 単体テストでファイルI/Oを使わずソースを与える
  • 論理パスと実ファイルの場所を分離する

関連項目

Diagnostics

Mana Compiler は、警告やエラーを単なる文字列として標準出力へ流すだけでなく、mana::Diagnostic として構造化して返します。

エディタ、IDE、CI、ゲーム内ツールへ組み込む場合は、CompileResult::mDiagnostics を利用すると、ファイル名や行番号を保ったまま独自表示できます。

CompileResult から取得する

最も基本的な方法は、コンパイル後に mDiagnostics を確認することです。

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

const mana::CompileResult result = mana::Compile(options);

for (const mana::Diagnostic& diagnostic : result.mDiagnostics)
{
    std::cout << diagnostic.ToString() << '\n';
}

診断が存在しても、すべてがコンパイル失敗を意味するわけではありません。

  • Warning: 警告。コンパイルは継続する
  • Error: エラー。解析は可能な範囲で継続するが、成果物は生成しない
  • Fatal: そのコンパイルを継続できない致命的エラー

CompileResult::mSucceeded を最終的な成功判定に使用してください。

Diagnostic の内容

mana::Diagnostic には次の情報があります。

struct Diagnostic
{
    DiagnosticSeverity mSeverity;
    DiagnosticPhase mPhase;
    std::string mFilename;
    int32_t mLineNo;
    std::string mMessage;
};

Severity

mana::DiagnosticSeverity::Warning
mana::DiagnosticSeverity::Error
mana::DiagnosticSeverity::Fatal

Phase

mana::DiagnosticPhase::Compile
mana::DiagnosticPhase::Link

Compile は字句解析、構文解析、意味解析、コード生成などで発生した問題を表します。

Link はシンボル解決や Program Image 生成段階の問題を表します。

ファイル名と行番号

mFilename と mLineNo を使うと、エディタ上で該当ソースへジャンプできます。

for (const auto& diagnostic : result.mDiagnostics)
{
    editor.ShowDiagnostic(
        diagnostic.mFilename,
        diagnostic.mLineNo,
        diagnostic.mMessage);
}

mLineNo == 0 は、診断に行情報がないことを表します。

include / import 先で発生した診断には、対象ソースのファイル名と行番号が保持されます。

独自 SourceResolver を使う場合、Resolve() が返す論理パスはそのまま診断上の位置として重要になります。

標準書式へ変換する

Diagnostic::ToString() は Mana の標準書式へ整形します。

const std::string text = diagnostic.ToString();

行番号がある場合、プラットフォームに応じて例えば次のような形式になります。

main.mn(12): error: message

または、

main.mn:12 error: message

独自UIでは構造化フィールドを使い、CLIやログでは ToString() を使うという分け方ができます。

発生時に受け取る

コンパイル完了後ではなく、診断が発生した時点で受け取りたい場合は CompileOptions::mDiagnosticHandler を使用します。

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

options.mDiagnosticHandler = [](const mana::Diagnostic& diagnostic)
{
    LogDiagnostic(diagnostic);
};

const mana::CompileResult result = mana::Compile(options);

Handler を設定しても、診断は CompileResult::mDiagnostics にも残ります。

そのため、

  • Handler: リアルタイム表示やログ
  • mDiagnostics: コンパイル後の一覧表示やテスト

という使い分けができます。

Handler から例外を出さない

組み込み側の Diagnostic Handler は例外を送出しない設計を推奨します。

現行 Compile() はコンパイル境界から例外を外へ出さないよう実装されており、Handler 内で例外が発生した場合もホスト側へ越境しないことを回帰テストしています。

ただし、診断処理そのものが失敗すると本来のエラー表示を失う原因になるため、Handler はできるだけ単純な処理にしてください。

Compiler のスレッド制約

現在の Mana Compiler は診断収集を含めてグローバル状態を使用しています。

そのため、mana::Compile() を複数スレッドから同時に呼び出すことはできません。

エディタでバックグラウンドコンパイルを行う場合でも、Mana Compiler の呼び出し自体は1本に直列化してください。

CI での利用

CI では mSucceeded と mDiagnostics を組み合わせて扱うと便利です。

const mana::CompileResult result = mana::Compile(options);

if (!result.mSucceeded)
{
    for (const auto& diagnostic : result.mDiagnostics)
        std::cerr << diagnostic.ToString() << '\n';

    return 1;
}

警告を独自ポリシーでエラー扱いする場合も、DiagnosticSeverity を見てホスト側で判断できます。

Compile Error と Runtime Error は別

このページで扱う Diagnostic は主に Compiler の診断です。

Program Image をロードした後に発生するスクリプト実行エラーや VM 内部エラーは、Trace、ScriptError、FatalError など別の経路で扱います。

それらは Error Handling を参照してください。

関連項目

Error Handling

Mana をゲームやツールへ組み込む場合、エラーを一種類として扱うのではなく、コンパイル時 / Program Image 読み込み時 / スクリプト実行時 / Mana 内部不整合 に分けて考えると扱いやすくなります。

全体像

Mana source
   |
   | Compile diagnostics
   v
mana::Compile()
   |
   | Program Image load error
   v
mana::VM::LoadProgram()
   |
   | ScriptError / runtime trace
   v
mana::VM::Run()
   |
   | FatalError / FaultHandler
   v
Mana または組み込み側の不整合

それぞれで復帰単位と通知経路が異なります。

コンパイル時のエラー

mana::Compile() は、失敗を CompileResult として返します。

const mana::CompileResult result = mana::Compile(options);

if (!result.mSucceeded)
{
    for (const auto& diagnostic : result.mDiagnostics)
        ShowError(diagnostic);
}

Compiler 内部で発生した例外も Compile() の境界で捕捉され、致命的診断として mDiagnostics へ格納されます。

そのため通常の組み込みコードでは、コンパイルエラーのために Compile() 全体を例外で制御する必要はありません。

詳細は Diagnostics を参照してください。

Program Image の読み込みエラー

VM::LoadProgram() は Compiler API と異なり、読み込みに失敗した場合に例外を送出する場合があります。

例えば現行実装では、次のような問題が検査されます。

  • ファイルを開けない
  • Mana Program Image のシグネチャではない
  • Program Image のバージョンが一致しない
  • 32bit / 64bit の形式が実行側と一致しない

ホスト側ではロード境界で例外を捕捉してください。

try
{
    vm->LoadProgram("event.mx");
}
catch (const std::exception& e)
{
    LogError(e.what());
    return false;
}

メモリから読み込む場合も、不正な Program Image に対する例外をホスト側で扱う設計にしてください。

ProgramImage で事前検査する

実行せずに Program Image を調べたい場合は mana::ProgramImage を利用できます。

mana::ProgramImage image;

if (!image.LoadProgram(program, size))
{
    LogError(image.GetLastError());
    return false;
}

ProgramImage::LoadProgram() は bool で成功を返し、失敗理由は GetLastError() から取得できます。

エディタやアセットインポータでは、VM に渡す前の検査として利用できます。

スクリプト実行エラー

0除算、配列範囲外、不正な自己待機など、Mana スクリプトの実行中に継続できない問題は mana::ScriptError として扱われます。

VM::RunActor() は ScriptError を Actor 単位で捕捉します。

Actor A
  ScriptError
     |
     v
  Actor A を停止

Actor B / C
  実行継続

エラーを起こした Actor は停止しますが、それだけを理由に VM 全体や他の Actor が停止する設計ではありません。

エラー内容は TraceLevel::Error として Trace に出力されます。

Trace をホスト側へ接続する

print() の出力、警告、実行時エラーなどは Trace 経由で受け取れます。

void OnTrace(
    void* userData,
    const mana::TraceLevel level,
    const char* message,
    const std::size_t length)
{
    // ゲームエンジン側のログへ送る
}

mana::SetTraceHandler(&OnTrace, hostContext);

重大度は次の3種類です。

mana::TraceLevel::Info
mana::TraceLevel::Warning
mana::TraceLevel::Error

標準出力が利用しにくいゲームエンジンへ組み込む場合は、起動時に TraceHandler を設定してエンジン側ログへ接続することを推奨します。

TraceHandler の注意点

TraceHandler は利用開始前に一度設定し、実行中に頻繁に差し替えないでください。

また、Handler 自身から例外を送出しないでください。

Trace は必ずしも1行単位でコールバックされるとは限らないため、行単位のログが必要ならホスト側で改行までバッファリングします。

Mana 内部の不整合

スクリプトの誤りではなく、Mana 自身または組み込み側の不整合によって内部の前提が崩れた場合は mana::FatalError が使用されます。

この系統は RaiseFault() から報告され、Error Trace を出力した後に FatalError を送出します。

Actor 実行中に発生した std::exception は VM::RunActor() の境界で捕捉され、その Actor が停止します。他の Actor は継続できます。

FaultHandler

内部不整合をデバッガーやクラッシュレポートへ接続したい場合は SetFaultHandler() を利用できます。

void OnFault(
    void* userData,
    const char* file,
    int line,
    const char* message)
{
    // debugger break / crash reporter / telemetry など
}

mana::SetFaultHandler(&OnFault, hostContext);

FaultHandler は巻き戻しが始まる前に呼ばれるため、Mana 開発中にその場でデバッガーを止めたい場合にも利用できます。

Handler から戻った後は FatalError が送出されます。

FaultHandler 自身から例外を送出してはいけません。

ScriptError と FatalError の違い

種類 意味 FaultHandler Actor
ScriptError スクリプト側の実行時エラー 呼ばれない 該当Actorを停止
FatalError Manaまたは組み込み側の内部不整合 呼ばれる Actor実行境界なら該当Actorを停止

スクリプト作者へ見せるエラーと、Mana/エンジン開発者が調査すべき内部不整合を分離することが重要です。

推奨するホスト側の方針

組み込み側では次のように責務を分けると扱いやすくなります。

  1. Compiler の問題は Diagnostic としてエディタへ表示する
  2. Program Image のロード失敗はロード処理の例外として扱う
  3. Runtime の ScriptError は Trace へ送り、該当Actorだけを停止する
  4. FatalError は Trace + FaultHandler で開発者へ通知する
  5. TraceHandler と FaultHandler はアプリケーション初期化時に設定する

これにより、1つの不正なスクリプトでゲームプロセス全体を即座に終了させるのではなく、問題の種類に応じた復帰単位を選べます。

関連項目

Clone this wiki locally