Skip to content

hadashiA/MRubyCS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MRubyCS

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.

Features

  • 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.

Limitations (Preview Release)

  • private and protected 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.

Most recent roadmap

Installation

NuGet

dotnet add package MRubyCS

Unity

Note

Requirements: Unity 2021.3 or later.

  1. Install NugetForUnity.
  2. Open the NuGet window by going to NuGet > Manage NuGet Packages, “Show Preleaseâ€toggled on, after search for the "MRubyCS" package, and install it.
  3. (Optional) To install utilities for generating mrb bytecode, refer to the How to compile .mrb section.

Basic Usage

Execute byte-code

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.

Handling MRubyValue

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

Define ruby class/module/method by C#

// 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"

Symbol/String

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));

Fiber (Coroutine)

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.

Basic Fiber Usage

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

Async/Await Integration

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.

Error Handling in Fibers

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}");
}

How to compile .mrb ?

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

MRubyCS.Compiler

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

Unity

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.

docs/screenshot_subasset

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.

Usage

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)");

LICENSE

MIT

Contact

@hadahsiA

About

A new mruby virtual machine implemented in C#.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published