Skip to content

Commit

Permalink
Reformulate the idea of JvmExpose
Browse files Browse the repository at this point in the history
  • Loading branch information
CommanderTvis committed Nov 18, 2022
1 parent 73ce51f commit 481e122
Showing 1 changed file with 74 additions and 93 deletions.
167 changes: 74 additions & 93 deletions proposals/annotation-to-mark-accessible-api-for-java.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>) {}
fun takeList(list: List<Long>) {}
// 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<Int>) {}

@JvmName("takeListOfLong")
fun takeList(list: List<Long>) {}
```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<Int> = 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<Int>? = 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() {}
}
```
Expand All @@ -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<Int>) = consumer.accept(42)
@JvmExpose internal fun a(consumer: java.util.function.Consumer<Int>) = 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<Int>) {}
@JvmExpose("takeListOfLong")
fun takeList(list: List<Long>) {}
```

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
Expand All @@ -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).

0 comments on commit 481e122

Please sign in to comment.