From 1fa43189b5b0392726f97e9b2576dc25b329699e Mon Sep 17 00:00:00 2001 From: Sebastien Pouliot Date: Tue, 27 Jul 2021 09:36:22 -0400 Subject: [PATCH] [generator] Do not generate `BindingImplOptions.Optimizable` when code snippets are provided (#12165) unless the snippet-based attribute also set the `Optimizable` property. IOW this is now required to _opt-in_ (to optimize) instead of default, with no ~easy~ way to turn it off. Re-enabled each of the `[Dispose]` cases after protecting the required extra calls they make. Fix https://github.com/xamarin/xamarin-macios/issues/12150 * Add test for [Dispose] and SnippetAttribute subclasses * Opt-in all [*Snippet] to be optimizable This is exactly what we have been shipping for years (no changes). Unlike [Dispose] there is no change in the generated code or the optimizations. Future snippets should come with tests - which is _normal_ for any manual code (which they are) added to the SDK --- src/CoreAnimation/CALayer.cs | 2 + src/UIKit/UIGestureRecognizer.cs | 2 + src/appkit.cs | 14 ++-- src/avfoundation.cs | 2 +- src/coreanimation.cs | 4 +- src/foundation.cs | 38 +++++----- src/generator-attributes.cs | 8 ++- src/generator.cs | 33 ++++++--- src/safariservices.cs | 2 +- src/uikit.cs | 6 +- tests/generator/BGenTests.cs | 79 +++++++++++++++++++++ tests/generator/tests/dispose-attribute.cs | 36 ++++++++++ tests/generator/tests/snippet-attributes.cs | 48 +++++++++++++ 13 files changed, 232 insertions(+), 42 deletions(-) create mode 100644 tests/generator/tests/dispose-attribute.cs create mode 100644 tests/generator/tests/snippet-attributes.cs diff --git a/src/CoreAnimation/CALayer.cs b/src/CoreAnimation/CALayer.cs index 79077f789c..5401de6add 100644 --- a/src/CoreAnimation/CALayer.cs +++ b/src/CoreAnimation/CALayer.cs @@ -103,6 +103,8 @@ namespace CoreAnimation { } } + // Note: preserving this member allows us to re-enable the `Optimizable` binding flag + [Preserve (Conditional = true)] void OnDispose () { if (calayerdelegate != null) { diff --git a/src/UIKit/UIGestureRecognizer.cs b/src/UIKit/UIGestureRecognizer.cs index 1325859193..cee5e86f4d 100644 --- a/src/UIKit/UIGestureRecognizer.cs +++ b/src/UIKit/UIGestureRecognizer.cs @@ -34,6 +34,8 @@ namespace UIKit { // Called by the Dispose() method, because this can run from a finalizer, we need to // (a) reference the handle, that we will release later, and (b) to remove the targets on the // UI thread. + // Note: preserving this member allows us to re-enable the `Optimizable` binding flag + [Preserve (Conditional = true)] void OnDispose () { var copyOfRecognizers = recognizers; diff --git a/src/appkit.cs b/src/appkit.cs index 2f4629c7af..5b81593020 100644 --- a/src/appkit.cs +++ b/src/appkit.cs @@ -15103,16 +15103,16 @@ namespace AppKit { void WillRemoveSubview ([NullAllowed] NSView subview); [Export ("removeFromSuperview")] - [PreSnippet ("var mySuper = Superview;")] - [PostSnippet ("if (mySuper != null) {\n\t#pragma warning disable 168\n\tvar flush = mySuper.Subviews;\n#pragma warning restore 168\n\t}")] + [PreSnippet ("var mySuper = Superview;", Optimizable = true)] + [PostSnippet ("if (mySuper != null) {\n\t#pragma warning disable 168\n\tvar flush = mySuper.Subviews;\n#pragma warning restore 168\n\t}", Optimizable = true)] void RemoveFromSuperview (); [Export ("replaceSubview:with:")][PostGet ("Subviews")] void ReplaceSubviewWith (NSView oldView, NSView newView); [Export ("removeFromSuperviewWithoutNeedingDisplay")] - [PreSnippet ("var mySuper = Superview;")] - [PostSnippet ("if (mySuper != null) {\n\t#pragma warning disable 168\n\tvar flush = mySuper.Subviews;\n#pragma warning restore 168\n\t}")] + [PreSnippet ("var mySuper = Superview;", Optimizable = true)] + [PostSnippet ("if (mySuper != null) {\n\t#pragma warning disable 168\n\tvar flush = mySuper.Subviews;\n#pragma warning restore 168\n\t}", Optimizable = true)] void RemoveFromSuperviewWithoutNeedingDisplay (); [Export ("resizeSubviewsWithOldSize:")] @@ -19670,16 +19670,16 @@ namespace AppKit { CGRect ContentRectFor (CGRect frameRect); [Export ("init")] - [PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }")] + [PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }", Optimizable = true)] IntPtr Constructor (); [DesignatedInitializer] [Export ("initWithContentRect:styleMask:backing:defer:")] - [PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }")] + [PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }", Optimizable = true)] IntPtr Constructor (CGRect contentRect, NSWindowStyle aStyle, NSBackingStore bufferingType, bool deferCreation); [Export ("initWithContentRect:styleMask:backing:defer:screen:")] - [PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }")] + [PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }", Optimizable = true)] IntPtr Constructor (CGRect contentRect, NSWindowStyle aStyle, NSBackingStore bufferingType, bool deferCreation, NSScreen screen); [Export ("title")] diff --git a/src/avfoundation.cs b/src/avfoundation.cs index eaaa2c354a..d7006e14f5 100644 --- a/src/avfoundation.cs +++ b/src/avfoundation.cs @@ -12412,7 +12412,7 @@ namespace AVFoundation { IntPtr Constructor (AVQueuePlayer player, AVPlayerItem itemToLoop, CMTimeRange loopRange); #if !XAMCORE_4_0 // This API got introduced in Xcode 8.0 binding but is not currently present nor in Xcode 8.3 or Xcode 9.0 needs research - [PostSnippet ("loopingEnabled = false;")] + [PostSnippet ("loopingEnabled = false;", Optimizable = true)] #endif [Export ("disableLooping")] void DisableLooping (); diff --git a/src/coreanimation.cs b/src/coreanimation.cs index 2b0b433207..c609d3a88b 100644 --- a/src/coreanimation.cs +++ b/src/coreanimation.cs @@ -186,7 +186,7 @@ namespace CoreAnimation { } [BaseType (typeof (NSObject))] - [Dispose ("OnDispose ();")] + [Dispose ("OnDispose ();", Optimizable = true)] interface CALayer : CAMediaTiming, NSSecureCoding { [Export ("layer")][Static] CALayer Create (); @@ -432,7 +432,7 @@ namespace CoreAnimation { string Name { get; set; } [Export ("delegate", ArgumentSemantic.Weak)][NullAllowed] - NSObject WeakDelegate { get; [PostSnippet (@"SetCALayerDelegate (value as CALayerDelegate);")] set; } + NSObject WeakDelegate { get; [PostSnippet (@"SetCALayerDelegate (value as CALayerDelegate);", Optimizable = true)] set; } [Wrap ("WeakDelegate")] [Protocolize] diff --git a/src/foundation.cs b/src/foundation.cs index ec61e937b2..b5969f8c29 100644 --- a/src/foundation.cs +++ b/src/foundation.cs @@ -473,7 +473,7 @@ namespace Foundation [Mac (10,15), iOS (13,0)] [Static] [Export ("loadFromHTMLWithRequest:options:completionHandler:")] - [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory")] + [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory", Optimizable = true)] [Async (ResultTypeName = "NSLoadFromHtmlResult")] [EditorBrowsable (EditorBrowsableState.Advanced)] void LoadFromHtml (NSUrlRequest request, NSDictionary options, NSAttributedStringCompletionHandler completionHandler); @@ -489,7 +489,7 @@ namespace Foundation [Mac (10,15), iOS (13,0)] [Static] [Export ("loadFromHTMLWithFileURL:options:completionHandler:")] - [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory")] + [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory", Optimizable = true)] [Async (ResultTypeName = "NSLoadFromHtmlResult")] [EditorBrowsable (EditorBrowsableState.Advanced)] void LoadFromHtml (NSUrl fileUrl, NSDictionary options, NSAttributedStringCompletionHandler completionHandler); @@ -505,7 +505,7 @@ namespace Foundation [Mac (10,15), iOS (13,0)] [Static] [Export ("loadFromHTMLWithString:options:completionHandler:")] - [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory")] + [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory", Optimizable = true)] [Async (ResultTypeName = "NSLoadFromHtmlResult")] [EditorBrowsable (EditorBrowsableState.Advanced)] void LoadFromHtml (string @string, NSDictionary options, NSAttributedStringCompletionHandler completionHandler); @@ -521,7 +521,7 @@ namespace Foundation [Mac (10,15), iOS (13,0)] [Static] [Export ("loadFromHTMLWithData:options:completionHandler:")] - [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory")] + [PreSnippet ("GC.KeepAlive (WebKit.WKContentMode.Recommended); // no-op to ensure WebKit.framework is loaded into memory", Optimizable = true)] [Async (ResultTypeName = "NSLoadFromHtmlResult")] [EditorBrowsable (EditorBrowsableState.Advanced)] void LoadFromHtml (NSData data, NSDictionary options, NSAttributedStringCompletionHandler completionHandler); @@ -3634,11 +3634,11 @@ namespace Foundation [BaseType (typeof (NSData))] interface NSMutableData { [Static, Export ("dataWithCapacity:")] [Autorelease] - [PreSnippet ("if (capacity < 0 || capacity > nint.MaxValue) throw new ArgumentOutOfRangeException ();")] + [PreSnippet ("if (capacity < 0 || capacity > nint.MaxValue) throw new ArgumentOutOfRangeException ();", Optimizable = true)] NSMutableData FromCapacity (nint capacity); [Static, Export ("dataWithLength:")] [Autorelease] - [PreSnippet ("if (length < 0 || length > nint.MaxValue) throw new ArgumentOutOfRangeException ();")] + [PreSnippet ("if (length < 0 || length > nint.MaxValue) throw new ArgumentOutOfRangeException ();", Optimizable = true)] NSMutableData FromLength (nint length); [Static, Export ("data")] [Autorelease] @@ -3648,7 +3648,7 @@ namespace Foundation IntPtr MutableBytes { get; } [Export ("initWithCapacity:")] - [PreSnippet ("if (capacity > (ulong) nint.MaxValue) throw new ArgumentOutOfRangeException ();")] + [PreSnippet ("if (capacity > (ulong) nint.MaxValue) throw new ArgumentOutOfRangeException ();", Optimizable = true)] IntPtr Constructor (nuint capacity); [Export ("appendData:")] @@ -5050,7 +5050,7 @@ namespace Foundation } [BaseType (typeof(NSObject))] - [Dispose ("if (disposing) { Invalidate (); } ")] + [Dispose ("if (disposing) { Invalidate (); } ", Optimizable = true)] // init returns NIL [DisableDefaultCtor] interface NSTimer { @@ -5086,6 +5086,8 @@ namespace Foundation [Export ("fireDate", ArgumentSemantic.Copy)] NSDate FireDate { get; set; } + // Note: preserving this member allows us to re-enable the `Optimizable` binding flag + [Preserve (Conditional = true)] [Export ("invalidate")] void Invalidate (); @@ -5402,9 +5404,9 @@ namespace Foundation // in turns, means that the Intents.framework is loaded into memory and this makes the // selectors (getter and setter) work at runtime. Other selectors do not need it. // reference: https://github.com/xamarin/xamarin-macios/issues/4894 - [PreSnippet ("GC.KeepAlive (Intents.INCallCapabilityOptions.AudioCall); // no-op to ensure Intents.framework is loaded into memory")] + [PreSnippet ("GC.KeepAlive (Intents.INCallCapabilityOptions.AudioCall); // no-op to ensure Intents.framework is loaded into memory", Optimizable = true)] get; - [PreSnippet ("GC.KeepAlive (Intents.INCallCapabilityOptions.AudioCall); // no-op to ensure Intents.framework is loaded into memory")] + [PreSnippet ("GC.KeepAlive (Intents.INCallCapabilityOptions.AudioCall); // no-op to ensure Intents.framework is loaded into memory", Optimizable = true)] set; } @@ -8481,11 +8483,11 @@ namespace Foundation [Export ("initWithCapacity:")] IntPtr Constructor (nint capacity); - [PreSnippet ("Check (index);")] + [PreSnippet ("Check (index);", Optimizable = true)] [Export ("insertString:atIndex:")] void Insert (NSString str, nint index); - [PreSnippet ("Check (range);")] + [PreSnippet ("Check (range);", Optimizable = true)] [Export ("deleteCharactersInRange:")] void DeleteCharacters (NSRange range); @@ -8495,7 +8497,7 @@ namespace Foundation [Export ("setString:")] void SetString (NSString str); - [PreSnippet ("Check (range);")] + [PreSnippet ("Check (range);", Optimizable = true)] [Export ("replaceOccurrencesOfString:withString:options:range:")] nuint ReplaceOcurrences (NSString target, NSString replacement, NSStringCompareOptions options, NSRange range); @@ -8840,12 +8842,12 @@ namespace Foundation [Export ("methodForSelector:")] IntPtr GetMethodForSelector (Selector sel); - [PreSnippet ("if (!(this is INSCopying)) throw new InvalidOperationException (\"Type does not conform to NSCopying\");")] + [PreSnippet ("if (!(this is INSCopying)) throw new InvalidOperationException (\"Type does not conform to NSCopying\");", Optimizable = true)] [Export ("copy")] [return: Release ()] NSObject Copy (); - [PreSnippet ("if (!(this is INSMutableCopying)) throw new InvalidOperationException (\"Type does not conform to NSMutableCopying\");")] + [PreSnippet ("if (!(this is INSMutableCopying)) throw new InvalidOperationException (\"Type does not conform to NSMutableCopying\");", Optimizable = true)] [Export ("mutableCopy")] [return: Release ()] NSObject MutableCopy (); @@ -10637,7 +10639,7 @@ namespace Foundation NSNotificationCenter DefaultCenter { get; } [Export ("addObserver:selector:name:object:")] - [PostSnippet ("AddObserverToList (observer, aName, anObject);")] + [PostSnippet ("AddObserverToList (observer, aName, anObject);", Optimizable = true)] void AddObserver (NSObject observer, Selector aSelector, [NullAllowed] NSString aName, [NullAllowed] NSObject anObject); [Export ("postNotification:")] @@ -10650,11 +10652,11 @@ namespace Foundation void PostNotificationName (string aName, [NullAllowed] NSObject anObject, [NullAllowed] NSDictionary aUserInfo); [Export ("removeObserver:")] - [PostSnippet ("RemoveObserversFromList (observer, null, null);")] + [PostSnippet ("RemoveObserversFromList (observer, null, null);", Optimizable = true)] void RemoveObserver (NSObject observer); [Export ("removeObserver:name:object:")] - [PostSnippet ("RemoveObserversFromList (observer, aName, anObject);")] + [PostSnippet ("RemoveObserversFromList (observer, aName, anObject);", Optimizable = true)] void RemoveObserver (NSObject observer, [NullAllowed] string aName, [NullAllowed] NSObject anObject); [Export ("addObserverForName:object:queue:usingBlock:")] diff --git a/src/generator-attributes.cs b/src/generator-attributes.cs index 4e96a7a1d1..70a55c2984 100644 --- a/src/generator-attributes.cs +++ b/src/generator-attributes.cs @@ -583,10 +583,13 @@ public class SnippetAttribute : Attribute { Code = s; } public string Code { get; set; } + + public bool Optimizable { get; set; } } // // PreSnippet code is inserted after the parameters have been validated/marshalled +// Adding this attribute will, by default, make the method non-optimizable by the SDK tools // public class PreSnippetAttribute : SnippetAttribute { public PreSnippetAttribute (string s) : base (s) {} @@ -594,6 +597,7 @@ public class PreSnippetAttribute : SnippetAttribute { // // PrologueSnippet code is inserted before any code is generated +// Adding this attribute will, by default, make the method non-optimizable by the SDK tools // public class PrologueSnippetAttribute : SnippetAttribute { public PrologueSnippetAttribute (string s) : base (s) {} @@ -601,13 +605,15 @@ public class PrologueSnippetAttribute : SnippetAttribute { // // PostSnippet code is inserted before returning, before paramters are disposed/released +// Adding this attribute will, by default, make the method non-optimizable by the SDK tools // public class PostSnippetAttribute : SnippetAttribute { public PostSnippetAttribute (string s) : base (s) {} } // -// Code to run from a generated Dispose method +// Code to run from a generated Dispose method, before any generated code is executed +// Adding this attribute will, by default, make the method non-optimizable by the SDK tools // [AttributeUsage(AttributeTargets.Interface, AllowMultiple=true)] public class DisposeAttribute : SnippetAttribute { diff --git a/src/generator.cs b/src/generator.cs index ee60a12ae0..7108ac964f 100644 --- a/src/generator.cs +++ b/src/generator.cs @@ -4034,7 +4034,18 @@ public partial class Generator : IMemberGatherer { print (l); } } - + + bool IsOptimizable (MemberInfo method) + { + var optimizable = true; + var snippets = AttributeManager.GetCustomAttributes (method); + if (snippets.Length > 0) { + foreach (SnippetAttribute snippet in snippets) + optimizable &= snippet.Optimizable; + } + return optimizable; + } + [Flags] public enum BodyOption { None = 0x0, @@ -4956,7 +4967,7 @@ public partial class Generator : IMemberGatherer { } } - print_generated_code (); + print_generated_code (optimizable: IsOptimizable (pi)); PrintPropertyAttributes (pi, minfo.type); PrintAttributes (pi, preserve:true, advice:true, bindAs:true); @@ -5511,7 +5522,7 @@ public partial class Generator : IMemberGatherer { var mod = minfo.GetVisibility (); var is_abstract = minfo.is_abstract; - print_generated_code (); + print_generated_code (optimizable: IsOptimizable (minfo.mi)); print ("{0} {1}{2}{3}", mod, minfo.GetModifiers (), @@ -7423,15 +7434,19 @@ public partial class Generator : IMemberGatherer { // Do we need a dispose method? // if (!is_static_class){ - var disposeAttr = AttributeManager.GetCustomAttributes (type); - if (disposeAttr.Length > 0 || instance_fields_to_clear_on_dispose.Count > 0){ - print_generated_code (optimizable: disposeAttr.Length == 0); + var attrs = AttributeManager.GetCustomAttributes (type); + // historical note: unlike many attributes our `DisposeAttribute` has `AllowMultiple=true` + var has_dispose_attributes = attrs.Length > 0; + if (has_dispose_attributes || (instance_fields_to_clear_on_dispose.Count > 0)) { + // if there'a any [Dispose] attribute then they all must opt-in in order for the generated Dispose method to be optimizable + bool optimizable = !has_dispose_attributes || IsOptimizable (type); + print_generated_code (optimizable: optimizable); print ("protected override void Dispose (bool disposing)"); print ("{"); indent++; - if (disposeAttr.Length > 0){ - var snippet = disposeAttr [0]; - Inject (snippet); + if (has_dispose_attributes) { + foreach (var da in attrs) + Inject (da); } print ("base.Dispose (disposing);"); diff --git a/src/safariservices.cs b/src/safariservices.cs index fda1527012..aba36e9b77 100644 --- a/src/safariservices.cs +++ b/src/safariservices.cs @@ -59,7 +59,7 @@ namespace SafariServices { [Static, Export ("supportsURL:")] // Apple says it's __nonnull so let's be safe and maintain compatibility with our current behaviour - [PreSnippet ("if (url is null) return false;")] + [PreSnippet ("if (url is null) return false;", Optimizable = true)] bool SupportsUrl ([NullAllowed] NSUrl url); [Export ("addReadingListItemWithURL:title:previewText:error:")] diff --git a/src/uikit.cs b/src/uikit.cs index 27b5804de7..e3ff019490 100644 --- a/src/uikit.cs +++ b/src/uikit.cs @@ -2035,7 +2035,7 @@ namespace UIKit { void OpenUrl (NSUrl url, UIApplicationOpenUrlOptions options, [NullAllowed] Action completion); [Export ("canOpenURL:")] - [PreSnippet ("if (url is null) return false;")] // null not really allowed (but it's a behaviour change with known bug reports) + [PreSnippet ("if (url is null) return false;", Optimizable = true)] // null not really allowed (but it's a behaviour change with known bug reports) bool CanOpenUrl ([NullAllowed] NSUrl url); [Export ("sendEvent:")] @@ -5562,7 +5562,7 @@ namespace UIKit { #if !WATCH [BaseType (typeof(NSObject), Delegates=new string [] {"WeakDelegate"}, Events=new Type[] {typeof (UIGestureRecognizerDelegate)})] - [Dispose ("OnDispose ();")] + [Dispose ("OnDispose ();", Optimizable = true)] interface UIGestureRecognizer { [DesignatedInitializer] [Export ("initWithTarget:action:")] @@ -13243,7 +13243,7 @@ namespace UIKit { [Export ("typingAttributes", ArgumentSemantic.Copy)] NSDictionary TypingAttributes { // this avoids a crash (see unit tests) and behave like UITextField does (return null) - [PreSnippet ("if (SelectedRange.Length == 0) return null;")] + [PreSnippet ("if (SelectedRange.Length == 0) return null;", Optimizable = true)] get; set; } diff --git a/tests/generator/BGenTests.cs b/tests/generator/BGenTests.cs index c6e479bd02..468b638028 100644 --- a/tests/generator/BGenTests.cs +++ b/tests/generator/BGenTests.cs @@ -645,6 +645,85 @@ namespace GeneratorTests [Test] public void GHIssue9065_Sealed () => BuildFile (Profile.iOS, nowarnings: true, "ghissue9065.cs"); + // looking for [BindingImpl (BindingImplOptions.Optimizable)] + bool IsOptimizable (MethodDefinition method) + { + const int Optimizable = 0x2; // BindingImplOptions flag + + if (!method.HasCustomAttributes) + return false; + + foreach (var ca in method.CustomAttributes) { + if (ca.AttributeType.Name != "BindingImplAttribute") + continue; + foreach (var a in ca.ConstructorArguments) + return (((int) a.Value & Optimizable) == Optimizable); + } + return false; + } + + [Test] + public void DisposeAttributeOptimizable () + { + var bgen = BuildFile (Profile.iOS, "tests/dispose-attribute.cs"); + + // processing custom attributes (like its properties) will call Resolve so we must be able to find the platform assembly to run this test + var platform_dll = Path.Combine (Configuration.SdkRootXI, "lib/mono/Xamarin.iOS/Xamarin.iOS.dll"); + var resolver = bgen.ApiAssembly.MainModule.AssemblyResolver as BaseAssemblyResolver; + resolver.AddSearchDirectory (Path.Combine (Configuration.SdkRootXI, "lib/mono/Xamarin.iOS/")); + + // [Dispose] is, by default, not optimizable + var with_dispose = bgen.ApiAssembly.MainModule.GetType ("NS", "WithDispose").Methods.First ((v) => v.Name == "Dispose"); + Assert.NotNull (with_dispose, "WithDispose"); + Assert.That (IsOptimizable (with_dispose), Is.False, "WithDispose/Optimizable"); + + // [Dispose] can opt-in being optimizable + var with_dispose_optin = bgen.ApiAssembly.MainModule.GetType ("NS", "WithDisposeOptInOptimizable").Methods.First ((v) => v.Name == "Dispose"); + Assert.NotNull (with_dispose_optin, "WithDisposeOptInOptimizable"); + Assert.That (IsOptimizable (with_dispose_optin), Is.True, "WithDisposeOptInOptimizable/Optimizable"); + + // Without a [Dispose] attribute the generated method is optimizable + var without_dispose = bgen.ApiAssembly.MainModule.GetType ("NS", "WithoutDispose").Methods.First ((v) => v.Name == "Dispose"); + Assert.NotNull (without_dispose, "WitoutDispose"); + Assert.That (IsOptimizable (without_dispose), Is.True, "WitoutDispose/Optimizable"); + } + + [Test] + public void SnippetAttributesOptimizable () + { + var bgen = BuildFile (Profile.iOS, "tests/snippet-attributes.cs"); + + // processing custom attributes (like its properties) will call Resolve so we must be able to find the platform assembly to run this test + var platform_dll = Path.Combine (Configuration.SdkRootXI, "lib/mono/Xamarin.iOS/Xamarin.iOS.dll"); + var resolver = bgen.ApiAssembly.MainModule.AssemblyResolver as BaseAssemblyResolver; + resolver.AddSearchDirectory (Path.Combine (Configuration.SdkRootXI, "lib/mono/Xamarin.iOS/")); + + // [SnippetAttribute] subclasses are, by default, not optimizable + var not_opt = bgen.ApiAssembly.MainModule.GetType ("NS", "NotOptimizable"); + Assert.NotNull (not_opt, "NotOptimizable"); + var pre_not_opt = not_opt.Methods.First ((v) => v.Name == "Pre"); + Assert.That (IsOptimizable (pre_not_opt), Is.False, "NotOptimizable/Pre"); + var prologue_not_opt = not_opt.Methods.First ((v) => v.Name == "Prologue"); + Assert.That (IsOptimizable (prologue_not_opt), Is.False, "NotOptimizable/Prologue"); + var post_not_opt = not_opt.Methods.First ((v) => v.Name == "Post"); + Assert.That (IsOptimizable (post_not_opt), Is.False, "NotOptimizable/Post"); + + // [SnippetAttribute] subclasses can opt-in being optimizable + var optin_opt = bgen.ApiAssembly.MainModule.GetType ("NS", "OptInOptimizable"); + Assert.NotNull (optin_opt, "OptInOptimizable"); + var pre_optin_opt = optin_opt.Methods.First ((v) => v.Name == "Pre"); + Assert.That (IsOptimizable (pre_optin_opt), Is.True, "OptInOptimizable/Pre"); + var prologue_optin_opt = optin_opt.Methods.First ((v) => v.Name == "Prologue"); + Assert.That (IsOptimizable (prologue_optin_opt), Is.True, "OptInOptimizable/Prologue"); + var post_optin_opt = optin_opt.Methods.First ((v) => v.Name == "Post"); + Assert.That (IsOptimizable (post_optin_opt), Is.True, "OptInOptimizable/Post"); + + // Without a [SnippetAttribute] subclass attribute the generated method is optimizable + var nothing = bgen.ApiAssembly.MainModule.GetType ("NS", "NoSnippet").Methods.First ((v) => v.Name == "Nothing"); + Assert.NotNull (nothing, "NoSnippet"); + Assert.That (IsOptimizable (nothing), Is.True, "Nothing/Optimizable"); + } + BGenTool BuildFile (Profile profile, params string [] filenames) { return BuildFile (profile, true, false, filenames); diff --git a/tests/generator/tests/dispose-attribute.cs b/tests/generator/tests/dispose-attribute.cs new file mode 100644 index 0000000000..4614673f08 --- /dev/null +++ b/tests/generator/tests/dispose-attribute.cs @@ -0,0 +1,36 @@ +using System; +using Foundation; +using ObjCRuntime; + +namespace NS { + + // injecting custom code makes the Dispose method not-optimizable by default + [Dispose ("Console.WriteLine (\"Disposing!\");")] + [BaseType (typeof (NSObject))] + interface WithDispose { + + [Export ("delegate", ArgumentSemantic.Weak)] + [NullAllowed] + NSObject WeakDelegate { get; set; } + } + + // but we can opt-in to make it optimizable + [Dispose ("// just a comment, that's safe to optimize, if not very useful otherwise", Optimizable = true)] + [BaseType (typeof (NSObject))] + interface WithDisposeOptInOptimizable { + + [Export ("delegate", ArgumentSemantic.Weak)] + [NullAllowed] + NSObject WeakDelegate { get; set; } + } + + // if nothing is injected then we know we generate code that our tools can optimize + [BaseType (typeof (NSObject))] + interface WithoutDispose { + + // this ensure we have a Dispose method generated for the type + [Export ("delegate", ArgumentSemantic.Weak)] + [NullAllowed] + NSObject WeakDelegate { get; set; } + } +} diff --git a/tests/generator/tests/snippet-attributes.cs b/tests/generator/tests/snippet-attributes.cs new file mode 100644 index 0000000000..646ef4f6c5 --- /dev/null +++ b/tests/generator/tests/snippet-attributes.cs @@ -0,0 +1,48 @@ +using System; +using Foundation; +using ObjCRuntime; + +namespace NS { + + // injecting custom code makes the method method not-optimizable by default + [BaseType (typeof (NSObject))] + interface NotOptimizable { + + [PreSnippet ("Console.WriteLine (\"Pre!\");")] + [Export ("pre")] + void Pre (); + + [PrologueSnippet ("Console.WriteLine (\"Prologue!\");")] + [Export ("prologue")] + void Prologue (); + + [PostSnippet ("Console.WriteLine (\"Post!\");")] + [Export ("post")] + void Post (); + } + + // but we can opt-in to make it optimizable + [BaseType (typeof (NSObject))] + interface OptInOptimizable { + + [PreSnippet ("Console.WriteLine (\"Pre!\");", Optimizable = true)] + [Export ("pre")] + void Pre (); + + [PrologueSnippet ("Console.WriteLine (\"Prologue!\");", Optimizable = true)] + [Export ("prologue")] + void Prologue (); + + [PostSnippet ("Console.WriteLine (\"Post!\");", Optimizable = true)] + [Export ("post")] + void Post (); + } + + // if nothing is injected then we know we generate code that our tools can optimize + [BaseType (typeof (NSObject))] + interface NoSnippet { + + [Export ("nothing")] + void Nothing (); + } +}