Details
-
Type:
Improvement
-
Status:
Closed
-
Priority:
Blocker
-
Resolution: Fixed
-
Affects Version/s: JRuby 1.0.0RC1
-
Fix Version/s: JRuby 1.0.0RC1
-
Component/s: Java Integration
-
Labels:None
-
Testcase included:yes
-
Number of attachments :
Description
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
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.