Daniele Segato
11/12/2022, 5:40 PMjava.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
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.jw
11/12/2022, 6:23 PMjw
11/12/2022, 6:24 PMDaniele Segato
11/12/2022, 6:24 PMjw
11/12/2022, 6:25 PMDaniele Segato
11/12/2022, 6:27 PMDaniele Segato
11/12/2022, 6:40 PM/**
* 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 dayDaniele Segato
11/12/2022, 6:52 PMjw
11/12/2022, 7:16 PM@Throws(Exception::class)
or your have to wrap it in an unchecked exception.Daniele Segato
11/12/2022, 7:19 PMDaniele Segato
11/13/2022, 12:17 AMjw
11/13/2022, 12:57 AMephemient
11/13/2022, 3:35 AMjava.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)jw
11/13/2022, 4:11 AMDaniele Segato
11/13/2022, 9:58 AMDaniele Segato
11/13/2022, 1:56 PMKotlinExtensions
from Java.
I needed a way to call that code from kotlin but escaping the compiler check for suspend
This is copied from retrofit
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
final class KotlinExtensionsJavaBridge {
static void suspendAndThrow(Exception e, Continuation<?> continuation) {
KotlinExtensions.suspendAndThrow(e, continuation);
}
}
Back to kotlin world via Java minus the suspend
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
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 outsideDaniele Segato
11/13/2022, 2:20 PMI'm not sure how to unit test this from outsideah, stupid me, it's actually very easy to test, just need a suspend fun that doesn't actually suspend and throw the exception...
Daniele Segato
11/13/2022, 2:22 PMNotImplementedError
Daniele Segato
11/13/2022, 2:57 PMstatic Object suspendAndThrow(Exception e, Continuation<?> continuation) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
and the kotlin wrapper also has to return
internal fun Exception.suspendAndThrowViaJava(continuation: Continuation<*>): Any {
return KotlinExtensionsJavaBridge.suspendAndThrow(this, continuation)
}
Finally the proxy need some more shenanigans...
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!ephemient
11/13/2022, 4:27 PMmove-exception
instruction, so it wouldn't work for thisephemient
11/27/2022, 12:28 PM