From 2a20e48ee602cf46a650469b478c472525896f5e Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Mon, 7 Nov 2022 19:25:43 +0400 Subject: [PATCH 01/10] Add @JvmExpose KEEP --- ...otation-to-mark-accessible-api-for-java.md | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 proposals/annotation-to-mark-accessible-api-for-java.md diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/annotation-to-mark-accessible-api-for-java.md new file mode 100644 index 000000000..3fa92025b --- /dev/null +++ b/proposals/annotation-to-mark-accessible-api-for-java.md @@ -0,0 +1,188 @@ +# `@JvmExpose` annotation to explicitly mark accessible API for Java + +This document describes an annotation for the transformation of API written in Kotlin to be convenient for use from Java. + +## Motivation and use-cases + +Currently, `@JvmName` is used in three use cases: + +1. To bypass the “Platform declaration crash” error in many cases when Kotlin overloads are more potent than JVM ones. In this case, the user is not interested in setting a specific name. + +Example: + +``` +// Example.kt + +fun takeList(list: List) {} +fun takeList(list: List) {} +// Platform declaration clash: The following declarations have the same JVM signature (takeList(Ljava/util/List;)V): +``` + +``` +// Example.kt + +@JvmName("takeListOfInt") +fun takeList(list: List) {} + +@JvmName("takeListOfLong") +fun takeList(list: List) {} +``` + +Such JVM names are usually chosen randomly (primarily when the API is not meant to be called from Java) and are not visible to Kotlin users of these overloads. + +1. To clarify API for Java users or to get away from name mangling of declarations sometimes performed by the compiler. In the example above, the names of overloads make sense when used from Java; it is still another use case: + +``` +ExampleKt.takeListOfInt(List.of(42)); +``` + +Sometimes names used in Kotlin are just unclear (mainly because of operator name conventions) when used from Java, and it can be improved by changing the JVM name: + +``` +class X + +@JvmName("sum") +operator fun X.plus(other: X) = X() + +``` + +``` +var result = ExampleKt.sum(new X(), new X()); +``` + +And in some cases, as it was said, `@JvmName` is used for exposing methods whose names were mangled by the compiler or just unavailable from Java: + +``` +class SomeThings { + @JvmName("forJava") // to avoid mangled name like forJava$example_module_main + internal fun forJava() {} + + @JvmName("dollarDollarDollar") + fun `$$$`() {} +} +``` + +**Use cases 1 and 2 are very different: while the first is just bypassing a technical limitation of JVM to declare the desired Kotlin API, the second one is related to the scenario when an API for Java is implemented in Kotlin.** + +1. Changing the name of file classes to be called from Java. + +``` +@file:JvmName("Utils") +``` + +## Proposed API + +### Exposing an API to Java + +`@kotlin.jvm.JvmExpose` annotation is proposed to handle use-case 2, so its purpose is to ensure that an API can be called from Java. It also has an optional `String` argument `jvmName` (or `name`) with the default value `""` assuming name defined in the code. + +Its behavior can be described as “annotated function is guaranteed to be available from Java by exactly the name defined either in the code or in the string parameter”; hence, the following limits are imposed: + +1. A `private` function cannot be marked as well as a function defined in private and local classes and objects. +2. A `@JvmSynthetic` function cannot be marked +3. Obviously, this annotation is incompatible with `@JvmName`. + +Marking a `public` function without any features leading to mangling is allowed, for example, to show that the API is “Java-friendly.” + +Example usages: + +``` +class X + +@JvmExpose("sum") // equivalent to @JvmName("sum") +operator fun X.plus(other: X) = X() + +class SomeThings { + @JvmExpose // equivalent to @JvmName("forJava") or @JvmExpose("") + internal fun forJava() {} +} +``` + +Additionally, marking an `internal` function with `@JvmExpose` designates that calling it from another module in Java is **not** an error, so IDE inspections should handle it. + +Combination of `@JvmExpose internal` and `@JvmSynthetic public` allows creating a completely non-overlapping API for Java and Kotlin; however, it looks like abuse, so, probably, it should be an antipattern: + +``` +class X { + @JvmExpose internal fun a(consumer: java.util.function.Consumer.Consumer) = consumer.accept(42) + @JvmSynthetic fun a(consumer: (Int) -> Unit) = consumer(42) +} +``` + +### Combination of cases + +To handle cases when clashing methods are to be both disambiguated and exposed to Java, only `@JvmExpose` should be enough: + +``` +@JvmExpose("takeListOfInt") +fun takeList(list: List) {} + +@JvmExpose("takeListOfLong") +fun takeList(list: List) {} +``` + +Applying both annotations to one function has to be prohibited. + +### Special treatment for exposed functions using `@JvmInline value class` + +Functions related to `@JvmInline value class` require special treatment since their representation in JVM differs significantly from ordinary ones. + +First, the value parameters and return value of a function marked with `@JvmExpose` should be boxed if their type is inline class. The reason is that inline classes are cumbersome to use from Java in their unboxed form. A mangled implementation of the function for unboxed Kotlin usage should be created as well. + +``` +// Example.kt + +@JvmInline +value class Example(val s: String) + +@JvmExpose +fun f(x: Example): Example = TODO() +``` + +``` +public static Example f-impl(java.lang.String x) +public static Example f(Example x) { } +``` + +The certain problem is that one cannot instantiate an inline class with its constructor because it is generated with `ACC_SYNTHETIC`. A solution for that could be annotating a constructor `@JvmExpose` to have a constructor exposed by the compiler (it also will create an internal synthetic overload of it taking something like `Nothing?`). Since this requirement is unobvious, an IDE inspection must report that instances of an inline class taken as an argument of the exposed function cannot be instantiated. + +``` +@JvmInline +value class Example @JvmExpose constructor(val s: String) +``` + +A `JvmExpose` annotation should be added to the constructor of `Example` to achieve the following Java syntax: + +``` +ExampleKt.f(new Example("42")); +``` + +### Suspending exposed functions + +Functions that are both `suspend` and annotated with `@JvmExpose` should not take a continuation as normal ones because it is impossible to implement it in Java conveniently, so they can block the thread like `suspend fun main` does. A possible problem is handling functions that actively use kotlinx.coroutines types (from `Deferred` to `Flow`), which are not convenient for Java users at all. As in the previous subsection, a mangled normal function should be created for Kotlin: + +``` +@JvmExpose +suspend fun f(x: Int): Int = TODO() +``` + +``` +public static Object f-impl(int x, @NotNull Continuation $completion) +public static int f(int x) +``` + +### The main function exposed + +``` +@JvmExpose("main") +fun mememain() { +} +``` + +An exposed function with a name `"main"` has to be the main method on JVM as well as ones named with `@JvmName`. + +## Interference with other KEEPs + +A related concept is introduced in [KEEP-302](https://github.com/Kotlin/KEEP/issues/302) “Binary Signature Name” because it also assumes refusal from using the `JvmName` annotation in some cases. + +This proposal is designed in such a way that `@BinarySignatureName` will be another mode of changing the JVM name but without the functions of JvmExpose (making API suitable for Java). From d507f37499eacb23ebbdfd3f5d00fbd0368d2643 Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Thu, 10 Nov 2022 18:04:35 +0400 Subject: [PATCH 02/10] Add KEEP header --- proposals/annotation-to-mark-accessible-api-for-java.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/annotation-to-mark-accessible-api-for-java.md index 3fa92025b..101330483 100644 --- a/proposals/annotation-to-mark-accessible-api-for-java.md +++ b/proposals/annotation-to-mark-accessible-api-for-java.md @@ -1,5 +1,10 @@ # `@JvmExpose` annotation to explicitly mark accessible API for Java +* Type: Design proposal +* Authors: Iaroslav Postovalov, Ilmir Usmanov +* Status: TODO +* Discussion and feedback: TODO + This document describes an annotation for the transformation of API written in Kotlin to be convenient for use from Java. ## Motivation and use-cases @@ -171,7 +176,7 @@ public static Object f-impl(int x, @NotNull Continuation $completion) public static int f(int x) ``` -### The main function exposed +### When the `main` function is exposed ``` @JvmExpose("main") From a18aa262cb38adf17734d15cb89e1e3b53372000 Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Fri, 18 Nov 2022 19:03:36 +0400 Subject: [PATCH 03/10] Reformulate the idea of JvmExpose --- ...otation-to-mark-accessible-api-for-java.md | 167 ++++++++---------- 1 file changed, 74 insertions(+), 93 deletions(-) diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/annotation-to-mark-accessible-api-for-java.md index 101330483..06454d55c 100644 --- a/proposals/annotation-to-mark-accessible-api-for-java.md +++ b/proposals/annotation-to-mark-accessible-api-for-java.md @@ -9,96 +9,102 @@ This document describes an annotation for the transformation of API written in K ## Motivation and use-cases -Currently, `@JvmName` is used in three use cases: +When creating an API for Java in Kotlin, one always has to handle a lot of problems with different tools: -1. To bypass the “Platform declaration crash” error in many cases when Kotlin overloads are more potent than JVM ones. In this case, the user is not interested in setting a specific name. +1. Mangling. -Example: +Internal functions and properties are mangled by Kotlin without any documentation of it. +However, +creating internal declarations seems to be a viable way to create API available for Java but not available for Kotlin. -``` -// Example.kt +The workaround is using `@JvmName`: -fun takeList(list: List) {} -fun takeList(list: List) {} -// Platform declaration clash: The following declarations have the same JVM signature (takeList(Ljava/util/List;)V): -``` +```kotlin +// Kotlin API +class Example { + internal fun noJvmName() {} + @JvmName("jvmName") + internal fun jvmName() { + } +} ``` -// Example.kt - -@JvmName("takeListOfInt") -fun takeList(list: List) {} -@JvmName("takeListOfLong") -fun takeList(list: List) {} +```java +// Java usage +new Example().noJvmName$example_main(); // looks awful +new Example().jvmName(); ``` -Such JVM names are usually chosen randomly (primarily when the API is not meant to be called from Java) and are not visible to Kotlin users of these overloads. +2. Overloads -1. To clarify API for Java users or to get away from name mangling of declarations sometimes performed by the compiler. In the example above, the names of overloads make sense when used from Java; it is still another use case: +Using default parameter values requires user to mark functions with `@JvmOverloads`. -``` -ExampleKt.takeListOfInt(List.of(42)); -``` - -Sometimes names used in Kotlin are just unclear (mainly because of operator name conventions) when used from Java, and it can be improved by changing the JVM name: +3. Inline classes -``` -class X +Functions taking and returning `@JvmInline` classes as well as properties of such classes are visible as its internal property's type, +and it breaks the idea of inline classes when used from Java. -@JvmName("sum") -operator fun X.plus(other: X) = X() +The only possible workaround is writing a part of API in Java taking boxed instances of value classes. +Moreover, constructors of inline classes are synthetic; hence, a factory method is needed. -``` +4. Suspending functions -``` -var result = ExampleKt.sum(new X(), new X()); -``` +Suspending functions can't be successfully called from Java because of necessity to instantiate `Continuation`. +A widely known workaround is to wrap the result of the function to `CompletableFuture`: -And in some cases, as it was said, `@JvmName` is used for exposing methods whose names were mangled by the compiler or just unavailable from Java: +```kotlin +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.future.future +import java.util.concurrent.CompletableFuture -``` -class SomeThings { - @JvmName("forJava") // to avoid mangled name like forJava$example_module_main - internal fun forJava() {} +suspend fun s(): Int = 42 - @JvmName("dollarDollarDollar") - fun `$$$`() {} +fun sForJava(): CompletableFuture = GlobalScope.future { + s() } ``` -**Use cases 1 and 2 are very different: while the first is just bypassing a technical limitation of JVM to declare the desired Kotlin API, the second one is related to the scenario when an API for Java is implemented in Kotlin.** +However, this way requires including `org.jetbrains.kotlinx:kotlinx-coroutines-jdk8`, +hence can't be proposed as a new feature of the language. -1. Changing the name of file classes to be called from Java. +Another way to make a workaround similar to way `suspend fun main` is implemented is calling `runSuspend`: +```kotlin +import kotlin.coroutines.jvm.internal.* -``` -@file:JvmName("Utils") +suspend fun s(): Int = 42 + +@Suppress("INVISIBLE_MEMBER") +fun sForJava(): Int { + var result: Result? = null + runSuspend { + result = Result.success(s()) + } + return result!!.getOrThrow() +} ``` ## Proposed API -### Exposing an API to Java - -`@kotlin.jvm.JvmExpose` annotation is proposed to handle use-case 2, so its purpose is to ensure that an API can be called from Java. It also has an optional `String` argument `jvmName` (or `name`) with the default value `""` assuming name defined in the code. +Adding a new `@kotlin.jvm.JvmExpose` annotation is proposed to address all the listed problems at once, +so its purpose is to ensure that an API can be called from Java freely +and to handle Kotlin features +that were designed without attention to calling from Java +in order to simplify development of libraries for Java and Kotlin written in Kotlin. Its behavior can be described as “annotated function is guaranteed to be available from Java by exactly the name defined either in the code or in the string parameter”; hence, the following limits are imposed: 1. A `private` function cannot be marked as well as a function defined in private and local classes and objects. 2. A `@JvmSynthetic` function cannot be marked -3. Obviously, this annotation is incompatible with `@JvmName`. +3. `@JvmOverloads` is assumed Marking a `public` function without any features leading to mangling is allowed, for example, to show that the API is “Java-friendly.” Example usages: -``` -class X - -@JvmExpose("sum") // equivalent to @JvmName("sum") -operator fun X.plus(other: X) = X() - +```kotlin class SomeThings { - @JvmExpose // equivalent to @JvmName("forJava") or @JvmExpose("") + @JvmExpose // equivalent to @JvmName("forJava") internal fun forJava() {} } ``` @@ -107,34 +113,20 @@ Additionally, marking an `internal` function with `@JvmExpose` designates that c Combination of `@JvmExpose internal` and `@JvmSynthetic public` allows creating a completely non-overlapping API for Java and Kotlin; however, it looks like abuse, so, probably, it should be an antipattern: -``` +```kotlin class X { - @JvmExpose internal fun a(consumer: java.util.function.Consumer.Consumer) = consumer.accept(42) + @JvmExpose internal fun a(consumer: java.util.function.Consumer) = consumer.accept(42) @JvmSynthetic fun a(consumer: (Int) -> Unit) = consumer(42) } ``` -### Combination of cases - -To handle cases when clashing methods are to be both disambiguated and exposed to Java, only `@JvmExpose` should be enough: - -``` -@JvmExpose("takeListOfInt") -fun takeList(list: List) {} - -@JvmExpose("takeListOfLong") -fun takeList(list: List) {} -``` - -Applying both annotations to one function has to be prohibited. - -### Special treatment for exposed functions using `@JvmInline value class` +### `JvmExpose` on functions with `@JvmInline` parameters or return type Functions related to `@JvmInline value class` require special treatment since their representation in JVM differs significantly from ordinary ones. First, the value parameters and return value of a function marked with `@JvmExpose` should be boxed if their type is inline class. The reason is that inline classes are cumbersome to use from Java in their unboxed form. A mangled implementation of the function for unboxed Kotlin usage should be created as well. -``` +```kotlin // Example.kt @JvmInline @@ -144,50 +136,39 @@ value class Example(val s: String) fun f(x: Example): Example = TODO() ``` -``` +```java public static Example f-impl(java.lang.String x) public static Example f(Example x) { } ``` The certain problem is that one cannot instantiate an inline class with its constructor because it is generated with `ACC_SYNTHETIC`. A solution for that could be annotating a constructor `@JvmExpose` to have a constructor exposed by the compiler (it also will create an internal synthetic overload of it taking something like `Nothing?`). Since this requirement is unobvious, an IDE inspection must report that instances of an inline class taken as an argument of the exposed function cannot be instantiated. -``` +```kotlin @JvmInline value class Example @JvmExpose constructor(val s: String) ``` A `JvmExpose` annotation should be added to the constructor of `Example` to achieve the following Java syntax: -``` +```java ExampleKt.f(new Example("42")); ``` ### Suspending exposed functions -Functions that are both `suspend` and annotated with `@JvmExpose` should not take a continuation as normal ones because it is impossible to implement it in Java conveniently, so they can block the thread like `suspend fun main` does. A possible problem is handling functions that actively use kotlinx.coroutines types (from `Deferred` to `Flow`), which are not convenient for Java users at all. As in the previous subsection, a mangled normal function should be created for Kotlin: - -``` +Functions that are both `suspend` and annotated with `@JvmExpose` should not take a continuation as normal ones +because it is impossible to use it in Java conveniently, +so they can block the thread like `suspend fun main` or `@Test suspend` do. +A possible problem is handling functions that actively use kotlinx.coroutines types (from `Deferred` to `Flow`), +which are not convenient for Java users at all. +As in the previous subsection, a mangled normal function should be created for Kotlin: +М +```kotlin @JvmExpose suspend fun f(x: Int): Int = TODO() ``` -``` +```java public static Object f-impl(int x, @NotNull Continuation $completion) public static int f(int x) ``` - -### When the `main` function is exposed - -``` -@JvmExpose("main") -fun mememain() { -} -``` - -An exposed function with a name `"main"` has to be the main method on JVM as well as ones named with `@JvmName`. - -## Interference with other KEEPs - -A related concept is introduced in [KEEP-302](https://github.com/Kotlin/KEEP/issues/302) “Binary Signature Name” because it also assumes refusal from using the `JvmName` annotation in some cases. - -This proposal is designed in such a way that `@BinarySignatureName` will be another mode of changing the JVM name but without the functions of JvmExpose (making API suitable for Java). From 464c951e9b8bfd487fdc0474bd3386fb8d900a20 Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Wed, 23 Nov 2022 18:47:09 +0400 Subject: [PATCH 04/10] Add a remark on boxing constructor of inline class --- proposals/annotation-to-mark-accessible-api-for-java.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/annotation-to-mark-accessible-api-for-java.md index 06454d55c..716a130b1 100644 --- a/proposals/annotation-to-mark-accessible-api-for-java.md +++ b/proposals/annotation-to-mark-accessible-api-for-java.md @@ -154,6 +154,11 @@ A `JvmExpose` annotation should be added to the constructor of `Example` to achi ExampleKt.f(new Example("42")); ``` +Usually, constructor of the inline class is used to perform boxing of it. +Annotating it with `JvmExpose` will lead to creating a new, +synthetic constructor (with placeholder parameter of type `Void`, probably) for boxing, +enabling the default one for user. + ### Suspending exposed functions Functions that are both `suspend` and annotated with `@JvmExpose` should not take a continuation as normal ones From 87dbb11df0629be58c224539ed098e2f2c9acdea Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Wed, 23 Nov 2022 18:56:40 +0400 Subject: [PATCH 05/10] Formatting --- .../annotation-to-mark-accessible-api-for-java.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/annotation-to-mark-accessible-api-for-java.md index 716a130b1..385767038 100644 --- a/proposals/annotation-to-mark-accessible-api-for-java.md +++ b/proposals/annotation-to-mark-accessible-api-for-java.md @@ -1,9 +1,10 @@ # `@JvmExpose` annotation to explicitly mark accessible API for Java -* Type: Design proposal -* Authors: Iaroslav Postovalov, Ilmir Usmanov -* Status: TODO -* Discussion and feedback: TODO +* **Type**: Design proposal +* **Authors**: Iaroslav Postovalov, Ilmir Usmanov +* **Status**: TODO +* **Prototype**: In progress +* **Discussion and feedback**: TODO This document describes an annotation for the transformation of API written in Kotlin to be convenient for use from Java. @@ -155,8 +156,9 @@ ExampleKt.f(new Example("42")); ``` Usually, constructor of the inline class is used to perform boxing of it. -Annotating it with `JvmExpose` will lead to creating a new, -synthetic constructor (with placeholder parameter of type `Void`, probably) for boxing, +Annotating it with `@JvmExpose` will lead to creating a new, +synthetic constructor (with placeholder parameter of type `java.lang.Void`, probably, +to avoid signature clash) for boxing, enabling the default one for user. ### Suspending exposed functions From 27e598b9ed8d6a270e3bb71ae89f05076532f533 Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Mon, 12 Dec 2022 09:11:27 +0400 Subject: [PATCH 06/10] Redesign the proposal leaving only inline classes behavior --- ...otation-to-mark-accessible-api-for-java.md | 230 ++++++++---------- 1 file changed, 105 insertions(+), 125 deletions(-) diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/annotation-to-mark-accessible-api-for-java.md index 385767038..88261e198 100644 --- a/proposals/annotation-to-mark-accessible-api-for-java.md +++ b/proposals/annotation-to-mark-accessible-api-for-java.md @@ -1,4 +1,4 @@ -# `@JvmExpose` annotation to explicitly mark accessible API for Java +# `@JvmExposeBoxed` annotation for opening API of inline classes to Java * **Type**: Design proposal * **Authors**: Iaroslav Postovalov, Ilmir Usmanov @@ -6,176 +6,156 @@ * **Prototype**: In progress * **Discussion and feedback**: TODO -This document describes an annotation for the transformation of API written in Kotlin to be convenient for use from Java. +This document describes an annotation for the transformation of inline classes to be convenient for use from Java. ## Motivation and use-cases -When creating an API for Java in Kotlin, one always has to handle a lot of problems with different tools: +Functions taking and returning `@JvmInline` classes are unavailable from Java because their name contains `-` with hash suffix. -1. Mangling. +Functions declared in an inline class are compiled to hyphen-mangled static methods taking underlying type, except in cases when a bridge method to implement interface is required. -Internal functions and properties are mangled by Kotlin without any documentation of it. -However, -creating internal declarations seems to be a viable way to create API available for Java but not available for Kotlin. +The constructor of an inline class is generated private and synthetic, so inaccessible from Java. -The workaround is using `@JvmName`: +All these characteristics lead to that inline classes being completely cut off from Java API; however, they can be useful for interoperability inside one module, for framework compatibility, and for writing libraries providing support of Java. -```kotlin -// Kotlin API -class Example { - internal fun noJvmName() {} +Related issues: - @JvmName("jvmName") - internal fun jvmName() { - } -} -``` +1. To access the property of an inline class, reflection has to be used ([KT-50518](https://youtrack.jetbrains.com/issue/KT-50518)). +2. Java frameworks like Mockito have problems with methods returning unboxed inline classes ([KT-51641](https://youtrack.jetbrains.com/issue/KT-51641)). +3. Inaccessible constructors of inline classes ([KT-47686](https://youtrack.jetbrains.com/issue/KT-47686)). +4. Issues of inline class methods with kapt (https://github.com/Kotlin/KEEP/issues/104#issuecomment-449782492). +5. A general issue about JVM compatibility of value classes ([KT-50689](https://youtrack.jetbrains.com/issue/KT-50689)). -```java -// Java usage -new Example().noJvmName$example_main(); // looks awful -new Example().jvmName(); -``` +The issues that are related to the behavior of frameworks can be addressed by documenting the existence of mangled boxing methods and the getter method of the inline class main property, but others require changing the ABI of inline classes and functions related to them. -2. Overloads +## Proposed API -Using default parameter values requires user to mark functions with `@JvmOverloads`. +Adding a new `@kotlin.jvm.JvmExposeBoxed` annotation is proposed to address the problem. Hence, its purpose is to ensure that the inline class is exposed as its JVM class in all APIs to simplify the development of libraries designed for use from Java written in Kotlin. -3. Inline classes +The annotation is defined as -Functions taking and returning `@JvmInline` classes as well as properties of such classes are visible as its internal property's type, -and it breaks the idea of inline classes when used from Java. +```kotlin +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +@MustBeDocumented +@SinceKotlin("...") +@OptionalExpectation +public expect annotation class JvmExposeBoxed +``` -The only possible workaround is writing a part of API in Java taking boxed instances of value classes. -Moreover, constructors of inline classes are synthetic; hence, a factory method is needed. +The actual class is present only on JVM. -4. Suspending functions +Its behavior consists in adding API of the marked inline class as its boxed variant; hence, only `@JvmInline` value classes can be annotated. -Suspending functions can't be successfully called from Java because of necessity to instantiate `Continuation`. -A widely known workaround is to wrap the result of the function to `CompletableFuture`: +In added functions, the value parameters and return value are boxed if their type is inline class marked with new annotation. A hash code mangled implementation of the function for usage from Kotlin should be created as usual. ```kotlin -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.future.future -import java.util.concurrent.CompletableFuture +@JvmInline +@JvmExposeBoxed +value class Example(val s: String) -suspend fun s(): Int = 42 +fun f(x: Example): Example = TODO() +``` -fun sForJava(): CompletableFuture = GlobalScope.future { - s() -} +```java +public static f-NmaSWX8(Ljava/lang/String;)Ljava/lang/String; // for Kotlin +public static f(LExample;)LExample; // for Java ``` -However, this way requires including `org.jetbrains.kotlinx:kotlinx-coroutines-jdk8`, -hence can't be proposed as a new feature of the language. +The current design does not affect properties. If one needs to expose a getter method returning an inline class, it should be created as a function: -Another way to make a workaround similar to way `suspend fun main` is implemented is calling `runSuspend`: ```kotlin -import kotlin.coroutines.jvm.internal.* - -suspend fun s(): Int = 42 +@get:JvmName("getX-impl") +val x: Example = TODO() -@Suppress("INVISIBLE_MEMBER") -fun sForJava(): Int { - var result: Result? = null - runSuspend { - result = Result.success(s()) - } - return result!!.getOrThrow() -} +fun getX() = x ``` -## Proposed API - -Adding a new `@kotlin.jvm.JvmExpose` annotation is proposed to address all the listed problems at once, -so its purpose is to ensure that an API can be called from Java freely -and to handle Kotlin features -that were designed without attention to calling from Java -in order to simplify development of libraries for Java and Kotlin written in Kotlin. - -Its behavior can be described as “annotated function is guaranteed to be available from Java by exactly the name defined either in the code or in the string parameter”; hence, the following limits are imposed: - -1. A `private` function cannot be marked as well as a function defined in private and local classes and objects. -2. A `@JvmSynthetic` function cannot be marked -3. `@JvmOverloads` is assumed - -Marking a `public` function without any features leading to mangling is allowed, for example, to show that the API is “Java-friendly.” +```java +public static getX-impl()Ljava/lang/String; // for Kotlin +public static getX()LExample; // for Java +``` -Example usages: +One of the problems is that one cannot instantiate an inline class with its constructor because it is generated with `private` and `synthetic` byte code flags. A solution for that could be annotating a constructor `@JvmExposeBoxed` to have a constructor exposed by the compiler (it also will create an internal synthetic overload of it taking something like `Nothing?`). ```kotlin -class SomeThings { - @JvmExpose // equivalent to @JvmName("forJava") - internal fun forJava() {} -} +@JvmInline +@JvmExposeBoxed +value class Example(val s: String) ``` -Additionally, marking an `internal` function with `@JvmExpose` designates that calling it from another module in Java is **not** an error, so IDE inspections should handle it. - -Combination of `@JvmExpose internal` and `@JvmSynthetic public` allows creating a completely non-overlapping API for Java and Kotlin; however, it looks like abuse, so, probably, it should be an antipattern: +A `JvmExpose` annotation should be added to the class to achieve the following Java syntax: -```kotlin -class X { - @JvmExpose internal fun a(consumer: java.util.function.Consumer) = consumer.accept(42) - @JvmSynthetic fun a(consumer: (Int) -> Unit) = consumer(42) -} +```java +ExampleKt.f(new Example("42")); ``` -### `JvmExpose` on functions with `@JvmInline` parameters or return type +Usually, the constructor of the inline class is used to perform boxing of it. Annotating it with `@JvmExpose` will lead to creating a new, synthetic constructor (with placeholder parameter of type `java.lang.Void`, probably, to avoid signature clash) for boxing, enabling the default one for the user (making it not synthetic). -Functions related to `@JvmInline value class` require special treatment since their representation in JVM differs significantly from ordinary ones. +All other functions and properties declared in the boxed exposed inline class become available as normal object methods on JVM. Bridges are generated for all of them as it is already done for inline classes implementing interfaces. If one of the bridge methods takes another instance of inline classes, it is taken boxed, too. However, `box-impl` and `unbox-impl` methods are intentionally left unavailable for users to not break encapsulation and constructor invariants. -First, the value parameters and return value of a function marked with `@JvmExpose` should be boxed if their type is inline class. The reason is that inline classes are cumbersome to use from Java in their unboxed form. A mangled implementation of the function for unboxed Kotlin usage should be created as well. +Example of ABI for the following class: ```kotlin -// Example.kt - @JvmInline -value class Example(val s: String) - -@JvmExpose -fun f(x: Example): Example = TODO() +@JvmExposeBoxed +value class Example(val s: String) { + fun x() + fun y(another: Example) +} ``` ```java -public static Example f-impl(java.lang.String x) -public static Example f(Example x) { } +public final class Example { + //// Public ABI intended for Java callers: + + // Visibility matches with the visibility of the s property in the code + public getS()Ljava/lang/String; + public x()V + public y(LExample;)V + + public toString()Ljava/lang/String; + public hashCode()I + public equals(Ljava/lang/Object;)Z + + // Not synthetic constructor, + // visibility matches with the one declared in the code, + // calls constructor-impl + public (Ljava/lang/String;)V + + //// Mangled ABI for Kotlin callers: + public synthetic unbox-impl()Ljava/lang/String; + public static x-impl(Ljava/lang/String;)V + public static y-NmaSWX8(Ljava/lang/String;Ljava/lang/String;)V + public static toString-impl(Ljava/lang/String;)Ljava/lang/String; + public static hashCode-impl(Ljava/lang/String;)I + public static equals-impl(Ljava/lang/String;Ljava/lang/Object;)Z + public static constructor-impl(Ljava/lang/String;)Ljava/lang/String; + public static synthetic box-impl(Ljava/lang/String;)LExample; + public static equals-impl0(Ljava/lang/String;Ljava/lang/String;)Z + + // Synthetic constructor, not calls constructor-impl for boxing + public synthetic (Ljava/lang/String;Ljava/lang/Void;)V + + @Lkotlin/jvm/JvmInline;() + @Lkotlin/jvm/JvmExposeBoxed;() +} ``` -The certain problem is that one cannot instantiate an inline class with its constructor because it is generated with `ACC_SYNTHETIC`. A solution for that could be annotating a constructor `@JvmExpose` to have a constructor exposed by the compiler (it also will create an internal synthetic overload of it taking something like `Nothing?`). Since this requirement is unobvious, an IDE inspection must report that instances of an inline class taken as an argument of the exposed function cannot be instantiated. +(Insignificant details like the `final` flag of methods, all inline classes are final themselves, and nullity annotations are omitted. Order of generation is insignificant, too.) -```kotlin -@JvmInline -value class Example @JvmExpose constructor(val s: String) -``` +## Questions -A `JvmExpose` annotation should be added to the constructor of `Example` to achieve the following Java syntax: +* No interaction with Valhalla described for the value classes case + * https://openjdk.org/projects/valhalla/ + * https://openjdk.org/projects/valhalla/design-notes/state-of-valhalla/01-background + * The investigation for Valhalla compatibility is postponed until the prototype is ready. +* What about MPP + * On other platforms, `@JvmInline` classes do not differ from the usual ones, so they will not be affected by `@JvmExposeBoxed` as well. -```java -ExampleKt.f(new Example("42")); -``` +## Other considered name variants -Usually, constructor of the inline class is used to perform boxing of it. -Annotating it with `@JvmExpose` will lead to creating a new, -synthetic constructor (with placeholder parameter of type `java.lang.Void`, probably, -to avoid signature clash) for boxing, -enabling the default one for user. - -### Suspending exposed functions - -Functions that are both `suspend` and annotated with `@JvmExpose` should not take a continuation as normal ones -because it is impossible to use it in Java conveniently, -so they can block the thread like `suspend fun main` or `@Test suspend` do. -A possible problem is handling functions that actively use kotlinx.coroutines types (from `Deferred` to `Flow`), -which are not convenient for Java users at all. -As in the previous subsection, a mangled normal function should be created for Kotlin: -М -```kotlin -@JvmExpose -suspend fun f(x: Int): Int = TODO() -``` - -```java -public static Object f-impl(int x, @NotNull Continuation $completion) -public static int f(int x) -``` +* `JvmInlineExposed` +* `JvmInlineBoxed` +* `JvmBoxedValue` +* `**JvmExposeBoxed**` From 9b93e955d8647f33b97ada79e18b82ef8c8ce9ce Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Mon, 12 Dec 2022 09:13:04 +0400 Subject: [PATCH 07/10] Formatting --- proposals/annotation-to-mark-accessible-api-for-java.md | 1 - 1 file changed, 1 deletion(-) diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/annotation-to-mark-accessible-api-for-java.md index 88261e198..428ed9a8a 100644 --- a/proposals/annotation-to-mark-accessible-api-for-java.md +++ b/proposals/annotation-to-mark-accessible-api-for-java.md @@ -158,4 +158,3 @@ public final class Example { * `JvmInlineExposed` * `JvmInlineBoxed` * `JvmBoxedValue` -* `**JvmExposeBoxed**` From fa148243d748817e3be34abfac0d0d9c544e4006 Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Mon, 12 Dec 2022 11:53:58 +0400 Subject: [PATCH 08/10] Rename file --- ...-accessible-api-for-java.md => jvm-expose-boxed-annotation.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{annotation-to-mark-accessible-api-for-java.md => jvm-expose-boxed-annotation.md} (100%) diff --git a/proposals/annotation-to-mark-accessible-api-for-java.md b/proposals/jvm-expose-boxed-annotation.md similarity index 100% rename from proposals/annotation-to-mark-accessible-api-for-java.md rename to proposals/jvm-expose-boxed-annotation.md From 95ec688bff9e8091142ff3c4ecda7cc1d941535b Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Mon, 12 Dec 2022 13:30:03 +0400 Subject: [PATCH 09/10] Fix visibility --- proposals/jvm-expose-boxed-annotation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/jvm-expose-boxed-annotation.md b/proposals/jvm-expose-boxed-annotation.md index 428ed9a8a..97c24a9d7 100644 --- a/proposals/jvm-expose-boxed-annotation.md +++ b/proposals/jvm-expose-boxed-annotation.md @@ -135,7 +135,7 @@ public final class Example { public static equals-impl0(Ljava/lang/String;Ljava/lang/String;)Z // Synthetic constructor, not calls constructor-impl for boxing - public synthetic (Ljava/lang/String;Ljava/lang/Void;)V + private synthetic (Ljava/lang/String;Ljava/lang/Void;)V @Lkotlin/jvm/JvmInline;() @Lkotlin/jvm/JvmExposeBoxed;() From 9e87e46ef1c7f5c34ca11218f6bcbae23826c66d Mon Sep 17 00:00:00 2001 From: Iaroslav Postovalov Date: Mon, 12 Dec 2022 17:10:59 +0400 Subject: [PATCH 10/10] Add comment to code --- proposals/jvm-expose-boxed-annotation.md | 67 +++++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/proposals/jvm-expose-boxed-annotation.md b/proposals/jvm-expose-boxed-annotation.md index 97c24a9d7..5e3c91a17 100644 --- a/proposals/jvm-expose-boxed-annotation.md +++ b/proposals/jvm-expose-boxed-annotation.md @@ -107,38 +107,41 @@ value class Example(val s: String) { ```java public final class Example { - //// Public ABI intended for Java callers: - - // Visibility matches with the visibility of the s property in the code - public getS()Ljava/lang/String; - public x()V - public y(LExample;)V - - public toString()Ljava/lang/String; - public hashCode()I - public equals(Ljava/lang/Object;)Z - - // Not synthetic constructor, - // visibility matches with the one declared in the code, - // calls constructor-impl - public (Ljava/lang/String;)V - - //// Mangled ABI for Kotlin callers: - public synthetic unbox-impl()Ljava/lang/String; - public static x-impl(Ljava/lang/String;)V - public static y-NmaSWX8(Ljava/lang/String;Ljava/lang/String;)V - public static toString-impl(Ljava/lang/String;)Ljava/lang/String; - public static hashCode-impl(Ljava/lang/String;)I - public static equals-impl(Ljava/lang/String;Ljava/lang/Object;)Z - public static constructor-impl(Ljava/lang/String;)Ljava/lang/String; - public static synthetic box-impl(Ljava/lang/String;)LExample; - public static equals-impl0(Ljava/lang/String;Ljava/lang/String;)Z - - // Synthetic constructor, not calls constructor-impl for boxing - private synthetic (Ljava/lang/String;Ljava/lang/Void;)V - - @Lkotlin/jvm/JvmInline;() - @Lkotlin/jvm/JvmExposeBoxed;() + //// Public ABI intended for Java callers: + + // Visibility matches with the visibility of the s property in the code + public getS()Ljava/lang/String; + public x()V + public y(LExample;)V + + public toString()Ljava/lang/String; + public hashCode()I + public equals(Ljava/lang/Object;)Z + + // Not synthetic constructor, + // visibility matches with the one declared in the code, + // calls constructor-impl + public (Ljava/lang/String;)V + + //// Mangled ABI for Kotlin callers: + public synthetic unbox-impl()Ljava/lang/String; + public static x-impl(Ljava/lang/String;)V + public static y-NmaSWX8(Ljava/lang/String;Ljava/lang/String;)V + public static toString-impl(Ljava/lang/String;)Ljava/lang/String; + public static hashCode-impl(Ljava/lang/String;)I + public static equals-impl(Ljava/lang/String;Ljava/lang/Object;)Z + + // Constructor for unboxed value + public static constructor-impl(Ljava/lang/String;)Ljava/lang/String; + public static synthetic box-impl(Ljava/lang/String;)LExample; + public static equals-impl0(Ljava/lang/String;Ljava/lang/String;)Z + + // Synthetic constructor, + // not calls constructor-impl for boxing + private synthetic (Ljava/lang/String;Ljava/lang/Void;)V + + @Lkotlin/jvm/JvmInline;() + @Lkotlin/jvm/JvmExposeBoxed;() } ```