Search code examples
javakotlininteropinline

Kotlin Inline interop: enforce boxing


I'd like to better understand java/kotlin interop relative to inline value classes. I have some example code below with my attempts for interop, a few notes:

  1. You can directly import inline value classes in java, i.e. import kotlin.Result
  2. You cannot use their constructor methods in java, i.e. Result.success
  3. You cannot use their methods once constructed in java, i.e. myResult.isSuccess

I created some workaround below. They work... but seem to have their own edge-cases and oddities. I have a few questions:

  1. "boxing" seems to be necessary to preserve type information. Is a way to enforce boxing other than ? (which has the negative of triggering unnecessary null checking). I tried something like fun <T, R: Result<T>> success(value: T): R = Result.success(value as T) as R -- it compiled but threw a java.lang.NoSuchMethodError error at runtime (!!)

  2. is there some other way that this can be done? Are there any modules/libraries that help with kotlin/java inline value interop?

  3. it seems to be a bug that the java <-> kotlin interop here causes the isFailure methods to loose the success/failure status when called from Java (without explicit ? boxing) but I'd appreciate any thoughts!

Boxing as defined here, from which I can see that you can box by using generic and interfaces, but I don't see how I can use those to force a box AND perform logic and type checking.

Kotlin:

@file:JvmName("Inline")

package play

// Note: doesn't compile without `?`: 
//   incompatible types: no instance(s) of type variable(s) T exist so that
//   Object conforms to Result<String>
@JvmName("success") fun <T> success(value: T): Result<T>?
  = Result.success(value)
@JvmName("failure") fun <T> failure(t: Throwable): Result<T>?
  = Result.failure(t)

// Note: java compiles without `?` BUT "failures" report isSuccess=true (!?!)
@JvmName("isSuccess") fun <T> isSuccess(r: Result<T>?): Boolean
  = r!!.isSuccess
@JvmName("isFailure") fun <T> isFailure(r: Result<T>?): Boolean
  = r!!.isFailure

java test code:

package play;

import static org.junit.Assert.assertTrue;

import kotlin.Result;
import org.junit.Test;                                                    
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;                            
                                                                          
@RunWith(JUnit4.class)
public class InlineTest { 
  @Test                                                                   
  public void testResult() {
    Result<String> rString = Inline.success("hi");                        
    assertTrue(Inline.isSuccess(rString));
    assertSuccess(rString);

    Result<Integer> rInt = Inline.success(42);
    assertTrue(Inline.isSuccess(rInt));

    Result<Throwable> fail = Inline.failure(new Exception("test"));
    // Note: fails unless isFailure using `?` type
    assertTrue(Inline.isFailure(fail));
  }

  // just proves you can pass them to java functions
  <T>void assertSuccess(Result<T> r) {
    assertTrue(Inline.isSuccess(r));
  }
}

Solution

  • You cannot use their constructor methods in java, i.e. Result.success

    Normally, methods of the companion object of a value class can be accessed from Java as usual. However, Result.success and Result.failure are marked @InlineOnly.

    You cannot use their methods once constructed in java, i.e. myResult.isSuccess

    This is expected. Properties and methods of inline classes are compiled to static methods taking a parameter of the wrapped type, to avoid boxing. They also have a mangled name, so you cannot access them from Java.

    The NoSuchMethodError is a bug of either the Java or Kotlin compiler (I'm not sure). It should either not compile your code, or compile it without throwing a NoSuchMethodError at runtime. This is possibly related to KT-26131.

    What happens here is that the success you wrote gets compiled to

    Object success(Object value)
    

    instead of

    Result success(Object value)
    

    The Java code looks for a method that matches the latter, which doesn't exist.

    isFailure always returns false when using the unboxed Result<T>, because isFailure determines whether the result is a failure by checking if it is an instance of an internal Failure type. Obviously, the instance of Result you passed in from Java is not Failure. If you use the boxed Result<T>?, then an extra unbox operation will be generated.

    Some Java pseudocode to illustrate the difference:

    // for Result<T>
    public static boolean isFailure(Object r) {
        return r instanceof Failure;
    }
    
    // for Result<T>?
    public static boolean isFailure(Result r) {
        return r.unbox() instanceof Failure;
    }
    

    If you have control over the inline class, an alternative way to use it in Java is to have it implement an interface, like my answer demonstrates here.

    Otherwise, I'd just write my own non-inline wrapper, that wraps the inline class.