You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[class-parse] Loosely match parameter names, backwards
Context: 8ccb837
Context: dotnet/android-libraries#413
Context: https://discord.com/channels/732297728826277939/732297837953679412/902301741159182346
Context: https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.5.31/kotlin-stdlib-1.5.31.jar
Context: https://discord.com/channels/732297728826277939/732297837953679412/902554256035426315dotnet/android-libraries#413 ran into an issue:
D:\a\1\s\generated\org.jetbrains.kotlin.kotlin-stdlib\obj\Release\net6.0-android\generated\src\Kotlin.Coroutines.AbstractCoroutineContextElement.cs(100,8):
error CS1002: ; expected
The offending line:
var this = Java.Lang.Object.GetObject<Java.Lang.Object> (native_this, JniHandleOwnership.DoNotTransfer);
(Assigning to `this` makes for a very weird error message.)
This was eventually tracked down to commit 8ccb837; @jpobst wrote:
> previously it produced:
>
> <parameter name="initial" type="R" jni-type="TR;" />
> <parameter name="operation" type="kotlin.jvm.functions.Function2<? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R>" />
>
> now it produces:
>
> <parameter name="this" type="R" jni-type="TR;" />
> <parameter name="initial" type="kotlin.jvm.functions.Function2<? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R>" />
The (a?) "source" of the problem is that Kotlin is "weird": it emits
a Java method with signature:
/* partial */ class AbstractCoroutineContextElement {
public Object fold(Object initial, Function2 operation);
}
However, the local variables table declares *three* local variables:
1. `this` of type `kotlin.coroutines.CoroutineContext.Element`
2. `initial` of type `java.lang.Object`
3. `operation` of type `Function2`
This is an instance method, so normally we would skip the first
local variable, as "normally" the first local variable of an instance
method has the same type as the declaring type.
The "weirdness" with Kotlin is that the first local parameter type
is *not* the same as the declaring type, it's of the implemented
interface type!
% mono class-parse.exe --dump kotlin/coroutines/AbstractCoroutineContextElement.class
…
ThisClass: Utf8("kotlin/coroutines/AbstractCoroutineContextElement")
…
3: fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; Public
Code(13, Unknown[LineNumberTable](6),
LocalVariableTableAttribute(
LocalVariableTableEntry(Name='this', Descriptor='Lkotlin/coroutines/CoroutineContext$Element;', StartPC=0, Index=0),
LocalVariableTableEntry(Name='initial', Descriptor='Ljava/lang/Object;', StartPC=0, Index=1),
LocalVariableTableEntry(Name='operation', Descriptor='Lkotlin/jvm/functions/Function2;', StartPC=0, Index=2)))
Signature(<R:Ljava/lang/Object;>(TR;Lkotlin/jvm/functions/Function2<-TR;-Lkotlin/coroutines/CoroutineContext$Element;+TR;>;)TR;)
RuntimeInvisibleParameterAnnotationsAttribute(Parameter0(), Parameter1(Annotation('Lorg/jetbrains/annotations/NotNull;', {})))
…
Here, we "expect" the `this` local variable to be of type
`kotlin.coroutines.AbstractCoroutineContextElement`, but it is
instead of type `kotlin.coroutines.CoroutineContext.Element`.
This "type mismatch" means that our logic to skip the first local
variable doesn't actually skip the first local variable.
But wait, Kotlin can throw differently weird stuff at us, too.
See e.g. [inline and reified type parameters][0], which can result in
local parameter names such as `$i$f$get`, or see e.g.
[`CoroutinesRoom.execute()`][1], in which the local variable table
*lacks* a name for one of the JNI signature parameter types.
To better address these scenarios, relax and rework the logic in
`MethodInfo.UpdateParametersFromLocalVariables()`: instead of
requiring that we know the "start" offset between the local variable
names and the parameters (previous world order), instead:
1. Given `names` from local variables table, and `parameters` from
the JNI signature,
2. For each element in `parameters`, going *backards*, from the end
of `parameters` to the front,
3. Compare the `parameters` element type to each item in `names`,
traversing `names` backwards as well.
4. When the parameter types match, set the `parameters` element name
to the `names` element name, then *remove* the `names` element
from `names`. This prevents us from reusing the local variable
for other parameters.
We need to do this backwards so that we "skip"/"ignore" extra
parameters at the start of the local variable name table (which is
usually the `this` parameter).
*Not* requiring that a "run" of parameter types match also grants
flexibility, as when there is no local variable for a given
parameter, we won't care.
This allows to "cleanly" handle `fold()`: we'll look at `names` for
a match for the `Function2` type, find `operation`, then look at
`names` to match the `Object` type, find `initial`, then finish.
Update `Xamarin.Android.Tools.Bytecode-Tests.targets` so that there
are more `.java` files built *without* `java -parameters`, so that
the "parameter name inference" logic is actually tested.
(When `javac -parameters` is used, the `MethodParametersAttribute`
blob is emitted, which contains proper parameter names.)
[0]: https://medium.com/swlh/inline-and-reified-type-parameters-in-kotlin-c7585490e103
[1]: dotnet#900 (comment)
$"Local variables array {localsDesc} element {index+namesStart} with type `{descriptorType}` doesn't match expected descriptor list {paramsDesc} element {index} with type `{parameterType}`.");
0 commit comments