SpaceBison
SpaceBison

Reputation: 3171

Call a function with default parameter values using reflection

Consider an interface:

interface Foo {
    fun func(argWithDefault: String = "default")
}

and its (minimal) implementation:

class FooImpl() : Foo {
    override fun func(argWithDefault: String) {
        println(argWithDefault)
    }
}

I'm trying too call func on FooImpl via reflection:

val func = Foo::func
val instance = FooImpl()
func.callBy(mapOf(func.instanceParameter!! to instance))

but I get the following exception:

kotlin.reflect.jvm.internal.KotlinReflectionInternalError: This callable does not support a default call: public abstract fun func(argWithDefault: kotlin.String = ...): kotlin.Unit defined in com.example.Foo[DeserializedSimpleFunctionDescriptor@2d7275fc]

Foo::func.parameters[1].isOptional returns true, and, according to the docs, isOptional returns

true if this parameter is optional and can be omitted when making a call via KCallable.callBy, or false otherwise.

so I guess this should be possible.

Upvotes: 2

Views: 2112

Answers (1)

Roland
Roland

Reputation: 23262

tl;dr: this seems to be a known issue: KT-13936: KotlinReflectionInternalError on invoking callBy on overridden member with inherited default argument value

Some analysis which I did before:

If we look at what Kotlin actually generates here, it rather becomes obvious why you don't have any default values anymore. Bytecode of Foo:

public abstract interface Foo {

  public abstract func(Ljava/lang/String;)V
    LOCALVARIABLE this LFoo; L0 L1 0
    LOCALVARIABLE argWithDefault Ljava/lang/String; L0 L1 1

  public final static INNERCLASS Foo$DefaultImpls Foo DefaultImpls
}


// ================Foo$DefaultImpls.class =================
  public static synthetic func$default(LFoo;Ljava/lang/String;ILjava/lang/Object;)V
    ALOAD 3
    IFNULL L0
    NEW java/lang/UnsupportedOperationException
    DUP
    LDC "Super calls with default arguments not supported in this target, function: func"
    INVOKESPECIAL java/lang/UnsupportedOperationException.<init> (Ljava/lang/String;)V
    ATHROW
   L0
    ILOAD 2
    ICONST_1
    IAND
    IFEQ L1
   L2
    LINENUMBER 2 L2
    LDC "default" // <--- here is our default value? but where are we? Foo$Defaults.func$default?
    ASTORE 1
   L1
    ALOAD 0
    ALOAD 1
    INVOKEINTERFACE Foo.func (Ljava/lang/String;)V (itf)
    RETURN
    MAXSTACK = 3
    MAXLOCALS = 4

  public final static INNERCLASS Foo$DefaultImpls Foo DefaultImpls
  // compiled from: Foo.kt
}

So the declared function within Foo is just fun foo(String)... No default value anywhere, except in a synthetic function in its own DefaultImpls-class.

So where is that function called... FooImpl maybe?

If we look at the FooImpl-bytecode it becomes clear that it isn't there:

public final class FooImpl implements Foo {

  public func(Ljava/lang/String;)V
   L0
    ALOAD 1
    LDC "argWithDefault"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 3 L1
   L2
    ICONST_0
    ISTORE 2
   L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L4
   L5
    LINENUMBER 4 L5
    RETURN
   L6
    LOCALVARIABLE this LFooImpl; L0 L6 0
    LOCALVARIABLE argWithDefault Ljava/lang/String; L0 L6 1
    MAXSTACK = 2
    MAXLOCALS = 3

...

So for completeness I also created a Caller.kt just containing the following code:

fun main() = FooImpl().func()

and finally, bytecode again, but also our DefaultImpls.func$default-call:

public final class CallerKt {


  public final static main()V
   L0
    LINENUMBER 1 L0
    NEW FooImpl
    DUP
    INVOKESPECIAL FooImpl.<init> ()V
    ACONST_NULL
    ICONST_1
    ACONST_NULL
    INVOKESTATIC Foo$DefaultImpls.func$default (LFoo;Ljava/lang/String;ILjava/lang/Object;)V // aha! here is our DefaultImpls.func$default!
    RETURN
   L1
    MAXSTACK = 4
    MAXLOCALS = 0

So ... as the DefaultImpls is just generated by the compiler and used when needed, the reflect utilities could (and in my opinion should) reflect this fact too... I added a similar known issue regarding open classes (which use synthetic ...$default-functions as well) at the top of the answer. If that issue doesn't fully address your concern, you may want to open up a new Kotlin issue instead.

Just played with the linked issue a bit... actually the sample there would work if A::foo is used instead of B::foo (A is the open class and B extends A). However as the A there would basically be similar to your Foo-interface, the interface needs special treatment regarding this nonetheless.

Upvotes: 2

Related Questions