Skip to content

Commit 9e0a469

Browse files
authored
[generator] deep clone methods to avoid NREs (#1080)
Context: 4ec5d4e Context: #1083 Context: `generator` crash while binding Google Material 1.6.1 Imagine: 1. A `public` class that inherits from a *package-private* class, and 2. The *package-private* class contains `public` or `protected` members which should be exposed on (1). For example: /* package-private*/ class BaseClass<V> { public void doThing(V value) {} } public class MyClass extends BaseClass<Object> { } We won't bind `BaseClass<V>` as it is *package-private*, but we want (need) it's publicly accessible members to be available to users of the `public` type `MyClass`; that is, *in Java*, `MyClass.doThing(Object)` will be an accessible method, and our binding of `MyClass` should likewise have a `MyClass.DoThing(Object)` method binding. In 4ec5d4e we supported this scenario by copying members from the *package-private* base class into the derived class. Or at least, that's what the commit says it does. It does not actually create a *copy* of the `Method`; rather, it simply adds the existing `Method` instance to the public derived class, meaning that the same `Method` instance is referenced by both the base & derived types! In general, this is fine. However consider `BaseClass<V>`, above: the derived type `MyClass` doesn't have a generic type argument `V`, and thus "copying" `BaseClass<V>.doThing(V)` as-is into `MyClass` is a non-sequitur; when we later `Validate()` the `Method` instance, this incongruity is detected and a warning is emitted: warning BG8800: Unknown parameter type 'V' for member 'MyClass.DoThing(V)'. As part of validation, the `Method` instance is also marked as invalid, by setting the `MethodBase.IsValid` property to `false`, and the instance is removed from `GenBase.Methods` for `MyClass`. Unfortunately that's not the end of it, because the `Method` instance is shared and thus is also in the `GenBase.Methods` collection for `BaseClass<V>`. This instance is expected/assumed to be valid, but was invalidated by `MyClass` validation. `generator` assumes that after validation, everything within `GenBase.Methods` is still valid. This assumption is no longer true for `BaseClass<V>`. The result is that if we later attempt to process `BaseClass<V>`, things blow up when trying to use the parameter types on `BaseClass<V>.doThing(V)`: System.NullReferenceException: Object reference not set to an instance of an object. at MonoDroid.Generation.Parameter.get_JniType() in C:\code\xamarin-android\external\Java.Interop\tools\generator\Java.Interop.Tools.Generator.ObjectModel\Parameter.cs:line 103 at MonoDroid.Generation.ParameterList.get_JniSignature() in C:\code\xamarin-android\external\Java.Interop\tools\generator\Java.Interop.Tools.Generator.ObjectModel\ParameterList.cs:line 203 at MonoDroid.Generation.Method.get_JniSignature() in C:\code\xamarin-android\external\Java.Interop\tools\generator\Java.Interop.Tools.Generator.ObjectModel\Method.cs:line 210 at MonoDroid.Generation.GenBase.ContainsMethod(Method method, Boolean check_ifaces, Boolean check_base_ifaces) in C:\code\xamarin-android\external\Java.Interop\tools\generator\Java.Interop.Tools.Generator.ObjectModel\GenBase.cs:line 225 at MonoDroid.Generation.GenBase.ContainsMethod(Method method, Boolean check_ifaces) in C:\code\xamarin-android\external\Java.Interop\tools\generator\Java.Interop.Tools.Generator.ObjectModel\GenBase.cs:line 208 at MonoDroid.Generation.GenBase.FixupMethodOverrides(CodeGenerationOptions opt) in C:\code\xamarin-android\external\Java.Interop\tools\generator\Java.Interop.Tools.Generator.ObjectModel\GenBase.cs:line 341 The fix is "simple": do what 4ec5d4e said it was doing and make a *copy* of the `Method` instance using a new `Clone()` method. With this fix, the copied method will be invalidated and removed without corrupting the original `Method` instance. *Note*: As seen in the unit test changes, the XPath comments are updated to point to the public type rather than the original package- private type. While this isn't "correct", neither was the previous XPath specification. After discussion, we have decided that the needed change is to not add XPath comments on "created" methods (that is, API that is created by `generator` and is not part of the Java public API). However, the cost of doing this change is higher than we currently wish to spend, so we will live with this minor issue for now. TODO: #1083 suggests a "complete fix" which allows `BaseClass<V>.doThing(V)` to be bound as `MyClass.DoThing(Object)`.
1 parent 5fa7ac4 commit 9e0a469

File tree

10 files changed

+126
-8
lines changed

10 files changed

+126
-8
lines changed

tests/generator-Tests/Unit-Tests/CodeGeneratorTestBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ protected List<GenBase> ParseApiDefinition (string xml)
7171
foreach (var gen in gens)
7272
options.SymbolTable.AddType (gen);
7373

74+
foreach (var gen in gens)
75+
gen.FixupAccessModifiers (options);
76+
7477
foreach (var gen in gens)
7578
gen.Validate (options, new GenericParameterDefinitionList (), generator.Context);
7679

tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,55 @@ public void DontWarnIfNestedTypeNameMatchesNamespace ()
827827
Assert.False (sb.ToString ().Contains ("warning BG8403"));
828828
}
829829

830+
831+
[Test]
832+
public void AvoidNREOnInvalidBaseMethod ()
833+
{
834+
// We copy methods from the package-private base class to the public class, however
835+
// the copied method is not valid because it doesn't understand the generic argument
836+
// type. The method is remove from the public class, but we need to ensure the
837+
// base class method still exists and IsValid.
838+
var xml = @"<api>
839+
<package name='java.lang' jni-name='java/lang'>
840+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
841+
</package>
842+
843+
<package name='android.view' jni-name='android/view'>
844+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' final='false' name='View' static='false' visibility='public' jni-signature='Landroid/view/View;' />
845+
</package>
846+
847+
<package name='com.google.android.material.behavior' jni-name='com/google/android/material/behavior'>
848+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='ViewOffsetBehavior' static='false' visibility='' jni-signature='Lcom/google/android/material/appbar/ViewOffsetBehavior;'>
849+
<typeParameters>
850+
<typeParameter name='V' classBound='android.view.View' jni-classBound='Landroid/view/View;'>
851+
<genericConstraints>
852+
<genericConstraint type='android.view.View' />
853+
</genericConstraints>
854+
</typeParameter>
855+
</typeParameters>
856+
<method abstract='false' deprecated='not deprecated' final='false' name='layoutChild' jni-signature='(Landroid/view/View;)V' bridge='false' native='false' return='void' jni-return='V' static='false' synchronized='false' synthetic='false' visibility='protected'>
857+
<parameter name='child' type='V' jni-type='TV;' not-null='true' />
858+
</method>
859+
</class>
860+
861+
<class abstract='false' deprecated='not deprecated' extends='com.google.android.material.behavior.ViewOffsetBehavior' extends-generic-aware='com.google.android.material.behavior.ViewOffsetBehavior&lt;android.view.View&gt;' jni-extends='Lcom/google/android/material/appbar/ViewOffsetBehavior;' final='false' name='Behavior' static='false' visibility='public' jni-signature='Lcom/google/android/material/appbar/Behavior;'>
862+
</class>
863+
</package>
864+
</api>";
865+
866+
var gens = ParseApiDefinition (xml);
867+
868+
var public_class = gens.Single (g => g.Name == "Behavior");
869+
var base_class = gens.Single (g => g.Name == "ViewOffsetBehavior");
870+
871+
// Method got removed
872+
Assert.AreEqual (0, public_class.Methods.Count);
873+
874+
// Method still exists and is valid
875+
Assert.AreEqual (1, base_class.Methods.Count);
876+
Assert.AreEqual (true, base_class.Methods [0].IsValid);
877+
}
878+
830879
static string StripRegisterAttributes (string str)
831880
{
832881
// It is hard to test if the [Obsolete] is on the setter/etc due to the [Register], so remove all [Register]s

tests/generator-Tests/expected.ji/AccessModifiers/Xamarin.Test.PublicFinalClass.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public unsafe void PublicMethod ()
4040
}
4141
}
4242

43-
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PackageClassB']/method[@name='packageMethodB' and count(parameter)=0]"
43+
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PublicFinalClass']/method[@name='packageMethodB' and count(parameter)=0]"
4444
[global::Java.Interop.JniMethodSignature ("packageMethodB", "()V")]
4545
public unsafe void PackageMethodB ()
4646
{
@@ -51,7 +51,7 @@ public unsafe void PackageMethodB ()
5151
}
5252
}
5353

54-
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PackageClassA']/method[@name='packageMethodA' and count(parameter)=0]"
54+
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PublicFinalClass']/method[@name='packageMethodA' and count(parameter)=0]"
5555
[global::Java.Interop.JniMethodSignature ("packageMethodA", "()V")]
5656
public unsafe void PackageMethodA ()
5757
{

tests/generator-Tests/expected.xaji/AccessModifiers/Xamarin.Test.PublicFinalClass.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public unsafe void PublicMethod ()
5757
}
5858
}
5959

60-
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PackageClassB']/method[@name='packageMethodB' and count(parameter)=0]"
60+
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PublicFinalClass']/method[@name='packageMethodB' and count(parameter)=0]"
6161
[Register ("packageMethodB", "()V", "")]
6262
public unsafe void PackageMethodB ()
6363
{
@@ -68,7 +68,7 @@ public unsafe void PackageMethodB ()
6868
}
6969
}
7070

71-
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PackageClassA']/method[@name='packageMethodA' and count(parameter)=0]"
71+
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PublicFinalClass']/method[@name='packageMethodA' and count(parameter)=0]"
7272
[Register ("packageMethodA", "()V", "")]
7373
public unsafe void PackageMethodA ()
7474
{

tests/generator-Tests/expected/AccessModifiers/Xamarin.Test.PublicFinalClass.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public unsafe void PublicMethod ()
4949
}
5050

5151
static IntPtr id_packageMethodB;
52-
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PackageClassB']/method[@name='packageMethodB' and count(parameter)=0]"
52+
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PublicFinalClass']/method[@name='packageMethodB' and count(parameter)=0]"
5353
[Register ("packageMethodB", "()V", "")]
5454
public unsafe void PackageMethodB ()
5555
{
@@ -62,7 +62,7 @@ public unsafe void PackageMethodB ()
6262
}
6363

6464
static IntPtr id_packageMethodA;
65-
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PackageClassA']/method[@name='packageMethodA' and count(parameter)=0]"
65+
// Metadata.xml XPath method reference: path="/api/package[@name='xamarin.test']/class[@name='PublicFinalClass']/method[@name='packageMethodA' and count(parameter)=0]"
6666
[Register ("packageMethodA", "()V", "")]
6767
public unsafe void PackageMethodA ()
6868
{

tools/generator/Java.Interop.Tools.Generator.ObjectModel/ClassGen.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public override void FixupAccessModifiers (CodeGenerationOptions opt)
6363
foreach (var baseMethod in baseClass.Methods) {
6464
var method = Methods.FirstOrDefault (m => m.Matches (baseMethod));
6565
if (method == null)
66-
Methods.Add (baseMethod);
66+
Methods.Add (baseMethod.Clone (this));
6767
}
6868
BaseType = baseClass.BaseType;
6969
} else {

tools/generator/Java.Interop.Tools.Generator.ObjectModel/GenericParameterDefinition.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public bool Validate (CodeGenerationOptions opt, GenericParameterDefinitionList
4949
validated = is_valid = true;
5050
return true;
5151
}
52+
53+
public GenericParameterDefinition Clone ()
54+
{
55+
return new GenericParameterDefinition (Name, ConstraintExpressions);
56+
}
5257
}
5358

5459
public class GenericParameterDefinitionList : List<GenericParameterDefinition>

tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,57 @@ internal string CalculateEventName (Func<string, bool> checkNameDuplicate)
107107

108108
public bool CanHaveStringOverload => IsReturnCharSequence || Parameters.HasCharSequence;
109109

110+
public Method Clone (GenBase declaringType)
111+
{
112+
var clone = new Method (declaringType);
113+
114+
// MethodBase
115+
clone.Annotation = Annotation;
116+
clone.ApiAvailableSince = ApiAvailableSince;
117+
clone.AssemblyName = AssemblyName;
118+
clone.Deprecated = Deprecated;
119+
clone.DeprecatedSince = DeprecatedSince;
120+
clone.IsAcw = IsAcw;
121+
clone.Name = Name;
122+
clone.Visibility = Visibility;
123+
clone.LineNumber = LineNumber;
124+
clone.LinePosition = LinePosition;
125+
clone.SourceFile = SourceFile;
126+
clone.JavadocInfo = JavadocInfo;
127+
128+
if (GenericArguments != null)
129+
foreach (var ga in GenericArguments)
130+
clone.GenericArguments.Add (ga.Clone ());
131+
132+
foreach (var p in Parameters)
133+
clone.Parameters.Add (p.Clone ());
134+
135+
// Method
136+
clone.ArgsType = ArgsType;
137+
clone.CustomAttributes = CustomAttributes;
138+
clone.EventName = EventName;
139+
clone.GenerateAsyncWrapper = GenerateAsyncWrapper;
140+
clone.GenerateDispatchingSetter = GenerateDispatchingSetter;
141+
clone.IsAbstract = IsAbstract;
142+
clone.IsFinal = IsFinal;
143+
clone.IsInterfaceDefaultMethod = IsInterfaceDefaultMethod;
144+
clone.OverriddenInterfaceMethod = OverriddenInterfaceMethod;
145+
clone.IsReturnEnumified = IsReturnEnumified;
146+
clone.IsStatic = IsStatic;
147+
clone.IsVirtual = IsVirtual;
148+
clone.JavaName = JavaName;
149+
clone.ManagedOverride = ManagedOverride;
150+
clone.ManagedReturn = ManagedReturn;
151+
clone.PropertyNameOverride = PropertyNameOverride;
152+
clone.Return = Return;
153+
clone.ReturnNotNull = ReturnNotNull;
154+
clone.RetVal = RetVal.Clone (clone);
155+
clone.SourceApiLevel = SourceApiLevel;
156+
clone.ExplicitInterface = ExplicitInterface;
157+
158+
return clone;
159+
}
160+
110161
public string ConnectorName => $"Get{Name}{IDSignature}Handler";
111162

112163
public string EscapedCallbackName => IdentifierValidator.CreateValidIdentifier ($"cb_{JavaName}{IDSignature}", true);

tools/generator/Java.Interop.Tools.Generator.ObjectModel/Parameter.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ internal Parameter (string name, string type, string managedType, bool isEnumifi
3232
this.is_enumified = isEnumified;
3333
NotNull = notNull;
3434
}
35-
35+
36+
public Parameter Clone ()
37+
{
38+
return new Parameter (name, type, managed_type, is_enumified, rawtype, NotNull);
39+
}
40+
3641
public string GetCall (CodeGenerationOptions opt)
3742
{
3843
var rgm = sym as IRequireGenericMarshal;

tools/generator/Java.Interop.Tools.Generator.ObjectModel/ReturnValue.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public ReturnValue (Method owner, string java_type, string managed_type, bool is
2828

2929
public string CallMethodPrefix => TypeNameUtilities.GetCallPrefix (sym);
3030

31+
public ReturnValue Clone (Method owner)
32+
{
33+
return new ReturnValue (owner, java_type, managed_type, is_enumified, NotNull);
34+
}
35+
3136
public string DefaultValue {
3237
get { return sym.DefaultValue; }
3338
}

0 commit comments

Comments
 (0)