diff --git a/src/Xamarin.SourceWriter/Models/TypeReferenceWriter.cs b/src/Xamarin.SourceWriter/Models/TypeReferenceWriter.cs index 4d6746ce6..75be16f67 100644 --- a/src/Xamarin.SourceWriter/Models/TypeReferenceWriter.cs +++ b/src/Xamarin.SourceWriter/Models/TypeReferenceWriter.cs @@ -40,12 +40,17 @@ public TypeReferenceWriter (string ns, string name) public virtual void WriteTypeReference (CodeWriter writer) { - if (Namespace.HasValue ()) - writer.Write ($"{Namespace}.{Name}{NullableOperator} "); - else - writer.Write ($"{Name}{NullableOperator} "); + writer.Write ($"{ToString ()} "); } string NullableOperator => Nullable ? "?" : string.Empty; + + public override string ToString () + { + if (Namespace.HasValue ()) + return $"{Namespace}.{Name}{NullableOperator}"; + + return $"{Name}{NullableOperator}"; + } } } diff --git a/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteCachedReferenceTypeField.txt b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteCachedReferenceTypeField.txt new file mode 100644 index 000000000..784df97b9 --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteCachedReferenceTypeField.txt @@ -0,0 +1,24 @@ +// Metadata.xml XPath class reference: path="/api/package[@name='java.code']/class[@name='MyClass']" +[global::Java.Interop.JniTypeSignature ("java/code/MyClass", GenerateJavaPeer=false)] +public partial class MyClass { + private static java.code.Example? _field_cache; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/class[@name='MyClass']/field[@name='field']" + public static java.code.Example? field { + get { + if (_field_cache != null) return (java.code.Example)_field_cache; + + const string __id = "field.Ljava/code/Example;"; + + var __v = _members.StaticFields.GetObjectValue (__id); + return (java.code.Example?)(_field_cache = global::Java.Interop.JniEnvironment.Runtime.ValueManager.GetValue(ref __v, JniObjectReferenceOptions.Copy)); + } + } + + static readonly JniPeerMembers _members = new JniPeerMembers ("java/code/MyClass", typeof (MyClass)); + + protected MyClass (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (ref reference, options) + { + } + +} diff --git a/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteCachedValueTypeField.txt b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteCachedValueTypeField.txt new file mode 100644 index 000000000..f253c6fc8 --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteCachedValueTypeField.txt @@ -0,0 +1,24 @@ +// Metadata.xml XPath class reference: path="/api/package[@name='java.code']/class[@name='MyClass']" +[global::Java.Interop.JniTypeSignature ("java/code/MyClass", GenerateJavaPeer=false)] +public partial class MyClass { + private static int? _field_cache; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/class[@name='MyClass']/field[@name='field']" + public static int field { + get { + if (_field_cache != null) return (int)_field_cache; + + const string __id = "field.I"; + + var __v = _members.StaticFields.GetInt32Value (__id); + return (int)(_field_cache = __v); + } + } + + static readonly JniPeerMembers _members = new JniPeerMembers ("java/code/MyClass", typeof (MyClass)); + + protected MyClass (ref JniObjectReference reference, JniObjectReferenceOptions options) : base (ref reference, options) + { + } + +} diff --git a/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteCachedReferenceTypeField.txt b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteCachedReferenceTypeField.txt new file mode 100644 index 000000000..9fb51c040 --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteCachedReferenceTypeField.txt @@ -0,0 +1,29 @@ +// Metadata.xml XPath class reference: path="/api/package[@name='java.code']/class[@name='MyClass']" +[global::Android.Runtime.Register ("java/code/MyClass", DoNotGenerateAcw=true)] +public partial class MyClass { + private static java.code.Example? _field_cache; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/class[@name='MyClass']/field[@name='field']" + [Register ("field")] + public static java.code.Example? field { + get { + if (_field_cache != null) return (java.code.Example)_field_cache; + + const string __id = "field.Ljava/code/Example;"; + + var __v = _members.StaticFields.GetObjectValue (__id); + return (java.code.Example?)(_field_cache = global::Java.Lang.Object.GetObject (__v.Handle, JniHandleOwnership.TransferLocalRef)); + } + } + + static readonly JniPeerMembers _members = new XAPeerMembers ("java/code/MyClass", typeof (MyClass)); + + internal static IntPtr class_ref { + get { return _members.JniPeerType.PeerReference.Handle; } + } + + protected MyClass (IntPtr javaReference, JniHandleOwnership transfer) : base (javaReference, transfer) + { + } + +} diff --git a/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteCachedValueTypeField.txt b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteCachedValueTypeField.txt new file mode 100644 index 000000000..9cb2a8023 --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1-NRT/WriteCachedValueTypeField.txt @@ -0,0 +1,29 @@ +// Metadata.xml XPath class reference: path="/api/package[@name='java.code']/class[@name='MyClass']" +[global::Android.Runtime.Register ("java/code/MyClass", DoNotGenerateAcw=true)] +public partial class MyClass { + private static int? _field_cache; + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/class[@name='MyClass']/field[@name='field']" + [Register ("field")] + public static int field { + get { + if (_field_cache != null) return (int)_field_cache; + + const string __id = "field.I"; + + var __v = _members.StaticFields.GetInt32Value (__id); + return (int)(_field_cache = __v); + } + } + + static readonly JniPeerMembers _members = new XAPeerMembers ("java/code/MyClass", typeof (MyClass)); + + internal static IntPtr class_ref { + get { return _members.JniPeerType.PeerReference.Handle; } + } + + protected MyClass (IntPtr javaReference, JniHandleOwnership transfer) : base (javaReference, transfer) + { + } + +} diff --git a/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteCachedReferenceTypeField.txt b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteCachedReferenceTypeField.txt new file mode 100644 index 000000000..abf2b3b5a --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteCachedReferenceTypeField.txt @@ -0,0 +1,26 @@ +// Metadata.xml XPath class reference: path="/api/package[@name='java.code']/class[@name='MyClass']" +[global::Android.Runtime.Register ("java/code/MyClass", DoNotGenerateAcw=true)] +public partial class MyClass { + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/class[@name='MyClass']/field[@name='field']" + [Register ("field")] + public static java.code.Example field { + get { + const string __id = "field.Ljava/code/Example;"; + + var __v = _members.StaticFields.GetObjectValue (__id); + return global::Java.Lang.Object.GetObject (__v.Handle, JniHandleOwnership.TransferLocalRef); + } + } + + static readonly JniPeerMembers _members = new XAPeerMembers ("java/code/MyClass", typeof (MyClass)); + + internal static IntPtr class_ref { + get { return _members.JniPeerType.PeerReference.Handle; } + } + + protected MyClass (IntPtr javaReference, JniHandleOwnership transfer) : base (javaReference, transfer) + { + } + +} diff --git a/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteCachedValueTypeField.txt b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteCachedValueTypeField.txt new file mode 100644 index 000000000..598aeac08 --- /dev/null +++ b/tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/XAJavaInterop1/WriteCachedValueTypeField.txt @@ -0,0 +1,26 @@ +// Metadata.xml XPath class reference: path="/api/package[@name='java.code']/class[@name='MyClass']" +[global::Android.Runtime.Register ("java/code/MyClass", DoNotGenerateAcw=true)] +public partial class MyClass { + + // Metadata.xml XPath field reference: path="/api/package[@name='java.code']/class[@name='MyClass']/field[@name='field']" + [Register ("field")] + public static int field { + get { + const string __id = "field.I"; + + var __v = _members.StaticFields.GetInt32Value (__id); + return __v; + } + } + + static readonly JniPeerMembers _members = new XAPeerMembers ("java/code/MyClass", typeof (MyClass)); + + internal static IntPtr class_ref { + get { return _members.JniPeerType.PeerReference.Handle; } + } + + protected MyClass (IntPtr javaReference, JniHandleOwnership transfer) : base (javaReference, transfer) + { + } + +} diff --git a/tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs b/tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs index a8393c61b..3fbefe57c 100644 --- a/tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs +++ b/tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs @@ -1644,5 +1644,47 @@ public void WriteBoundMethodAbstractDeclarationWithGenericReturn () // Ignore nullable operator so this test works on all generators ;) Assert.AreEqual (expected.NormalizeLineEndings (), writer.ToString ().NormalizeLineEndings ().Replace ("?", "")); } + + [Test] + public void WriteCachedReferenceTypeField () + { + options.SymbolTable.AddType (new TestClass (null, "Java.Lang.Object")); + var eClass = new TestClass ("Java.Lang.Object", "java.code.Example"); + options.SymbolTable.AddType (eClass); + + var klass = new TestClass ("Object", "java.code.MyClass"); + + var field = SupportTypeBuilder.CreateField ("field", options, "java.code.Example", true); + field.IsFinal = true; + + klass.Fields.Add (field); + + generator.Context.ContextTypes.Push (klass); + generator.WriteType (klass, string.Empty, new GenerationInfo ("", "", "MyAssembly")); + generator.Context.ContextTypes.Pop (); + + AssertTargetedExpected (nameof (WriteCachedReferenceTypeField), writer.ToString ()); + } + + [Test] + public void WriteCachedValueTypeField () + { + options.SymbolTable.AddType (new TestClass (null, "Java.Lang.Object")); + var eClass = new TestClass ("Java.Lang.Object", "java.code.Example"); + options.SymbolTable.AddType (eClass); + + var klass = new TestClass ("Object", "java.code.MyClass"); + + var field = SupportTypeBuilder.CreateField ("field", options, "int", true); + field.IsFinal = true; + + klass.Fields.Add (field); + + generator.Context.ContextTypes.Push (klass); + generator.WriteType (klass, string.Empty, new GenerationInfo ("", "", "MyAssembly")); + generator.Context.ContextTypes.Pop (); + + AssertTargetedExpected (nameof (WriteCachedValueTypeField), writer.ToString ()); + } } } diff --git a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs index 8477fab1d..f560abe28 100644 --- a/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs +++ b/tools/generator/Java.Interop.Tools.Generator.ObjectModel/Field.cs @@ -42,6 +42,8 @@ public class Field : ApiVersionsSupport.IApiAvailability, ISourceLineInfo public bool NeedsProperty => !IsStatic || !IsFinal || string.IsNullOrEmpty (Value) || Symbol.IsArray || !primitive_types.Contains (Symbol.JavaName); + public string CachedMemberName => $"_{Name}_cache"; + public bool Validate (CodeGenerationOptions opt, GenericParameterDefinitionList type_params, CodeGeneratorContext context) { Symbol = opt.SymbolTable.Lookup (TypeName, type_params); diff --git a/tools/generator/SourceWriters/BoundFieldAsProperty.cs b/tools/generator/SourceWriters/BoundFieldAsProperty.cs index c7f2560cd..2b577d0fd 100644 --- a/tools/generator/SourceWriters/BoundFieldAsProperty.cs +++ b/tools/generator/SourceWriters/BoundFieldAsProperty.cs @@ -16,6 +16,7 @@ public class BoundFieldAsProperty : PropertyWriter { readonly Field field; readonly CodeGenerationOptions opt; + readonly FieldWriter? cached_field; public BoundFieldAsProperty (GenBase type, Field field, CodeGenerationOptions opt) { @@ -59,10 +60,23 @@ public BoundFieldAsProperty (GenBase type, Field field, CodeGenerationOptions op if (!field.IsConst) HasSet = true; + + // This is considerably harder to support if we don't have NRT, due to the + // differences in handling nullable value and reference types. + if (field.IsConst && opt.SupportNullableReferenceTypes) + cached_field = new FieldWriter { + Name = field.CachedMemberName, + Type = new TypeReferenceWriter (fieldType.TrimEnd ('?')) { Nullable = true }, + IsStatic = true, + IsPrivate = true, + UseExplicitPrivateKeyword = type is InterfaceGen, + }; } public override void Write (CodeWriter writer) { + cached_field?.Write (writer); + // This is just a temporary hack to write the [GeneratedEnum] attribute before the // Metadata.xml // comment so that we are 100% equal to pre-refactor. var generated_attr = Attributes.OfType ().FirstOrDefault (); @@ -82,6 +96,13 @@ public override void WriteAttributes (CodeWriter writer) protected override void WriteGetterBody (CodeWriter writer) { + var cached_field_type = cached_field is not null ? new TypeReferenceWriter (cached_field.Type.Namespace, cached_field.Type.Name) : null; + + if (cached_field is not null) { + writer.WriteLine ($"if ({field.CachedMemberName} != null) return ({cached_field_type}){field.CachedMemberName};"); + writer.WriteLine (); + } + writer.WriteLine ($"const string __id = \"{field.JavaName}.{field.Symbol.JniName}\";"); writer.WriteLine (); @@ -93,24 +114,27 @@ protected override void WriteGetterBody (CodeWriter writer) writer.WriteLine ($"var __v = {field.Symbol.ReturnCast}_members.{indirect}.{invoke} (__id{(field.IsStatic ? "" : ", this")});"); + var cache_setter = cached_field is not null ? $"({PropertyType})({field.CachedMemberName} = " : ""; + var cache_setter_end = cached_field is not null ? ")" : ""; + if (opt.CodeGenerationTarget == CodeGenerationTarget.JavaInterop1) { if (field.Symbol.NativeType == field.Symbol.FullName) { - writer.WriteLine ("return __v;"); + writer.WriteLine ($"return {cache_setter}__v{cache_setter_end};"); return; } - writer.Write ("return global::Java.Interop.JniEnvironment.Runtime.ValueManager.GetValue<"); + writer.Write ($"return {cache_setter}global::Java.Interop.JniEnvironment.Runtime.ValueManager.GetValue<"); PropertyType.WriteTypeReference (writer); writer.Write (">(ref __v, JniObjectReferenceOptions.Copy)"); - writer.WriteLine (";"); + writer.WriteLine ($"{cache_setter_end};"); return; } if (field.Symbol.IsArray) { - writer.WriteLine ($"return global::Android.Runtime.JavaArray<{opt.GetOutputName (field.Symbol.ElementType)}>.FromJniHandle (__v.Handle, JniHandleOwnership.TransferLocalRef);"); + writer.WriteLine ($"return {cache_setter}global::Android.Runtime.JavaArray<{opt.GetOutputName (field.Symbol.ElementType)}>.FromJniHandle (__v.Handle, JniHandleOwnership.TransferLocalRef){cache_setter_end};"); } else if (field.Symbol.NativeType != field.Symbol.FullName) { - writer.WriteLine ($"return {field.Symbol.ReturnCast}{(field.Symbol.FromNative (opt, invokeType != "Object" ? "__v" : "__v.Handle", true) + opt.GetNullForgiveness (field))};"); + writer.WriteLine ($"return {cache_setter}{field.Symbol.ReturnCast}{(field.Symbol.FromNative (opt, invokeType != "Object" ? "__v" : "__v.Handle", true) + opt.GetNullForgiveness (field))}{cache_setter_end};"); } else { - writer.WriteLine ("return __v;"); + writer.WriteLine ($"return {cache_setter}__v{cache_setter_end};"); } } diff --git a/tools/generator/generator.csproj b/tools/generator/generator.csproj index f58833442..c88eb7296 100644 --- a/tools/generator/generator.csproj +++ b/tools/generator/generator.csproj @@ -4,6 +4,7 @@ $(DotNetTargetFramework) Exe $(DefineConstants);GENERATOR;HAVE_CECIL;JCW_ONLY_TYPE_NAMES + annotations