Skip to content

Latest commit

 

History

History
326 lines (235 loc) · 8.94 KB

File metadata and controls

326 lines (235 loc) · 8.94 KB

Limitations and Differences

The philosophy of mruby is to be a lightweight implementation of the Ruby ISO standard. These two objectives are partially contradicting. Ruby is an expressive language with complex implementation details which are difficult to implement in a lightweight manner. To cope with this, limitations to the "Ruby Compatibility" are defined.

This document is collecting these limitations.

Integrity

This document does not contain a complete list of limitations. Please help to improve it by submitting your findings.

Features provided by mrbgems

Many Ruby features that CRuby builds into its core are provided by mrbgems in mruby. Which features are actually available depends on which mrbgems are linked into the build. The default.gembox and stdlib.gembox cover the common cases, but a minimal build can omit familiar features such as Kernel#binding (provided by mruby-binding), Kernel#catch/throw (by mruby-catch), Enumerable extensions, Comparable, IO, regular expressions, and many more.

This is by design rather than a limitation per se. When porting Ruby code to mruby, a NoMethodError or NameError often means "the gem providing this feature is not linked in" rather than "mruby does not support it." Adding the relevant gem to the build configuration is usually enough.

Fiber execution can't cross C function boundary

mruby's Fiber is implemented similarly to Lua's co-routine. This results in the consequence that you can't switch context within C functions. Only exception is mrb_fiber_yield at return.

Array does not support instance variables

To reduce memory consumption Array does not support instance variables.

class Liste < Array
  def initialize(str = nil)
    @field = str
  end
end

p Liste.new "foobar"

CRuby

[]

mruby

ArgumentError is raised.

defined?

The answer is not the same object each time, and a constant path of more than 32 names is answered nil. A name mruby does not have, such as $PROGRAM_NAME or __dir__, is answered nil as well, since it is absent rather than undefined differently.

defined?(self).equal?(defined?(self))
defined?(A::B::C::D::E::F::G::H::I::J::K::L::M::N::O::P::Q::R::S::T::U::V::W::X::Y::Z::A::B::C::D::E::F::G)

CRuby

true
"constant"

mruby

false
nil

alias on global variables

Aliasing a global variable works in CRuby but is not part of the ISO standard.

alias $a $__a__

CRuby

nil

mruby

Syntax error

nil? redefinition in conditional expressions

Redefinition of nil? is ignored in conditional expressions.

a = "a"
def a.nil?
  true
end
puts(a.nil? ? "truthy" : "falsy")

Ruby outputs truthy. mruby outputs falsy.

No Double Dispatch in Module Loading

To make implementation simpler, mruby does not use double dispatching in module loading (include/prepend/extend). Those method internally called corresponding actual load methods (append_features/prepend_features/extend_object). But they are rarely overloaded, consumes more memory, and make loading little bit slower. As a Ruby implementation for the smaller device, we decided mruby simpler.

module M
  def self.append_features(mod)
     p :append
  end
end

class C
  include M
end

CRuby

Prints :append.

mruby

Nothing printed (since include does not call append_features internally).

No #hash call for small hashes

For performance reasons, mruby avoids calling the #hash method on keys when a hash table is small. This means that custom #hash methods on key objects may not be executed.

Pattern Matching

case/in and the patterns it takes are answered here, and what a pattern answers is what CRuby answers, with one exception: a #deconstruct that gives back something other than an Array is passed over rather than refused, where CRuby raises TypeError.

class Odd
  def deconstruct = 42
end
case Odd.new
in [x] then x
else :no
end

CRuby

TypeError is raised.

mruby

:no, the value falling through to the next clause.

#deconstruct_keys is held to the Hash it has to answer, and a value carrying neither hook falls through rather than raising, both as CRuby does.

No Refinements

Module refinements (refine, using) are not supported in mruby.

No Encoding Class

mruby does not have an Encoding class. Strings are treated as byte sequences by default. UTF-8 aware string operations can be enabled with the MRB_UTF8_STRING compile flag, which is also what makes case conversion follow Unicode rather than ASCII; MRB_USE_ASCII_CTYPE narrows that half back without giving up the indexing. A Unicode conversion refuses bytes that spell no character with ArgumentError; one narrowed to ASCII reads no characters and hands those bytes back untouched.

Integer Precision Varies by Boxing Mode

Integer size depends on the value boxing configuration:

Configuration Integer range
Word boxing, 64-bit (default) roughly +/- 2^62
Word boxing, 32-bit (default) roughly +/- 2^30
NaN boxing (64-bit only) -2^31 to 2^31-1

Code relying on 64-bit integer precision may behave differently across configurations. The mruby-bigint gem provides arbitrary-precision integers when included.

No ObjectSpace.each_object by Default

ObjectSpace is only available via the mruby-objectspace gem (included in the stdlib gembox). Even with the gem, ObjectSpace.each_object has limited functionality compared to CRuby.

No Implicit Type Conversion (to_int, to_str, to_ary, ...)

mruby does not perform implicit type conversion through methods like to_int, to_str, to_ary, or to_hash. CRuby uses these to let user-defined classes duck-type as built-in types — for example Array#[] calls to_int on its argument, String#+ calls to_str, and multiple assignment calls to_ary on its right-hand side. mruby's built-in operations require the actual built-in type and do not consult these conversion methods.

class MyInt;  def to_int; 42; end;     end
class MyStr;  def to_str; "x"; end;    end
class MyAry;  def to_ary; [1,2,3]; end; end

CRuby

[1,2,3][MyInt.new]   # => nil   (to_int called -> ary[42])
"a" + MyStr.new      # => "ax"  (to_str called)
a, b, c = MyAry.new  # => a=1, b=2, c=3   (to_ary called)

mruby

[1,2,3][MyInt.new]   # TypeError
"a" + MyStr.new      # TypeError
a, b, c = MyAry.new  # a=<MyAry obj>, b=nil, c=nil   (treated as single value)

Identity versions of to_int, to_str, to_sym, and to_hash remain defined on the corresponding built-in types so that respond_to?(:to_str)-style checks work for built-in instances. Float#to_int and Array#to_ary are intentionally not defined.

Explicit conversion methods (to_i, to_s, to_a) work as in CRuby and are called by features such as string interpolation and the splat operator (*obj).

This is a deliberate trade-off: implicit conversion forces every coercion site to go through method dispatch and can silently mask type-mismatch bugs.

Proc#dup / Proc#clone is Always Orphan

A dup or clone of a block given to a method is always treated as an orphan block in mruby — calling it raises LocalJumpError if the block contains break or return. CRuby is finer-grained: the copy inherits the orphan status of its original, so the copy only becomes orphan once the original yielding method returns.

def m(&b)
  b.dup
end

x = m { break 1 }
x.call

CRuby

LocalJumpError  # raised only after m returns; if called inside m,
                # the dup is still a live block

mruby

LocalJumpError  # always raised — the dup is orphan from the moment
                # it is created

mruby's stricter rule keeps RProc from needing a back-pointer to the original block (which would also enlarge the GC mark set).

Class#initialize Can Be Re-Invoked

CRuby raises TypeError: already initialized class when initialize is invoked on a class that has already been set up. mruby's Class#initialize has no such guard — invoking it on an existing class through __send__, send, or UnboundMethod#bind_call silently succeeds. The superclass argument is ignored in this case, so the call cannot rewrite the class hierarchy; only the block (if any) is evaluated with the class as receiver.

Klass = Class.new
Klass.__send__(:initialize) {}

CRuby

TypeError: already initialized class

mruby

The block is evaluated in the context of Klass; no error is raised.
The superclass is not changed even when one is passed as an argument.

Module#initialize is re-callable in both implementations, so this divergence is Class-specific. Adding the CRuby check would require an additional flag bit on every RClass; mruby leaves the bit unspent because no destructive side effects are possible through this path.