しまねソフト研究開発センター(略称 ITOC)にいます、東です。
この記事は、mrubyファミリ (組み込み向け軽量Ruby) Advent Calendar 2024 の11日目に投稿しています。
今回は、今日(2024/12/11)現在の mruby/c の仕様みたいなもの、結果的に仕様になったもの、できること、できないこと、等をだらだらと書いていこうと思います。なお、執筆にあたり、パーフェクトRuby 改訂2版 の章立てを、大いに参考にさせてもらっています。
設計思想
- 実行時のメモリ(RAM)使用量を十分小さく保つ
- 機能的にメモリを多く使用しないと実現できない機能は、使用頻度などを総合的に考えて、実装しないかまたは代替手段を実装する
- 実行速度の考慮はするが、プライオリティーは メモリ使用量 > 実行速度 とする
- VMのみの実装とし、前段(コンパイラ)は mruby と共用する
- 複数のrubyプログラムを、同時実行することができる
- 実装を可能な限り簡潔に保つ
mruby/c の実行環境として、ワンチップマイコンを想定しています。そのため、32〜64KBのRAM容量で、ある程度実用的なプログラムを動作させることが必須と考えています。
また、九州工業大学との共同研究でもあり、教育用途として実装を可能な限り簡潔に保ちたいと思っています。(が、C言語の特性上そうも言ってられない箇所が随所に・・・)
class
v1.1から、クラス定義をサポートしています。
インスタンス変数
v1.2から、インスタンス変数をサポートしています。
クラス変数
サポートしていません。
class Class1
@@class_var # not supported.
end
クラスメソッド、インスタンスメソッド
クラスメソッドと、インスタンスメソッドの区別はありません。ただし、self はちゃんと違うので、それを使えば区別可能です。
class Class1
def method1
if self == Class1
puts "Class method"
else
puts "Instance method"
end
end
end
Class1.method1 # => "Class method"
Class1.new.method1 # => "Instance method"
この仕様が正しいかは、議論の余地があります。つまり将来変更の可能性があります。
しかし、クラスメソッドとインスタンスメソッドで同じ名前のメソッドを定義することは、利用者にとってほとんどの場合混乱の元になると考えられ、そういったこと自体をすべきでは無いとも言えます。
継承
ユーザー定義クラスの継承を、サポートしています。
class Class1
end
class SubClass1 < Class1
end
組み込みクラスの継承はできません。
class MyArray < Array # Not supported
end
注:組み込みクラスへのメソッドの追加はできます
class Array
def my_method
end
end
クラスのネスト
v3.3以降、問題なく動作します。
v3.2までは、2レベルまでサポートしており 3レベル以上のネストは、実行できるが予期せぬ挙動を示すかもしれません。
class Cls1 # OK
class Cls2 # OK
class Cls3 # not supported
end
end
end
module
現リリースバージョンではサポートしていません。
master ブランチへは module サポートコードがマージされましたので、基本的な動作は可能です。
include
サポートしています。
名前空間としての利用
サポートしています。
module Module1
class Class1
...
obj = Module1::Class1.new
定数
サポートしています。
グローバル変数
サポートしています。
多重代入
基本的な構文は動きます。
a,b = 1,2
a,b = [1,2,3]
a,*b = [1,2,3]
右辺を動的に分離しなければならない場合で、左辺の前側にアスタリスクを付けるパターンのみ、唯一エラーになることがわかっています。使用頻度が非常に低いものとして判断し、サポートしていません。
ary = [1,2,3]
*a,b = ary # Not supported
例外処理
v2.1 からサポートしています。
ただし、rescue, ensure, retry を複雑にからめて使うと、CRubyと非互換の動作をする場合があるようです。これは、mrubyコンパイラ側と協調して今後修正します。
例外の元となった例外
サポートしていません。
rescue => ex
ex.cause # Not supported
メソッド定義
メソッドの引数の数
255個までです。
ただし、MAX_REGS_SIZE プリプロセッサ定数に上限を制限されます。
省略可能な仮引数
サポートしています。
def greet(name, message = "Hi")
可変長引数
サポートしています。
def greet(name, *messages)
第2通常パラメータ
この例のa2です。サポートしていません。
def func1(a1,b=22,*c,a2)
キーワード引数
v3.2からサポートしています。
ブロック
v2.1 からサポートしています。
別名 (alias)
サポートしています。
メソッドオーバライド
サポートしています。
特異メソッド
サポートしていません。
組み込みクラス
mruby/c の組み込みクラスです。
各クラスは、CRuby, mruby のクラスが持っているメソッド全てを実装しているわけではありません。また、可能な限り CRuby, mruby とメソッドの仕様を合致させるように努力していますが、完全互換にできていないものもあります。
ここでは、互換性に注意が必要な箇所に的を絞って解説します。
クラスツリー
Object
NilClass
TrueClass
FalseClass
Integer
Float
String
Math
Proc
Array
Hash
Range
Exception
NoMemoryError
NotImplementedError
StandardError
ArgumentError
IndexError
IOError
NameError
NoMethodError
RangeError
RuntimeError
TypeError
ZeroDivisionError
Integer
C言語の整数型と対応づけられた型で、コンパイル時に指定される MRBC_INT** プリプロセッサマクロによって、16bit, 32bit, 64bit のサイズが定義できます。C言語の整数型そのままの性質を受け継ぐため、オーバーフローやアンダーフローに対する保護はありません。
Float
C言語の浮動小数点型と対応づけられた型で、通常は倍精度小数点 (double) 型です。
String
エンコーディングの概念は無く、Ruby1.8互換とも言えます。
Hash
実装は Array クラスと共用しており、速度的には Hash ではありません ( O(n) ) 。
mruby/c では、ハッシュテーブルが必要になるほどオンメモリで多くのデータを扱うことは無いだろうという判断です。
each pair
Hash#each で以下の例のようにペアを受け取るコードは、現リリース全バージョンで意図通り動作しませんが、master では(動作速度と引き換えに)修正されています。
{a:1, b:2, c:3}.each do |pair|
p pair
end
Range
数値を使った通常の、(1..3)
のようなオブジェクトであれば、全く問題なく使用できます。
(1..nil)
といった nil 終端は、Rangeというよりも Range を利用するクラスが十分対応していない可能性があります。
('A'..'Z')
といった数値以外のレンジは、オブジェクトを作ることは問題なくできますが、内包するオブジェクトが対応していないことによって意図した結果が得られない可能性が高いです。
クロージャ
不完全なので、意図的にクロージャを使ったプログラミングを行う事はできません。
def create_proc
str = "from create proc"
Proc.new { str }
end
p create_proc.call # Error. 実行時保護もまだ不十分なので、そのうち頑張る
これは、mruby/c の省メモリ戦略の一つである「実行時コンテキストを保存せず callinfo で代用する」という根幹の部分からくる制限ですので、おそらく今後も完全なクロージャを実装できることは無いと思われます。
同時実行機能
簡易なスケジューラを同梱しており、複数の Ruby プログラムを同時にタイムスライスで実行することができます。
see) sample_c/sample_concurrent.c
データ共有
グローバル変数は、全てのプログラムで共有しており、これを使ってプログラム間でデータ共有できます。
排他機能
Mutex クラスを持っており、それを使います。
$mutex1 = Mutex.new
$mutex1.lock
...
$mutex1.unlock
その他の制限
その他、mruby/c にはできない事を落ち穂拾いします。
大域脱出 (catch, throw)
mruby, mruby/c とも、サポートしていません。
後判定ループ
mruby, mruby/c とも、サポートしていません。
i = 10
begin
puts i += 1
end while i < 3
require
mruby, mruby/c とも、サポートしていません。
(参考)
mrubyは mruby-require gem を導入すれば、その仕様の範囲内でサポートされます。
また、mruby/c VM を利用している PicoRuby も、require の仕組みを持っています。
undef
サポートしていません。
public, private, protected
サポートしていません。
C言語で書いたメソッドから、Rubyで書かれたメソッドを呼ぶ
できません。(できるようにしたいとは考えています)
よって、ユーザークラスの to_s, inspect メソッドなどが、以下のような場合に動作しません。
class MyClass
def to_s()
return "MyClass#to_s called."
end
end
obj1 = MyClass.new()
puts "obj1: #{obj1}"
CRuby実行結果
obj1: MyClass#to_s called.
mruby/c実行結果
obj1:
意図しない(と思われる)関数的メソッドの呼び出し
以下のコードでは、mruby, mruby/c とも、意図せず func1 が呼び出されます。これは public, private, protected が未サポートであることによって、意図しない呼び出しが行われます。
def func1
p "func1 called."
end
class Class1
end
Class1.new.func1 # => "func1 called." と表示
おわりに
書き連ねてみると、できない事がとても多いように感じられますが、実際には Rubyらしいプログラムがいくつも mruby/c を使って書かれています。繰り返しになりますが、ここに書いた仕様・制限は、今日(2024/12/11)現在のものですので、今後リクエストなどいただければ、実装もしくは制限が緩和されるかもしれません。一方、それによって実行時のメモリ使用量を十分小さく保つという最も重要な設計思想が守られなくなると本末転倒ですので、そういった場合は mruby のVMを使うという方向にシフトしてもらう方が良いと思います。