Skip to content

Commit 443bd03

Browse files
committed
[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/902554256035426315 dotnet/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&lt;? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R&gt;" /> > > now it produces: > > <parameter name="this" type="R" jni-type="TR;" /> > <parameter name="initial" type="kotlin.jvm.functions.Function2&lt;? super R, ? super kotlin.coroutines.CoroutineContext.Element, ? extends R&gt;" /> 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)
1 parent e56a8c8 commit 443bd03

File tree

6 files changed

+110
-92
lines changed

6 files changed

+110
-92
lines changed

src/Xamarin.Android.Tools.Bytecode/Methods.cs

Lines changed: 11 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public ParameterInfo[] GetParameters ()
9393
UpdateParametersFromMethodParametersAttribute (parameters);
9494
return parameters;
9595
}
96+
9697
static IEnumerable<string> ExtractTypesFromSignature (string signature)
9798
{
9899
if (signature == null || signature.Length < "()V".Length)
@@ -106,6 +107,7 @@ static IEnumerable<string> ExtractTypesFromSignature (string signature)
106107
yield return Signature.ExtractType (signature, ref index);
107108
}
108109
}
110+
109111
List<ParameterInfo> GetParametersFromDescriptor (out int endParams)
110112
{
111113
var signature = Descriptor;
@@ -165,43 +167,18 @@ void UpdateParametersFromLocalVariables (ParameterInfo[] parameters)
165167
return;
166168

167169
var names = locals.LocalVariables.Where (p => p.StartPC == 0).ToList ();
168-
int namesStart = 0;
169-
if (!AccessFlags.HasFlag (MethodAccessFlags.Static) &&
170-
names.Count > namesStart &&
171-
names [namesStart].Descriptor == DeclaringType.FullJniName) {
172-
namesStart++; // skip `this` parameter
173-
}
174-
if (!DeclaringType.IsStatic &&
175-
IsConstructor &&
176-
names.Count > namesStart &&
177-
DeclaringType.InnerClass != null && DeclaringType.InnerClass.OuterClassName != null &&
178-
names [namesStart].Descriptor == "L" + DeclaringType.InnerClass.OuterClassName + ";") {
179-
namesStart++; // "outer `this`", for non-static inner classes
180-
}
181-
if (!DeclaringType.IsStatic &&
182-
IsConstructor &&
183-
names.Count > namesStart &&
184-
DeclaringType.TryGetEnclosingMethodInfo (out var declaringClass, out var _, out var _) &&
185-
names [namesStart].Descriptor == "L" + declaringClass + ";") {
186-
namesStart++; // "outer `this`", for non-static inner classes
187-
}
188-
189-
// For JvmOverloadsConstructor.<init>.(LJvmOverloadsConstructor;IILjava/lang/String;)V
190-
if (namesStart > 0 &&
191-
names.Count > namesStart &&
192-
parameters.Length > 0 &&
193-
names [namesStart].Descriptor != parameters [0].Type.BinaryName &&
194-
names [namesStart-1].Descriptor == parameters [0].Type.BinaryName) {
195-
namesStart--;
196-
}
197170

198171
int parametersCount = GetDeclaredParametersCount (parameters);
199-
CheckDescriptorVariablesToLocalVariables (parameters, parametersCount, names, namesStart);
200172

201-
int max = Math.Min (parametersCount, names.Count - namesStart);
202-
for (int i = 0; i < max; ++i) {
203-
parameters [i].Name = names [namesStart+i].Name;
204-
CheckLocalVariableTypeToDescriptorType (i, parameters, names, namesStart);
173+
for (int pi = parametersCount-1; pi >= 0; --pi) {
174+
for (int ni = names.Count-1; ni >= 0; --ni) {
175+
if (parameters [pi].Type.BinaryName != names [ni].Descriptor) {
176+
continue;
177+
}
178+
parameters [pi].Name = names [ni].Name;
179+
names.RemoveAt (ni);
180+
break;
181+
}
205182
}
206183
}
207184

@@ -273,60 +250,6 @@ void UpdateParametersFromSignature (ParameterInfo[] parameters)
273250
}
274251
}
275252

276-
void CheckDescriptorVariablesToLocalVariables (ParameterInfo[] parameters, int parametersCount, List<LocalVariableTableEntry> names, int namesStart)
277-
{
278-
if (AccessFlags.HasFlag (MethodAccessFlags.Synthetic))
279-
return;
280-
if ((names.Count - namesStart) == parametersCount)
281-
return;
282-
if (IsEnumCtor)
283-
return;
284-
285-
var paramsDesc = CreateParametersList (parameters, (v, i) => $"`{v.Type.BinaryName}` {v.Name}{(i >= parametersCount ? " /* abi; ignored */" : "")}");
286-
var localsDesc = CreateParametersList (names, (v, i) => $"`{v.Descriptor}` {v.Name}{(i < namesStart ? " /* abi; skipped */" : "")}");
287-
288-
Log.Debug ($"class-parse: method {DeclaringType.ThisClass.Name.Value}.{Name}.{Descriptor}: namesStart={namesStart}; " +
289-
$"Local variables array has {names.Count - namesStart} entries {localsDesc}; " +
290-
$"descriptor has {parametersCount} entries {paramsDesc}!");
291-
}
292-
293-
static string CreateParametersList<T>(IEnumerable<T> values, Func<T, int, string> createElement)
294-
{
295-
var description = new StringBuilder ()
296-
.Append ("(");
297-
298-
int index = 0;
299-
var first = true;
300-
foreach (var v in values) {
301-
if (!first) {
302-
description.Append (", ");
303-
}
304-
first = false;
305-
description.Append (createElement (v, index));
306-
index++;
307-
}
308-
description.Append (")");
309-
310-
return description.ToString ();
311-
}
312-
313-
void CheckLocalVariableTypeToDescriptorType (int index, ParameterInfo[] parameters, List<LocalVariableTableEntry> names, int namesStart)
314-
{
315-
if (AccessFlags.HasFlag (MethodAccessFlags.Synthetic))
316-
return;
317-
318-
var parameterType = parameters [index].Type.BinaryName;
319-
var descriptorType = names [index + namesStart].Descriptor;
320-
if (parameterType == descriptorType)
321-
return;
322-
323-
var paramsDesc = CreateParametersList (parameters, (v, i) => $"`{v.Type.BinaryName}` {v.Name}");
324-
var localsDesc = CreateParametersList (names, (v, i) => $"`{v.Descriptor}` {v.Name}{(i < namesStart ? " /* abi; skipped */" : "")}");
325-
326-
Log.Debug ($"class-parse: method {DeclaringType.ThisClass.Name.Value}.{Name}.{Descriptor}: " +
327-
$"Local variables array {localsDesc} element {index+namesStart} with type `{descriptorType}` doesn't match expected descriptor list {paramsDesc} element {index} with type `{parameterType}`.");
328-
}
329-
330253
void CheckDescriptorVariablesToSignatureParameters (ParameterInfo[] parameters, int parametersCount, MethodTypeSignature sig)
331254
{
332255
if (IsEnumCtor)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System;
2+
3+
using Xamarin.Android.Tools.Bytecode;
4+
5+
using NUnit.Framework;
6+
7+
namespace Xamarin.Android.Tools.BytecodeTests {
8+
9+
[TestFixture]
10+
public class JavaTypeNoParametersTests : ClassFileFixture {
11+
12+
const string JavaType = "JavaTypeNoParameters";
13+
14+
[Test]
15+
public void ClassFile_WithNonGenericGlobalType_class ()
16+
{
17+
var c = LoadClassFile (JavaType + ".class");
18+
new ExpectedTypeDeclaration {
19+
MajorVersion = 0x34,
20+
MinorVersion = 0,
21+
ConstantPoolCount = 18,
22+
AccessFlags = ClassAccessFlags.Public | ClassAccessFlags.Super,
23+
FullName = "com/xamarin/JavaTypeNoParameters",
24+
Superclass = new TypeInfo ("java/lang/Object", "Ljava/lang/Object;"),
25+
Methods = {
26+
new ExpectedMethodDeclaration {
27+
Name = "<init>",
28+
AccessFlags = MethodAccessFlags.Public,
29+
ReturnDescriptor = "V",
30+
Parameters = {
31+
new ParameterInfo ("copy", "Lcom/xamarin/JavaTypeNoParameters;", "Lcom/xamarin/JavaTypeNoParameters;"),
32+
},
33+
},
34+
}
35+
}.Assert (c);
36+
}
37+
38+
[Test]
39+
public void XmlDeclaration_WithNonGenericGlobalType_class ()
40+
{
41+
AssertXmlDeclaration (JavaType + ".class", JavaType + ".xml");
42+
}
43+
}
44+
}
45+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<api
2+
api-source="class-parse">
3+
<package
4+
name="com.xamarin"
5+
jni-name="com/xamarin">
6+
<class
7+
abstract="false"
8+
deprecated="not deprecated"
9+
jni-extends="Ljava/lang/Object;"
10+
extends="java.lang.Object"
11+
extends-generic-aware="java.lang.Object"
12+
final="false"
13+
name="JavaTypeNoParameters"
14+
jni-signature="Lcom/xamarin/JavaTypeNoParameters;"
15+
source-file-name="JavaTypeNoParameters.java"
16+
static="false"
17+
visibility="public">
18+
<constructor
19+
deprecated="not deprecated"
20+
final="false"
21+
name="JavaTypeNoParameters"
22+
static="false"
23+
visibility="public"
24+
bridge="false"
25+
synthetic="false"
26+
jni-signature="(Lcom/xamarin/JavaTypeNoParameters;)V">
27+
<parameter
28+
name="copy"
29+
type="com.xamarin.JavaTypeNoParameters"
30+
jni-type="Lcom/xamarin/JavaTypeNoParameters;" />
31+
</constructor>
32+
</class>
33+
</package>
34+
</api>

tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\JavaType%24RNC%24RPNC.class" />
3939
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\JavaType%24RNC.class" />
4040
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\JavaType.class" />
41+
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\JavaTypeNoParameters.class" />
4142
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\NestedInterface%24DnsSdTxtRecordListener.class" />
4243
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\NestedInterface.class" />
4344
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\ParameterAbstractClass.class" />

tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.targets

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
<Project>
22

33
<ItemGroup>
4-
<TestJar Include="java\**\*.java" Exclude="java\java\util\Collection.java,java\android\annotation\NonNull.java" />
5-
<TestJarNoParameters Include="java\java\util\Collection.java" />
4+
<TestJarNoParameters Include="java/java/util/Collection.java" />
5+
<TestJarNoParameters Include="java/**/*NoParameters.java" />
6+
<TestJar Include="java\**\*.java" Exclude="@(TestJarNoParameters);java\android\annotation\NonNull.java" />
67
<TestKotlinJar Include="kotlin\**\*.kt" />
78
</ItemGroup>
89

10+
<ItemGroup>
11+
<_BuildClassOutputs Include="@(TestJar->'$(IntermediateOutputPath)classes\%(RecursiveDir)%(Filename).class')" />
12+
<_BuildClassOutputs Include="@(TestJarNoParameters->'$(IntermediateOutputPath)classes\%(RecursiveDir)%(Filename).class')" />
13+
</ItemGroup>
14+
915
<Target Name="BuildClasses"
1016
BeforeTargets="BeforeBuild"
11-
Inputs="@(TestJar)"
12-
Outputs="@(TestJar->'$(IntermediateOutputPath)classes\%(RecursiveDir)%(Filename).class')">
17+
Inputs="@(TestJar);@(TestJarNoParameters)"
18+
Outputs="@(_BuildClassOutputs)">
1319
<MakeDir Directories="$(IntermediateOutputPath)classes" />
1420
<Exec Command="&quot;$(JavaCPath)&quot; -parameters $(_JavacSourceOptions) -g -d &quot;$(IntermediateOutputPath)classes&quot; java/android/annotation/NonNull.java @(TestJar->'%(Identity)', ' ')" />
1521
<Exec Command="&quot;$(JavaCPath)&quot; $(_JavacSourceOptions) -g -d &quot;$(IntermediateOutputPath)classes&quot; @(TestJarNoParameters->'%(Identity)', ' ')" />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.xamarin;
2+
3+
public class JavaTypeNoParameters {
4+
/**
5+
* JNI sig: (Lcom/xamarin/JavaTypeNoParameters;)V
6+
*/
7+
public JavaTypeNoParameters (JavaTypeNoParameters copy) {
8+
}
9+
}

0 commit comments

Comments
 (0)