History | Log In     View a printable version of the current page.  
Issue Details (XML | Word | Printable)

Key: JRUBY-903
Type: Improvement Improvement
Status: Closed Closed
Resolution: Fixed
Priority: Blocker Blocker
Assignee: Charles Oliver Nutter
Reporter: Bill Dortch
Votes: 0
Watchers: 2
Operations

If you were logged in you would be able to see more operations.
JRuby

Java interface modules

Created: 29/Apr/07 03:54 PM   Updated: 06/May/07 09:49 PM
Component/s: Java Integration
Affects Version/s: JRuby 1.0.0RC1
Fix Version/s: JRuby 1.0.0RC1

Time Tracking:
Not Specified

File Attachments: 1. Text File interface_modules.patch (66 kb)
2. Text File interface_modules_2.patch (69 kb)


Testcase included: yes


 Description  « Hide
The attached patch implements Java interfaces as modules. It also improves the performance of generated JavaProxyClass instances. Affected unit tests have been updated to handle interface modules. All tests run. (More tests to come.)

I intended to include support for protected methods / fields with this patch, but encountered a number of issues. I don't believe that support can make it for 1.0. I'll post a separate JIRA issue detailing what I ran up against. (I do have a lot of the code done, so once a few ideas get kicked around sufficiently, it won't take too long to get it implemented.)

This patch enables interfaces as modules by default. This will break existing code. However, the old syntax can be enabled easily:

Java.set_deprecated_interface_syntax true

If used, this should be set first thing upon starting up a runtime (or at least before running any Java applications). Though it is possible to change this while running, many problems will likely ensue, as interface classes will be bound to some constants, and interface modules to others. (Changing in mid-stream won't hurt applications loaded from Java, only user Ruby/Java applications.)

We could enable the old syntax by default, but I think it makes sense to go with our best solution for 1.0. I'll discuss working in deprecated mode farther down, but first, the new syntax.

Implementing an interface:

class Runner
  include java.lang.Runnable
  def run
  ...
  end
end
runner = Runner.new
runner.run

The impl syntax is still supported:

runner = java.lang.Runnable.impl do
  puts 'running'
end
runner.run

You can implement multiple interfaces:

import java.lang.Runnable
import java.lang.Comparable
class RunComp
  include Runnable
  include Comparable
  def run
  ...
  end
  def compareTo(other)
  ...
  end
end

You can extend a class and implement interfaces:

class MyApp < javax.swing.JFrame
  include java.awt.event.ActionListener
  include java.lang.Runnable
  def initialize(*args)
    super
    button = javax.swing.JButton.new 'Howdy'
    button.add_action_listener self
    add button
    pack
  end
  def actionPerformed(event)
    puts "Howdy back"
  end
  def run
  ...
  end
end

You can group interfaces in modules and include them together:

module RunComp
  include Runnable
  include Comparable
end
...
class RunAndComp
  include RunComp
  def run
  ...
  end
  def compareTo(other)
  ...
  end
end

You can automatically implement interface methods (useful for event listeners when you only care about one method):

class Listener
  include MenuListener
  implement MenuListener #=> creates empty methods menuCanceled, menuDeselected, menuSelected
  def menuSelected(event) #=> the one we care about
  ...
  end
end

You can automatically implement all interface methods

class MegaListener
  include KeyListener
  include MouseListener
  include WindowListener
  implement_all
  ...
end

You can modify an interface, and have the change reflected in all implementing classes (Charlie's example):

include Java

import java.sql.Connection
import java.sql.DriverManager
import java.sql.ResultSet

java.lang.Class.for_name("com.mysql.jdbc.Driver")

module Connection
  def execute(query)
    statement = create_statement
    results = statement.execute_query(query)
    yield results
  ensure
    results.close if results
    statement.close if statement
  end
end

begin
  connection =
    DriverManager.get_connection("jdbc:mysql://localhost/mephisto_development",
    "mephisto", "mephisto")

  connection.execute("select * from users") do |results|
    puts results.get_string("email") while results.next
  end
ensure
  connection.close if connection
end

You can not open an existing Java class and add an interface:

class JFrame
  include ActionListener
end
ArgumentError: can't add Java interface to existing Java class!

You can not open a generated subclass and add an interface once it's been instantiated:

class MyFrame < JFrame
  include ActionListener
end
# oops, forgot one
class MyFrame
  include WindowListener #=> OK, no class generated yet
end
frame = MyFrame.new

class MyFrame
  include MouseListener #=># too late now
end
ArgumentError: can't add Java interface to existing Java class!

Classes and interfaces are associated with package modules, which you'll see when classes are displayed (note that interfaces are now there as well):

irb(main):012:0> JFrame.ancestors
=> [Java::JavaxSwing::JFrame, Java::JavaLang::Runnable, Java::JavaxSwing::WindowConstants,
Java::JavaxSwing::RootPaneContainer, #<Module:01xb9b618>, Java::JavaAwt::Frame, Java::JavaAwt::Window,
Java::JavaxAccessibility::Accessible, Java::JavaAwt::Container, Java::JavaAwt::Component, 
Java::JavaAwtImage::ImageObserver, Java::JavaAwt::MenuContainer, Java::JavaIo::Serializable,
Java::JavaLang::Object, ConcreteJavaProxy, JavaProxy, JavaProxyMethods, Object, Java, Kernel]

You can optionally use the package name syntax when referring to classes/interfaces. The Java, Javax, Com, Org, and Edu roots are recognized at the top level:

class Java::JavaLang::System
 ...
end
-- or --
class JavaLang::System
...
end
module JavaUtil::List
...
end
JavaLang::System.nano_time

The kind_of?, is_a?, < and > methods now work for interfaces as well as classes:

class MyClass
  include Comparable
  ...
end
MyClass < Comparable #=> true
MyClass.new.kind_of?Comparable #=> true

So, now for the bad news.

This change breaks two types of interface syntax, though only one is commonly used:

class MyClass < java.lang.Runnable; end  #=> breaks
anonymous = Class.new(java.lang.Runnable) #=> breaks

The easy (lazy!) workaround, as noted above:

Java.set_deprecated_interface_syntax true

The next-easiest (but still lazy!) workaround:

# we have *not* set_deprecated_interface_syntax true
class Runner < java.lang.Runnable.deprecated  #=> a constant reminder...
...
end

The workaround for the anonymous class syntax:

# one way to do this
anonymous = Class.new(Object)
anonymous.send :include, java.lang.Runnable

If you must run in the deprecated mode, you can still get most of the benefits of the new syntax by referring to the Includable constant on old-style interface classes. However, this will break if you then go to the new mode (I tried to make this transition smoother; you should see how fast JRuby hangs when you assign a constant that refers to the assignee.)

Java.set_deprecated_interface_syntax true #=> in deprecated mode
class MyApp < JFrame
  include java.lang.Runnable::Includable
  include java.awt.event.ActionListener::Includable
end

module JavaSql::Connection::Includable
  ...
end

There's probably more to say about interfaces; ask and I shall answer.

The changes (fixes) to JavaProxyClass generation will greatly improve the performance of derived classes. As I've noted elsewhere, every call (even/especially internal ones) in generated classes was being passed in and out of JRuby, operands/results being converted in both directions:

class MyFrame < JFrame
end
MyFrame.new.visible = true
# debug code just spews as methods go in and out of JRuby

Now only methods overridden in the subclass (and any modules it includes) are intercepted:

class MyFrame < JFrame
  ...
  def  getBounds
    puts "getBounds called"
    super
  end
end
frame = MyFrame.new
frame.visible = true
# only getBounds calls are overriden

I'll get more tests and documentation done for all this over the next couple of days.

-Bill



 All   Comments   Work Log   Change History      Sort Order: Ascending order - Click to sort in descending order
Charles Oliver Nutter - 29/Apr/07 10:58 PM
Bill, this is outstanding work. I can't think of any examples you haven't covered here, and the new interfaces-as-modules looks beautiful and really feels right. After all this time and all our debate, we circle back around to one of the first things we discounted. I guess it really pays to have a working implementation.

I will try playing with your current patch and we'll look forward to including it ASAP for 1.0Beta/RC.


Bill Dortch - 30/Apr/07 02:33 PM
The attached patch (interface_modules_2.patch) supersedes the original patch. It includes the functionality of the original patch, plus:

Fixes a couple of bugs-in-wait:

1. Hooks #append_features instead of #included. Raising an ArgumentError in #included to disallow inclusion into an existing Java class didn't prevent the module from being included (though no changes would be made to the class). This actually shows up in the JFrame.ancestors example in my original post – your garden-variety JFrame is not a Runnable.

2. Includes overridden method names in the key for the generated proxies cache in JavaProxyClassFactory. Now that proxy generation takes into account which methods are actually overridden, different proxies will be generated when different methods are overridden.

3. Fixes some pre-existing synchronization issues in JavaProxyClassFactory.


Thomas E Enebo - 30/Apr/07 05:21 PM
Patch applied by Bill Dortch in commit 3601. Great Job Bill!

Nick Sieger - 01/May/07 10:52 PM
There was a redefinition of Object.const_missing that broke Rails/ActiveSupport. I yanked it in 3605. Bill, can you find a workaround for this or figure out why?

See http://dev.rubyonrails.org/browser/branches/1-2-stable/activesupport/lib/active_support/dependencies.rb for details.


Bill Dortch - 01/May/07 11:51 PM
Hmm, they use const_missing to load files? Must have some that conflict with /^((Java|Javax|Com|Org|Edu)[A-Z])/ (or some other issue, I haven't looked that closely).

In any case, it isn't critical to the Java Integration code, just a nicety. Users can still refer to classes using the (java.|javax.|com.|org.|Java::)package.name.dot.ClassName or Java::PackageName::ClassName notations. I'll just not document the other way for now, and revisit it at a later date (too many bigger fish to fry right now).

-Bill


Charles Oliver Nutter - 02/May/07 12:02 AM
I've actually kinda disliked the JavaLang, JavaxSwing stuff...they seem a little ugly to me. So we'll dump them for now, leave them undocumented, and talk it over during the RC-to-final push.

Bill Dortch - 02/May/07 12:23 AM
Well, they haven't gone away altogether, you just can't refer to them (unqualified) at the top level. They're still defined under the Java module and are displayed (prefixed with Java: when a class name is displayed. Less ugly than the earlier JAVA::LANG, JAVAX::SWING notation of my first approach, imho. And useful if you want to open up a class/module without having to predefine a constant (directly or via import/include_class:
module Java::JavaSql::Connection
...
end

-Bill


Charles Oliver Nutter - 02/May/07 12:26 AM
When discussing these naming issues, I did make one discovery. The following syntax works too:

java::lang::Whatever

It uses methods, but those methods already exist for java.lang.Whatever. But the colon syntax has the added bonus that it allows classes to be reopened. More fuel for the debate.

Also, perhaps we should close this issue and open a separate one for that debate...


Charles Oliver Nutter - 05/May/07 07:16 PM
Need to either resolve this and file a new bug for the behavior we had to remove, or post a fix for that behavior...moving to 1.0, blocker.

Charles Oliver Nutter - 06/May/07 06:50 PM
Applied the bulk of this. I'll open a separate bug for the const_missing behavior that we had to remove.

Charles Oliver Nutter - 06/May/07 09:49 PM
Closing for 1.0RC1