Skip to content

Commit

Permalink
feat(pacmak/java): emit default interface implementations (#2076)
Browse files Browse the repository at this point in the history
It should be possible to add properties or methods to interfaces that
are only meant to be returned by APIs (i.e: non-`@sublassable`) without
breaking extensions of those interfaces in other modules. Unfortunatley,
when such an interface is extended in another module, it's generated
proxy class would previously not have implementations for the new
member, creating the potential for a `NoSuchMethodException` at runtime.

This PR changes the code generation so that a new `Jsii$Default` nested
interface is generated for each interface, which contains default
implementations (that forward calls to the *jsii kernel*) for all of its
members.

> *Note:* members whose name collide with members declared on
> `java.lang.Object` cannot have `default` implementations, since these
> would always be overridden by `java.lang.Object` implementations.

The new `Jsii$Default` interface is implemented (and the default
implementations used) by the `Jsii$Proxy` class. When an interface
extends one or more bases, its `Jsii$Default` implementation extends
those of it's parent interfaces (and inherits their implementations).

In order to support backwards compatibility with libraries generated
with versions of `jsii-pacmak` that do not implement this feature, a
new `metadata.jsii.pacmak.hasDefaultInterfaces` entry is added to the
*jsii assembly* at compilation time. It's value is used to determine
whether `jsii-pacmak` can rely on the existence of the `Jsii$Default`
interface being present.

Finally, new elements were added to the Java runtime library for jsii:
- The `@Internal` annotation is used to designate API elements that are
  not meant for public usage
- The `IJsiiObject` interface is the base interface of all
  `Jsii$Default` interfaces, and is implemented by `JsiiObject`. Since
  interface members need be `public`, the methods declared by this
  interface have been maked `@Internal`, and were renamed from `jsiiFoo`
  to `$jsii$foo` (in order to completely remove the risk for name
  clashes with user-defined APIs)

Fixes #2014



---

By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license].

[Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
  • Loading branch information
RomainMuller authored Oct 21, 2020
1 parent 3f84daf commit c618de3
Show file tree
Hide file tree
Showing 29 changed files with 2,919 additions and 1,331 deletions.
7 changes: 7 additions & 0 deletions packages/@jsii/java-runtime-test/pom.xml.t.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ process.stdout.write(`<?xml version="1.0" encoding="UTF-8"?>
</properties>
<dependencies>
<dependency>
<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<version>${version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>software.amazon.jsii.tests</groupId>
<artifactId>calculator</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package software.amazon.jsii;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotates API elements that are intended for jsii's internal use only. Such APIs elements are not public by design
* and likely to be removed or renamed, have their signature change, or have their access level decreased in future
* versions of the library without notice.
*
* Annotated elements are eligible for immediate modification or removal and are not subject to semantic versioning.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE})
public @interface Internal {
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
/**
* HTTP client for jsii-server.
*/
@Internal
public final class JsiiClient {
/**
* JSON node factory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import com.fasterxml.jackson.databind.JsonNode;

import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
Expand All @@ -21,6 +23,7 @@
/**
* The javascript engine which supports jsii objects.
*/
@Internal
public final class JsiiEngine implements JsiiCallbackHandler {
/**
* The singleton instance.
Expand All @@ -32,6 +35,14 @@ public final class JsiiEngine implements JsiiCallbackHandler {
*/
private static final String INTERFACE_PROXY_CLASS_NAME = "Jsii$Proxy";

/**
* A map that associates value instances with the {@link JsiiEngine} that
* created them or which first interacted with them. Using a weak hash map
* so that {@link JsiiEngine} instances can be garbage collected after all
* instances they are assigned to are themselves collected.
*/
private static Map<Object, JsiiEngine> engineAssociations = new WeakHashMap<>();

/**
* Object cache.
*/
Expand All @@ -47,6 +58,14 @@ public final class JsiiEngine implements JsiiCallbackHandler {
*/
private Map<String, JsiiModule> loadedModules = new HashMap<>();

/**
* A map that associates value instances with the {@link JsiiObjectRef} that
* represents them across the jsii process boundary. Using a weak hash map
* so that {@link JsiiObjectRef} instances can be garbage collected after
* all instances they are assigned to are themselves collected.
*/
private Map<Object, JsiiObjectRef> objectRefs = new WeakHashMap<>();

/**
* @return The singleton instance.
*/
Expand All @@ -57,6 +76,47 @@ public static JsiiEngine getInstance() {
return INSTANCE;
}

/**
* Retrieves the {@link JsiiEngine} associated with the provided instance. If none was assigned yet, the current
* value of {@link JsiiEngine#getInstance()} will be assigned then returned.
*
* @param instance The object instance for which a {@link JsiiEngine} is requested.
*
* @return a {@link JsiiEngine} instance.
*/
static JsiiEngine getEngineFor(final Object instance) {
return JsiiEngine.getEngineFor(instance, null);
}

/**
* Retrieves the {@link JsiiEngine} associated with the provided instance. If none was assigned yet, the current
* value of {@code defaultEngine} will be assigned then returned. If {@code instance} is a {@link JsiiObject}
* instance, then the value will be recorded on the instance itself (the responsibility of this process is on the
* {@link JsiiObject} constructors).
*
* @param instance The object instance for which a {@link JsiiEngine} is requested.
* @param defaultEngine The engine to use if none was previously assigned. If {@code null}, the value of
* {@link #getInstance()} is used instead.
*
* @return a {@link JsiiEngine} instance.
*/
static JsiiEngine getEngineFor(final Object instance, @Nullable final JsiiEngine defaultEngine) {
Objects.requireNonNull(instance, "instance is required");

if (instance instanceof JsiiObject) {
final JsiiObject jsiiObject = (JsiiObject) instance;
if (jsiiObject.jsii$engine != null) {
return jsiiObject.jsii$engine;
}
return defaultEngine != null ? defaultEngine : JsiiEngine.getInstance();
}

return engineAssociations.computeIfAbsent(
instance,
(_k) -> defaultEngine != null ? defaultEngine : JsiiEngine.getInstance()
);
}

/**
* Resets the singleton instance of JsiiEngine. This will cause a new process to be spawned (the previous process
* will terminate itself). This method is intended to be used by compliance tests to ensure a complete and
Expand Down Expand Up @@ -131,15 +191,6 @@ public void loadModule(final Class<? extends JsiiModule> moduleClass) {
this.loadedModules.put(module.getModuleName(), module);
}

/**
* Registers an object into the object cache.
* @param objRef The object reference.
* @param obj The object to register.
*/
public void registerObject(final JsiiObjectRef objRef, final Object obj) {
this.objects.put(objRef.getObjId(), obj);
}

/**
* Returns the native java object for a given jsii object reference.
* If it already exists in our native objects cache, we return it.
Expand All @@ -160,32 +211,58 @@ public Object nativeFromObjRef(final JsiiObjectRef objRef) {
}

/**
* Returns the jsii object reference given a native object.
* Assigns a {@link JsiiObjectRef} to a given instance.
*
* If the native object extends JsiiObject (directly or indirectly), we can grab the objref
* from within the JsiiObject.
* @param objRef The object reference to be assigned.
* @param instance The instance to which the JsiiObjectRef is to be linked.
*
* Otherwise, we have a "pure" native object on our hands, so we will first perform a reverse lookup in
* the objects cache to see if it was already created, and if it wasn't, we create a new empty JS object.
* Note that any native overrides will be applied by createNewObject().
* @throws IllegalStateException if another {@link JsiiObjectRef} was
* previously assigned to {@code instance}.
*/
final void registerObject(final JsiiObjectRef objRef, final Object instance) {
Objects.requireNonNull(instance, "instance is required");
Objects.requireNonNull(objRef, "objRef is required");

final JsiiObjectRef assigned;
if (instance instanceof JsiiObject) {
final JsiiObject jsiiObject = (JsiiObject) instance;
if (jsiiObject.jsii$objRef == null) {
jsiiObject.jsii$objRef = objRef;
}
assigned = jsiiObject.jsii$objRef;
} else {
assigned = this.objectRefs.computeIfAbsent(
instance,
(key) -> objRef
);
}
if (!assigned.equals(objRef)) {
throw new IllegalStateException("Another object reference was previously assigned to this instance!");
}
this.objects.put(assigned.getObjId(), instance);
}

/**
* Returns the jsii object reference given a native object. If the object
* does not have one yet, a new object reference is requested from the jsii
* kernel, and gets assigned to the instance before being returned.
*
* @param nativeObject The native object to obtain the reference for
*
* @return A jsii object reference
*/
public JsiiObjectRef nativeToObjRef(final Object nativeObject) {
if (nativeObject instanceof JsiiObject) {
return ((JsiiObject) nativeObject).getObjRef();
}

for (String objid : this.objects.keySet()) {
Object obj = this.objects.get(objid);
if (obj == nativeObject) {
return JsiiObjectRef.fromObjId(objid);
final JsiiObject jsiiObject = (JsiiObject) nativeObject;
if (jsiiObject.jsii$objRef == null) {
jsiiObject.jsii$objRef = this.createNewObject(jsiiObject);
}
return jsiiObject.jsii$objRef;
}

// we don't know of an jsii object that represents this object, so we will need to create it.
return createNewObject(nativeObject);
return this.objectRefs.computeIfAbsent(
nativeObject,
(_k) -> this.createNewObject(nativeObject)
);
}

/**
Expand Down Expand Up @@ -526,10 +603,6 @@ public JsiiObjectRef createNewObject(final Object uninitializedNativeObject, fin
JsiiObjectRef objRef = this.getClient().createObject(fqn, Arrays.asList(args), overrides, interfaces);
registerObject(objRef, uninitializedNativeObject);

if (uninitializedNativeObject instanceof JsiiObject) {
((JsiiObject) uninitializedNativeObject).setObjRef(objRef);
}

return objRef;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/**
* Represents a jsii JavaScript module.
*/
@Internal
public abstract class JsiiModule {
/**
* The module class.
Expand Down
Loading

0 comments on commit c618de3

Please sign in to comment.