Saturday, March 08, 2008

Tips for JRuby engine: how to share objects mutually

This is the second tips for JRuby engine in addition to the first one. In this post, I'll show you how to share the same instances in both Java and Ruby, in other words, referring Java-created objects in Ruby, and contrarily, Ruby-created objects in Java.

To refer the same instances in both languages, a global variable is a key concept. If a coder have experienced Ruby programming, he or she must know what is a Ruby's global variable. In Ruby, a name of a global variable is defined to start with a symbol, '$,' such as $name or $file. An object that Java instantiated is always referred as the global variable in Ruby code; consequently, Java-created object's name must start with '$' in a script. The name of a global variable is exactly the same as the attribute name in Java, but those names don't start with '$'. Instead, the name taken off the beginning '$' character such as "name" or "file" is used for Java's attribute name.

For example, suppose a java.util.List typed instance is created and set to the context of a script engine with the attribute name, "list" by using put() method in Java. Then, Ruby runtime knows that the global variable, $list, and the associated instance exist. Here's code snippets that demonstrates how the instance is created and refered:

List list = new ArrayList();
list.add("What's up?");
list.add("How're you doing?");
list.add("How have you been?");
engine.put("list", list);
String script = "$list.each {|msg| puts msg }";
engine.eval(script);


If a coder created an object in Ruby and wanted to refer it in Java, he or she should name the instance like $something in Ruby and retrieve it from engine's context by the name, "something". Here's another snippets that shows how to do this:

String script = "$seasons = ['spring', 'summer', 'fall', 'winter']";
engine.eval(script);
List seasons = (List) engine.get("seasons");
for (String season : seasons) {
System.out.println(season);
}


Like this, global variables are often used to share the same instance in both languages; moreover, we have one more way to share a Ruby-created instance in Java. This is done by returning a value. In this case, both Ruby and Java don't use any global varibale while Ruby takes local variables, and Java assigns them to whatever it needs. Suppose Ruby code returns an array whose local variable name is colors, then Java receives a java.util.List typed object as a return value by invoking script engine's eval method(). The snippets is shown below:

script = "colors = ['red', 'green', 'white', 'blue'];" +
"colors.reverse";
List colors = (List) engine.eval(script);
for (String color : colors) {
System.out.println(color);
}

Although only the first line of the script is enough to get the return value from Ruby, I added the second line because it is too short for users to understand the script is Ruby code.

As I talked about, we have two ways to share instances between Ruby and Java by using a JSR 223 scripting engine. If you use global variables, you can share multiple instances at a time. On the other hand, you can share only one instance when you do it by returning value. However, sometimes, users need to get a returned value after processing some algorithm, so the choice is on the users. Of course, users can mix two ways to share instances.

The code below is an entire class used for this tips:
package canna;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ReferringObjectsExample {

private ReferringObjectsExample() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");
createObjectsInJava(engine);
createObjectsInRuby(engine);
}

private void createObjectsInJava(ScriptEngine engine) throws ScriptException {
// giving an object to Ruby as a global variable
List list = new ArrayList();
list.add("What's up?");
list.add("How're you doing?");
list.add("How have you been?");
engine.put("list", list);
String script = "$list.each {|msg| puts msg }";
engine.eval(script);
engine.put("first", 2008);
script = "$first.step(2015, 2) {|i| puts i }";
engine.eval(script);
}

private void createObjectsInRuby(ScriptEngine engine) throws ScriptException {
// referring an object as a global variable
String script = "$seasons = ['spring', 'summer', 'fall', 'winter']";
engine.eval(script);
List seasons = (List) engine.get("seasons");
for (String season : seasons) {
System.out.println(season);
}
// receiving an array object returned from Ruby
script = "colors = ['red', 'green', 'white', 'blue'];"+
"colors.reverse";
List colors = (List) engine.eval(script);
for (String color : colors) {
System.out.println(color);
}
// receiving a hash object returned from Ruby
script = "gpas1 = {\"Alice\" => 3.75, \"Bob\" => 4.0};"+
"gpas2 = {\"Alice\" => 3.92, \"Chris\" => 3.55};"+
"gpas1.merge!(gpas2)";
Map gpas = (Map)engine.eval(script);
for (String name : gpas.keySet()) {
System.out.println(name +": " + gpas.get(name));
}
}

public static void main(String[] args) throws ScriptException {
new ReferringObjectsExample();
}
}
When the code gets run, it outputs like this:

What's up?
How're you doing?
How have you been?
2008
2010
2012
2014
spring
summer
fall
winter
blue
white
green
red
Alice: 3.92
Bob: 4.0
Chris: 3.55

3 comments:

Unknown said...

Something I have been wondering... currently the script engine passes in variables as global variables.

I'm not particularly fond of globals personally, so I have started to wonder if there is some way I can have them passed in as local variables instead. It would make the script user's life a little bit easier, as well as making it work more closely to how the Rhino engine works.

I don't mind if I have to hack the script engine code, but I have been looking at it and can't figure out how I would change the behaviour.

Anonymous said...

Take a look at the InvocableEngine and invokeMethod(), you can pass parameters.

Anonymous said...

If you are stepping though a class file and decide to change something, Eclipse can do a "hot class load" through JDWP, then you can start stepping through the updated class' code immediately. Saves time having to restart an application for debugging. (Caveat: The JVM has limits on what and how much you can change without a restart.) Any capable Java debugger should have this facility.

java training in chennai