VitalRouter.MRuby v2
Installation
VitalRouter.MRuby uses ChibiRuby (formerly MRubyCS), a pure C# mruby VM implementation, from version 2 onwards. Follow the steps below to install ChibiRuby-related packages and the VitalRouter.MRuby package.
VitalRouter.MRuby v2 supports all platforms where C# runs.
NuGet
Install following packages.
dotnet add ChibiRuby
dotnet add ChibiRuby.Compiler
dotnet add ChibiRuby.Serializer
dotnet add VitalRouter.MRuby
Unity
- Install NuGetForUnity
- Open the NuGet window by going to NuGet > Manage NuGet Packages, and install following packages.
- ChibiRuby
- ChibiRuby.Compiler
- ChibiRuby.Serializer
- VitalRouter.MRuby
- Install the ChibiRuby.Compiler unity package (the Editor extension that adds the
.rb/.mrbScriptedImporter; it depends on theChibiRuby.CompilerNuGet package installed in step 2).
Getting Started
First, add the [MRubyObject] attribute to the command you want to execute from Ruby.
Objects with this attribute will be support mutual conversion with objects in the Ruby world.
// Your custom command decralations
[MRubyObject]
partial struct CharacterMoveCommand : ICommand
{
public string Id;
public Vector3 To;
}
[MRubyObject]
partial struct CharacterSpeakCommand : ICommand
{
public string Id;
public string Text;
}
To execute mruby, create a MRubyState.
All object creation and method execution in the mruby world happens through MRubyState.
using ChibiRuby;
var mrb = MRubyState.Create();
When the VitalRouter.MRuby package is installed, the MRubyState.DefineVitalRouter extension method becomes available.
Register the ICommand implementations you want to publish through mruby scripts to the MRubyState.
mrb.DefineVitalRouter(x =>
{
x.AddCommand<CharacterMoveCommand>("move");
x.AddCommand<CharacterSpeakCommand>("speak");
});
The string arguments (like "move" or "speak") can be freely chosen as the names for handling commands from Ruby.
For the type parameter of AddCommand, specify any ICommand type you want to publish from Ruby.
Note that the types specified here require the [MRubyObject] attribute.
Now you can publish the above commands from Ruby.
For example, executing the following Ruby source code will publish a command.
cmd :speak, id: 'Yogoroza', text: 'What a strange cat.'
The execution result of this Ruby script is essentially equivalent to the following C# code:
await Router.Default.PublishAsync(new CharacterSpeakCommand
{
Id = 'Yogoroza',
Text = 'What a strange cat.',
});
Ruby's cmd method pauses the Ruby world and delegates processing to C#. Then, after all async methods in C#'s PublishAsync have completed, the Ruby world resumes.
To execute this, do the following:
var bytecode = File.ReadAllBytes("your_script.mrb");
await mrb.ExecuteAsync(Router.Default, mrb.ParseBytecode(bytecode));
In other words, the cmd in the Ruby world delegates processing to C# when executed, enters a suspended state, and resumes when the C# await completes.
This is done without blocking threads.
Naming Convention
- C# property/field names are converted to underscore style in Ruby
- e.g) FooBar <-> foo_bar
- C# enum values are converted to underscore-style symbols in Ruby
- e.g) EnumType.FooBar <-> :foo_bar
Exporting RBS type signatures
The set of commands you register with AddCommand determines the valid signatures of the cmd method on the Ruby side.
VitalRouter.MRuby can export these as an RBS (Ruby Signature) file, so editors and type checkers such as Steep can offer completion and type checking for cmd calls in your scripts.
Call ExportRbsTo inside DefineVitalRouter with the destination path. Any missing directories are created automatically.
mrb.DefineVitalRouter(x =>
{
x.AddCommand<CharacterMoveCommand>("move");
x.AddCommand<CharacterSpeakCommand>("speak");
x.ExportRbsTo("sig/vitalrouter.rbs");
});
Each registered command becomes one overload of cmd. The command name is emitted as a symbol literal, and the command's [MRubyObject] members become optional keyword arguments â€applying the same snake_case conversion and type mapping used at runtime.
Given the CharacterMoveCommand / CharacterSpeakCommand above, the generated sig/vitalrouter.rbs looks like:
# <auto-generated>
# This file is generated by VitalRouter.MRuby. Do not edit manually.
# </auto-generated>
class Object
def cmd: (:move, ?id: String, ?to: untyped) -> void
| (:speak, ?id: String, ?text: String) -> void
end
The keyword argument types follow the same C# mruby conversions used at runtime, so each member's RBS type corresponds to its mruby type. For the full list, see ChibiRuby.Serializer's Builtin Supported types. Members whose type has no known mapping (for example a nested [MRubyObject] type or any unsupported type) are emitted as untyped.
ExportRbsTo relies on reflection over the command types, so it is intended for build/editor tooling (for example the Unity Editor or a build pipeline), not for application runtime.
ChibiRuby.Compiler
Note that in VitalRouter.MRuby v2, the runtime part that executes Ruby bytecode is clearly separated from the "compiler" that converts Ruby source code to bytecode.
By excluding the Ruby "compiler" at application runtime, the runtime part is faster and more lightweight.
If the ChibiRuby.Compiler package is included in your .NET or Unity project, you can execute Ruby source code as follows:
using ChibiRuby.Compiler;
var source = """
def f(a)
1 * a
end
f 100
"""u8;
var mrb = MRubyState.Create();
var compiler = MRubyCompiler.Create(mrb);
// Compile to bytecode (a CompilationResult holds the native byte buffer)
using var compilation = compiler.Compile(source);
// You can run it via irep (internal executable representation)
var irep = compilation.ToIrep();
var result = mrb.Execute(irep); // => 100
// Or save the compiled bytecode (mruby calls this format "Rite") to a file or any other storage
File.WriteAllBytes("compiled.mrb", compilation.AsBytecode().ToArray());
// Can be used later from file
mrb.LoadBytecode(File.ReadAllBytes("compiled.mrb")); //=> 100
// or, you can evaluate source code directly
result = compiler.LoadSourceCode("f(100)"u8);
result = compiler.LoadSourceCode("f(100)");
ChibiRuby.Compiler is a package intended for use in build pipelines, and currently only supports Windows, macOS, and Linux.
For example, in Unity, it is recommended to use it only in the Editor or build tools.
In Unity, importing .rb source code under the Assets folder automatically generates .mrb bytecode as a sub-asset.
For details, see the ChibiRuby.Compiler documentation.
[MRubyObject]
Types marked with the [MRubyObject] attribute support mutual conversion with objects in the Ruby world.
This can be used for ICommand types registered to MRubyState with AddCommand,
as well as for values registered in SharedVariableTable.
C# types that can be converted to Ruby objects with the [MRubyObject] attribute must meet the following conditions:
- class, struct, and record are all supported.
- A partial declaration is required.
- Members that meet the following conditions are converted from mruby:
- public fields or properties, or fields or properties with the [MRubyMember] attribute.
- And have a setter (private is acceptable).
Ruby objects converted by this feature become Hash in the format { field1: value, field2: value }.
For details, see the ChibiRuby.Serializer documentation.
SharedVariableTable
You often want to share some data between Ruby scripts and C#. Typically, this is when you want to reference state such as game flags from scripts and branch processing.
VitalRouter.MRuby has a feature called SharedVariableTable for this purpose.
For example, on the C# side, you can set arbitrary data by specifying a specific key as follows:
var shared = mrb.GetSharedVariables();
shared.Set("flag_a", new MRubyValue(true));
From Ruby, you can read data keyed as "flag_a" from the state method.
if state[:flag_a]
# flag_a is ON !
end
Conversely, setting data from the Ruby side is also supported.
state[:flag_b] = true
In this case, you can read data keyed as "flag_b" from C#.
var shared = mrb.GetSharedVariables();
shared.GetOrDefault<bool>("flag_b") //=> true
All types supported by ChibiRuby.Serializer are available as data values. All C# primitive types and basic collection types such as Array and Dictionary are supported.
User-defined types marked with [MRubyObject] can also be set in SharedVariableTable.
[MRubyObject]
class CustomSharedData
{
public string Field1;
public string Field2;
}
sahred.Set("customdata", new CustomSharedData
{
Field1 = "hoge",
Field2 = "fuga"
});;
state[:customdata][:field1] #=> "hoge"
state[:customdata][:field2] #=> "fuga"
You can also directly retrieve MRubyValue, which is a value in the mruby world.
state[:a] = 1
var value = shared.GetAsMRubyValue("a");
value.IsIntegere //=> true
value.IntegerValue //=> 1
Define Ruby Method from C#
ChibiRuby is implemented in C#, and it is possible to define Ruby methods from C#.
Here is an example of defining a Ruby method in C# that takes one argument and calls underlying logging.
mrb.DefineMethod(mrb.ObjectClass, mrb.Intern("log"), (mrb, self) =>
{
var message = mrb.GetArgumentAt(0);
UnityEngine.Debug.Log(mrb.Stringify(message).ToString());
return default;
});
For details on the ChibiRuby API, see the ChibiRuby documentation.
Migration Guide
- Uninstall VitalRouter (v1), VitalRouter.MRuby (v1) packages and install v2.
- Replace
MRubyContexttoMRubyState.
- If you were using MRubyContext or MRubyCompiler directly, you need to change to the new MRubyState and MRubyCompiler APIs.
- For details, see ChibiRuby.
- Change
CommandPresetdefinitions toMRubyState.DefineVitalRouter. - The built-in Ruby methods
logandwaitin v1 have been removed. If you want to continue using them, redefine them in MRubyState.
Example:
v1 (old)
[MRubyCommand("move", typeof(CharacterMoveCommand))] // < Your custom command name and type list here
[MRubyCommand("speak", typeof(CharacterSpeakCommand))]
partial class MyCommandPreset : MRubyCommandPreset { }
var context = MRubyContext.Create();
context.Router = Router.Default
context.CommandPreset = new MyCommandPreset());
using var script = context.CompileScript(rubySource);
await script.RunAsync();
v2
var mrb = MRubyState.Create();
mrb.DefineVitalRouter(x =>
{
x.AddCommand<CharacterMoveCommand>("move");
x.AddCommand<CharacterSpeakCommand>("speak");
});
using var compilation = compiler.Compile(rubySource);
await mrb.ExecuteAsync(Router.Default, compilation.ToIrep());
Examples of defining additional methods
Example of defining a log method:
mrb.DefineMethod(mrb.ObjectClass, mrb.Intern("log"), (_, self) =>
{
var message = mrb.GetArgumentAt(0);
UnityEngine.Debug.Log(mrb.Stringify(message).ToString());
return default;
});
Usage example from Ruby:
log 'This is a log'
Example of defining a wait method:
[MRubyObject]
public readonly partial record struct WaitCommand(int Seconds) : ICommand;
mrb.DefineVitalRouter(x =>
{
x.AddCommand<WaitCommand>("wait");
});
def wait(secs) = cmd :wait, seconds: secs
wait 1