Skip to content

Commit

Permalink
[Xamarin.Android.Tools.Bytecode] Support @jvmoverloads (#651)
Browse files Browse the repository at this point in the history
@moljac ran into an interesting warning from `class-parse` when
binding [OfficeUIFrabric][0]:

	warning : class-parse: warning: method com/microsoft/officeuifabric/widget/BottomNavigationView.<init>(Landroid/content/Context;)V: Local variables array has 0 entries ('LocalVariableTableAttribute(LocalVariableTableEntry(Name='context', Descriptor='Landroid/content/Context;'))'); descriptor has 1 entries!

For starters, this shouldn't be a warning; it's not actionable.
There is nothing a user can do to fix this warning; it's only
meaningful to the Java.Interop team.

Update `Xamarin.Android.Tools.Bytecode.MethodInfo.GetParameters()` so
that these messages are *debug* messages, not warnings.

`class-parse` knows how many parameters are present based on the JNI
method descriptor, in this case `(Landroid/content/Context;)V`, which
specifies one parameter type.  The JNI descriptor *doesn't* contain
parameter name information; where do parameter names come from?

If the `.class` file was built via `javac -parameters`, then the
`MethodParametersAttribute` blob can be used; see 4273e5c.

However, before checking for `MethodParametersAttribute`,
`class-parse` will attempt to use the `LocalVariableTableAttribute`
and `LocalVariableTableEntry` values to infer parameter names.
As part of this inference, method parameters are assumed to be
variables with `LocalVariableTableEntry.StartPC` is 0; from
`class-parse --dump BottomNavigationView.class`:

	LocalVariableTableAttribute(
	  LocalVariableTableEntry(Name='context', Descriptor='Landroid/content/Context;', StartPC=0, Index=1))

Next, `class-parse` will "skip" the first variable for instance
methods.  This results in "skipping" the `context` variable, resulting
in the message:

	Local variables array has 0 entries …; descriptor has 1 entries!

To fix this, `class-parse` needs a stricter "skip the `this`
parameter" check: the first parameter should *only* be skipped when:

 1. The method is an instance method, not a `static` method, *and*
 2. The JNI descriptor of the first parameter is the same as the
    descriptor of the declaring type.

Additionally, update the code style to use `enumValue.HasFlag(V)`
instead of `(enumValue & V) == V` for readability.

Finally, why was this failing in the first place?  In some
circumstances, when the Kotlin [`@JvmOverloads`][2] annotation is used
on a `constructor`, Kotlin will emit all possible overloads for the
constructor, and in many of those overloads the `this` parameter
*won't* be present in the `LocalVariableTableAttribute` data, as seen
above with `BottomNavigationView`.

[0]: https://jcenter.bintray.com/com/microsoft/uifabric/OfficeUIFabric/
[1]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-overloads/
  • Loading branch information
jonpryor committed Jun 11, 2020
1 parent 76d1ac7 commit a3f148c
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/Xamarin.Android.Tools.Bytecode/AttributeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ public string Descriptor {

public override string ToString ()
{
return string.Format ("LocalVariableTableEntry(Name='{0}', Descriptor='{1}')", Name, Descriptor);
return $"LocalVariableTableEntry(Name='{Name}', Descriptor='{Descriptor}', StartPC={StartPC}, Index={Index})";
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/Xamarin.Android.Tools.Bytecode/ClassFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public string PackageName {
}
}

public string FullJniName => "L" + ThisClass.Name.Value + ";";

public string SourceFileName {
get {
var sourceFile = Attributes.Get<SourceFileAttribute> ();
Expand Down
14 changes: 9 additions & 5 deletions src/Xamarin.Android.Tools.Bytecode/Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,21 @@ public ParameterInfo[] GetParameters ()
if (locals != null) {
var names = locals.LocalVariables.Where (p => p.StartPC == 0).ToList ();
int start = 0;
if ((AccessFlags & MethodAccessFlags.Static) == 0)
if (names.Count != parameters.Length &&
!AccessFlags.HasFlag (MethodAccessFlags.Static) &&
names.Count > start &&
names [start].Descriptor == DeclaringType.FullJniName) {
start++; // skip `this` parameter
}
if (!DeclaringType.IsStatic &&
names.Count > start &&
(parameters.Length == 0 || parameters [0].Type.BinaryName != names [start].Descriptor)) {
start++; // JDK 8?
}
if (((AccessFlags & MethodAccessFlags.Synthetic) != MethodAccessFlags.Synthetic) &&
if (!AccessFlags.HasFlag (MethodAccessFlags.Synthetic) &&
((names.Count - start) != parameters.Length) &&
!enumCtor) {
Log.Warning (1,"class-parse: warning: method {0}.{1}{2}: " +
Log.Debug ("class-parse: method {0}.{1}{2}: " +
"Local variables array has {3} entries ('{4}'); descriptor has {5} entries!",
DeclaringType.ThisClass.Name.Value, Name, Descriptor,
names.Count - start,
Expand All @@ -111,7 +115,7 @@ public ParameterInfo[] GetParameters ()
for (int i = 0; i < max; ++i) {
parameters [i].Name = names [start+i].Name;
if (parameters [i].Type.BinaryName != names [start + i].Descriptor) {
Log.Warning (1, "class-parse: warning: method {0}.{1}{2}: " +
Log.Debug ("class-parse: method {0}.{1}{2}: " +
"Local variable type descriptor mismatch! Got '{3}'; expected '{4}'.",
DeclaringType.ThisClass.Name.Value, Name, Descriptor,
parameters [i].Type.BinaryName,
Expand All @@ -122,7 +126,7 @@ public ParameterInfo[] GetParameters ()
var sig = GetSignature ();
if (sig != null) {
if ((sig.Parameters.Count != parameters.Length) && !enumCtor) {
Log.Warning (1,"class-parse: warning: method {0}.{1}{2}: " +
Log.Debug ("class-parse: method {0}.{1}{2}: " +
"Signature ('{3}') has {4} entries; Descriptor '{5}' has {6} entries!",
DeclaringType.ThisClass.Name.Value, Name, Descriptor,
Attributes.Get<SignatureAttribute>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public XElement ToXElement ()
GetExtendsGenericAware (),
new XAttribute ("final", (classFile.AccessFlags & ClassAccessFlags.Final) != 0),
new XAttribute ("name", GetThisClassName ()),
new XAttribute ("jni-signature", "L" + classFile.ThisClass.Name.Value + ";"),
new XAttribute ("jni-signature", classFile.FullJniName),
GetSourceFile (),
new XAttribute ("static", classFile.IsStatic),
new XAttribute ("visibility", GetVisibility (classFile.Visibility)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;

using Xamarin.Android.Tools.Bytecode;

using NUnit.Framework;

using Assembly = System.Reflection.Assembly;

namespace Xamarin.Android.Tools.BytecodeTests {

[TestFixture]
public class JvmOverloadsConstructorTests : ClassFileFixture {

const string ClassFile = "JvmOverloadsConstructor.class";
const string XmlFile = "JvmOverloadsConstructor.xml";

[Test]
public void ClassFile_WithJavaType_class ()
{
var c = LoadClassFile (ClassFile);
new ExpectedTypeDeclaration {
MajorVersion = 0x32,
MinorVersion = 0,
ConstantPoolCount = 59,
AccessFlags = ClassAccessFlags.Public | ClassAccessFlags.Final | ClassAccessFlags.Super,
FullName = "JvmOverloadsConstructor",
Superclass = new TypeInfo ("java/lang/Object", "Ljava/lang/Object;"),
Methods = {
new ExpectedMethodDeclaration {
Name = "<init>",
AccessFlags = MethodAccessFlags.Public,
ReturnDescriptor = "V",
Parameters = {
new ParameterInfo ("something", "LJvmOverloadsConstructor;", "LJvmOverloadsConstructor;"),
new ParameterInfo ("id", "I", "I"),
new ParameterInfo ("imageId", "I", "I"),
new ParameterInfo ("title", "Ljava/lang/String;", "Ljava/lang/String;"),
new ParameterInfo ("useDivider", "Z", "Z"),
},
},
new ExpectedMethodDeclaration {
Name = "<init>",
AccessFlags = MethodAccessFlags.Public | MethodAccessFlags.Synthetic,
ReturnDescriptor = "V",
Parameters = {
new ParameterInfo ("p0", "LJvmOverloadsConstructor;", "LJvmOverloadsConstructor;"),
new ParameterInfo ("p1", "I", "I"),
new ParameterInfo ("p2", "I", "I"),
new ParameterInfo ("p3", "Ljava/lang/String;", "Ljava/lang/String;"),
new ParameterInfo ("p4", "Z", "Z"),
new ParameterInfo ("p5", "I", "I"),
new ParameterInfo ("p6", "Lkotlin/jvm/internal/DefaultConstructorMarker;", "Lkotlin/jvm/internal/DefaultConstructorMarker;"),
},
},
new ExpectedMethodDeclaration {
Name = "<init>",
AccessFlags = MethodAccessFlags.Public,
ReturnDescriptor = "V",
Parameters = {
new ParameterInfo ("something", "LJvmOverloadsConstructor;", "LJvmOverloadsConstructor;"),
new ParameterInfo ("id", "I", "I"),
new ParameterInfo ("imageId", "I", "I"),
new ParameterInfo ("title", "Ljava/lang/String;", "Ljava/lang/String;"),
},
},
new ExpectedMethodDeclaration {
Name = "<init>",
AccessFlags = MethodAccessFlags.Public,
ReturnDescriptor = "V",
Parameters = {
new ParameterInfo ("something", "LJvmOverloadsConstructor;", "LJvmOverloadsConstructor;"),
new ParameterInfo ("id", "I", "I"),
new ParameterInfo ("title", "Ljava/lang/String;", "Ljava/lang/String;"),
},
},
new ExpectedMethodDeclaration {
Name = "<init>",
AccessFlags = MethodAccessFlags.Public,
ReturnDescriptor = "V",
Parameters = {
new ParameterInfo ("something", "LJvmOverloadsConstructor;", "LJvmOverloadsConstructor;"),
new ParameterInfo ("title", "Ljava/lang/String;", "Ljava/lang/String;"),
},
},
},
}.Assert (c);
}

[Test]
public void XmlDeclaration_WithJavaType_class ()
{
AssertXmlDeclaration (ClassFile, XmlFile);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<api
api-source="class-parse">
<package
name=""
jni-name="">
<class
abstract="false"
deprecated="not deprecated"
jni-extends="Ljava/lang/Object;"
extends="java.lang.Object"
extends-generic-aware="java.lang.Object"
final="true"
name="JvmOverloadsConstructor"
jni-signature="LJvmOverloadsConstructor;"
source-file-name="JvmOverloadsConstructor.kt"
static="false"
visibility="public">
<constructor
deprecated="not deprecated"
final="false"
name="JvmOverloadsConstructor"
static="false"
visibility="public"
bridge="false"
synthetic="false"
jni-signature="(LJvmOverloadsConstructor;IILjava/lang/String;)V">
<parameter
name="something"
type="JvmOverloadsConstructor"
jni-type="LJvmOverloadsConstructor;"
not-null="true" />
<parameter
name="id"
type="int"
jni-type="I" />
<parameter
name="imageId"
type="int"
jni-type="I" />
<parameter
name="title"
type="java.lang.String"
jni-type="Ljava/lang/String;"
not-null="true" />
</constructor>
<constructor
deprecated="not deprecated"
final="false"
name="JvmOverloadsConstructor"
static="false"
visibility="public"
bridge="false"
synthetic="false"
jni-signature="(LJvmOverloadsConstructor;IILjava/lang/String;Z)V">
<parameter
name="something"
type="JvmOverloadsConstructor"
jni-type="LJvmOverloadsConstructor;"
not-null="true" />
<parameter
name="id"
type="int"
jni-type="I" />
<parameter
name="imageId"
type="int"
jni-type="I" />
<parameter
name="title"
type="java.lang.String"
jni-type="Ljava/lang/String;"
not-null="true" />
<parameter
name="useDivider"
type="boolean"
jni-type="Z" />
</constructor>
<constructor
deprecated="not deprecated"
final="false"
name="JvmOverloadsConstructor"
static="false"
visibility="public"
bridge="false"
synthetic="true"
jni-signature="(LJvmOverloadsConstructor;IILjava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V">
<parameter
name="p0"
type="JvmOverloadsConstructor"
jni-type="LJvmOverloadsConstructor;" />
<parameter
name="p1"
type="int"
jni-type="I" />
<parameter
name="p2"
type="int"
jni-type="I" />
<parameter
name="p3"
type="java.lang.String"
jni-type="Ljava/lang/String;" />
<parameter
name="p4"
type="boolean"
jni-type="Z" />
<parameter
name="p5"
type="int"
jni-type="I" />
<parameter
name="p6"
type="kotlin.jvm.internal.DefaultConstructorMarker"
jni-type="Lkotlin/jvm/internal/DefaultConstructorMarker;" />
</constructor>
<constructor
deprecated="not deprecated"
final="false"
name="JvmOverloadsConstructor"
static="false"
visibility="public"
bridge="false"
synthetic="false"
jni-signature="(LJvmOverloadsConstructor;ILjava/lang/String;)V">
<parameter
name="something"
type="JvmOverloadsConstructor"
jni-type="LJvmOverloadsConstructor;"
not-null="true" />
<parameter
name="id"
type="int"
jni-type="I" />
<parameter
name="title"
type="java.lang.String"
jni-type="Ljava/lang/String;"
not-null="true" />
</constructor>
<constructor
deprecated="not deprecated"
final="false"
name="JvmOverloadsConstructor"
static="false"
visibility="public"
bridge="false"
synthetic="false"
jni-signature="(LJvmOverloadsConstructor;Ljava/lang/String;)V">
<parameter
name="something"
type="JvmOverloadsConstructor"
jni-type="LJvmOverloadsConstructor;"
not-null="true" />
<parameter
name="title"
type="java.lang.String"
jni-type="Ljava/lang/String;"
not-null="true" />
</constructor>
</class>
</package>
</api>
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class JvmOverloadsConstructor {
@JvmOverloads
constructor(
something : JvmOverloadsConstructor,
id: Int = 1,
imageId: Int = 2,
title: String,
useDivider: Boolean = false
) {
}
}

0 comments on commit a3f148c

Please sign in to comment.