Skip to content

Repository files navigation

ChibiRuby

ChibiRuby is a pure C# implementation of the mruby virtual machine. It lets Unity and .NET applications run Ruby scripts with the performance and extensibility of modern C#.

It is useful for game scripting, embedded DSLs, scenario logic, and runtime-configurable behavior.

Note

VitalRouter.MRuby provides a high-level framework for integrating ChibiRuby with Unity (and .NET), including command routing and script lifecycle management.

Note

The project has since been restarted as ChibiRuby; before v1.0, it was known as MRubyCS.

Why mruby for scripting?

Ruby's clean, expressive syntax makes it perfect for building DSLs. Game designers and scenario writers can describe game logic — event triggers, dialogue trees, and AI behavior — in simple, readable scripts.

# Example: game event DSLwith(:Yogoroza)dotalk"Who are you?"motion:surpriseendwith(:BlackCat)dotalk"It's you, isn't it."motion:laughtalk"You seem to be gradually forgetting who you are."talk"Isn't that right?"end

Features

  • Supports mruby 4.0 bytecode.
  • Pure C# mruby VM with zero native dependencies — runs anywhere Unity/.NET runs. No per-platform native builds to maintain.
  • High performance — leverages .NET JIT, GC, and modern C# optimizations with minimal overhead.
  • Ruby compatible — all opcodes implemented; passes mruby's official test suite
  • Fiber & async/await integration — suspend Ruby execution and await C# async methods without blocking threads.
  • Debugger (DAP) — line breakpoints, stepping, locals view, and expression evaluation. Attach from VSCode / JetBrains / Zed to a running Unity or .NET host over TCP. See Debugger.
  • Prism-based compiler — uses mruby-compiler2, the next-generation mruby compiler built on Prism (the official CRuby parser), for more accurate and modern Ruby syntax support.

Quick Start

In a .NET project, install the runtime and compiler packages:

dotnet add package ChibiRuby
dotnet add package ChibiRuby.Compiler

Then compile and execute Ruby source from C#:

usingChibiRuby;usingChibiRuby.Compiler;usingvarmrb=MRubyState.Create();usingvarcompiler=MRubyCompiler.Create(mrb);varresult=compiler.LoadSourceCode(""" def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end fibonacci 10 """u8);Console.WriteLine(result.IntegerValue);// 55

For production builds, prefer compiling Ruby files to .mrb bytecode ahead of time:

dotnet tool install -g ChibiRuby.Cli
chibiruby compile fibonacci.rb -o fibonacci.mrb

Performance

In the .NET JIT environment, execution speeds are equal to or faster than the original native mruby.

ChibiRuby vs mruby benchmark

The above results were obtained on macOS with Apple M4 over 10 iterations.

Please refer to the following for the benchmark code.

Table of Contents

Installation

Warning

The current version supports mruby 4.0 bytecode. Versions 0.70.0 and older supported mruby 3.0 bytecode. If you have bytecode from an older ChibiRuby.Compiler (or mrbc), please regenerate it with the latest version.

NuGet

PackageDescriptionLatest version
ChibiRubyRuntime package: a pure C# mruby VM.NuGet
ChibiRuby.NIOOptional IO / File / HTTP / JSON modules — see Optional ClassesNuGet
ChibiRuby.CompilerRuby source compiler utility (native binding).NuGet
ChibiRuby.Clidotnet tool with subcommands (e.g. compile) for ChibiRuby workflowsNuGet
ChibiRuby.SerializerConverts between Ruby and C# objectsNuGet
ChibiRuby.DebuggerProtocol-agnostic debugger core (breakpoints, stepping, binding.break suspension)NuGet
ChibiRuby.Debugger.DapDAP server (TCP) for any DAP-compatible editor — see DebuggerNuGet

Unity

Note

Requirements: Unity 2021.3 or later.

Important

As of v0.107.0, ChibiRuby.Compiler is distributed via NuGetForUnity. Users of earlier versions should refer to this migration guide. v0.107.0

  1. Install NuGetForUnity (v4.3.0 or later — required for native plugin support).
  2. Install following packages via NuGetForUnity
    • Utf8StringInterpolation
    • ChibiRuby
    • (Optional) ChibiRuby.NIO — IO / File / HTTP / JSON modules. No extra dependencies.
    • (Optional) ChibiRuby.Compiler — runtime Ruby compiler. Native binaries (macOS, Linux, Windows, Android, iOS, WebGL) ship inside the NuGet package.
    • (Optional) ChibiRuby.Serializer
  3. (Optional) For an Editor extension that auto-imports .rb / .mrb files as TextAsset subassets, install ChibiRuby.Compiler Unity package as well — see Unity AssetImporter.

Note

For macOS Editor users

NuGetForUnity v4.3.0's default NativeRuntimeSettings ships broken Editor settings for the osx-arm64 / osx-x64 runtimes (the Apple Silicon variant has no Editor target, and the Intel variant defaults to "Any CPU"), so libmruby.dylib may fail to load in the Editor. A fix has been submitted upstream — NuGetForUnity#755. Once that is merged and released, this workaround will no longer be needed. In the meantime, fix the two dylibs from Unity's Inspector:

  1. In the Project window, select Assets/Packages/ChibiRuby.Compiler.*/runtimes/osx-arm64/native/libmruby.dylib. In the Inspector, under Platform settings → Editor, check Include Platforms → Editor, set CPU to ARM64, set OS to OSX, then click Apply.
  2. Select Assets/Packages/ChibiRuby.Compiler.*/runtimes/osx-x64/native/libmruby.dylib. In the Inspector, under Platform settings → Editor, uncheck Editor (or set CPU to x86_64 and OS to OSX if you want it kept for Intel Editors), then click Apply.
  3. Right-click each of the two libmruby.dylib files in the Project window and choose Reimport. NuGetForUnity skips reprocessing assets that already have its label, so the explicit reimport is required to apply the corrected Editor/CPU settings.

Basic Usage

Compiling and Executing Ruby Code

mruby allows the compiler and runtime to be separated. By distributing only precompiled bytecode, you can keep the mruby compiler out of your production deployment.

graph TB
subgraph host["host machine"]
A[source code<br/>.rb files]
C[byte-code<br/>.mrb files]
A -->|compile| C
end
C -->|deploy/install| E
subgraph application["application"]
D{{mruby VM}}
E[byte-code<br>.mrb files]
E -->|execute bytecode| D
end
style D fill:#ff4444,stroke:#cc0000,color:#ffffff,stroke-width:2px
Loading

You can choose whether to deploy precompiled bytecode or raw source code:

  • Bytecode only:
    • extremely compact and recommended for production environments.
  • Source code:
    • compiled on the target machine.
    • Note that compilation relies on the native compiler, so supported platforms are limited to those where mruby-compiler runs.

Tip

Option A is recommended for production. Option B is convenient for development and prototyping.

Option A: Pre-compile bytecode

Pre-compile Ruby source to .mrb bytecode with the CLI tool:

dotnet tool install -g ChibiRuby.Cli
chibiruby compile fibonacci.rb -o fibonacci.mrb

Or with the C# API:

usingChibiRuby;usingChibiRuby.Compiler;varmrb=MRubyState.Create();varcompiler=MRubyCompiler.Create(mrb);varsource=""" def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end fibonacci 10 """u8;// Compile and save as .mrb fileusingvarcompilation=compiler.Compile(source);File.WriteAllBytes("fibonacci.mrb",compilation.AsBytecode());

Then execute the pre-compiled bytecode:

usingChibiRuby;varmrb=MRubyState.Create();varbytecode=File.ReadAllBytes("/path/to/fibonacci.mrb");varresult=mrb.LoadBytecode(bytecode);result.IntegerValue//=> 55

Option B: Use the Compiler Library at Runtime

dotnet add package ChibiRuby
dotnet add package ChibiRuby.Compiler
usingChibiRuby;usingChibiRuby.Compiler;varmrb=MRubyState.Create();varcompiler=MRubyCompiler.Create(mrb);varresult=compiler.LoadSourceCode(""" def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end fibonacci 10 """u8);result.IntegerValue//=> 55

See also ChibiRuby.Compiler (library) for installation details.

Irep

You can also parse bytecode in advance. The result is called Irep in mruby terminology. Pre-parsing is useful when you want to execute the same bytecode multiple times without re-parsing overhead.

Irepirep=mrb.ParseBytecode(bytecode);mrb.Execute(irep);

Irep can be executed as is, or converted to Proc, Fiber before use. For details on Fiber, refer to the Fiber section.

Note

  • Dispose when finishedMRubyState is IDisposable. The VM itself has no unmanaged resources, but an installed MRubyFiberScheduler may hold cancellation tokens for parked fibers; Dispose cleans those up. A finalizer is in place as a backstop, but explicit disposal is preferred. If you never call UseFiberScheduler, omitting Dispose is harmless.
  • Not thread-safe — each MRubyState instance must be used from a single thread. For multi-threaded scenarios, create a separate instance per thread.

Compiler Reference

The ChibiRuby runtime is pure C#, but the mrb compiler uses the native prism compiler. Note that the compiler's supported target platforms are subject to the following limitations.

ChibiRuby.Cli (dotnet tool)

The chibiruby compile CLI supports additional output formats beyond simple .mrb:

# Dump bytecode in human-readable format
$ chibiruby compile input.rb --dump
# Generate C# code with embedded bytecode
$ chibiruby compile input.rb -o Bytecode.cs --format csharp --csharp-namespace MyApp

Tip

For local tool installation, use dotnet tool install ChibiRuby.Cli and run with dotnet chibiruby compile.

OptionDescription
-o, --outputOutput file path (default: same directory as input with .mrb/.cs extension). Use - for stdout.
--dumpDump bytecode in human-readable format (outputs to stdout)
--formatOutput format: binary (default) or csharp
--csharp-namespaceC# namespace for generated code (used with --format csharp)
--csharp-class-nameC# class name for generated code (used with --format csharp)
mrbc (original mruby compiler)

Alternatively, you can use the original mruby project's compiler.

$ git clone git@github.com:mruby/mruby.git
$ cd mruby
$ rake
$ ./build/host/bin/mrbc -o output.mrb input.rb
ChibiRuby.Compiler (library)

ChibiRuby.Compiler is a thin wrapper of the C# API for the native compiler.

NOTE: This is a wrapper for native compilers. Currently, the following platforms are supported:

OS / RuntimeArchitecture.NET RID
Windowsx64win-x64
Linuxx64, arm64linux-x64, linux-arm64
macOSx64, arm64osx-x64, osx-arm64
Androidarm64-v8a, x86_64android-arm64, android-x64
iOSarm64 (device + Apple Silicon simulator)ios-arm64, iossimulator-arm64
WebAssemblywasm32 (Unity WebGL / .NET Browser WASM)browser-wasm
dotnet add package ChibiRuby.Compiler

Unity: install ChibiRuby.Compiler via NuGetForUnity (v4.3.0 or later). The native compiler binaries (libmruby.dylib / .so / .dll) are bundled in the NuGet package and resolved automatically.

If you also want the Editor extension that auto-imports .rb / .mrb files as TextAsset subassets, additionally install the Unity package. Open Window > Package Manager, click [+] > Add package from git URL, and enter:

https://github.com/hadashiA/ChibiRuby.git?path=src/ChibiRuby.Unity/Assets/ChibiRuby.Compiler.Unity#1.6.1

See Unity AssetImporter for details.

usingChibiRuby.Compiler;varsource="""def f(a) 1 * aendf 100"""u8;varmrb=MRubyState.Create();varcompiler=MRubyCompiler.Create(mrb);// Compile source code (returns CompilationResult)usingvarcompilation=compiler.Compile(source);// Convert to irep (internal executable representation)varirep=compilation.ToIrep();// irep can be used later..varresult=mrb.Execute(irep);// => 100// Or, get bytecode (mruby calls this format "Rite")// bytecode can be saved to a file or any other storageFile.WriteAllBytes("compiled.mrb",compilation.AsBytecode());// Can be used later from filemrb.LoadBytecode(File.ReadAllBytes("compiled.mrb"));//=> 100// or, you can evaluate source code directlyresult=compiler.LoadSourceCode("f(100)"u8);result=compiler.LoadSourceCode("f(100)");
Unity AssetImporter

In Unity, 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 that can be assigned via the inspector or loaded from code:

varmrb=MRubyState.Create();varbytecodeAsset=(TextAsset)AssetDatabase.LoadAllAssetsAtPath("Assets/hoge.rb").First(x =>x.name.EndsWith(".mrb"));mrb.LoadBytecode(bytecodeAsset.GetData<byte>().AsSpan());

To read a subasset in Addressables, you would do the following.

Addressables.LoadAssetAsync<TextAsset>("Assets/hoge.rb[hoge.mrb]")
Hot reload in the Editor

Bundling pre-compiled .mrb bytecode via the importer is the production path, but it is not the only option.

In environments where ChibiRuby.Compiler is supported (macOS, Windows, Linux), it is possible to dynamically load .rb source code at any time, even while it is running.

  • Hot-reload Ruby scripts in Play Mode — re-LoadSourceCode a modified .rb file without exiting Play Mode and reattaching the player.
usingvarcompiler=MRubyCompiler.Create(mrb);varsrc=File.ReadAllText(Path.Combine(Application.streamingAssetsPath,"scripts/player.rb"));compiler.LoadSourceCode(src);// re-evaluates, replacing previous definitions

Define Ruby classes, modules, and methods from C#

varclassA=mrb.DefineClass(mrb.Intern("A"u8), c =>{c.DefineMethod(mrb.Intern("plus100"u8),(_,self)=>{vararg0=mrb.GetArgumentAsIntegerAt(0);returnarg0+100;});});
a=A.newa.plus100(123)#=> 223

Block / keyword / rest arguments

Methods can also receive blocks, keyword arguments, and rest arguments:

varclassA=mrb.DefineClass(mrb.Intern("A"u8), c =>{// Block argumentc.DefineMethod(mrb.Intern("with_block"u8),(_,self)=>{vararg0=mrb.GetArgumentAt(0);varblockArg=mrb.GetBlockArgument();if(!blockArg.IsNil){mrb.Send(blockArg,mrb.Intern("call"u8),arg0);}});// Keyword and rest argumentsc.DefineMethod(mrb.Intern("with_kwargs"u8),(_,self)=>{varkeywordArg=mrb.GetKeywordArgument(mrb.Intern("foo"u8));mrb.EnsureValueType(keywordArg,MRubyVType.Integer);varrestArguments=mrb.GetRestArgumentsAfter(0);for(vari=0;i<restArguments.Length;i++){Console.WriteLine($"rest arg({i}): {restArguments[i]}");}});});

Class methods / modules

// Class methodvarclassA=mrb.DefineClass(mrb.Intern("A"u8), c =>{c.DefineClassMethod(mrb.Intern("greet"u8),(_,self)=>{returnmrb.NewString("hello"u8);});});// Monkey patching — add methods after class definitionclassA.DefineMethod(mrb.Intern("extra"u8),(_,self)=>{/* ... */});// Define module and includevarmoduleA=mrb.DefineModule(mrb.Intern("ModuleA"u8));mrb.DefineMethod(moduleA,mrb.Intern("module_method"u8),(_,self)=>123);mrb.IncludeModule(classA,moduleA);
A.greet#=> "hello"A.new.extraA.new.module_method#=> 123

Error handling & validation in C# methods

Inside C#-defined methods, you can raise Ruby exceptions and validate arguments:

varmyClass=mrb.DefineClass(mrb.Intern("MyClass"u8));mrb.DefineMethod(myClass,mrb.Intern("safe_divide"u8),(s,self)=>{s.EnsureArgumentCount(2,2);// require exactly 2 argumentsvara=s.GetArgumentAsIntegerAt(0);varb=s.GetArgumentAsIntegerAt(1);if(b==0){s.Raise(s.StandardErrorClass,"division by zero"u8);}returna/b;});
// Available validation helpersmrb.EnsureArgumentCount(min,max);// check argument countmrb.EnsureValueType(value,MRubyVType.Integer);// check value typemrb.EnsureBlockGiven(block);// check block is providedmrb.EnsureNotFrozen(value);// check object is not frozen// Raise Ruby exceptionsmrb.Raise(mrb.StandardErrorClass,"message"u8);mrb.Raise(mrb.ExceptionClass,mrb.NewString($"detail: {info}"));

To catch Ruby exceptions raised during execution on the C# side:

try{mrb.Send(obj,mrb.Intern("may_raise"u8));}catch(MRubyRaiseExceptionex){Console.WriteLine($"Ruby exception: {ex.Message}");}

Constants

// Define a constant under Object (global)mrb.DefineConst(mrb.Intern("MAX_SIZE"u8),1024);// Define a constant under a specific class/modulemrb.DefineConst(myClass,mrb.Intern("VERSION"u8),mrb.NewString("1.0"u8));// Check if a constant existsmrb.ConstDefinedAt(mrb.Intern("MAX_SIZE"u8));//=> truemrb.ConstDefinedAt(mrb.Intern("VERSION"u8),myClass);//=> truemrb.ConstDefinedAt(mrb.Intern("VERSION"u8),myClass,recursive:true);// search ancestors// Safe lookupif(mrb.TryGetConst(mrb.Intern("MAX_SIZE"u8),outvarconstValue)){// use constValue...}

Call Ruby Methods from C#

Use mrb.Send() to call Ruby methods from C#:

// Call a class methodvarclassA=mrb.GetConst(mrb.Intern("A"u8),mrb.ObjectClass);mrb.Send(classA,mrb.Intern("foo="u8),123);mrb.Send(classA,mrb.Intern("foo"u8));//=> 123// Call a global-scope method — use TopSelf as the receivermrb.Send(mrb.TopSelf,mrb.Intern("puts"u8),mrb.NewString("hello"u8));// Access instance variablesvarinstanceB=mrb.GetInstanceVariable(mrb.TopSelf,mrb.Intern("@b"u8));mrb.Send(instanceB,mrb.Intern("bar="u8),456);mrb.Send(instanceB,mrb.Intern("bar"u8));//=> 456// Resolve nested constantsvarclassC=mrb.Send(mrb.ObjectClass,mrb.Intern("const_get"u8),mrb.NewString("M::C"u8));
Ruby code assumed by the examples above
classAdefself.foo=@@foodefself.foo=(x)@@foo=xendendclassBattr_accessor:barend@b=B.newmoduleMclassCdefself.foo=999endend

Send with block / keyword arguments

// Send with a block (RProc)varproc=mrb.CreateProc(irep);mrb.Send(obj,mrb.Intern("each"u8),proc);// Send with keyword argumentsmrb.Send(obj,mrb.Intern("configure"u8),args:[],kargs:[new(mrb.Intern("verbose"u8),MRubyValue.True)],block:null);

Warning

Unity: The Send overload with params ReadOnlySpan<MRubyValue> is not supported because Unity's C# compiler does not support params ReadOnlySpan<T>. You must explicitly allocate an array instead:

// This does NOT compile in Unity:// mrb.Send(klass, sym, arg0, arg1);// Use an explicit array:mrb.Send(klass,sym,newMRubyValue[]{arg0,arg1});

The single-argument overload Send(self, methodId, arg0) works without this workaround.

Type conversion & introspection

The following examples use value, a, b as MRubyValue instances obtained from prior operations (e.g. Send, LoadBytecode).

// Convert values (calls Ruby's to_i / to_f / to_sym internally)longi=mrb.AsInteger(value);doublef=mrb.AsFloat(value);Symbols=mrb.AsSymbol(value);// Convert to string (Ruby's to_s / inspect)RStringstr=mrb.Stringify(value);// to_sRStringinspect=mrb.Inspect(value);// inspect// Class introspectionRClassklass=mrb.ClassOf(value);RStringname=mrb.ClassNameOf(value);// Type checking (Ruby's instance_of? / kind_of?)mrb.InstanceOf(value,mrb.StringClass);//=> true if exact classmrb.KindOf(value,mrb.ObjectClass);//=> true if class or ancestor// Equality and comparison (calls Ruby's == / <=>)mrb.ValueEquals(a,b);//=> true/falsemrb.ValueCompare(a,b);//=> -1, 0, 1// Check if method exists (Ruby's respond_to?)mrb.RespondTo(value,mrb.Intern("to_s"u8));//=> true

Instance variables / class variables / global variables

// Instance variablesmrb.SetInstanceVariable(obj,mrb.Intern("@name"u8),mrb.NewString("Alice"u8));varname=mrb.GetInstanceVariable(obj,mrb.Intern("@name"u8));mrb.RemoveInstanceVariable(obj,mrb.Intern("@name"u8));// Class variablesmrb.SetClassVariable(myClass,mrb.Intern("@@count"u8),0);varcount=mrb.GetClassVariable(myClass,mrb.Intern("@@count"u8));// Global variables (the symbol name includes the leading `$`)mrb.SetGlobalVariable(mrb.Intern("$game_map"u8),gameMapValue);vargameMap=mrb.GetGlobalVariable(mrb.Intern("$game_map"u8));// returns nil if undefinedmrb.GlobalVariableDefined(mrb.Intern("$game_map"u8));//=> truemrb.RemoveGlobalVariable(mrb.Intern("$game_map"u8),out_);

Clone / Dup / Freeze

// Clone (deep copy with singleton class)varcloned=mrb.CloneObject(value);// Dup (shallow copy)varduped=mrb.DupObject(value);// Freeze an object (RObject level)varstr=mrb.NewString("immutable"u8);str.MarkAsFrozen();str.IsFrozen//=> true

MRubyValue

MRubyValue represents a Ruby value. It is returned from methods like LoadBytecode, Execute, Send, etc.

value.IsNil//=> true if `nil`value.IsInteger //=> true if integer
value.IsFloat //=> true if float
value.IsSymbol //=> true if Symbol
value.IsObject //=> true if any allocated object typevalue.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 representationvalue.As<RArray>()//=> get as internal Array representation
value.As<RHash>()//=> get as internal Hash representation// pattern matching
if (value.ObjectisRStringstr){// ...}switch(value){case{IsInteger:true}:// ...break;case{Object:RStringstr}:// ...break;}// Creating MRubyValuevarintValue=newMRubyValue(100);varfloatValue=newMRubyValue(1.234f);varobjValue=newMRubyValue(str);// Implicit conversions are available — useful when passing argumentsmrb.Send(obj,mrb.Intern("method"u8),42);// int → MRubyValuemrb.Send(obj,mrb.Intern("method"u8),3.14);// double → MRubyValuemrb.Send(obj,mrb.Intern("method"u8),true);// bool → MRubyValuemrb.Send(obj,mrb.Intern("method"u8),sym);// Symbol → MRubyValuemrb.Send(obj,mrb.Intern("method"u8),rstring);// RObject → MRubyValue// Static constantsMRubyValue.Nil// Ruby nilMRubyValue.True // Ruby true
MRubyValue.False // Ruby false// Boolean / truthinessvalue.BoolValue //=> C# bool
value.Truthy //=> true unless nil or false (Ruby semantics)
value.Falsy //=> true if nil or false

Symbol/String

The string representation within mruby is UTF-8. Therefore, to generate a Ruby string from C#, Utf8StringInterpolation is used internally.

// Create string literal.varstr1=mrb.NewString("HOGE HOGE"u8);// use u8 literal (C# 11 or newer)varstr2=mrb.NewString($"FOO BAR");// use string interpolationvarx=123;varstr3=mrb.NewString($"x={x}");// wrap MRubyValue..MRubyValuestrValue=str1;

There is a concept in mruby similar to String called Symbol. Like String, it is created using UTF-8 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 literalvarsym1=mrb.Intern("sym");// Create a symbol from string interpolationvarx=123;varsym2=mrb.Intern($"sym{x}");// Symbol to UTF-8 bytesmrb.NameOf(sym1);//=> "sym"u8mrb.NameOf(sym2);//=> "sym123"u8// Create a symbol from a stringvarsym2=mrb.AsSymbol(mrb.NewString($"hoge"));

Note

Both Intern("str") and Intern("str"u8) are valid, but the u8 literal is faster. We recommend using the u8 literal whenever possible.

RString also provides methods for in-place manipulation and direct UTF-8 byte access:

varstr=mrb.NewString("hello"u8);// UTF-8 byte accessReadOnlySpan<byte>bytes=str.AsSpan();// raw UTF-8 bytes// In-place modificationstr.Concat(" world"u8);// Append bytesstr.Upcase();// "HELLO WORLD"str.Downcase();// "hello world"str.Capitalize();// "Hello world"str.Chomp();// Remove trailing newlinestr.Chop();// Remove last character

Array/Hash

RArray and RHash are the internal representations of Ruby's Array and Hash.

// Create arrayvararray=mrb.NewArray(3);// with capacityvararray2=mrb.NewArray(1,2,3);// Access elements (supports negative indices)varfirst=array2[0];//=> 1varlast=array2[-1];//=> 3// Add elementsarray.Push(100);array.Push(200);// Get lengtharray.Length//=> 2// Iterate over elementsforeach(varitemin array){Console.WriteLine(item.IntegerValue);}// Pop / Shiftif(array.TryPop(outvarpopped)){/* ... */}varshifted=array.Shift();// remove and return first element// Extract RArray from MRubyValuevarvalue=mrb.LoadBytecode(bytecode);// returns MRubyValuevararr=value.As<RArray>();
// Create hashvarhash=mrb.NewHash();// Set values (key can be any MRubyValue — Symbol, String, Integer, etc.)hash[mrb.Intern("name"u8)]=mrb.NewString("Alice"u8);hash[mrb.Intern("age"u8)]=30;// Get valuesvarname=hash[mrb.Intern("name"u8)];// Check existencehash.ContainsKey(mrb.Intern("name"u8));//=> truehash.TryGetValue(mrb.Intern("age"u8),outvarage);//=> true, age = 30// Get lengthhash.Length//=> 2// Iterate over key-value pairsforeach(varkvinhash){// kv.Key, kv.Value are MRubyValue}// Deletehash.TryDelete(mrb.Intern("age"u8),outvardeleted);// Extract RHash from MRubyValuevarhashValue=mrb.LoadBytecode(bytecode);varh=hashValue.As<RHash>();

Embedded custom C# data into MRubyValue

You can stuff any C# object into an MRubyValue via RData. The RData.Data property accepts any object and can be freely get/set from C#.

This is useful when calling C# functionality from Ruby methods defined in C#.

classYourCustomClass{publicstringValue{get;set;}}varcsharpInstance=newYourCustomClass{Value="abcde"};varmrb=MRubyState.Create();vardata=newRData(csharpInstance);mrb.SetConst(mrb.Intern("MYDATA"u8),mrb.ObjectClass,data);// Use custom data from Rubymrb.DefineMethod(mrb.ObjectClass,mrb.Intern("from_csharp_data"u8),(_,self)=>{vardataValue=mrb.GetConst(mrb.Intern("MYDATA"u8),mrb.ObjectClass);varcsharpInstance=dataValue.As<RData>().DataasYourCustomClass;// ...});

Embedded custom C# data with Ruby class

// Instances of classes that specify `MRubyVType.CSharpData` have `self` as RData.varyourClass=mrb.DefineClass(mrb.Intern("MyCustomClass"u8),mrb.ObjectClass,MRubyVType.CSharpData);// Define custom `initialize` with C# datamrb.DefineMethod(yourClass,mrb.Intern("initialize"u8),(s,self)=>{if(self.ObjectisRDatax){x.Data=newYourCustomClass{Value="abcde"};}returnself;});// Use custom C# datamrb.DefineMethod(yourClass,mrb.Intern("foo_method"u8),(s,self)=>{if(self.ObjectisRData{Data:YourCustomClasscsharpInstance}){// Use C# data..csharpInstance.Value="fghij";}// ...});

Optional Classes (opt-in)

Some classes are not registered by MRubyState.Create() so that embedding hosts only pay for the surface area they actually need. Enable them explicitly per MRubyState instance:

PackageEnable withAdds
ChibiRuby (built-in)mrb.DefineRegexp()Regexp, MatchData, and regexp-related String methods (=~ / match / sub / gsub / scan / index)
ChibiRuby.NIOmrb.DefineIO()IO, File, IOError
ChibiRuby.NIOmrb.DefineHttp()HTTP module, HTTP::Response / Headers / Body, HTTP::Error / TimeoutError / ConnectionError
ChibiRuby.NIOmrb.DefineJson()JSON module (stdlib-compatible), #to_json on builtin types, JSON::ParserError / GeneratorError / NestingError

The IO / File, HTTP, and JSON modules live in the separate ChibiRuby.NIO NuGet package so that hosts embedding Ruby with no OS access (the default posture) don't ship file or network code at all. The package has no dependencies beyond ChibiRuby itself; the Define* methods become available as extension methods on MRubyState.

All calls are idempotent and must be made before compiling/running Ruby code that references the classes.

usingvarmrb=MRubyState.Create(x =>{x.DefineRegexp();// built into ChibiRubyx.DefineIO();// requires ChibiRuby.NIOx.DefineHttp();// requires ChibiRuby.NIOx.DefineJson();// requires ChibiRuby.NIO});

Regexp

Once enabled, both literal /.../ regular expressions and Regexp.new are available, along with MatchData and the regexp-related String methods.

usingvarmrb=MRubyState.Create(x =>{x.DefineRegexp();});usingvarcompiler=MRubyCompiler.Create(mrb);compiler.LoadSourceCode(""" re = /(\w+)@(\w+\.\w+)/ if m = "contact: alice@example.com".match(re) puts m[0] # => "alice@example.com" puts m[1] # => "alice" puts m[2] # => "example.com" end # case-insensitive flag via Regexp.new Regexp.new("hello", Regexp::IGNORECASE) =~ "HELLO" # => 0 # sub / gsub / scan "foo bar foo".gsub(/foo/, "baz") # => "baz bar baz" "a1 b2 c3".scan(/[a-z]\d/) # => ["a1", "b2", "c3"] """u8);

IO / File

File.read / File.write provide a quick round-trip; File.open returns an IO/File instance for streaming reads and writes. IOError is raised when operating on a closed handle.

usingvarmrb=MRubyState.Create(x =>{x.DefineIO();});usingvarcompiler=MRubyCompiler.Create(mrb);compiler.LoadSourceCode(""" File.write("/tmp/greeting.txt", "hello world") puts File.read("/tmp/greeting.txt") # => "hello world" puts File.exist?("/tmp/greeting.txt") # => true f = File.open("/tmp/greeting.txt") begin puts f.read ensure f.close end """u8);

When a FiberScheduler is installed, IO/File reads and writes route through MRubyFiberScheduler.Await so the host thread isn't blocked on stream I/O. See Defining async Ruby methods with Await for the same mechanism applied to host-defined methods.

HTTP

An HTTP client built on .NET HttpClient, with an API inspired by httpx. One-shot verb methods take per-request keyword options and return an HTTP::Response.

usingvarmrb=MRubyState.Create(x =>{x.DefineHttp();});usingvarcompiler=MRubyCompiler.Create(mrb);compiler.LoadSourceCode(""" resp = HTTP.get("https://example.com", headers: { "user-agent" => "myapp/1.0" }) resp.status # => 200 resp.headers["content-type"] resp.body.to_s HTTP.post("https://example.com/things", form: { "name" => "alice" }) HTTP.get("https://example.com/search", params: { "q" => "ruby" }, timeout: 5) HTTP.get("https://example.com", basic_auth: ["user", "pass"]) """u8);

Error behavior follows HttpClient: 4xx/5xx responses do not raise — check resp.success? / resp.error?, or call resp.ensure_success_status! to raise HTTP::Error explicitly. Transport failures raise HTTP::ConnectionError; timeouts raise HTTP::TimeoutError.

When a FiberScheduler is installed, requests issued inside a fiber park the fiber instead of blocking the host thread — the same mechanism as IO/File above.

JSON

A JSON module compatible with Ruby's bundled json stdlib, implemented with a dependency-free UTF-8 parser/writer.

usingvarmrb=MRubyState.Create(x =>{x.DefineJson();});usingvarcompiler=MRubyCompiler.Create(mrb);compiler.LoadSourceCode(""" obj = JSON.parse('{"name":"alice","tags":["admin"]}') obj["name"] # => "alice" JSON.parse('{"a":1}', symbolize_names: true) # => {a: 1} JSON.generate({ "x" => 1, "y" => [true, nil] }) # => '{"x":1,"y":[true,null]}' JSON.pretty_generate({ "a" => 1 }) { a: 1 }.to_json # => '{"a":1}' """u8);

JSON numbers that fit Int64 parse as Integer; larger ones fall back to Float. During encoding, obj.to_json is dispatched for non-builtin values, so user-defined classes can serialize themselves by implementing to_json.

When both DefineHttp() and DefineJson() are enabled, they compose: HTTP.post(url, json: obj) encodes the request body via JSON.generate (with Content-Type: application/json), and resp.json parses the response body lazily with caching.

Fiber (Coroutine)

ChibiRuby supports Ruby Fibers, which are lightweight concurrency primitives that allow you to pause and resume code execution. In addition to standard Ruby Fiber features, ChibiRuby provides seamless integration with C#'s async/await pattern.

Basic Fiber Usage

usingChibiRuby;usingChibiRuby.Compiler;// Create state and compilervarmrb=MRubyState.Create();varcompiler=MRubyCompiler.Create(mrb);// Define a fiber that yields valuesvarcode=""" Fiber.new do |x| Fiber.yield(x * 2) Fiber.yield(x * 3) x * 4 end """u8;// Load the Ruby code as a Fiberusingvarcompilation=compiler.Compile(code);varfiber=mrb.Execute(compilation.ToIrep()).As<RFiber>();// Resume the fiber with initial valuevarresult1=fiber.Resume(10);// => 20varresult2=fiber.Resume(10);// => 30varresult3=fiber.Resume(10);// => 40 (final return value)// Check if fiber is still alivefiber.IsAlive// => false

If you want to execute arbitrary code snippets as fibers, do the following.

varcode=""" x = 1 y = 2 Fiber.yield (x + y) * 100 Fiber.yield (x + y) * 200"""u8;varfiber=compiler.LoadSourceCodeAsFiber(code);// `LoadSourceCodeAsFiber` is the same as:// using var compilation = compiler.Compile(code);// var proc = mrb.CreateProc(compilation.ToIrep());// var fiber = mrb.CreateFiber(proc);fiber.Resume();//=> 300fiber.Resume();//=> 600

Async/Await Integration

ChibiRuby provides unique C# async integration features for working with Fibers:

// Wait for fiber to terminatevarcode=""" Fiber.new do |x| Fiber.yield Fiber.yield "done" end """u8;usingvarcompilation=compiler.Compile(code);varfiber=mrb.Execute(compilation.ToIrep()).As<RFiber>();// Start async wait before resumingvarterminateTask=fiber.WaitForTerminateAsync();// Resume the fiber multiple timesfiber.Resume();fiber.Resume();fiber.Resume();// Wait for completionawaitterminateTask;Console.WriteLine("Fiber has terminated");

You can consume fiber results as async enumerable:

varcode=""" Fiber.new do |x| 3.times do |i| Fiber.yield(x * (i + 1)) end end """u8;usingvarcompilation=compiler.Compile(code);varfiber=mrb.Execute(compilation.ToIrep()).As<RFiber>();// Process each yielded value asynchronouslyawaitforeach(varvalueinfiber.AsAsyncEnumerable()){Console.WriteLine($"Yielded: {value.IntegerValue}");}

ChibiRuby supports multiple consumers waiting for fiber results simultaneously:

usingvarcompilation=compiler.Compile(code);varfiber=mrb.Execute(compilation.ToIrep()).As<RFiber>();// Create multiple consumersvarconsumer1=Task.Run(async()=>{while(fiber.IsAlive){varresult=awaitfiber.WaitForResumeAsync();Console.WriteLine($"Consumer 1 received: {result}");}});varconsumer2=Task.Run(async()=>{while(fiber.IsAlive){varresult=awaitfiber.WaitForResumeAsync();Console.WriteLine($"Consumer 2 received: {result}");}});// Resume fiber and both consumers will receive the resultsfiber.Resume(10);fiber.Resume(20);fiber.Resume(30);awaitTask.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:

varcode=""" Fiber.new do |x| Fiber.yield(x) raise "Something went wrong" end """u8;usingvarcompilation=compiler.Compile(code);varfiber=mrb.Execute(compilation.ToIrep()).As<RFiber>();// First resume succeedsvarresult1=fiber.Resume(10);// => 10// Second resume will throwtry{fiber.Resume();}catch(MRubyRaiseExceptionex){Console.WriteLine($"Ruby exception: {ex.Message}");}// Async wait will also propagate the exceptionvarwaitTask=fiber.WaitForResumeAsync();try{fiber.Resume();awaitwaitTask;}catch(MRubyRaiseExceptionex){Console.WriteLine($"Async exception: {ex.Message}");}

yield/resume from C#

It is possible to resume/yield from a method defined in C#.

mrb.DefineMethod(mrb.FiberClass,mrb.Intern("resume_by_csharp"u8),(state,self)=>{returnself.As<RFiber>().Resume();});
fiber=Fiber.newdo3.timesdoFiber.yieldendendfiber.resume_by_csharp

Define async Ruby method (FiberScheduler)

Default behavior (no scheduler)

By default, no scheduler is installed. In this mode:

  • Kernel#sleep calls Thread.Sleep and blocks the calling thread.
  • Thread.pass is a no-op.
  • IO / File reads & writes (when registered via DefineIO()) use synchronous Stream.Read / Write.
  • Fiber#resume / Fiber.yield work exactly as in CRuby.
  • The VM is fully synchronous from C#'s perspective.
varmrb=MRubyState.Create();varcompiler=MRubyCompiler.Create(mrb);// Blocks the calling thread for 1 second.compiler.LoadSourceCode("sleep 1; :done"u8);

This is the right default for CLI tools and tests that don't need cooperative scheduling.

With a scheduler installed

mrb.useFiberScheduler(...) swaps blocking primitives for cooperative ones. When a non-root fiber calls sleep, the VM yields back to its caller instead of blocking; the scheduler arranges for the fiber to be resumed when the deadline expires.

usingvarmrb=MRubyState.Create(x =>{x.UseFiberScheduler();});usingvarcompiler=MRubyCompiler.Create(mrb);varfiber=compiler.LoadSourceCodeAsFiber(""" sleep 0.05 // -> same as `await Task.Delay(TimeSpan.FromSeconds(0.05))` Thread.pass // -> same as `await Task.Yield()` :done """u8);fiber.Resume();awaitfiber.WaitForTerminateAsync();// `sleep`, `pass` did not block any thread; the scheduler wakes the fiber.

Note

The root fiber still falls back to Thread.Sleep, even when a scheduler is installed — there is no caller to yield to. The scheduler hooks only fire from inside Fiber.new { ... } bodies (including LoadSourceCodeAsFiber).

Defining async Ruby methods with Await

Await(async mrb => …) is the high-level convenience for bridging an async C# lambda into a Ruby method. The body runs starting on the caller (VM) thread; after the first await, thread routing is determined by the ambient SynchronizationContext at the await site — the scheduler doesn't install any dispatch of its own.

usingvarmrb=MRubyState.Create(x =>{x.UseFiberScheduler();});// Defines `await_http(url)` — fetches a URL without blocking the VM.mrb.DefineMethod(mrb.KernelModule,mrb.Intern("await_http"u8),(state,_)=>{varurl=state.GetArgumentAsStringAt(0).ToString();state.FiberScheduler!.Await(async mrb =>{usingvarclient=newHttpClient();varbody=awaitclient.GetStringAsync(url);returnmrb.NewString(body);});returnMRubyValue.Nil;// unreached on the async path — Ruby observes body's return});varfiber=compiler.LoadSourceCodeAsFiber(""" body = await_http("https://example.com") puts body.length """u8);fiber.Resume();awaitfiber.WaitForTerminateAsync();

Body contract:

  • The body receives (MRubyState mrb). There is also an allocation-free overload Await<TState>(TState state, Func<MRubyState, TState, ValueTask<MRubyValue>> body) — pass closed-over data as state plus a static lambda to avoid closure allocation on hot paths.
  • Body returns ValueTask<MRubyValue>; the value is delivered to Ruby as the apparent return of the host MRubyMethod. The host method must still end with return MRubyValue.Nil; — that return is unreached on the async path.
  • OperationCanceledException from body → fiber resumes with nil (CRuby fiber-scheduler convention; the OCE's own token is preserved).
  • Any other exception → delivered as a Ruby exception, catchable by surrounding begin/rescue.

To time out, wire a CancellationTokenSource into body via closure:

state.FiberScheduler!.Await(async mrb =>{usingvarcancellationSource=newCancellationTokenSource(TimeSpan.FromSeconds(5));usingvarclient=newHttpClient();varbody=awaitclient.GetStringAsync(url,cancellationSource.Token);returnmrb.NewString(body);});

Low-level: Suspend + FiberContinuation

When the resume signal arrives from somewhere other than a single async lambda — an external event source, a Subject/IObservable, a callback you don't control — use Suspend(). It parks the current fiber and returns a FiberContinuation handle that arbitrary code can call Resume(value) / SetCancelled() / SetException(ex) on.

mrb.DefineMethod(mrb.KernelModule,mrb.Intern("await_event"u8),(state,_)=>{varcontinuation=state.FiberScheduler!.Suspend();// yields the fiber internallymyEventSource.Once(payload =>{continuation.Resume(state.NewString(payload));// arbitrary callback site});returnMRubyValue.Nil;});

Mechanics:

  • Suspend() registers the parking state, then calls Fiber.yield to unwind the VM back to the caller of Resume. The returned FiberContinuation captures the parked fiber.
  • continuation.Resume(value) runs fiber.Resume(value). The settle path uses an atomic TryRemove on the park slot before completing the underlying TaskCompletionSource, so the fiber can re-park (next sleep, next Suspend) inside the synchronous continuation without hitting "already parked".
  • continuation.SetCancelled() resumes the fiber with nil (cancellation semantics).
  • continuation.SetException(ex) injects ex as a Ruby exception on resume (catchable by rescue).
  • Settling is one-shot — the first of Resume/SetCancelled/SetException wins; subsequent calls are no-op.
  • The fiber is yielded insideSuspend — there's no "arrange-Resume-before-Suspend" race window.

Tip

Prefer Await when the body fits as a single async lambda. Drop to Suspend only when you need to hand the continuation to external code that completes asynchronously without an awaitable surface.

Unity (UnityFiberScheduler)

The ChibiRuby.Unity package ships a player-loop driven scheduler. Kernel#sleep and Thread.pass route through Unity's Awaitable (WaitForSecondsAsync / NextFrameAsync) instead of Task.Delay / Task.Yield, so fibers resume on the main Unity thread at the next frame boundary. Parking uses AwaitableCompletionSource<T> (pool-backed) rather than TaskCompletionSource<T> to keep allocations down.

usingChibiRuby;usingChibiRuby.Unity;varmrb=MRubyState.Create();mrb.UseUnityFiberScheduler();// == mrb.UseFiberScheduler(new UnityFiberScheduler())

When the scheduler is Disposed (e.g. on scene unload / MonoBehaviour.OnDestroy), any in-flight WaitForSecondsAsync / NextFrameAsync is cancelled via the base DisposalToken, parked fibers are resumed with nil, and the scheduler's own dictionary of AwaitableCompletionSource entries is drained.

Install via the Unity Package Manager (Window > Package Manager > + > Add package from git URL):

https://github.com/hadashiA/ChibiRuby.git?path=src/ChibiRuby.Unity/Assets/ChibiRuby.Unity#1.2.2

See UnityFiberScheduler.cs for the implementation.

Custom Schedulers (subclassing)

MRubyFiberScheduler is a concrete class — host customization is done by subclassing and overriding KernelSleep / Yield / Suspend as needed. The default implementations cover most hosts; subclass only when you need different timer behavior, a custom yield primitive, or an alternative parking mechanism (e.g. AwaitableCompletionSource as in UnityFiberScheduler).

Contract:

  • All wait hooks yield internally (CRuby Fiber::Scheduler convention). Override implementations must call fiber.Yield() before returning — the default impls do this via AwaitSuspend.
  • No Ruby re-entrancy. Hooks must not call back into Ruby code (no state.Send, no synchronous fiber.Resume). fiber.Yield() is the one expected call into the VM — it unwinds rather than invokes.
  • Exceptions are deliverable to Ruby. Any exception inside Await's body is wrapped and delivered as a Ruby exception on resume; surrounding begin/rescue catches it.
  • No double-parking. A fiber is only parked under one wait at a time. Suspend throws InvalidOperationException on a re-park; subclass overrides should preserve this.
  • Honor DisposalToken. Link your own CancellationTokens with DisposalToken so in-flight waits unwind cleanly when the scheduler is disposed.

See MRubyFiberScheduler.cs for the reference implementation and UnityFiberScheduler.cs for a complete subclass example.

Debugger

demo

Attach a DAP-compatible editor to a running Unity (or any .NET) host and step through Ruby code: line breakpoints, step in/over/out, locals view, expression evaluation. The debug server is embedded in your host process — no separate adapter to ship.

Host setup

By executing MRubyDapServer.StartAsync, the Debug Adapter Protocol TCP server begins listening. Any DAP-compatible editor can perform an Attach to the process in this state.

usingChibiRuby;usingChibiRuby.Compiler;usingChibiRuby.Debugger.Dap;varmrb=MRubyState.Create();varcompiler=MRubyCompiler.Create(mrb);// Start the DAP server on loopback:4711. Pass `bindAddress: IPAddress.Any`// to allow attaches from another machine on your LAN (iPhone, etc.).usingvardap=newMRubyDapServer(mrb,compiler,port:4711);_=Task.Run(async()=>awaitdap.StartAsync());// Compile with an absolute path so the editor can navigate to the source.usingvarcompilation=compiler.CompileFile("/abs/path/to/game.rb");mrb.LoadBytecode(compilation.AsBytecode());

End-to-end demos: sandbox/SampleDebuggerEmbedded (dotnet console host) and src/ChibiRuby.Unity/Assets/SampleBehaviour.cs (Unity MonoBehaviour).

Editor setup

VSCode and Zed need a small extension to register the chibiruby debug type; Rider needs the LSP4IJ plugin from the JetBrains Marketplace. Pick your editor:

VSCode
  1. Install the extension — download chibiruby-debugger-*.vsix from the latest release, then install it via either:

    • VSCode UI: Extensions panel → ... menu → Install from VSIX… → pick the downloaded file.
    • CLI: code --install-extension chibiruby-debugger-*.vsix.

    (Contributors can also dev-install: open editor-extensions/vscode in VSCode and press F5 to launch an Extension Development Host.)

  2. Create launch.json in your project (.vscode/launch.json):

    {
    "version": "0.2.0",
    "configurations": [
    {
    "type": "chibiruby",
    "request": "attach",
    "name": "Attach to ChibiRuby",
    "host": "127.0.0.1",
    "port": 4711
    }
    ]
    }
  3. Start the host so MRubyDapServer is listening, then press F5 in VSCode.

Rider / IntelliJ
  1. Install LSP4IJ (Settings → Plugins → Marketplace → search "LSP4IJ" → Install). Restart the IDE if prompted. The plugin provides both LSP and DAP integration.

  2. Add a Debug Adapter Protocol run configuration:

    • Run → Edit Configurations…+Debug Adapter Protocol.
    • In the Server tab, click create a new server.
    • In the dialog: pick a name (e.g. ChibiRuby DAP), set Connection type to TCP socket, Host = 127.0.0.1, Port = 4711. Save.
    • Back in the run configuration, select the server you just created from the dropdown.

  3. Start the host, then run the configuration in Debug mode.

Zed
  1. Prerequisites (one-time):
    • Rust toolchain.
    • wasm32-wasip2 target: rustup target add wasm32-wasip2.
  2. Dev-install the extension:
    • In Zed, open the command palette (cmd-shift-p).
    • Run zed: install dev extension and pick the editor-extensions/zed folder.
    • Zed compiles the WASM blob and registers the adapter.
  3. Add .zed/debug.json to your workspace:
    [
    {
    "label": "Attach to ChibiRuby",
    "adapter": "chibiruby",
    "request": "attach",
    "tcp_connection": { "host": "127.0.0.1", "port": 4711 }
    }
    ]
  4. Start the host, then open Zed's debug panel (cmd-shift-d), pick Attach to ChibiRuby, and run.

Setting breakpoints

There are two ways to pause execution: set a breakpoint from the editor, or write binding.break directly in your Ruby code.

From the editor

Once host + editor are wired:

  1. Open the .rb file in the editor.
  2. Click the gutter next to the line you want to pause at — a red breakpoint marker appears.
  3. Run the host. Execution stops at the breakpoint; the editor surfaces the call stack and locals.
  4. Use the editor's debug controls (Continue / Step Over / Step In / Step Out) and the REPL pane (variables view + expression evaluation) as usual.

From code (binding.break)

Write binding.break at the line you want to pause at — same API as ruby/debug:

defupdatehp=@hpbinding.break# execution suspends here; inspect `hp` from the editorhp - 1end

binding.b and debugger are also available as aliases:

binding.b# same as binding.breakdebugger# same as binding.break on the caller's frame

When the VM hits binding.break, the thread suspends until a DAP client attaches and resumes it — handy for stopping at a precise point before you've had a chance to attach the editor. Once a client has attached and disconnected, later binding.break calls become no-ops so the host doesn't hang after the editor goes away.

Note

binding.break requires the debugger to be installed on the state (MRubyDapServer does this automatically). Without it, calling binding.break raises RuntimeError.

Note

Please ensure that ChibiRubyCompiler passes the filename when compiling Ruby. APIs such as CompileFile or Unity's ScriptedImporter resolve the filename automatically. When compiling without going through a file, such as with Compile(bytes), the debugger will not work unless the file path is passed as an additional argument.

Serializer

Using the MRuby.Serializer package enables conversion between MRubyValue and C# objects.

// Deserialize (MRubyValue -> C#)MRubyValueresult1=mrb.LoadSourceCode("111 + 222");MRubyValueSerializer.Deserialize<int>(result1,mrb);//=> 333MRubyValueresult2=mrb.LoadSourceCode("'hoge'.upcase");MRubyValueSerializer.Deserialize<string>(result2,mrb);//=> "HOGE"
// Serialize (C# -> MRubyValue)varintArray=newint[]{111,222,333};MRubyValuevalue=MRubyValueSerializer.Serialize(intArray,mrb);varmrubyArray=value.As<RArray>();mrubyArray[0]//=> 111mrubyArray[1]//=> 222mrubyArray[2]//=> 333
MRubyValuemrubyStringValue=MRubyValueSerializer.Serialize("hoge fuga",mrb);// Use the serialized value...mrb.Send(mrubyStringValue,mrb.Intern("upcase"u8));//=> MRubyValue("UPCASE")

Built-in supported types

The following C# types and MRubyValue type conversions are supported natively:

mrubyC#
Integerint, uint, long, ulong, short, ushort, byte, sbyte, char
Floatfloat, double, decimal
ArrayT, List<>, T[,], T[,,],
Tuple<...>, ValueTuple<...>,
, Stack<>, Queue<>, LinkedList<>, HashSet<>, SortedSet<>,
Collection<>, BlockingCollection<>,
ConcurrentQueue<>, ConcurrentStack<>, ConcurrentBag<>,
IEnumerable<>, ICollection<>, IReadOnlyCollection<>,
IList<>, IReadOnlyList<>, ISet<>
HashDictionary<,>, SortedDictionary<,>, ConcurrentDictionary<,>,
IDictionary<,>, IReadOnlyDictionary<,>
Stringstring, byte[]
SymbolEnum
nilT?, Nullable<T>

Unity-specific types

By introducing the following packages, serialization of Unity-specific types will also be supported.

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/ChibiRuby.git?path=src/ChibiRuby.Unity/Assets/ChibiRuby.Serializer.Unity#1.6.1
mrubyC#
[Float, Float]Vector2, Resolution
[Integer, Integer]Vector2Int
[Float, Float, Float]Vector3
[Int, Int, Int]Vector3Int
[Float, Float, Float, Float]Vector4, Quaternion, Rect, Bounds, Color
[Int, Int, Int, Int]RectInt, BoundsInt, Color32

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

[MRubyObject] attribute

Marking with [MRubyObject] enables bidirectional conversion between custom C# types and MRubyValue.

  • Converts C# type properties/fields into Ruby world Hash key/value pairs.
  • 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).
[MRubyObject]partialstructSerializeExample{// this is serializable memberspublicstringId{get;privateset;}publicintX{get;init;}publicintFooBar;[MRubyMember]publicintZ;// ignore members[MRubyIgnore]publicfloatFoo;}
// Deserialize (MRubyValue -> C#)varvalue=mrb.LoadSourceCode("{ id: 'aiueo', x: 1234, foo_bar: 4567, z: 8901 }");SerializeExampledeserialized=MRubyValueSerializer.Deserialize<SerializeExample>(value,mrb);deserialized.Id//=> "aiueo"deserialized.X //=> 1234
deserialized.FooBar //=> 4567
deserialized.Z //=> 8901
// Serialize (C# -> MRubyValue)varvalue=MRubyValueSerializer.Serialize(newSerializeExample{Id="aiueo",X=1234,FooBar=4567});varprops=value.As<RHash>();props[mrb.Intern("id"u8)]//=> "aiueo"props[mrb.Intern("x"u8)]//=> 1234props[mrb.Intern("foo_bar"u8)]//=> 4567

The list of properties specified by mruby is assigned to the C# member names that match the key names.

Note:

  • The names on the Ruby side are converted to CamelCase.
    • Example: Ruby's foo_bar maps to C#'s FooBar.
  • The values of C# enums are serialized as Ruby symbols.
    • Example: Season.Summer becomes Ruby's :summer.

You can change the member name specified from Ruby by using [MRubyMember("alias name")].

[MRubyObject]partialclassFoo{[MRubyMember("alias_y")]publicintY;}

Also, you can receive data from Ruby via any constructor by using the [MRubyConstructor] attribute.

[MRubyObject]partialclassFoo{publicintX{get;}[MRubyConstructor]publicFoo(intx){X=x;}}

Dynamic serialization

Specifying a dynamic type parameter allows conversion to C# Array/Dictionary and primitive types.

vararray=mrb.NewArray();array.Push(123);varresult=MRubyValueSerializer.Deserialize<dynamic>(array,mrb);((object[])result).Length//=> 1((object[])result)[0]//=> 123

Custom Formatter

You can also customize the conversion of any C# type to an MRubyValue.

// custom type examplestructVector3{publicintX;publicintY;publicintZ;}
// Implement `IMRubyValueFormatter`classCustomVector3Formatter:IMRubyValueFormatter<Vector3>{publicstaticreadonlyCustomVector3FormatterInstance=new();publicMRubyValueSerialize(Vector3value,MRubyStatemrb,MRubyValueSerializerOptionsoptions){vararray=mrb.NewArray();array.Push(value.X);array.Push(value.Y);array.Push(value.Z);returnarray;}publicVector3Deserialize(MRubyValuevalue,MRubyStatemrb,MRubyValueSerializerOptionsoptions){// validationMRubySerializationException.ThrowIfTypeMismatch(value,MRubyVType.Array);MRubySerializationException.ThrowIfNotEnoughArrayLength(value,3);vararray=value.As<RArray>();returnnewVector3{X=array[0].IntegerValue,Y=array[1].IntegerValue,Z=array[2].IntegerValue,}}}

To set a custom formatter, specify options as an argument to MRubyValueSerializer.

Specify the enumeration of Formatter and Formatter's Resolver instances. StandardResolver supports the default behavior, so specify this along with additional formatters.

// Create a new formatter resolver.varresolver=CompositeResolver.Create([CustomVector3Formatter.Instance],[StandardResolver.Instance]);varoptions=newMRubyValueSerializerOptions{Resolver=resolver,};varvalue=mrb.LoadSourceCode("[111, 222, 333]");Vector3deserialized=MRubyValueSerializer.Deserialize<Vector3>(value,mrb,options);deserialized.X//=> 111deserialized.Y //=> 222
deserialized.Z //=> 333

License

MIT

About

A new mruby virtual machine implemented in C#.

Topics

Resources

Stars

236 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages