🪡

VitalRouterのmruby拡張をVContainerと合わせて使う際の注意点

2024/11/25に公開

VitalRouter.MRuby

VitalRouterには動的にRubyのスクリプトを読み込んでVitalRouterで定義されたコマンドを操作できる拡張実装があります。

導入方法など説明はGitHubリポジトリのreadmeに詳しいです。
https://github.com/hadashiA/VitalRouter?tab=readme-ov-file#mruby-scripting

VContainer と使う上での注意

色々わかってる人向けに先に結論を書きますが、Rubyスクリプトを起点に送られてくるコマンドを[Routes]を付けたクラスで受け取るためには、MRubyContext.Router にはreadmeにあるような Router.Default でなく、VContainer 経由で取得される Router のインスタンスを渡す必要があります。

LifetimeScopeの実装内で IContainerBuilder.RegisterVitalRouter を実行すると先に Router クラスのインスタンスが登録されるので、そのあとに以下のように書いてもRubyスクリプト起点のコマンドは別Routerに送られるようなので MRubyScript.RunAsync 実行後もコマンドが飛んでこない現象に遭遇しました。

ダメな例
public class MyLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // 以下のように送信先を設定した場合、RegisterVitalRouterの実装内部で
        // Router のインスタンスが登録されるので、
        builder.RegisterVitalRouter(routing =>
        {
            routing.Map<MyPresenter>();
        });

        var context = MRubyContext.Create();
        // ここで設定される Router.Default とは別物になってしまう。
        context.Router = Router.Default;
        context.CommandPreset = new MyCommandPreset();
        var rubySource = "cmd :speak, id: 'Bob', text: 'Hello'";
        using MRubyScript script = context.CompileScript(rubySource);
        script.RunAsync();
        // そもそも LifetimeScope でやるなという話だが、とりあえず試したい場合はやってしまいがちでは
        // (私のように)
    }
}

VCointainerの流儀に倣って別 EntryPoint などから Router を取得してrubyスクリプトを実行しましょう。

修正後
public class MyLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterVitalRouter(routing =>
        {
            routing.Map<MyPresenter>();
        });

        // 実行は MyEntryPoint へ回す
        builder.RegisterEntryPoint<MyEntryPoint>();
    }
}

public class MyEntryPoint : IAsyncStartable
{
    [Inject] readonly Router _router;
    
    public async Awaitable StartAsync(CancellationToken cancellation)
    {
        var context = MRubyContext.Create();
        // [Inject]で取得されるRouterを使う
        context.Router = _router;
        context.CommandPreset = new MyCommandPreset();
        var rubySource = "cmd :speak, id: 'Bob', text: 'Hello'";
        using MRubyScript script = context.CompileScript(rubySource);
        await script.RunAsync(cancellation);
    }
}

// 一応残りの実装も
[MRubyObject]
public partial struct CharacterSpeakCommand : ICommand
{
    public string Id;
    public string Text;
}

[MRubyCommand("speak", typeof(CharacterSpeakCommand))]
partial class MyCommandPreset : MRubyCommandPreset { }

[Routes]
public partial class MyPresenter
{
    public void On(CharacterSpeakCommand cmd)
    {
        Debug.Log($"{cmd.Id}: {cmd.Text}");
    }
}

Discussion