Skip to content

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

File tree

2 files changed

+47
-23
lines changed

2 files changed

+47
-23
lines changed

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 (var met in metadata.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 (var java_method in c.Methods)
53+
if (FindKotlinFunctionMetadata (metadata, java_method) is KotlinFunction function_metadata)
54+
FixupFunction (java_method, function_metadata, class_metadata);
5255
}
5356

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

337-
static MethodInfo? FindJavaMethod (KotlinFile kotlinFile, KotlinFunction function, ClassFile klass)
339+
static KotlinFunction? FindKotlinFunctionMetadata (KotlinFile? kotlinFile, MethodInfo javaMethod)
338340
{
339-
var possible_methods = klass.Methods.Where (method => method.Name == function.JvmName).ToArray ();
340-
var signature = function.JvmSignature;
341+
if (kotlinFile?.Functions is null)
342+
return null;
343+
344+
var java_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) is MethodInfo m)
344-
return m;
346+
// The method name absolutely has to match
347+
var possible_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) is KotlinFunction kf)
351+
return kf;
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) is KotlinFunction kf2)
355+
return kf2;
348356

349-
if (possible_methods.SingleOrDefault (method => method.Descriptor == signature) is MethodInfo m2)
350-
return m2;
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+
var java_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 (var method in possible_methods.Where (method => method.GetFilteredParameters ().Length == function.ValueParameters?.Count)) {
361+
foreach (var function in possible_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-
return method;
370+
return function;
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
return null;
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+
public void MatchMetadataToMultipleMethodsWithSameMangledName ()
380+
{
381+
var klass = LoadClassFile ("UByteArray.class");
382+
var meta = GetClassMetadataForClass (klass);
383+
384+
// There is only one Kotlin metadata for Java method "contains-7apg3OU"
385+
var kotlin_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+
var java_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)