Thursday, June 7, 2007

GWT Demystified: Generators Part Deux

In the last part, I described what generators are and what they can be used for. In this post, I'll be explaining how I used them to build the GWT Exporter.

GWT Exporter

The first step is to decide what your generator is going to do. In my case, I want the generator to introspect one of my GWT classes, and generate an exported JS API with bridge methods.

What's a bridge method?

Imagine you have the following GWT Class:


package org.foo.bar;
public class Util {
public static String doSomethingUseful(int x) {
return "Hello "+x;
}
}


And you want to allow JS users to call the doSomethingUseful() function?

Today, you would use JSNI to export 'bridge methods' like so:

package org.foo.bar;
public class Util {
public static String doSomethingUseful(int x) {
return "Hello "+x;
}
public native void export() /*-{
$wnd.doSomethingUseful = function(x) {
@org.foo.bar.Util::doSomethingUseful(I)(x);
}-*/;
}
}



$wnd stands for the top-level window object, and by assigning to its doSomethingUseful property, you ensure GWT won't obfuscate it. The JSNI call to Util.doSomethingUseful will be obfuscated however, thus the bridge method is neccessary to export the obfuscated symbol.

It gets more complicated if you want to bridge a non-static function, but here's an example:

public class Employee {
private String firstName, lastName;
public Employee(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return getLastName;
}

public static Employee createEmployee(String firstName,
String lastName) {
return new Employee(firstName, lastName);
}

public native void export() /*-{
$wnd.Employee = function(firstName, lastName) {
// call factory method and store GWT reference
this.instance =
@org.foo.bar.Employee::createEmployee(Ljava/lang/String;
Ljava/lang/String)(firstName, lastName);
}

var _=$wnd.Employee.prototype;
_.getFirstName = function() {
this.instance.@org.foo.bar.Employee::getFirstName()();
}
_.getLastName = function() {
this.instance.@org.foo.bar.Employee::getLastName()();
}
}-*/;
}


Which you may use as

x = new Employee('Ray', 'Cromwell');
alert(x.getFirstName());



As you can see, manual bridging gets tedious!

Generators to the rescue!
The first step in implementing a generator is to decide on a type that will be used to trigger the generator. So let's introduce a new marker interface called 'Exportable'. We also want the generated class to implement the Exporter interface, primarily, the export() method.

Here they are:

package org.timepedia.exporter.client.Exportable;
public interface Exportable {
}

public interface Exporter {
public void export();
}


Simple eh? Next we'll add the following line to our GwtExporter.gwt.xml module file:

<generate-with
class="org.timepedia.exporter.rebind.ExporterGenerator">
<when-type-assignable
class="org.timepedia.exporter.client.Exportable"/>
</generate-with>


What does this mean?

It means that when GWT.create() is invoked with a type that can be assigned to an Exportable, invoke the generator. That is, we want to write

public class Employee implements Exportable { ... }

Exporter x=(Exporter)GWT.create(Employee.class);
x.export();


and have it invoke the generator.

Specifying what gets exported

Next we have to decide on the rules for exporting. Which methods of an Exportable get exported? How do we control the generated JS namespace? etc. For now, let's settle on the following logic -- a method is exportable IF and ONLY IF:

  1. The class enclosing the method implements Exportable
  2. Metadata has determined it's ok to export (more on this later)
Also, we need error checking. It is an error to export a method if any parameter or return type is not one of:
  1. a primitive type (int, float, etc)
  2. another Exportable
  3. an immutable JRE type (String, Integer, Double, etc)
  4. JavaScriptObject
Metadata

GWT has its own form of annotations similar to JavaDoc/XDocLet tags. We will use this to control export policy. We will support two forms of export policy:
  1. White List
    1. By default, nothing exported.
    2. Each method to be exported must have a "@gwt.export" metadata annotation
  2. Black List
    1. Place "@gwt.export" on class itself (in JavaDoc for class)
    2. By default, all public methods exported
    3. Each method to be removed from export consideration tagged with "@gwt.noexport"
Finally, by default, the GWT Class's package will be used as the JS's namespace, (e.g. new $wnd.org.foo.bar.Employee). If you wish to use another package for the JS export, place "@gwt.exportPackage [package1.package2...]" on the class JavaDoc.

As an example:

package org.foo.bar;
/**
* @gwt.export
* @gwt.exportPackage examples
*/
public class Employee implements Exportable {
private String firstName, lastName;
public Employee(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String getFirstName() {
return firstName;
}
/**
* @gwt.noexport
*/
public String getLastName() {
return getLastName;
}

and uses black-list policy to export getFirstName() but supress the export of getLastName(); Using white-list policy, you would write:

package org.foo.bar;
/**
* @gwt.exportPackage examples
*/
public class Employee implements Exportable {
private String firstName, lastName;
/**
* @gwt.export
*/
public Employee(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
/**
* @gwt.export
*/
public String getFirstName() {
return firstName;
}

public String getLastName() {
return getLastName;
}

to achieve the same effect.

At this time, we are not considering function overloading.

This concludes the specification process, and part 2. Coming up: Part 3, the guts of the generator implementation.

-Ray

4 comments:

GK said...

Brilliant article! thanks, Garry

Pierre said...

Excellent description. Finally ONE usufull article on JSNI that does not ONLY describe the obvious

Pierre said...

Excellent article!!! Finally one article that does not ONLY state the obvious

Keith said...

Delete all the chinese stuff, it's spam. Thanks for the article BTW