-
Notifications
You must be signed in to change notification settings - Fork 245
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
feat(pacmak/java): emit default interface implementations #2076
Conversation
When generating interface proxies, and the proxied interface has exactly one super-interface, it is possible for it's proxy to re-use the implementation of the base's proxy by extending it, instead of repeating the whole implementation. Doing so reduces the amount of code that is generated, compiled and released, making it faster to compile, and smaller to download. Additionally this means proxies for interfaces that extend a single foreign (i.e: from a dependency) interface are safer to use (the proxy remains "fully valid" even if the resolved version of the dependency has additional methods or properties). The same approach can unfortunately not be used for abstract class proxies, or for interfaces with more than one super-interface, since Java does not allow multiple inheritance (making it impossible to inherit from more than one base proxy class). Fixes #2014
I guess you didn't go default-implementations-in-interfaces on purpose? Is there still a point to this change if we don't go all the way? Aren't we setting ourselves up to fail in the future, again, just in more complicated cases? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay with suggestion and questions about long-term plans
const ownProps = props.filter( | ||
(prop) => baseInterface == null || !prop.inherited, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is stretching the definition of ownProps
somewhat, in my opinion.
I mean, I guess it's "own" for someone (the generated class or the generatee interface), but this is not the someone I would have expected. I would have expected "own" to mean "as declared", not "as generated".
Maybe call it hereProps
/thereProps
instead? (For funzies)
Or declaredProps
/inheritedProps
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I guess it's "contextually own props" sort of... Can definitely be misleading when looking only at usage sites...
I'm going for descriptive:
propsToGenerate
propsToInherit
"Default implementation in interface" is an interesting approach however it will require introducing a whole additional set of interfaces: if the default behavior on the currently generated interface is "forward call to the jsii kernel", then a user implementing the interface (in Java-land) who "forgot" to implement one method would get a crash on any attempt to call that method (java -> asks kernel to call method -> method does not exist in kernel-owned instance -> 💥). The current change improves the situation in a good chunk of uses; and which must be done anyway, because I cannot just assume all existing Java interfaces - including from dependencies - have been generated WITH default implementations; and might even be a breaking change if we happen to actually have to emit additional interfaces (cannot assume those are PRESENT in dependencies). |
After further conversation, even the current change is a breaking change, since this assumes the So I guess since this must break, we should probably go "all in" with the default implementation-having interfaces... And we'll need to gate this behind a feature flag. |
current change would be breaking, cannot ship as-is
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love the metadata annotation solution!
One question: the devil is usually in the transitive dependencies. Will the following daisy-chain of linking work properly?
cdk-without-$Default
used by
library-with-$Default
used by
library-with-$Default
I'll trust you on the implementation front :)
packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/IJsiiObject.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And so with respects to the "daisy chaining", I believe I have tested this to work properly. The usage of the interfaces is conditioned to the dependency that owns a class having the flag... Worst case scenario we'll be generating sub-par code if the dependency closure is "unfortunate" here; but it should work (at least - as good as it did before this change, no worse). |
Not too keen on the |
0bcce0d
to
b59c0b4
Compare
Good idea! Implemented! |
Thank you for contributing! ❤️ I will now look into making sure the PR is up-to-date, then proceed to try and merge it! |
Merging (with squash)... |
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
Merging (with squash)... |
It should be possible to add properties or methods to interfaces that
are only meant to be returned by APIs (i.e: non-
@sublassable
) withoutbreaking 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
nestedinterface is generated for each interface, which contains default
implementations (that forward calls to the jsii kernel) for all of its
members.
The new
Jsii$Default
interface is implemented (and the defaultimplementations used) by the
Jsii$Proxy
class. When an interfaceextends one or more bases, its
Jsii$Default
implementation extendsthose 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, anew
metadata.jsii.pacmak.hasDefaultInterfaces
entry is added to thejsii assembly at compilation time. It's value is used to determine
whether
jsii-pacmak
can rely on the existence of theJsii$Default
interface being present.
Finally, new elements were added to the Java runtime library for jsii:
@Internal
annotation is used to designate API elements that arenot meant for public usage
IJsiiObject
interface is the base interface of allJsii$Default
interfaces, and is implemented byJsiiObject
. Sinceinterface members need be
public
, the methods declared by thisinterface have been maked
@Internal
, and were renamed fromjsiiFoo
to
$jsii$foo
(in order to completely remove the risk for nameclashes 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.