"no-JSNI interop" aka "JavaScript API importing", formerly known as "Automatic JSON wrappers"

62 views
Skip to first unread message

BobV

unread,
Jun 9, 2007, 5:14:19 AM6/9/07
to Google Web Toolkit Contributors
Writing wrappers around JavaScript APIs is tedious and repetitive,
which becomes error-prone, especially at 2am.

The attached HelloMaps demo shows a quick example of how no-JSNI
interop can be use the Google Maps API in a GWT application. There's
not a drop of JSNI code. As a reading companion to the demo code, you
can see it running at http://vawter.org/maptest/Hello.html. It's a
simple app, but it shows that standard Java/GWT idioms can be readily
used to describe and interact with a JS api.

This started out as a lightweight JSON facade generator and turned
into something a bit more robust. (Sorry for the delay, Rob) The term
"wrapper" isn't accurate in the current implementation, but calling an
object a "JSOInterop" seemed awkward. Most of the function
invocations and field accesses are straight-out calls to the
underlying JS object. Now that I have a generally working prototype,
I figure it's time to kick it out to GWTC for comments and feature
discussions.

Most of the documentation is in the interfaces in json.client
package and the HelloMaps provides more details about the interop
annotations.

What works:
- Passing [boxed] primitives, Strings, JSONWrappers, JSLists, and
JSFunctions through the interface.
- Automatically binding Java interfaces to JS object fields (as
bean-style properties) and to functions declared on the backing JS
object.
- Updates to the data from JS are immediately visible to the Java
consumers. The inverse is also true.
- Passing Java functions into a JS API to act as callbacks.
- Completing abstract classes to mix developer-supplied logic and
generated interop code

Explicitly not supported:
- Passing Java array objects through. Because arrays can't be
subclassed, interposed, or otherwise faked in Java, it's not possible
to read-through to a backing JavaScript array. Thus, you wind up
having to copy every element in the array. Multi-dimensional arrays
compound the problem.

Todo:
- Bulk exporting of functions; I need to catch up on the
Ray/Chronoscope thread and check out his API.
- Renaming, documenting, more test cases around JSFunction interops.
- Tightening up the generated code. Because Java objects can't be
directly instantiated in generated JSNI blocks (nor can GWT.create()
be invoked), there is a lot of generated createFoo functions.
- Determine what degree of safety the interop generator should
support at runtime. Right now, the same return semantics apply to
bound functions and regular JSNI functions. Returning the wrong type
from JS into Java produces undefined results, though this can be
detected in hosted mode. Also, undefined, bound fields are
pre-initialized to 0, null, or false when the interop implementation
object is created so that there's no need to make a check in every
property getter. This could be something that's configurable via
annotations or deferred-binding properties.
- Once the compiler supports JSNI inlining, it'll be much closer to
"zero-overhead interop" as many of the generated statements are of the
form "return foo" or would otherwise benefit from compiler mojo.


diffstat:
src/com/google/gwt/json/JSON.gwt.xml
| 3 3 + 0 - 0 !
src/com/google/gwt/json/client/JSFunction.java
| 24 24 + 0 - 0 !
src/com/google/gwt/json/client/JSList.java
| 25 25 + 0 - 0 !
src/com/google/gwt/json/client/JSONParser.java
| 17 4 + 13 - 0 !
src/com/google/gwt/json/client/JSONWrapper.java
| 101 101 + 0 - 0 !
src/com/google/gwt/json/client/JSONWrapperException.java
| 32 32 + 0 - 0 !
src/com/google/gwt/json/client/impl/JSListWrapper.java
| 172 172 + 0 - 0 !
src/com/google/gwt/json/client/impl/JSONWrapperUtil.java
| 181 181 + 0 - 0 !
src/com/google/gwt/json/rebind/ArrayGenerator.java
| 52 52 + 0 - 0 !
src/com/google/gwt/json/rebind/BoxedPrimitiveGenerator.java
| 135 135 + 0 - 0 !
src/com/google/gwt/json/rebind/FragmentGenerator.java
| 166 166 + 0 - 0 !
src/com/google/gwt/json/rebind/FragmentGeneratorOracle.java
| 72 72 + 0 - 0 !
src/com/google/gwt/json/rebind/JSFunctionGenerator.java
| 199 199 + 0 - 0 !
src/com/google/gwt/json/rebind/JSONWrapperGenerator.java
| 875 875 + 0 - 0 !
src/com/google/gwt/json/rebind/JavaScriptObjectFragmentGenerator.java
| 63 63 + 0 - 0 !
src/com/google/gwt/json/rebind/ListFragmentGenerator.java
| 158 158 + 0 - 0 !
src/com/google/gwt/json/rebind/NamePolicy.java
| 86 86 + 0 - 0 !
src/com/google/gwt/json/rebind/PrimitiveGenerator.java
| 82 82 + 0 - 0 !
src/com/google/gwt/json/rebind/StringFragmentGenerator.java
| 59 59 + 0 - 0 !
src/com/google/gwt/json/rebind/WrapperFragmentGenerator.java
| 89 89 + 0 - 0 !
test/com/google/gwt/json/client/JSONInvokerTest.java
| 124 124 + 0 - 0 !
test/com/google/gwt/json/client/JSONWrapperTest.java
| 514 514 + 0 - 0 !
test/com/google/gwt/json/rebind/NamePolicyTest.java
| 41 41 + 0 - 0 !
test/com/google/gwt/json/rebind/TestNamePolicy.java
| 26 26 + 0 - 0 !
24 files changed, 3283 insertions(+), 13 deletions(-)


--
Bob Vawter
Google Web Toolkit Team

json_wrapper_r1184_r6.patch.gz
Hello.java

Ray Cromwell

unread,
Jun 9, 2007, 7:13:31 AM6/9/07
to Google-Web-Tool...@googlegroups.com

Bob, this looks kick ass! I think I'll have the third installment of my generators/gwt exporter article done on monday, in which case I'll be dropping the code for anyone to pick up. Perhaps this can result in some kind of harmonization with the importer stuff, in terms of philosophy/metadata naming/etc perhaps as a kind of unified 'external' JS import/export package. I'll need help, because I'm too busy to bring my code up to full maturity ( e.g. write unit tests, full documentation)

-Ray





* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.sample.hello.client ;

import com.google.gwt.core.client.*;
import com.google.gwt.user.client.*;
import com.google.gwt.json.client.JSFunction;
import com.google.gwt.json.client.JSList;
import com.google.gwt.json.client.JSONWrapper ;
import com.google.gwt.json.client.JSONWrapperException;
import com.google.gwt.user.client.ui.*;
import java.util.Iterator;

/**
* HelloMaps application.
*/
public class Hello implements EntryPoint {

  /**
   * The naming of the class is arbitrary, but chosen for consistency with
   * the underlying JS API
   */
  interface GLatLng extends JSONWrapper {
    /**
     * The naming of the method is arbitrary, the only thing that's important
     * is the presence of the gwt.constructor annotation.
     * @gwt.constructor $wnd.GLatLng
     */
    public GLatLng construct(double lat, double lng);

    public double lat();
    public double lng();
  }

  /**
   * This interface is named differently from the underlying name of the
   * JS class.
   */
  interface GoogleMap extends JSONWrapper {
    /**
     * @gwt.constructor $wnd.GMap2
     */
    public GoogleMap construct(Element elt);

    /**
     * We need to be able to add some controls to the map, so we'll expose
     * the method to do so.  See below for the two "implementations" of
     * GControl.
     */
    public void addControl(GControl control);

    /**
     * Tells the map code to update its visual state when the size of the
     * containing element changes.
     */
    public void checkResize();

    /**
     * Pan the map to a new position.
     */
    public void panTo(GLatLng position);

    /**
     * No annotations are required since the arguments make this not a bean-
     * style setter.
     */
    public void setCenter(GLatLng position, int zoomLevel);
  }

  /**
   * Polymorphism works just fine.  Again, the name of this interface
   * has no direct bearing on the linkage, it's simply convenient to use the
   * same name as the Maps API documentation.
   */
  interface GControl extends JSONWrapper {
  }

  /**
   * This is the small map controller (directional control and zoom).
   * @gwt.constructor $wnd.GSmallMapControl
   */
  interface GSmallMapControl extends GControl {
  }

  /**
   * This is the map type controller (map, satellite, hybrid)
   * @gwt.constructor $wnd.GMapTypeControl
   */
  interface GMapTypeControl extends GControl {
  }

  /**
   * Provides access to the Google Maps geocoding service
   * @gwt.constructor $wnd.GClientGeocoder
   */
  interface Geocoder extends JSONWrapper {
    /**
     * Sends a request to the geocoding service.  The Geocoder will issue a
     * callback, so it's important that we can "pass" a Java code closure into
     * a JavaScript API.  That's where JSFunction comes into play.
     * @param address The location to geocode
     * @param callback A callback that will be invoked when a result is
     *    returned.
     * @gwt.fieldName getLatLng
     */
    void lookup(String address, GeocoderCallback callback);
  }

  /**
   * A callback for the geocoding service.  We explicity name the desired
   * export function so that the interface coulde additional functions.  If
   * the class has only a single function, the annotation
   * isn't strictly necessary.
   * @gwt.exported onGeocode
   */
  interface GeocoderCallback extends JSFunction {
    public void onGeocode(GLatLng position);
  }


  public void onModuleLoad() {
    // GWT.create the Map and geocoding service
    final GoogleMap map = (GoogleMap)GWT.create(GoogleMap.class);
    final Geocoder geocoder = (Geocoder)GWT.create(Geocoder.class);

    // Use the SimplePanel's Element for the base of the map
    SimplePanel panel = new SimplePanel();
    panel.setPixelSize(400, 400);

    map.construct(panel.getElement());

    // Add some standard controllers
    map.addControl((GControl)GWT.create(GSmallMapControl.class ));
    map.addControl((GControl)GWT.create(GMapTypeControl.class));

    // Create a position wrapper
    final GLatLng center = (GLatLng)GWT.create(GLatLng.class);
    // Set the position of the wrapper (Palto Alto)
    center.construct(37.4419, -122.1419);

    // Set the center and zoom level
    map.setCenter(center, 12);

    // Provide input to the geocoding service
    Button b = new Button("Go to address", new ClickListener() {
        public void onClick(Widget w) {
          String address = Window.prompt("Address", "");
          if (address != null && address.length() > 0) {
            // Notice that the callbacks work correctly with anonymous classes
            geocoder.lookup(address, new GeocoderCallback() {
                // This will move the map to the location retured by the service
                public void onGeocode(GLatLng position) {
                  if (position == null) {
                    Window.alert("The name you entered couldn't be found.");
                  } else {
                    map.panTo(position);
                  }
                }
            });
          }
        }
    });
    RootPanel.get().add(b);


    // Set the map up in a Dialog box, just for fun.
    DialogBox dialog = new DialogBox(false, false);
    dialog.setText("Drag me!");
    dialog.setWidget(panel);
    dialog.show();
    // The map control needs to be kicked when its enclosing element changes size.
    map.checkResize();
  }
}



Sascha Matzke

unread,
Jun 10, 2007, 6:31:39 AM6/10/07
to Google Web Toolkit Contributors
Hi,

I'm really thrilled by this. It makes my life sooo much easier.

I've been writing wrappers for a JS library I use manually and this is
exactly what I need.

Thank you for this great addition to GWT. I hope it end's up in the
svn repository soon.

Sascha

Sascha Matzke

unread,
Jun 10, 2007, 7:13:47 AM6/10/07
to Google Web Toolkit Contributors
Hi,

one question that occurred to me while playing around with this.

How can JavaScript singletons be integrated with this?

Sascha

BobV

unread,
Jun 10, 2007, 12:42:40 PM6/10/07
to Google-Web-Tool...@googlegroups.com
On 6/10/07, Sascha Matzke <sascha...@gmail.com> wrote:
> one question that occurred to me while playing around with this.
>
> How can JavaScript singletons be integrated with this?

One more item for the todo list to make that easy.

The generator always assumes that the constructor annotation argument
is a function name, so you might try:
@gwt.constructor (function(){return($wnd.Singleton)})

If that doesn't work, then calling setJavaScriptObject() will be less
elegant, but it'll get the job done.

Sascha Matzke

unread,
Jun 10, 2007, 6:43:00 PM6/10/07
to Google Web Toolkit Contributors
Hi,

I tried the second option (calling setJavaScriptObject) but somehow
that doesn't work (I didn't test this further because I found another
way to work around it).

But I have another problem.... I'm trying to create a configuration
object for a native JavaScript prototype. It looks like this:

>>>>
public static abstract class Config implements JSONWrapper
{
public static Config create()
{
return (Config) GWT.create(Config.class);
}

public abstract void setMsg(final String text);

}

<<<<

It's a static inner class. As far as I understood
JSONWrapperGenerator,
if no explicit constructor is specified, an empty JS object ({}) will
be
created.

But compile this gives me the following error:

Internal compiler error
java.lang.NullPointerException
at
com.google.gwt.json.rebind.JSONWrapperGenerator.writeEmptyFieldInitializer(JSONWrapperGenerator.java:
629)
at
com.google.gwt.json.rebind.JSONWrapperGenerator.generate(JSONWrapperGenerator.java:
177)
at
com.google.gwt.dev.cfg.RuleGenerateWith.realize(RuleGenerateWith.java:
43)
[...]

Another thing I thought about... How does JSONWrapperGenerator handle
static inner classes? Just looking at the compilation logs, it seems
that XXX.InnerClass is generated to __InnerClass, which could cause
conflicts.

But anyway... I'm happy that JSONWrapper works in most cases. It saves
me a huge amount of stupid wrappers and I can concentrate the project
I'm working on.

Sascha

PS: If this is a duplicated... Did I mention that the Google Group
WebInterface sometimes produces indeterminate errors?

Sascha Matzke

unread,
Jun 10, 2007, 6:45:23 PM6/10/07
to Google Web Toolkit Contributors
Hi again,

On Jun 11, 12:43 am, Sascha Matzke <sascha.mat...@gmail.com> wrote:
> com.google.gwt.json.rebind.JSONWrapperGenerator.writeEmptyFieldInitializer(JSONWrapperGenerator.java:
> 629)

This has to be line 628 - I added a TreeLogger output before that line
in my copy of the code.

Sascha

BobV

unread,
Jun 11, 2007, 1:48:01 AM6/11/07
to Google-Web-Tool...@googlegroups.com
On 6/10/07, Sascha Matzke <sascha...@gmail.com> wrote:
> Another thing I thought about... How does JSONWrapperGenerator handle
> static inner classes? Just looking at the compilation logs, it seems
> that XXX.InnerClass is generated to __InnerClass, which could cause
> conflicts.

That's an oversight on my part, the code ignores containing classes.
I'll try to get the code checked into a subversion repository this
week so folks can link it into a checkout of the GWT trunk with
svn:external.

> But anyway... I'm happy that JSONWrapper works in most cases. It saves
> me a huge amount of stupid wrappers and I can concentrate the project
> I'm working on.

That's good to hear.

Sascha Matzke

unread,
Jun 11, 2007, 7:23:38 AM6/11/07
to Google Web Toolkit Contributors
One more thing that came to my mind.

Say I'm using a JSFunction as an event handler on a native JS object.

To remove this handler I need the same JS function that is passed to
the "addListener" method in native JS (I'm using ExtJS here -
http://www.extjs.com).

Here is an example how the JSFunction is generated by GWT:

function on_0(event_0, command) {
var jso0 = event_0;
var jso1 = function () {command.execute();};
var toReturn = this.__jsonObject.on(jso0, jso1);
}

To implement the matching "un" (or removeListener) method I would
need
the value of jso1. Maybe JSFunction could be extends to that it holds
onto the native function() object so that it can be reused later?
Basically
that would mean something like setJavaScriptObject/getJavaScriptObject
in
the JSFunction interface.

If getJavaScriptObject is null, a new function is generated, if not
the
value stored in the JSFunction object is used.

Sascha

Sascha Matzke

unread,
Jun 11, 2007, 8:11:57 AM6/11/07
to Google Web Toolkit Contributors
And even more...

It seems that methods which are defined by a parent interface are not
implemented by the generator.

Hierarchy:

JSONWrapper -> Interface A (methods a() and b()) -> Abstract class B
(methods c() and d())

c() and d() are generated, but a() and b() not. The compiler doesn't
like that very much:

[ERROR] Line 8: The type __SeparatorImpl must implement the inherited
abstract method Observable.hasListener(String)


Sascha

BobV

unread,
Jun 12, 2007, 3:25:33 AM6/12/07
to Google Web Toolkit Contributors
The code for the interop generator is now living in its own Google Code project.

See:
http://gwt-api-interop.googlecode.com/svn/trunk/README

for how to link it into your own GWT checkout.

Changes since the 1184_r6 patch:
- Renamed the module to com.google.gwt.jsio.JSIO
- JSONWrapper -> JSWrapper
- Renamed and moved many of the internal classes
- Inner classes are now generated with the names of their enclosing
class to prevent name conflicts.
- More logging.
- Added an @gwt.global annotation. This is like the class-level
gwt.constructor annotation, but it allows you to initialize a
JSWrapper from a global singleton. Example:
/** @gwt.global $wnd.LogFoo */
public interface Log implements JSWrapper {....}

- When using a JSFunction object, there is now a 1-1 correspondence
between the JSFunction and the linking JavaScript function object.
This allows register/unregister APIs to work correctly.

Miguel Méndez

unread,
Jun 12, 2007, 9:48:02 AM6/12/07
to Google-Web-Tool...@googlegroups.com
Nice Bob.  I look forward to using it.

Cheers,
Reply all
Reply to author
Forward
0 new messages