Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

java_import can override constants used in dependencies, causing hard-to-debug errors #3737

Closed
aviflax opened this issue Mar 16, 2016 · 7 comments

Comments

@aviflax
Copy link

aviflax commented Mar 16, 2016

I’m new to Ruby and JRuby, so please forgive any errors.

I’m working on a JRuby project with a co-worker, and we encountered some very strange behavior in our app. It took him awhile to track it down, but eventually he found that we had inadvertently used java_import to redefine the Ruby constant Set and thereby broken a dependency of a dependency.

Just to be clear:

  1. Our file app.rb called require 'faye/websocket' and then java_import java.util.Set
  2. The code in app.rb called some code in faye/websocket that in turn called Set.new in headers.rb which returned a new instance of java.util.Set
    1. Rather than Set from the built-in module set that ships with the Ruby standard library
  3. Subsequent to that, something caused the method add? to be called on the value returned in step 2.
  4. Since java.util.Set has no add? method, an exception was raised.

Once we figured this out, we were quite surprised. We had had no idea that java_import would redefine Set globally, even in modules and packages that had already been loaded. Now that I’ve had some time to think it over I think I can see why this would be the case in Ruby, wherein most things by default seem to be in the global scope. But my coworker and I are newbies to Ruby, and I suspect we were mentally mapping java_import to how import works in Java — now clearly a mistake.

OK, so on to my point: given this behavior, java_import seems really dangerous. It redefines any existing constant in the global scope, and given modern software development practices wherein people regularly require many levels of dependencies and can’t be expected to be familiar with all the symbols used by all those dependencies, it seems quite easy for someone to inadvertently break a dependency, like we did. And it took us a non-neglibile amount of time to debug this situation.

So:

  • Is there a generally accepted methodology of avoiding such problems in the Ruby community?
  • Is there any way to change the behavior of java_import so that it works more like import in Java, merely acting as a shorthand for a symbol, and only in the current file?
  • I suggest modifying the docs on java_import by adding a strong warning about this problem and maybe even advising users to avoid java_import.
    • I’d be happy to volunteer if the maintainers would support this.

Other than this, JRuby is working great so far in our project — thank you for such an impressive system!

@enebo enebo added this to the Invalid or Duplicate milestone Mar 16, 2016
@enebo
Copy link
Member

enebo commented Mar 16, 2016

java_import setting a constant in the current namespace in and of itself is maybe confusing but it is not really all that much different than frameworks like Rails which define constants in namespaces for you as a side-effect. In fact, java_import is implemented as a Ruby method.

I hope these two examples will show why this is not really something we can provide extra warnings for and how it is really just a Ruby pitfall.

Let's say we require set and we java_import java.util.Set. Both define Set in ::Object:

require 'java'
require 'set';
 java_import 'java.util.Set'

When we run this we see:

uri:classloader:/jruby/java/core_ext/object.rb:93: warning: already initialized constant Set

So Ruby is working here and showing that you are trying to overwrite an existing constant. However, let's now consider we generally work within classes or modules? So we do something like:

require 'set'

module Foo
   java_import 'java.util.Set'
end

In this case we get no warning. If you call Set inside Foo it finds Java and outside Foo it finds Ruby's set. This is really identical to:

require 'set'

module Foo
   Set = 1
end

If you would like some more clarity or perhaps you have some suggestion in how we can make this better then please comment and potentially reopen.

@aviflax
Copy link
Author

aviflax commented Mar 16, 2016

@enebo thanks for the rapid, thorough, and cogent response, I appreciate it!

I still think that the JRuby project could help out Ruby newbies by updating the docs section Using a Java Class Without The Full Path Name to mention explicitly that java_import defines a constant in the global context and can therefore redefine existing constants, causing unexpected behavior at runtime. I’d be happy to do so if this is desired; as a newcomer I just don’t want to do so unilaterally.

Thanks!

@enebo
Copy link
Member

enebo commented Mar 16, 2016

@aviflax One thing which confuses me about what you are saying is when you say 'the global context'. Are you saying it is more confusing that you can redefine a constant in Ruby in top-level like in a one file script than in a nested scope like within a module?

Regardless of that question, I do find that section to be pretty lacking. It says in that example that it is put in 'global name space' but it is just that particular example and I do not think global name space is a good description. I think top-level since it implies a topology (and really we are putting it on the type of the top-level object which happens to be ::Object).

I tell you what. I will make a quick edit to that section and I can ask you whether it is more clear or not. I fear I am too well versed in this to objectively judge whether it is easy to understand.

@enebo
Copy link
Member

enebo commented Mar 16, 2016

@aviflax ok made a pass over it. I think I covered the two problems people could run into and I hope explained better how java_import works. In addition, I removed mention to 'import' to even further move us from ever mentioning it. Let me know if anything should change or even change it yourself.

Thanks for bringing this up. I think the documentation got a little better as a result at least.

@aviflax
Copy link
Author

aviflax commented Mar 17, 2016

@enebo thanks, that's a huge improvement! Great work.

I can think of only 2 possible tweaks:

  • it might help people to specifically mention that using java_import at the "top level" i.e. the object scope can impact any dependencies or transient dependencies in use, leading to unexpected consequences.
  • it might be helpful to recommend that people avoid using java_import at the "top level" and use it only within class and module definitions.

Thanks again!

@enebo
Copy link
Member

enebo commented Mar 17, 2016

I did one more round on this. I feel going any further on warning against this would be turning into a how Ruby works section. So I merely included a nice article on the complexities of Constant resolution.

You recommendation is a good one of course in anything which is not a single file script you should probably not be defining constants in the top-scope past namespacing gems. Of course this is true of all constants in Ruby, but I think it is easy to think of including a Java class as something special so pointing it out is good.

@aviflax
Copy link
Author

aviflax commented Mar 17, 2016

@enebo great work, thanks so much!

@aviflax aviflax closed this as completed Mar 17, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants