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

Commit 275fa75

Browse files
authored
[generator] Kotlin metadata can apply to multiple Java method (#1009)
Fixes: #984 There exists a scenario with Kotlin metadata where an instance method and an extension method can have the same *Kotlin* signature: // Kotlin class UByteArray { fun contains(element: UByte): Boolean } // Extension method operator fun <T> Array<out T>.contains(element: T): Boolean This results in 2 different Java methods: // "Java" class UByteArray { public boolean contains-7apg3OU(byte element) { return contains-WZ4Q5Ns(this.storage, element); } public static boolean contains-7apg3OU(byte[] arg0, byte element) { Intrinsics.checkNotNullParameter(arg0, "arg0"); return ArraysKt.contains(arg0, element); } } Kotlin only generates 1 metadata entry, for JvmSignature=`([BB)Z`: ![image](https://user-images.githubusercontent.com/179295/178528374-a6e9a568-dc68-4b88-af6b-1c51946d7997.png) Previously (439bd83) we assumed that a piece of Kotlin metadata would apply to only a single Java method. In this case, we would find `contains-7apg3OU.([BB)Z`, then apply it to only the `static` method, resulting in a binding of: // C#; previous binding partial class UByteArray { public bool Contains(int element); public static bool Contains(int[] arg0, uint element); } Note: `element` is of type `int` for the instance method, `uint` for the static method. They *should* be consistent, and are not. If we don't exclusively look at `JvmSignature`, and also check `ValueParameters`, then the Kotlin metadata will match the *instance* `contains-7apg3OU` method. We surmise that this is intentional on Kotlin's part, in order to save bytes by omitting the second metadata entry, and that we should apply this metadata to both Java methods. *Note*: the "hash" appended to the Java method name (eg: `7apg3OU`) is a short hash of the Kotlin method parameter types. Fix this scenario by "inverting" our lookup logic: instead of starting with Kotlin metadata and looking for an applicable Java method, start with the Java method and look for applicable Kotlin metadata. This way, each Java method can consume a matching Kotlin metadata entry, even if another Java method already matched it. This allows us to emit a binding of: // C#; new binding partial class UByteArray { public bool Contains(uint element); public static bool Contains(int[] arg0, uint element); } This fixes the 4 `contains` methods for `UIntArray`, `UByteArray`, `ULongArray`, and `UShortArray`.
1 parent 3ff6f8f commit 275fa75

2 files changed

Lines changed: 47 additions & 23 deletions

File tree

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

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ public static void Fixup (IList<ClassFile> classes)
4747
FixupJavaMethods(c.Methods);
4848

4949
if(metadata.Functions!=null){
50-
foreach(varmetinmetadata.Functions)
51-
FixupFunction(FindJavaMethod(metadata,met,c),met,class_metadata);
50+
// We work from Java methods to Kotlin metadata because they aren't a 1:1 relation
51+
// and we need to find the "best" match for each Java method.
52+
foreach(varjava_methodinc.Methods)
53+
if(FindKotlinFunctionMetadata(metadata,java_method)isKotlinFunctionfunction_metadata)
54+
FixupFunction(java_method,function_metadata,class_metadata);
5255
}
5356

54-
5557
if(metadata.Properties!=null){
5658
foreach(varpropinmetadata.Properties){
5759
vargetter=FindJavaPropertyGetter(metadata,prop,c);
@@ -334,40 +336,40 @@ static void FixupField (FieldInfo? field, KotlinProperty metadata)
334336
returnnull;
335337
}
336338

337-
staticMethodInfo?FindJavaMethod(KotlinFilekotlinFile,KotlinFunctionfunction,ClassFileklass)
339+
staticKotlinFunction?FindKotlinFunctionMetadata(KotlinFile?kotlinFile,MethodInfojavaMethod)
338340
{
339-
varpossible_methods=klass.Methods.Where(method =>method.Name==function.JvmName).ToArray();
340-
varsignature=function.JvmSignature;
341+
if(kotlinFile?.Functionsisnull)
342+
returnnull;
343+
344+
varjava_descriptor=javaMethod.Descriptor;
341345

342-
// If the Descriptor/Signature match, that means all parameters and return type match
343-
if(signature!=null&&possible_methods.SingleOrDefault(method =>method.Descriptor==signature)isMethodInfom)
344-
returnm;
346+
// The method name absolutely has to match
347+
varpossible_functions=kotlinFile.Functions.Where(f =>f.JvmName==javaMethod.Name).ToArray();
348+
349+
// If we have metadata with a Descriptor/JvmSignature match, that means all parameters and return type match
350+
if(possible_functions.SingleOrDefault(f =>f.JvmSignature!=null&&f.JvmSignature==java_descriptor)isKotlinFunctionkf)
351+
returnkf;
345352

346353
// Sometimes JvmSignature is null (or unhelpful), so we're going to construct one ourselves and see if they match
347-
signature=function.ConstructJvmSignature();
354+
if(possible_functions.SingleOrDefault(f =>f.ConstructJvmSignature()==java_descriptor)isKotlinFunctionkf2)
355+
returnkf2;
348356

349-
if(possible_methods.SingleOrDefault(method =>method.Descriptor==signature)isMethodInfom2)
350-
returnm2;
357+
// If that didn't work, let's try it the hard way!
358+
// This catches cases where Kotlin only wrote one metadata entry for multiple methods with the same mangled JvmName (ex: contains-WZ4Q5Ns)
359+
varjava_param_count=javaMethod.GetFilteredParameters().Length;
351360

352-
// If that didn't work, let's do it the hard way!
353-
// I don't know if this catches anything additional, but it was the original code we shipped, so
354-
// we'll keep it just in case something in the wild requires it.
355-
foreach(varmethodinpossible_methods.Where(method =>method.GetFilteredParameters().Length==function.ValueParameters?.Count)){
361+
foreach(varfunctioninpossible_functions.Where(f =>f.ValueParameters?.Count==java_param_count)){
356362
if(function.ReturnType==null)
357363
continue;
358-
if(!TypesMatch(method.ReturnType,function.ReturnType,kotlinFile))
364+
if(!TypesMatch(javaMethod.ReturnType,function.ReturnType,kotlinFile))
359365
continue;
360366

361-
if(!ParametersMatch(kotlinFile,method,function.ValueParameters!))
367+
if(!ParametersMatch(kotlinFile,javaMethod,function.ValueParameters!))
362368
continue;
363369

364-
returnmethod;
370+
returnfunction;
365371
}
366372

367-
// Theoretically this should never be hit, but who knows. At worst, it just means
368-
// Kotlin niceties won't be applied to the method.
369-
Log.Debug($"Kotlin: Could not find Java method to match '{function.Name} ({function.ConstructJvmSignature()})'");
370-
371373
returnnull;
372374
}
373375

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,5 +374,27 @@ public void MatchParametersWithStaticThis ()
374374
Assert.AreEqual(1,start);
375375
Assert.AreEqual(2,end);
376376
}
377+
378+
[Test]
379+
publicvoidMatchMetadataToMultipleMethodsWithSameMangledName()
380+
{
381+
varklass=LoadClassFile("UByteArray.class");
382+
varmeta=GetClassMetadataForClass(klass);
383+
384+
// There is only one Kotlin metadata for Java method "contains-7apg3OU"
385+
varkotlin_function=meta.Functions.Single(m =>m.Name=="contains");
386+
387+
// However there are 2 Java methods named "contains-7apg3OU"
388+
// - public boolean contains-7apg3OU (byte element)
389+
// - public static boolean contains-7apg3OU (byte[] arg0, byte element)
390+
varjava_methods=klass.Methods.Where(m =>m.Name=="contains-7apg3OU");
391+
Assert.AreEqual(2,java_methods.Count());
392+
393+
KotlinFixups.Fixup(new[]{klass});
394+
395+
// Ensure the fixup "fixed" the parameter "element" type to "ubyte"
396+
Assert.AreEqual("ubyte",java_methods.ElementAt(0).GetParameters().Single(p =>p.Name=="element").KotlinType);
397+
Assert.AreEqual("ubyte",java_methods.ElementAt(1).GetParameters().Single(p =>p.Name=="element").KotlinType);
398+
}
377399
}
378400
}

0 commit comments

Comments
 (0)