Note
This project was initially called MRubyD, but was renamed to MRubyCS.
MRubyCS is a new mruby virtual machine implemented in pure C#. The name "mruby/cs" stands for mruby implemented in C#. Designed with seamless integration in mind for C#-based game engines, and emphasize ruby level compatibility. MRubyCS leverages the latest C# features for high performance and high extensibility.
-
Implemented in C# Utilizes the robust capabilities of C# to ensure seamless integration with C#-based game engines.
-
High Performance Takes advantage of modern C# language features—such as managed pointers,
Span
, and the performance benefits of the .NET runtime’s GC and JIT compiler—to deliver superior speed and efficiency. -
High compatibility with Ruby-level APIs (Work in progress) It is intended for use in software with a certain amount of resources, such as games. For this reason, we are focusing on Ruby API compatibility. At this time, all opcodes are implemented and pass the syntax.rb, class.rb, module.rb tests from the mruby repository.
-
Rich Library Integration & Extensibility Compared to the original C implementation, calling C#’s extensive libraries from Ruby is straightforward, making the VM highly extensible.
private
andprotected
visibitily is not yet implemented. (mruby got support for this in 3.4)- This project provides only the VM implementation; it does not include a compiler. To compile mruby scripts, you need the native mruby-compiler.
- VitalRouter.MRuby for the new version.
dotnet add package MRubyCS
Note
Requirements: Unity 2021.3 or later.
- Install NugetForUnity.
- Open the NuGet window by going to NuGet > Manage NuGet Packages, “Show Preleaseâ€toggled on, after search for the "MRubyCS" package, and install it.
- (Optional) To install utilities for generating mrb bytecode, refer to the How to compile .mrb section.
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
fibonacci 10
$ mrbc -o fibonaci.mrbc fibonacci.rb
using MRubyCS;
// initialize state
var state = MRubyState.Create();
// Read the .mrb byte-code.
var bytecode = File.ReadAllBytes("fibonacci.mrb");
// execute bytecoe
var result = state.LoadBytecode(bytecode);
result.IsInteger //=> true
result.IntegerValue //=> 55
You can also parse bytecode in advance.
The result of parsing bytecode is called Irep
in mruby terminology.
Irep irep = state.RiteParser.Parse(bytecode);
state.Exuecute(irep);
Irep
can be executed as is, or converted to Fiber before use. For details on Fiber, refer to the Fiber section.
This is a sample of executing bytecode. See the How to compile .mrb section for information on how to convert Ruby source code to mruby bytecode.
Above result
is MRubyValue
. This represents a Ruby value.
value.IsNil //=> true if `nil`
value.IsInteger //=> true if integrr
value.IsFloat //=> true if float
value.IsSymbol //=> true if Symbol
value.IsObject //=> true if any allocated object type
value.VType //=> get known ruby-type as C# enum.
value.IntegerValue //=> get as C# Int64
value.FloatValue //=> get as C# float
value.SymbolValue //=> get as `Symbol`
value.As<RString>() //=> get as internal String representation
value.As<RArray>() //=> get as internal Array representation
value.As<RHash>() //=> get as internal Hash representation
// pattern matching
if (vlaue.Object is RString str)
{
// ...
}
swtich (value)
{
case { IsInteger: true }:
// ...
break;
case { Object: RString str }:
// ...
break;
}
var intValue = MRubyValue.From(100); // create int value
var floatValue = MRubyValue.From(1.234f); // create float value
var objValue = MRubyValue.From(str); // create allocated ruby object value
// Create MRubyState object.
var state = MRubyState.Create();
// Define class
var classA = state.DefineClass(Intern("A"u8), c =>
{
// Method definition that takes a required argument.
c.DefineMethod(Intern("plus100"u8), (state, self) =>
{
var arg0 = state.GetArgumentAsIntegerAt(0); // get first argument (index:0)
return MRubyValue.From(arg0 + 100);
});
// Method definition that takes a block argument.
c.DefineMethod(Intern("method2"), (state, self) =>
{
var arg0 = state.GetArgumentAt(0);
var blockArg = state.GetBlockArgument();
if (!blockArg.IsNil)
{
// Execute `Proc#call`
state.Send(blockArg, state.Intern("call"u8), arg0);
}
});
// Other complex arguments...
c.DefineMethod(Intern("method3"), (state, self) =>
{
var keywordArg = state.GetKeywordArgument(state.Intern("foo"))
Console.WriteLine($"foo: {keywordArg}");
// argument type checking
state.EnsureValueType(keywordArg, MrubyVType.Integer);
var restArguments = state.GetRestArgumentsAfter(0);
for (var i = 0; i < restArguments.Length; i++)
{
Console.WriteLine($"rest arg({i}: {restArguments[i]})");
}
});
// class method
c.DefineClassMethod(Intern("classmethod1"), (state, self) =>
{
var str = state.NewString($"hoge fuga");
return MRubyValue.From(str);
});
});
// Monkey patching
classA.DefineMethod(Intern("additional_method1"u8), (state, self) => { /* ... */ });
// Define module
var moduleA = state.DefineModule(Intern("ModuleA");)
state.DefineMethod(moduleA, Intern("additional_method2"u8), (state, self) => MRubyValue.From(123));
state.IncludeModule(classA, moduleA);
a = A.new
a.plus100(123) #=> 223
a.method2(1) { |a| a } #=> 1
a.additionoa_method2 #=> 123
A.classmethod1 #=> "hoge fuga"
The string representation within mruby is utf8. Therefore, to generate a ruby string from C#, Utf8StringInterpolation is used internally.
// Create string literal.
var str1 = state.NewString("HOGE HOGE"u8);
// Create string via interpolation
var x = 123;
var str2 = state.NewString($"x={x}");
// wrap MRubyValue..
var strValue = MRubyValue.From(str1);
There is a concept in mruby similar to String called Symbol
.
Like String, it is created using utf8 strings, but internally it is a uint integer.
Symbols are usually used for method IDs and class IDs.
To create a symbol from C#, use Intern
.
// symbol literal
var sym1 = state.Intern("sym"u8)
// symbol to utf8 bytes
var utf8 = state.NameOf(sym1); //=> "sym"u8
// symbol from string
var sym2 = state.ToSymbol(state.NewString("sym2"u8));
MRubyCS supports Ruby Fibers, which are lightweight concurrency primitives that allow you to pause and resume code execution. In addition to standard Ruby Fiber features, MRubyCS provides seamless integration with C#'s async/await pattern.
using MRubyCS;
using MRubyCS.Compiler;
// Create state and compiler
var state = MRubyState.Create();
var compiler = MRubyCompiler.Create(state);
// Define a fiber that yields values
var code = """
Fiber.new do |x|
Fiber.yield(x * 2)
Fiber.yield(x * 3)
x * 4
end
"""u8;
// Load the Ruby code as a Fiber
var fiber = compiler.LoadSourceCode(code).As<RFiber>();
// Resume the fiber with initial value
var result1 = fiber.Resume(MRubyValue.From(10)); // => 20
var result2 = fiber.Resume(MRubyValue.From(10)); // => 30
var result3 = fiber.Resume(MRubyValue.From(10)); // => 40 (final return value)
// Check if fiber is still alive
fiber.IsAlive // => false
If you want to execute arbitrary code snippets as fibers, do the following.
var fiber = compiler.LoadSourceCodeAsFiber("""
x = 1
y = 2
Fiber.yield (x + y) * 100
Fiber.yield (x + y) * 200
"""u8);
fiber.Resume(); //=> 300
fiber.Resume(); //=> 600
MRubyCS provides unique C# async integration features for working with Fibers:
// Wait for fiber to terminate
var code = """
Fiber.new do |x|
Fiber.yield
Fiber.yield
"done"
end
"""u8;
var fiber = compiler.LoadSourceCode(code).As<RFiber>();
// Start async wait before resuming
var terminateTask = fiber.WaitForTerminateAsync();
// Resume the fiber multiple times
fiber.Resume();
fiber.Resume();
fiber.Resume();
// Wait for completion
await terminateTask;
Console.WriteLine("Fiber has terminated");
You can consume fiber results as async enumerable:
var code = """
Fiber.new do |x|
3.times do |i|
Fiber.yield(x * (i + 1))
end
end
"""u8;
var fiber = compiler.LoadSourceCode(code).As<RFiber>();
// Process each yielded value asynchronously
await foreach (var value in fiber.AsAsyncEnumerable())
{
Console.WriteLine($"Yielded: {value.IntegerValue}");
}
MRubyCS supports multiple consumers waiting for fiber results simultaneously:
var fiber = compiler.LoadSourceCode(code).As<RFiber>();
// Create multiple consumers
var consumer1 = Task.Run(async () =>
{
while (fiber.IsAlive)
{
var result = await fiber.WaitForResumeAsync();
Console.WriteLine($"Consumer 1 received: {result}");
}
});
var consumer2 = Task.Run(async () =>
{
while (fiber.IsAlive)
{
var result = await fiber.WaitForResumeAsync();
Console.WriteLine($"Consumer 2 received: {result}");
}
});
// Resume fiber and both consumers will receive the results
fiber.Resume(MRubyValue.From(10));
fiber.Resume(MRubyValue.From(20));
fiber.Resume(MRubyValue.From(30));
await Task.WhenAll(consumer1, consumer2);
Caution
Waiting for fiber can be performed in a separate thread. However, MRubyState and mruby methods are not thread-safe. Please note that when using mruby functions, you must always return to the original thread.
Exceptions raised within fibers are properly propagated:
var code = """
Fiber.new do |x|
Fiber.yield(x)
raise "Something went wrong"
end
"""u8;
var fiber = compiler.LoadSourceCode(code).As<RFiber>();
// First resume succeeds
var result1 = fiber.Resume(MRubyValue.From(10)); // => 10
// Second resume will throw
try
{
fiber.Resume();
}
catch (MRubyRaiseException ex)
{
Console.WriteLine($"Ruby exception: {ex.Message}");
}
// Async wait will also propagate the exception
var waitTask = fiber.WaitForResumeAsync();
try
{
fiber.Resume();
await waitTask;
}
catch (MRubyRaiseException ex)
{
Console.WriteLine($"Async exception: {ex.Message}");
}
MRubyCS only includes the mruby virtual machine. Therefore it is necessary to convert it to .mrb bytecode before executing the .rb source. Basically, you need the native compiler provided by the mruby project.
$ git clone [email protected]:mruby/mruby.git
$ cd mruby
$ rake
$ ./build/host/bin/mrubc
To simplify compilation from C#, we also provide the MRubyCS.Compiler package, which is a thin wrapper for the native compiler.
Note
This MRubyCS.Compiler package is a thin wrapper for the native binary. Currently, builds for linux (x64/arm64), macOS (x64/arm64), and windows (x64) are provided.
dotnet add package MRubyCS.Compiler
Open the Package Manager window by selecting Window > Package Manager, then click on [+] > Add package from git URL and enter the following URL:
https://github.com/hadashiA/VYaml.git?path=src/MRubyCS.Compiler.Unity/Assets/MRubyCS.Compiler#0.6.0-preview
If you install this extension, importing a .rb text file will generate .mrb bytecode as a subasset.
For example, importing the text file hoge.rb
into a project will result in the following.
This subasset is a TextAsset. To specify it in the inspector.
Or, to extract in C#, do the following:
var state = MRubyState.Create();
var bytecodeAsset = (TextAsset)AssetDatabase.LoadAllAssetsAtPath("Assets/hoge.rb")
.First(x => x.name.EndsWith(".mrb"));
state.LoadBytecode(bytecodeAsset.GetData<byte>().AsSpan());
For manual compilation, refer to the following.
using MRubyCS.Compiler;
var source = """
def f(a)
1 * a
end
f 100
"""u8;
var state = MRubyState.Create();
var compiler = MRubyCompiler.Create(state);
// Compile to irep (internal executable representation)
var irep = compiler.Compile(source);
var result = state.Execute(irep); // => 100
// Compile and evaluate:
result = compiler.LoadSourceCode("f(100)"u8);
result = compiler.LoadSourceCode("f(100)");
MIT