Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single impl interfaces? #2

Open
niloc132 opened this issue Oct 8, 2020 · 11 comments
Open

Single impl interfaces? #2

niloc132 opened this issue Oct 8, 2020 · 11 comments

Comments

@niloc132
Copy link

niloc132 commented Oct 8, 2020

In GWT2 with JavaScriptObject it was possible to have exactly one subclass implement an interface - the compiler would detect this and where necessary devirtualize the calls and dispatch correctly to either plain java implementations or the single native implementation.

With JSO, there were two ways to implement these:

  1. provide a simple native java method which would use JSNI to invoke the true method
  2. provide an overlay method which would then call the appropriate native methods/properties

From the original design doc option 1 is still apparently possible in jsinterop-annotations assuming the interface itself can be modified to be marked as native ("A native @JsType can only extend/implement native @JsTypes."), but option 2 is not possible at all ("A native @JsType class can only have ... Final non-native JsOverlay methods that do not override any other methods").

In the context of GWT2 these limitations may not necessarily make sense, but with j2cl they clearly seem to, since the interface and implementation might be compiled separately, so generating the dispatch mechanism when the interface is transpiled would need to depend on knowing about the implementation.

Are there any suggested patterns to solve these sorts of issues? Ideas I've entertained and their downsides:

  • Rewrite the interface to be marked as @JsType(isNative=true) and to only have declared members which 100% match the eventual JS implementation - this is not always possible when the interface comes from some upstream source (org.w3c.dom, org.json are some such examples where overlays can bridge the gap and let code share an interface).
  • "Monkeypatching" the underlying JS object solves the above by avoiding the need for overlays, but clearly adds its own level of ick
  • Provide a java wrapper per native instance - clearly this adds a bit of runtime overhead, depending on how the interfaces are declared
@gkdn
Copy link
Member

gkdn commented Oct 9, 2020

Unlike earlier interface with single JSO case, you can have interfaces declare @JsOverlay methods that could do whatever you would normally do in the single JSO implementation. The resulting code should be similar to JSO case.

But if you can give a more concrete example of what you are trying to achieve as the end result, I can try to provide better guidance.

@niloc132
Copy link
Author

Given the option of rewriting the contents of the interfaces (and setting aside the downsides above), we can put @JsOverlay on the interface methods, but this doesn't appear to work. Sample interface, declared explicitly to be a native interface, with an overlay method

@JsType(isNative = true)
public interface SharedInterface {

    @JsOverlay
    String doSomeWork(String param);
}

In both GWT2 and J2CL this fails with and without the @JsOverlay on the inherited method:

@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public class JsImpl implements SharedInterface {

    @JsMethod
    private native int actualUnderlyingMethod(String param);

    @Override
//    @JsOverlay // toggled on and off during testing, ensuring it has at least some effect
    public final String doSomeWork(String param) {
        return "work #" + actualUnderlyingMethod(param);
    }
}

GWT2 without inherited @JsOverlay:

[INFO]    Errors in com/example/JsImpl.java
[INFO]       [ERROR] Line 16: Method 'String JsImpl.doSomeWork(String)' cannot override a JsOverlay method 'String SharedInterface.doSomeWork(String)'.
[INFO]       [ERROR] Line 16: Native JsType method 'String JsImpl.doSomeWork(String)' should be native or abstract.
[INFO]    Errors in com/example/SharedInterface.java
[INFO]       [ERROR] Line 10: JsOverlay method 'String SharedInterface.doSomeWork(String)' cannot be non-final nor native.

GWT2 with inherited @JsOverlay (just in case it needs the hint to be explicit) has one less error, suggesting this could be on the right track:

[INFO]    Errors in com/example/JsImpl.java
[INFO]       [ERROR] Line 16: Method 'String JsImpl.doSomeWork(String)' cannot override a JsOverlay method 'String SharedInterface.doSomeWork(String)'.
[INFO]    Errors in com/example/SharedInterface.java
[INFO]       [ERROR] Line 10: JsOverlay method 'String SharedInterface.doSomeWork(String)' cannot be non-final nor native.

J2CL has the same errors with and without inherited @JsOverlay:

Error:JsImpl.java:16: Native JsType method 'String JsImpl.doSomeWork(String param)' should be native, abstract or JsOverlay.
Error:JsImpl.java:16: Method 'String JsImpl.doSomeWork(String param)' cannot override a JsOverlay method 'String SharedInterface.doSomeWork(String)'.
Error:SharedInterface.java:10: JsOverlay method 'String SharedInterface.doSomeWork(String)' cannot be non-final.

Omitting @JsType(isNative=true) on the interface also results in an error (Native JsType ''JsImpl'' can only implement native JsType interfaces.).

The goal though would be to not have to amend the original interfaces, but if that is supported (and I'm just doing it wrong), it would probably suffice.

@gkdn
Copy link
Member

gkdn commented Oct 15, 2020

You can only have default/private methods to provide method implementation on interfaces; and that's what you need to mark with @JsOVerlay.

interface A {
   @JsOverlay
   default void foo() {
      // single jso impl logic goes here
   }
}

This is effectively similar to single jso impl in terms of where you could have one implementation in GWT and multiple implementations on server size.

@rluble
Copy link
Collaborator

rluble commented Oct 15, 2020

@niloc132, are you confusing SingleImpl with DualImpl? In GWT you are able to have Java interfaces implemented by a JSO and Java class, those are called DualImp interfaces. In such case the GWT compiler generates a trampoline to handle the dispatch to JavaScript. This is not a functionality that is part of jsinterop. SingleImpl scenario, on the other hand, is completely supported, that means that the interface is only implemented by JavaScript objects hence there will not exist any override of that method.@JsOverlay methods cannot have overrides.

You could achieve something similar to dual impl interfaces by using @JsOverlay to write a trampoline code by hand with a little bit of extra code for wiring. But in general you can have a better solution using a native "*", "?" interface. e.g.

Say you have a JavaScript class C that has method m() and you want to have a common interface that is implemented by Java and JavaScript classes without needing to modify C to implement an interface.

@JsType(isNative = true, namespace = GLOBAL, name = "*") 
interface DualImplInterface {
  void m();
}

class JavaClass implements DualImplInterface {
  public void m() { ... }
}

HTH.

@niloc132
Copy link
Author

@rluble you may be right that I'm thinking of something else. I do definitely want an overlay method, defined by the interface, and implemented in plain Java for the actual Java implementation (typically only on the server), and also separately implemented with one or more overlay methods for the JS impl of the method, since the underlying native type doesn't always make sense - in your example, DualImplInterface.m would need to have the behavior defined on it using a default method or be "overridden" in the native JsType with an overlay - in your example, m() would only be the underlying JS method that is used so that the overlay/default method can get its job done, and as @gkdn suggested the @JsOverlay method would invoke it? From my quick testing, it isn't possible to get the last step then of permitting JavaClass to "correctly" override foo() to do the real work?

Updating the example from my last post:

@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public interface SharedInterface {
    @JsMethod
    int actualUnderlyingMethodThatExistsOnlyInJs(String param);

    @JsOverlay
    default String doSomeWork(String param) {
        //overlay impl so that JS can work, would be implemented differently in Java
        return "work #" + actualUnderlyingMethodThatExistsOnlyInJs(param);
    }
}
@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public class JsImpl implements SharedInterface {

    @JsMethod
    public native int actualUnderlyingMethodThatExistsOnlyInJs(String param);
}

Up until now things work, until you have to provide a non-JS implementation of the interface method:

public class JavaImpl implements SharedInterface {
    @Override
    public int actualUnderlyingMethodThatExistsOnlyInJs(String param) {
        throw new UnsupportedOperationException("this doesn't make sense outside of the JS type");
    }

    @Override
    public String doSomeWork(String param) {
        return "foo";// actual java impl of some kind
    }
}

My question is how do you provide both a Java and JS implementation, where the underlying APIs are different. com.google.web.bindery.autobean.shared.Splittable is an example where every method is implemented with overlays (in JsoSplittable), and implemented in plain java differently on both the client and the server (NullSplittable is usable only on the client, JsonSplittable on the JVM).

Assuming that both APIs are identical (org.w3c.dom.Node &co are 99% of the way there), "simply" redefining every interface with annotations could suffice, as long as it is permissible to have two copies of each source file (not ideal, but hopefully done only once?). My concern is when Java wants to redefine the method, either in different subtypes, or just to have an alternative implementation to the provided overlay method.

I'm sorry if this isn't very clear - it didn't seem to be covered in any of the docs I had found so far about JsInterop, and both answers seem tending in different directions. It does appear that I made a mistake about naming here (single vs dual), though the javadoc on com.google.gwt.core.client.SingleJsoImpl seems ambiguous - I agree there is no way for more than one JSO type to implement the interface, but that still leaves room for non-JSOs to implement it as well in the definition - and it appears at least that it is permissible for a SingleJsoImpl to also be a dual, since TypeOracle.computeDualJsoImplData() and JTypeOracle.computeDualJSO() both promote some single impls to also being dual impls.

I've been trying to work out the trampoline idea where overlays are needed - the closest I've gotten is something like three interfaces - one "actual" interface with default methods, and each default method tests if in JS or Java, and then casts this to one of the other two interfaces and invokes a method there, so that the overlay methods for JS don't end up in Java code and Java's methods don't require that JS changes how it works. This does end up requiring effectively an extra layer of abstraction above the real implementations.

@gkdn
Copy link
Member

gkdn commented Oct 15, 2020

I am still not sure if I fully understand your goal but in attempt to do my best, If you are simply looking for a way to do dual jso; the solution is still the same; you need to hand write the trampoline that was generated by GWT:

interface Foo {
   default int bar() {
      return (this instanceof ConcreteJavaType) <call something on ConcreteJavaType>
        : <call the thing that would be in the JSO implementation or implement it here>
   }
}

If this still doesn't answer your question; could you give a simple concrete example with your requirements as I asked earlier and how you solved in GWT2?
(The interface that you would like to implement, what is suppose to be implemented in java, what runs on JVM, what runs in JavaScript etc.)

@rluble
Copy link
Collaborator

rluble commented Oct 16, 2020

One last point you have to think that @JsOverlay method are a static method that has just a convenient way to call is if it was an instance method. You CANNOT override a @JsOverlay method. Depending on what you want to do, and the constraints you have (e.g. the JavaScript side is a library that you don't control) there might be many ways to achieve it.

If it is just to have a common API for Java and JavaScript then what @Goktug suggested is a way to go (just note that the interface there should be native @JsType and the method bar() should be @JsOverlay). Remember shared interfaces between Java and JavaScript need to be native. If you don't have a native interface you can use the generic object (GLOBAL *) or unknown (GLOBAL ?) that don't require an existing JavaScript interface.

@niloc132
Copy link
Author

I understand what an overlay method is (both the gwt2 and jsinterop variety) - and that you sorta-kinda-if-you-squint can override them, but only insofar as you first cast to the type that owns the method you are calling, its no longer dynamic dispatch, but just a static function that looks like an instance method, at least from the plain Java.

My hope was that there was some clever way to end up with the type check "is this object a non-java object? okay, delegate to a specific static method, otherwise call the java method" - sort of the opposite of the instanceof in Foo.bar(), allowing for more than one ConcreteJavaType in the hierarchy:

interface Foo {
  default int bar() {
    return Cast.isJavaScriptObject(this) ? <call overlay for JS impl> : <call "this.bar()" except without this check>
  }
}

To my knowledge this requires whole-world knowledge ("confirm there is exactly one implementation of the interface Foo so that Foo can be compiled with correct dispatch for it"), or that either Foo or Foo's JS impl is annotated in a clear way to reference the other. Likewise anything calling Foo.bar() needs to know to dispatch to this helper method instead of the "real" bar() method declared on the instance, so it probably would need to be in the bytecode for Foo itself.

Splittable from AutoBeans is a good example in GWT2 - any plain JS object can be traversed with splittable. At the same time, other java implementations can exist of the Splittable interface (the NullSplittable singleton I mentioned is one, GXT also has a XmlSplittable that makes the same API work on the browser's DOM types).

Number makes another example - Double has its own doubleValue() which of course just returns this - but J2CL already knows that Number and Double are connected in this way. This is of course a special case, but ends up with the same pattern in compiled code at least.

@rluble
Copy link
Collaborator

rluble commented Oct 16, 2020

I understand what an overlay method is (both the gwt2 and jsinterop variety) - and that you sorta-kinda-if-you-squint can override them.

Not sure what you mean that you can override them in J2CL. You can not; if there are cases where you can then that is a bug in the restriction checker.

You could have server code that overrides them though, as long as it is not compiled with J2CL. That would definitely work on server only code, but not on shared code.

My hope was that there was some clever way to end up with the type check "is this object a non-java object?

You could achieve that the following way.

@JsType(isNative = true,...)
interface Foo {
  interface JavaFoo extends Foo {
       void barImpl();
   }
  @JsOverlay
  default void bar() {
      if (this instanceof JavaFoo) {
         ((JavaFoo) this).barImpl();
      } else {
         // code for the javascript case.
      }
   }
}        

You could also do more hacky things like checking for the presence of a particular property in your JavaScript object to decide how to resolve the dispatch. But it is intended that scenarios like this be resolved with user code.

There is no clever way to universally distinguish them because J2CL blurs the lines between Java and JavaScript. E.g. there is no way to distinguish between a JavaScript array and Object[], Java class can extend JavaScript classes so they don't extend j.l.Object, etc.

To my knowledge this requires whole-world knowledge ("confirm there is exactly one implementation of the interface Foo so that Foo can be compiled with correct dispatch for it"), or that either Foo or Foo's JS impl is annotated in a clear way to reference the other.

jsinterop design is not only motivated by modular compilation but also by having a simple model that is easy to communicate. Even if there were an easy way to support this we would need a compelling reason to extend the spec. And I don't think there is a clean easy way to support it without extending the spec.

J2CL aims to give a consistent view and seamless (Closure) JavaScript interoperation. Many GWT idioms need to be replaced for more JavaScript like idioms. Instead of a dual impl JSO, it is better to have a JavaScript interface implemented by the native object with a native JsType interface to expose it to Java code.

Splittable from AutoBeans is a good example in GWT2 - any plain JS object can be traversed with splittable.

Splittable is a dual impl interface. A scenario that is not directly supported in J2CL for the reasons above, but you can use one of the many solutions mentioned here in the thread.

Number makes another example - Double has its own doubleValue() which of course just returns this - but J2CL already knows that Number and Double are connected in this way. This is of course a special case, but ends up with the same pattern in compiled code at least.

Yes. Number is special because it is implemented by Double which we represent directly as number. I agree that the pattern is similar in the sense that there is a dispatch trampoline. But in this case there is a way to distinguish number from other Java classes and this is done just as an optimization for the runtime.

@gkdn
Copy link
Member

gkdn commented Oct 16, 2020

The main limitation here is inability to use the same method names in pure Java classes, so you need to come up with 2 contracts; Foo and FooJava interfaces where overlay calls to the Java specific one. I don't see any other natural way to solve this. GWT2 solution is quite magical and it is one of the pieces we were sure that we didn't want to carry over.

Having 2 contracts is not ideal but something you can design an APT for to make it less error prone.

If you need something like isJavaScriptObject, it could be more or less implemented as .constructor == Object.constructor but since you need a second interface for Java you can just check for instance of that.

@gkdn
Copy link
Member

gkdn commented Oct 16, 2020

Well, we just run to each other with Roberto; but it looks like you got nearly same answer in two different wordings :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants