Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

Commit f91b077

Browse files
authored
[Xamarin.Android.Tools.Bytecode] Improve Kotlin metadata matching (#946)
Fixes: #945 Context: dotnet/android-libraries#436 (comment) Consider the following Kotlin function: fun get (index: Int) : UInt { … } When `get` is compiled with Kotlin 1.5, `class-parse` would emit: <method abstract="false" final="true" name="get-pVg5ArA" return="uint" jni-return="I" static="true" visibility="public" jni-signature="([II)I"> <parameter name="$this" type="int[]" jni-type="[I" /> <parameter name="index" type="int" jni-type="I" /> </method> However, when `get` is compiled with Kotlin 1.6, `class-parse` sees: <method abstract="false" final="true" name="get-pVg5ArA" return="int" jni-return="I" static="true" visibility="public" jni-signature="([II)I"> <parameter name="arg0" type="int[]" jni-type="[I" /> <parameter name="index" type="int" jni-type="I" /> </method> Note that the name of the first `int[]` parameter changes from `$this` to `arg0`, and the return type changed from `uint` to `int`. The parameter name change is annoying; the return type change is an ABI break, and makes many package updates impossible. What happened? Recall commit 71afce5: Kotlin-compiled `.class` files contain a `kotlin.Metadata` type-level annotation blob, which contains a Protobuf stream containing Kotlin metadata for the type. The `kotlin.Metadata` annotation contains information about Kotlin functions, and `class-parse` needs to associate the information about the Kotlin functions to Java methods. This can be tricky because there is not an explicit way to do this, and one must rely on trying to match function names and parameter types. Previously, this was accomplished by looping through the parameters and comparing types. However, the `kotlin.Metadata` annotation only includes "user created" method parameters, while the Java method may include "compiler created" method parameters. We attempted to skip these by ignoring Java parameters whose names begin with a dollar sign (ex `$this`). This "worked" most of the time. This broke in Kotlin 1.6, as the compiler generated method parameters stopped using `$` for compiler-generated parameter names. algorithm. We need something better. The `kKotlin.Metadata` annotation provides a `JvmSignature` property -- which matches the `JvmName` property which we already use -- which sounds promising. However, `JvmSignature` is sometimes `null`, and sometimes it doesn't actually match. But it's closer. So now we try to match with signatures. * If `JvmSignature` matches the Java signature we use that. * If `JvmSignature` is `null`, calculate it from the Kotlin parameters and try to match with that. * Note it wants unsigned types as `Lkotlin/UInt;` and not the primitive encoding (`I`). * If Kotlin metadata defines a `ReceiverType`, add that as the first parameter to the signature. Using `kotlin-stdlib-1.5.31.jar` as a baseline, we get much better results: | Version | Matched Functions | Unmatched Functions | | --------------- | ----------------: | --------------------: | | main @ 32635fd | 946 | 154 | | This PR | 1100 | 0 | Now that we can match Kotlin functions to Java methods, we still have to match up the parameters so we can apply parameter-level transforms. Do this by removing Java method parameters from the front and back of the list to account for cases where the compiler adds "hidden" parameters: * Remove Kotlin `ReceiverType` from beginning of list. * Remove Kotlin `Lkotlin/coroutines/Continuation;` from end of list. * Note this _could_ be a legitimate user supplied parameter so we try to restrict it to "unnamed" parameters. * Remove the `this` parameter added to `static` functions. Note: the test classes `DeepRecursiveKt.class`, `DeepRecursiveScope.class`, and `UByteArray.class` are pulled from [`kotlin-stdlib-1.5.31.jar`][0]. [0]: https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib/1.5.31
1 parent 32635fd commit f91b077

11 files changed

Lines changed: 185 additions & 46 deletions

File tree

‎src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinClassMetadata.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,9 @@ public class KotlinType
358358
};
359359
}
360360

361-
publicstringGetSignature()
361+
publicstringGetSignature(boolconvertUnsignedToPrimitive=true)
362362
{
363-
returnKotlinUtilities.ConvertKotlinTypeSignature(this);
363+
returnKotlinUtilities.ConvertKotlinTypeSignature(this,null,convertUnsignedToPrimitive);
364364
}
365365
}
366366

‎src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinFixups.cs‎

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,12 @@ static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinCl
175175
return;
176176
}
177177

178-
varjava_parameters=method.GetFilteredParameters();
178+
(varstart,varend)=CreateParameterMap(method,metadata,kotlinClass);
179179

180-
for(vari=0;i<java_parameters.Length;i++){
181-
varjava_p=java_parameters[i];
180+
varjava_parameters=method.GetParameters();
181+
182+
for(vari=0;i<end-start;i++){
183+
varjava_p=java_parameters[start+i];
182184
varkotlin_p=metadata.ValueParameters==null?null:metadata.ValueParameters[i];
183185
if(kotlin_p==null||kotlin_p.Type==null||kotlin_p.Name==null)
184186
continue;
@@ -197,6 +199,46 @@ static void FixupFunction (MethodInfo? method, KotlinFunction metadata, KotlinCl
197199
method.KotlinReturnType=GetKotlinType(method.ReturnType.TypeSignature,metadata.ReturnType?.ClassName);
198200
}
199201

202+
publicstatic(intstart,intend)CreateParameterMap(MethodInfomethod,KotlinFunctionfunction,KotlinClass?kotlinClass)
203+
{
204+
varparameters=method.GetParameters();
205+
varstart=0;
206+
varend=parameters.Length;
207+
208+
// If the parameter counts are the same, that's good enough (because we know signatures matched)
209+
if(IsValidParameterMap(method,function,start,end))
210+
return(start,end);
211+
212+
// Remove the "hidden" receiver type parameter from the start of the parameter list
213+
if(function.ReceiverType!=null)
214+
start++;
215+
216+
if(IsValidParameterMap(method,function,start,end))
217+
return(start,end);
218+
219+
varlast_p=parameters.Last();
220+
221+
// Remove the "hidden" coroutine continuation type parameter from the end of the parameter list
222+
// We try to restrict it to compiler generated paramteres because a user might have actually used it as a parameter
223+
if(last_p.Type.BinaryName=="Lkotlin/coroutines/Continuation;"&&(last_p.IsUnnamedParameter()||last_p.IsCompilerNamed()))
224+
end--;
225+
226+
if(IsValidParameterMap(method,function,start,end))
227+
return(start,end);
228+
229+
// Remove the "hidden" "this" type parameter for a static method from the start of the parameter list
230+
// Note we do this last because sometimes it isn't there.
231+
if(method.AccessFlags.HasFlag(MethodAccessFlags.Static))
232+
start++;
233+
234+
if(IsValidParameterMap(method,function,start,end))
235+
return(start,end);
236+
237+
return(0,0);
238+
}
239+
240+
staticboolIsValidParameterMap(MethodInfomethod,KotlinFunctionfunction,intstart,intend)=>function.ValueParameters?.Count==end-start;
241+
200242
staticvoidFixupExtensionMethod(MethodInfomethod)
201243
{
202244
// Kotlin "extension" methods give the first parameter an ugly name
@@ -271,10 +313,23 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
271313

272314
staticMethodInfo?FindJavaMethod(KotlinFilekotlinFile,KotlinFunctionfunction,ClassFileklass)
273315
{
274-
varpossible_methods=klass.Methods.Where(method =>method.Name==function.JvmName&&
275-
method.GetFilteredParameters().Length==function.ValueParameters?.Count);
316+
varpossible_methods=klass.Methods.Where(method =>method.Name==function.JvmName).ToArray();
317+
varsignature=function.JvmSignature;
318+
319+
// If the Descriptor/Signature match, that means all parameters and return type match
320+
if(signature!=null&&possible_methods.SingleOrDefault(method =>method.Descriptor==signature)isMethodInfom)
321+
returnm;
322+
323+
// Sometimes JvmSignature is null (or unhelpful), so we're going to construct one ourselves and see if they match
324+
signature=function.ConstructJvmSignature();
276325

277-
foreach(varmethodinpossible_methods){
326+
if(possible_methods.SingleOrDefault(method =>method.Descriptor==signature)isMethodInfom2)
327+
returnm2;
328+
329+
// If that didn't work, let's do it the hard way!
330+
// I don't know if this catches anything additional, but it was the original code we shipped, so
331+
// we'll keep it just in case something in the wild requires it.
332+
foreach(varmethodinpossible_methods.Where(method =>method.GetFilteredParameters().Length==function.ValueParameters?.Count)){
278333
if(function.ReturnType==null)
279334
continue;
280335
if(!TypesMatch(method.ReturnType,function.ReturnType,kotlinFile))
@@ -286,6 +341,10 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
286341
returnmethod;
287342
}
288343

344+
// Theoretically this should never be hit, but who knows. At worst, it just means
345+
// Kotlin niceties won't be applied to the method.
346+
Log.Debug($"Kotlin: Could not find Java method to match '{function.Name} ({function.ConstructJvmSignature()})'");
347+
289348
returnnull;
290349
}
291350

‎src/Xamarin.Android.Tools.Bytecode/Kotlin/KotlinUtilities.cs‎

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Xamarin.Android.Tools.Bytecode
88
{
99
publicstaticclassKotlinUtilities
1010
{
11-
publicstaticstringConvertKotlinTypeSignature(KotlinType?type,KotlinFile?metadata=null)
11+
publicstaticstringConvertKotlinTypeSignature(KotlinType?type,KotlinFile?metadata=null,boolconvertUnsignedToPrimitive=true)
1212
{
1313
if(typeisnull)
1414
returnstring.Empty;
@@ -27,15 +27,15 @@ public static string ConvertKotlinTypeSignature (KotlinType? type, KotlinFile? m
2727
return"Ljava/lang/Object;";
2828
}
2929

30-
varresult=ConvertKotlinClassToJava(class_name);
30+
varresult=ConvertKotlinClassToJava(class_name,convertUnsignedToPrimitive);
3131

3232
if(result=="[")
33-
result+=ConvertKotlinTypeSignature(type.Arguments?.FirstOrDefault()?.Type);
33+
result+=ConvertKotlinTypeSignature(type.Arguments?.FirstOrDefault()?.Type,null,convertUnsignedToPrimitive);
3434

3535
returnresult;
3636
}
3737

38-
publicstaticstringConvertKotlinClassToJava(string?className)
38+
publicstaticstringConvertKotlinClassToJava(string?className,boolconvertUnsignedToPrimitive=true)
3939
{
4040
if(className==null||string.IsNullOrWhiteSpace(className))
4141
returnstring.Empty;
@@ -45,6 +45,9 @@ public static string ConvertKotlinClassToJava (string? className)
4545
if(type_map.TryGetValue(className.TrimEnd(';'),outvarresult))
4646
returnresult;
4747

48+
if(convertUnsignedToPrimitive&&unsigned_type_map.TryGetValue(className.TrimEnd(';'),outvarresult2))
49+
returnresult2;
50+
4851
return"L"+className;
4952
}
5053

@@ -92,6 +95,14 @@ public static bool IsDefaultConstructorMarker (this MethodInfo method)
9295
parameters[parameters.Length-1].Type.TypeSignature=="Lkotlin/jvm/internal/DefaultConstructorMarker;";
9396
}
9497

98+
// Sometimes the Kotlin provided JvmSignature is null (or unhelpful), so we need to construct one ourselves
99+
publicstaticstringConstructJvmSignature(thisKotlinFunctionfunction)
100+
{
101+
// The receiver type (if specified) is a "hidden" parameter passed at the beginning
102+
// of the Java parameter list, so we include it so the Signature/Descriptors match.
103+
return$"({function.ReceiverType?.GetSignature(false)}{string.Concat(function.ValueParameters?.Select(p =>p.Type?.GetSignature(false))??Enumerable.Empty<string>())}){function.ReturnType?.GetSignature(false)}";
104+
}
105+
95106
internalstaticList<TResult>?ToList<TSource,TResult>(thisIEnumerable<TSource>?self,JvmNameResolverresolver,Func<TSource,JvmNameResolver,TResult?>creator)
96107
whereTResult:class
97108
{
@@ -114,37 +125,42 @@ public static bool IsDefaultConstructorMarker (this MethodInfo method)
114125

115126
publicstaticboolIsUnnamedParameter(thisParameterInfoparameter)=>parameter.Name.Length>1&&parameter.Name.StartsWith("p",StringComparison.Ordinal)&&int.TryParse(parameter.Name.Substring(1),outvar_);
116127

128+
publicstaticboolIsCompilerNamed(thisParameterInfoparameter)=>parameter.Name.Length>0&&parameter.Name.StartsWith("$",StringComparison.Ordinal);
129+
117130
publicstaticboolIsUnnamedParameter(thisKotlinValueParameterparameter)=>parameter.Name?.Length>1&&
118131
parameter.Name.StartsWith("p",StringComparison.Ordinal)&&
119132
int.TryParse(parameter.Name.Substring(1),outvar_);
120133

134+
staticDictionary<string,string>unsigned_type_map=newDictionary<string,string>{
135+
{"kotlin/UInt","I"},
136+
{"kotlin/ULong","J"},
137+
{"kotlin/UShort","S"},
138+
{"kotlin/UByte","B"},
139+
{"kotlin/UIntArray","[I"},
140+
{"kotlin/ULongArray","[J"},
141+
{"kotlin/UShortArray","[S"},
142+
{"kotlin/UByteArray","[B"},
143+
};
144+
121145
staticDictionary<string,string>type_map=newDictionary<string,string>{
122146
{"kotlin/Int","I"},
123-
{"kotlin/UInt","I"},
124147
{"kotlin/Double","D"},
125148
{"kotlin/Char","C"},
126149
{"kotlin/Long","J"},
127-
{"kotlin/ULong","J"},
128150
{"kotlin/Float","F"},
129151
{"kotlin/Short","S"},
130-
{"kotlin/UShort","S"},
131152
{"kotlin/Byte","B"},
132-
{"kotlin/UByte","B"},
133153
{"kotlin/Boolean","Z"},
134154
{"kotlin/Unit","V"},
135155

136156
{"kotlin/Array","["},
137157
{"kotlin/IntArray","[I"},
138-
{"kotlin/UIntArray","[I"},
139158
{"kotlin/DoubleArray","[D"},
140159
{"kotlin/CharArray","[C"},
141160
{"kotlin/LongArray","[J"},
142-
{"kotlin/ULongArray","[J"},
143161
{"kotlin/FloatArray","[F"},
144162
{"kotlin/ShortArray","[S"},
145-
{"kotlin/UShortArray","[S"},
146163
{"kotlin/ByteArray","[B"},
147-
{"kotlin/UByteArray","[B"},
148164
{"kotlin/BooleanArray","[Z"},
149165

150166
{"kotlin/Any","Ljava/lang/Object;"},

‎tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileFixture.cs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,46 @@ protected static void AssertXmlDeclaration (string[] classResources, string xmlR
105105
Assert.AreEqual(expected,actual.ToString());
106106
}
107107

108+
protectedstaticKotlinMetadataGetMetadataForClass(ClassFileklass)
109+
{
110+
varattr=klass.Attributes.OfType<RuntimeVisibleAnnotationsAttribute>().FirstOrDefault();
111+
varkotlin=attr?.Annotations.FirstOrDefault(a =>a.Type=="Lkotlin/Metadata;");
112+
113+
Assert.NotNull(kotlin);
114+
115+
varmeta=KotlinMetadata.FromAnnotation(kotlin);
116+
117+
returnmeta;
118+
}
119+
120+
protectedstaticKotlinClassGetClassMetadataForClass(ClassFileklass)
121+
{
122+
varmeta=GetMetadataForClass(klass);
123+
Assert.AreEqual(KotlinMetadataKind.Class,meta.Kind);
124+
125+
returnmeta.AsClassMetadata();
126+
}
127+
128+
protectedstaticKotlinFileGetFileMetadataForClass(ClassFileklass)
129+
{
130+
varmeta=GetMetadataForClass(klass);
131+
Assert.AreEqual(KotlinMetadataKind.File,meta.Kind);
132+
133+
returnmeta.AsFileMetadata();
134+
}
135+
136+
protectedstaticKotlinMetadataGetMetadataForClassFile(stringfile)
137+
{
138+
varc=LoadClassFile(file);
139+
returnGetMetadataForClass(c);
140+
}
141+
142+
protectedstaticKotlinClassGetClassMetadata(stringfile)
143+
{
144+
varc=LoadClassFile(file);
145+
returnGetClassMetadataForClass(c);
146+
}
147+
108148
staticStreamGetResourceStream(stringresource)
109149
{
110150
// Look for resources that end with our name, this allows us to

‎tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinFixupsTests.cs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,5 +310,53 @@ public void HandleKotlinNameShadowing ()
310310
Assert.True(klass.Methods.Single(m =>m.Name=="hitCount").AccessFlags.HasFlag(MethodAccessFlags.Public));
311311
Assert.True(klass.Methods.Single(m =>m.Name=="setType").AccessFlags.HasFlag(MethodAccessFlags.Public));
312312
}
313+
314+
[Test]
315+
publicvoidMatchParametersWithReceiver()
316+
{
317+
varklass=LoadClassFile("DeepRecursiveKt.class");
318+
varmeta=GetFileMetadataForClass(klass);
319+
320+
varjava_method=klass.Methods.Single(m =>m.Name=="invoke");
321+
varkotlin_function=meta.Functions.Single(m =>m.Name=="invoke");
322+
323+
(varstart,varend)=KotlinFixups.CreateParameterMap(java_method,kotlin_function,null);
324+
325+
// Start is 1 instead of 0 to skip the receiver
326+
Assert.AreEqual(1,start);
327+
Assert.AreEqual(2,end);
328+
}
329+
330+
[Test]
331+
publicvoidMatchParametersWithContinuation()
332+
{
333+
varklass=LoadClassFile("DeepRecursiveScope.class");
334+
varmeta=GetClassMetadataForClass(klass);
335+
336+
varjava_method=klass.Methods.Single(m =>m.Name=="callRecursive"&&m.GetParameters().Count()==2);
337+
varkotlin_function=meta.Functions.Single(m =>m.Name=="callRecursive"&&m.JvmSignature.Count(c =>c==';')==3);
338+
339+
(varstart,varend)=KotlinFixups.CreateParameterMap(java_method,kotlin_function,null);
340+
341+
// End is 1 instead of 2 to skip the trailing continuation
342+
Assert.AreEqual(0,start);
343+
Assert.AreEqual(1,end);
344+
}
345+
346+
[Test]
347+
publicvoidMatchParametersWithStaticThis()
348+
{
349+
varklass=LoadClassFile("UByteArray.class");
350+
varmeta=GetClassMetadataForClass(klass);
351+
352+
varjava_method=klass.Methods.Single(m =>m.Name=="contains-7apg3OU"&&m.GetParameters().Count()==2);
353+
varkotlin_function=meta.Functions.Single(m =>m.Name=="contains");
354+
355+
(varstart,varend)=KotlinFixups.CreateParameterMap(java_method,kotlin_function,null);
356+
357+
// Start is 1 instead of 0 to skip the static 'this'
358+
Assert.AreEqual(1,start);
359+
Assert.AreEqual(2,end);
360+
}
313361
}
314362
}

‎tests/Xamarin.Android.Tools.Bytecode-Tests/KotlinMetadataTests.cs‎

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -201,31 +201,6 @@ public void KotlinExtensionMethods ()
201201
Assert.AreEqual(KotlinClassInheritability.Final,klass_meta.Inheritability);
202202
Assert.AreEqual(KotlinClassVisibility.Public,klass_meta.Visibility);
203203
}
204-
205-
privateKotlinMetadataGetMetadataForClassFile(stringfile)
206-
{
207-
varc=LoadClassFile(file);
208-
varattr=c.Attributes.OfType<RuntimeVisibleAnnotationsAttribute>().FirstOrDefault();
209-
varkotlin=attr?.Annotations.FirstOrDefault(a =>a.Type=="Lkotlin/Metadata;");
210-
211-
Assert.NotNull(kotlin);
212-
213-
varmeta=KotlinMetadata.FromAnnotation(kotlin);
214-
215-
returnmeta;
216-
}
217-
218-
privateKotlinClassGetClassMetadata(stringfile)
219-
{
220-
varmeta=GetMetadataForClassFile(file);
221-
222-
Assert.AreEqual(KotlinMetadataKind.Class,meta.Kind);
223-
224-
Assert.AreEqual("1.0.3",meta.ByteCodeVersion.ToString());
225-
Assert.AreEqual("1.1.15",meta.MetadataVersion.ToString());
226-
227-
returnmeta.AsClassMetadata();
228-
}
229204
}
230205
}
231206

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
<ItemGroup>
2727
<EmbeddedResourceInclude="Resources\*" />
28-
<EmbeddedResourceInclude="kotlin\**\*.class" />
28+
<EmbeddedResourceInclude="kotlin*\**\*.class" />
2929

3030
<EmbeddedResourceInclude="$(IntermediateOutputPath)classes\com\xamarin\NotNullClass.class" />
3131
<EmbeddedResourceInclude="$(IntermediateOutputPath)classes\com\xamarin\IJavaInterface.class" />
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)