Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Inconsistency between MRI and JRuby when private methods are called and method_missing is present #540

Closed
jvshahid opened this Issue · 5 comments

3 participants

@jvshahid
#!/usr/bin/env ruby
class Foo
  def method_missing name
    super
  end
  private
  def foo
    'foo'
  end
end

Foo.new.foo

this code will generate an exception with different messages under MRI and JRuby.

MRI:

/home/jvshahid/foo.rb:4:in `method_missing': private method `foo' called for #<Foo:0x00000001eed260> (NoMethodError)
    from /home/jvshahid/foo.rb:14:in `<main>'

JRuby:

NoMethodError: undefined method `foo' for #<Foo:0x64e137c0>
  method_missing at org/jruby/RubyBasicObject.java:1652
  method_missing at /home/jvshahid/foo.rb:4
          (root) at /home/jvshahid/foo.rb:12

Notice that JRuby's exception doesn't mention that the method we're trying to access is private. This cause three specs to fail in rails since unfortunately they rely on the wording of the exception.

@rjnienaber

Still present in 1.7.20 and HEAD:

----------
ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-darwin14.0.0]
/tmp/test.rb:4:in `method_missing': private method `foo' called for #<Foo:0x007f90a392a7d0> (NoMethodError)
    from /tmp/test.rb:12:in `<main>'
----------
jruby 1.7.20 (1.9.3p551) 2015-05-04 3086e6a on Java HotSpot(TM) 64-Bit Server VM 1.8.0_05-b13 +jit [darwin-x86_64]
NoMethodError: undefined method `foo' for #<Foo:0x3551a94>
  method_missing at org/jruby/RubyBasicObject.java:1498
  method_missing at /tmp/test.rb:4
          (root) at /tmp/test.rb:12
==========
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
/tmp/test.rb:4:in `method_missing': private method `foo' called for #<Foo:0x007fb25480aa08> (NoMethodError)
    from /tmp/test.rb:12:in `<main>'
----------
jruby 9.0.0.0-SNAPSHOT (2.2.2) 2015-05-05 c0d9bc3 Java HotSpot(TM) 64-Bit Server VM 25.5-b02 on 1.8.0_05-b13 +jit [darwin-x86_64]
NoMethodError: undefined method `foo' for #<Foo:0x6eebc39e>
  method_missing at org/jruby/RubyBasicObject.java:1606
  method_missing at /tmp/test.rb:4
           <top> at /tmp/test.rb:12
----------
@headius
Owner

Obviously it is very unfortunate that Rails tests depend on the error message, but in this case we're not just worded differently...we're missing information. I'll have a look.

@headius
Owner

Hmm...this is a very unusual case.

In MRI, it seems that the call through user-defined method_missing keeps some state around indicating the original reason for the method_missing call. This data is not visible to users except when that method_missing calls super.

In JRuby, once method_missing has dispatched, we do not track the original cause; either an error will be thrown, or we'll go back into userland for the user to handle it.

It gets even weirder (to me, at least). The error message is preserved:

  • when there are intervening calls before super,
  • when the super happens in a block, even if method_missing has returned,
  def method_missing name
    ->{super}
  end
  • and when there are multiple levels of method_missing calling super

Given that there can be many changes to the call stack and the cause is still preserved, it seems likely that MRI has special call logic for method_missing that carries this information along.

@headius
Owner

Ah-ha! When I looked into MRI's code for this, I saw that they track the cause in a method_missing_reason variable on the current thread's VM structure. That variable is only read in one place: when calling the default method_missing logic that raises our error, and that's all it does. In other words, it is never captured or restored, and therefore it is not tied to the current call frame. It appears to be thread-local state.

So I thought I'd trigger another method_missing before calling super. The following code does not raise the correct error at all:

class A
  def method_missing(name)
    return if name == :bar
    bar rescue nil
    super
  end

  def foo; end
  private :foo
end

A.new.foo

__END__

[] ~/projects/ruby $ rvm ruby-2.2 do ruby ../jruby/super_thing.rb 
../jruby/super_thing.rb:11:in `method_missing': undefined local variable or method `foo' for #<A:0x007f8b648da990> (NameError)
    from ../jruby/super_thing.rb:18:in `<main>'

The output here seems to confirm that this value is indeed thread-local, and once wiped out by another value it cannot be recovered. I almost feel like the method_missing super behavior was just a lucky accident.

Nevertheless...it's a much smaller change to have method_missing dispatch briefly update a thread-local variable to indicate the cause. This should be fixable.

@headius headius referenced this issue from a commit
@headius headius Set thread-local call type and visibility around all m_m dispatch.
This also simplifies the pre-built "visibility m_m's" and
eliminates a few inner classes.

Fixes #540.
2799836
@headius headius closed this issue from a commit
@headius headius Set thread-local call type and visibility around all m_m dispatch.
This also simplifies the pre-built "visibility m_m's" and
eliminates a few inner classes.

Fixes #540.
76ba4b6
@headius headius closed this in 76ba4b6
@headius
Owner

Zing. Hope I didn't break anything.

I think we need some specs for this.

@headius headius added this to the JRuby 1.7.21 milestone
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.