Is there an idiomatic way to create a dynamic Prox...
# coroutines
d
Is there an idiomatic way to create a dynamic Proxy in Kotlin? I've been trying with
java.reflect.Proxy
with awful results This proxy job is to intercept, detect if a particular exception is throw and if so unwrap it and throw the underlying exception. This has to work with suspend functions
Copy code
class UnwrapExceptionProxy<T>(private val instance: T): InvocationHandler {
    @Suppress("UNCHECKED_CAST")
    override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
        val function = method.kotlinFunction!!

        return if (function.isSuspend) {
            val parameters = Array(args!!.size - 1) {args[it] }
            val continuation = args.last() as Continuation<Any?>
            method.invoke(instance, *parameters, Continuation<Any?>(continuation.context) { result ->
                val wrappedError = result.exceptionOrNull() as? MyWrapException
                if (wrappedError == null) {
                    continuation.resumeWith(result)
                } else {
                    continuation.resumeWith(Result.failure(wrappedError.wrapped))
                }
            })
        } else {
            val parameters = args ?: arrayOf()
            try {
                method.invoke(instance, *parameters)
            } catch (e: MyWrapException) {
                throw e.wrapped
            }
        }
    }
}
1. First, I'm not sure if I've handled the suspend function correctly 2. I get
java.lang.reflect.UndeclaredThrowableException
every time my kotlin code throws a non
RuntimeException
Is there no way to write a Proxy for kotlin? I cannot force declaring all checked exceptions and I wanted to make this as transparent as possible.
j
I'm on mobile but you can look at Retrofit
d
Thanks! reading!
j
Note that our workaround has changed thanks to Roman so check the source
d
I'll go look for it as soon as I finished reading the article
Copy code
/**
 * Force the calling coroutine to suspend before throwing [this].
 *
 * This is needed when a checked exception is synchronously caught in a [java.lang.reflect.Proxy]
 * invocation to avoid being wrapped in [java.lang.reflect.UndeclaredThrowableException].
 *
 * The implementation is derived from:
 * <https://github.com/Kotlin/kotlinx.coroutines/pull/1667#issuecomment-556106349>
 */
internal suspend fun Exception.suspendAndThrow(): Nothing {
  suspendCoroutineUninterceptedOrReturn<Nothing> { continuation ->
    Dispatchers.Default.dispatch(continuation.context) {
      continuation.intercepted().resumeWithException(this@suspendAndThrow)
    }
    COROUTINE_SUSPENDED
  }
}
that was quite a read, thanks Jake! Have a great day
I think that helps with coroutines, but for regular functions I've still no idea how fo stop Proxy to throw that exception :-)
j
you have no choice, really. you either annotate it with
@Throws(Exception::class)
or your have to wrap it in an unchecked exception.
d
Yeah. I wonder why there's no Proxy equivalency for Kotlin
I'm curious. How did you find out the exception was caused by preemption? Seems insanely difficult to reproduce and it's not obvious at all.
j
A hunch, a loop running a test 10000 times, and some print statements
e
you could do this with runtime bytecode generation instead of using
java.lang.invoke.Proxy
(my example here uses ASM and only works on Java 9+, but you could probably find ways to go lower… as long as you're not targeting Android)
j
You'd need dexmaker on Android
d
I am targeting Android 🙂 thanks tho' I didn't know it was possible. It turns out I can actually stick with suspend functions only so I will put to good use Jake link and the retrofit repository, but that other method is interesting. Thank you
May I ask if this looks good to you? I wanted to keep the Proxy code in Kotlin, in Retrofit you use
KotlinExtensions
from Java. I needed a way to call that code from kotlin but escaping the compiler check for
suspend
This is copied from retrofit
Copy code
internal suspend fun Exception.suspendAndThrow(): Nothing {
    suspendCoroutineUninterceptedOrReturn<Nothing> { continuation ->
        Dispatchers.Default.dispatch(continuation.context) {
            continuation.intercepted().resumeWithException(this@suspendAndThrow)
        }
        COROUTINE_SUSPENDED
    }
}
Java bridge: to be able to escape the suspend from kotlin -- couldn't find a way to with only kotlin
Copy code
final class KotlinExtensionsJavaBridge {
    static void suspendAndThrow(Exception e, Continuation<?> continuation) {
        KotlinExtensions.suspendAndThrow(e, continuation);
    }
}
Back to kotlin world via Java minus the
suspend
Copy code
internal fun Exception.suspendAndThrowViaJava(continuation: Continuation<*>): Nothing {
    KotlinExtensionsJavaBridge.suspendAndThrow(this, continuation)
    throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}
And finally my
InvokationHandler
(assumption = only suspend functions here) using the code above
Copy code
internal class UnwrapExceptionProxy<T>(private val instance: T): InvocationHandler {
    @Suppress("UNCHECKED_CAST")
    override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
        args!! // suspend function always have args
        val parameters = Array(args.size - 1) {args[it] }
        val continuation = args.last() as Continuation<Any?>
        val unWrapContinuation = Continuation(continuation.context) { result ->
            val wrappedError = result.exceptionOrNull() as? MyWrapException
            if (wrappedError == null) {
                continuation.resumeWith(result)
            } else {
                continuation.resumeWith(Result.failure(wrappedError.wrapped))
            }
        }

        return try {
            method.invoke(instance, *parameters, unWrapContinuation)
        } catch (e: Exception) {
            e.suspendAndThrowViaJava(unWrapContinuation)
        }
    }
}
I'm not sure how to unit test this from outside
I'm not sure how to unit test this from outside
ah, stupid me, it's actually very easy to test, just need a suspend fun that doesn't actually suspend and throw the exception...
and nope, my wrap isn't working: I receive the
NotImplementedError
Right I got it The java bridge has to return
Copy code
static Object suspendAndThrow(Exception e, Continuation<?> continuation) {
    return KotlinExtensions.suspendAndThrow(e, continuation);
}
and the kotlin wrapper also has to return
Copy code
internal fun Exception.suspendAndThrowViaJava(continuation: Continuation<*>): Any {
    return KotlinExtensionsJavaBridge.suspendAndThrow(this, continuation)
}
Finally the proxy need some more shenanigans...
Copy code
return try {
    method.invoke(instance, *parameters, unWrapContinuation)
} catch (e: InvocationTargetException) {
    val target = e.targetException.let {
        if (it is UndeclaredThrowableException) {
            it.undeclaredThrowable
        } else it
    }
    if (target !is Exception) throw e
    target.suspendAndThrowViaJava(unWrapContinuation)
}
seems like it is working now sorry for the spamming of messages, I wouldn't even know that I would be introducing a bug if you didn't answer me Jake, thank you!
e
took a peek at DexMaker - maybe I'm missing something but it doesn't seem to have a way to emit the
move-exception
instruction, so it wouldn't work for this
not that you should be using bytecode generation if you can help it, but just for the sake of completeness, I updated my earlier example to support Android via dalvik-dx and older Java via temporary classloaders (classloader trick only works if the interface is public though)
790 Views