From 68432a1550937d4e5e8e9432a2fc6e69564bbff6 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 6 Sep 2023 12:15:45 +0200 Subject: [PATCH] [bgen] Add ToArray and ToFlags extension methods for strongly typed [Flags] enums. (#18925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have a few different implementations of how to compute a set or array of NSString constants from an enum value, and also computing an enum value from a set or array of NSString constants. So add support to the generator for generating a ToArray and a ToFlags method for these conversions, and remove all the manual code. This has a few advantages: * The generated code is automatically updated with new enum fields (not all previous implementations were). * Reduces code duplication. * Reduces manual code. Additionally, when computing between a group of constants and the corresponding flags, we have to handle any missing constants or flags gracefully, otherwise the code won’t be forward compatible. Example (from bug #18833): * App developer writes code to fetch the AVCaptureMetadataOutput.AvailableMetadataObjectTypes property, which is an enum value of flags. * App developer ships their app in the app store. * Apple releases a new iOS update, adding new values and constants to the enum. * The native [AVCaptureMetadataOutput availableMetadataObjectTypes] method returns these new constants. * This means that if the AVCaptureMetadataOutput.AvailableMetadataObjectTypes property throws an exception if encountering unknown constants, our API wouldn’t be forward compatible. * The correct solution here is that AVCaptureMetadataOutput.AvailableMetadataObjectTypes must ignore any constants it doesn’t know about. Fixes https://github.com/xamarin/xamarin-macios/issues/18833. --- src/AVFoundation/AVCaptureMetadataOutput.cs | 6 +- src/AVFoundation/AVMetadataObject.cs | 36 --------- src/HomeKit/HMHome.cs | 77 +------------------ src/PassKit/PKPaymentRequest.cs | 30 +------- src/bgen/Enums.cs | 54 ++++++++++++- .../AVFoundation/CaptureMetadataOutputTest.cs | 26 +++---- 6 files changed, 72 insertions(+), 157 deletions(-) diff --git a/src/AVFoundation/AVCaptureMetadataOutput.cs b/src/AVFoundation/AVCaptureMetadataOutput.cs index 23cb37baa1..a4d7564bde 100644 --- a/src/AVFoundation/AVCaptureMetadataOutput.cs +++ b/src/AVFoundation/AVCaptureMetadataOutput.cs @@ -38,12 +38,12 @@ namespace AVFoundation { public partial class AVCaptureMetadataOutput { public AVMetadataObjectType AvailableMetadataObjectTypes { - get { return AVMetadataObject.ArrayToEnum (WeakAvailableMetadataObjectTypes); } + get { return AVMetadataObjectTypeExtensions.ToFlags (WeakAvailableMetadataObjectTypes); } } public AVMetadataObjectType MetadataObjectTypes { - get { return AVMetadataObject.ArrayToEnum (WeakMetadataObjectTypes); } - set { WeakMetadataObjectTypes = AVMetadataObject.EnumToArray (value); } + get { return AVMetadataObjectTypeExtensions.ToFlags (WeakMetadataObjectTypes); } + set { WeakMetadataObjectTypes = value.ToArray (); } } } diff --git a/src/AVFoundation/AVMetadataObject.cs b/src/AVFoundation/AVMetadataObject.cs index ab4ed418d8..50373362c0 100644 --- a/src/AVFoundation/AVMetadataObject.cs +++ b/src/AVFoundation/AVMetadataObject.cs @@ -41,42 +41,6 @@ namespace AVFoundation { return AVMetadataObjectTypeExtensions.GetValue (WeakType); } } - - internal static AVMetadataObjectType ArrayToEnum (NSString[]? arr) - { - AVMetadataObjectType rv = AVMetadataObjectType.None; - - if (arr is null || arr.Length == 0) - return rv; - - foreach (var str in arr) { - rv |= AVMetadataObjectTypeExtensions.GetValue (str); - } - - return rv; - } - - internal static NSString[]? EnumToArray (AVMetadataObjectType value) - { - if (value == AVMetadataObjectType.None) - return null; - - var rv = new List (); - var val = (ulong) value; - var shifts = 0; - - while (val != 0) { - if ((val & 0x1) == 0x1) { - var constant = ((AVMetadataObjectType) (0x1UL << shifts)).GetConstant (); - if (constant is not null) - rv.Add (constant); - } - val >>= 1; - shifts++; - } - - return rv.ToArray (); - } } } #endif diff --git a/src/HomeKit/HMHome.cs b/src/HomeKit/HMHome.cs index c5e64d26ec..61421db1e2 100644 --- a/src/HomeKit/HMHome.cs +++ b/src/HomeKit/HMHome.cs @@ -11,82 +11,7 @@ namespace HomeKit { public partial class HMHome { public HMService []? GetServices (HMServiceType serviceTypes) { - var arr = new ServiceTypeList (); - - if ((serviceTypes & HMServiceType.LightBulb) == HMServiceType.LightBulb) - arr.Add (HMServiceType.LightBulb.GetConstant ()); - if ((serviceTypes & HMServiceType.Switch) == HMServiceType.Switch) - arr.Add (HMServiceType.Switch.GetConstant ()); - if ((serviceTypes & HMServiceType.Thermostat) == HMServiceType.Thermostat) - arr.Add (HMServiceType.Thermostat.GetConstant ()); - if ((serviceTypes & HMServiceType.GarageDoorOpener) == HMServiceType.GarageDoorOpener) - arr.Add (HMServiceType.GarageDoorOpener.GetConstant ()); - if ((serviceTypes & HMServiceType.AccessoryInformation) == HMServiceType.AccessoryInformation) - arr.Add (HMServiceType.AccessoryInformation.GetConstant ()); - if ((serviceTypes & HMServiceType.Fan) == HMServiceType.Fan) - arr.Add (HMServiceType.Fan.GetConstant ()); - if ((serviceTypes & HMServiceType.Outlet) == HMServiceType.Outlet) - arr.Add (HMServiceType.Outlet.GetConstant ()); - if ((serviceTypes & HMServiceType.LockMechanism) == HMServiceType.LockMechanism) - arr.Add (HMServiceType.LockMechanism.GetConstant ()); - if ((serviceTypes & HMServiceType.LockManagement) == HMServiceType.LockManagement) - arr.Add (HMServiceType.LockManagement.GetConstant ()); - // iOS 9 - if ((serviceTypes & HMServiceType.AirQualitySensor) == HMServiceType.AirQualitySensor) - arr.Add (HMServiceType.AirQualitySensor.GetConstant ()); - if ((serviceTypes & HMServiceType.SecuritySystem) == HMServiceType.SecuritySystem) - arr.Add (HMServiceType.SecuritySystem.GetConstant ()); - if ((serviceTypes & HMServiceType.CarbonMonoxideSensor) == HMServiceType.CarbonMonoxideSensor) - arr.Add (HMServiceType.CarbonMonoxideSensor.GetConstant ()); - if ((serviceTypes & HMServiceType.ContactSensor) == HMServiceType.ContactSensor) - arr.Add (HMServiceType.ContactSensor.GetConstant ()); - if ((serviceTypes & HMServiceType.Door) == HMServiceType.Door) - arr.Add (HMServiceType.Door.GetConstant ()); - if ((serviceTypes & HMServiceType.HumiditySensor) == HMServiceType.HumiditySensor) - arr.Add (HMServiceType.HumiditySensor.GetConstant ()); - if ((serviceTypes & HMServiceType.LeakSensor) == HMServiceType.LeakSensor) - arr.Add (HMServiceType.LeakSensor.GetConstant ()); - if ((serviceTypes & HMServiceType.LightSensor) == HMServiceType.LightSensor) - arr.Add (HMServiceType.LightSensor.GetConstant ()); - if ((serviceTypes & HMServiceType.MotionSensor) == HMServiceType.MotionSensor) - arr.Add (HMServiceType.MotionSensor.GetConstant ()); - if ((serviceTypes & HMServiceType.OccupancySensor) == HMServiceType.OccupancySensor) - arr.Add (HMServiceType.OccupancySensor.GetConstant ()); - if ((serviceTypes & HMServiceType.StatefulProgrammableSwitch) == HMServiceType.StatefulProgrammableSwitch) - arr.Add (HMServiceType.StatefulProgrammableSwitch.GetConstant ()); - if ((serviceTypes & HMServiceType.StatelessProgrammableSwitch) == HMServiceType.StatelessProgrammableSwitch) - arr.Add (HMServiceType.StatelessProgrammableSwitch.GetConstant ()); - if ((serviceTypes & HMServiceType.SmokeSensor) == HMServiceType.SmokeSensor) - arr.Add (HMServiceType.SmokeSensor.GetConstant ()); - if ((serviceTypes & HMServiceType.TemperatureSensor) == HMServiceType.TemperatureSensor) - arr.Add (HMServiceType.TemperatureSensor.GetConstant ()); - if ((serviceTypes & HMServiceType.Window) == HMServiceType.Window) - arr.Add (HMServiceType.Window.GetConstant ()); - if ((serviceTypes & HMServiceType.WindowCovering) == HMServiceType.WindowCovering) - arr.Add (HMServiceType.WindowCovering.GetConstant ()); - // iOS 10.2 - if ((serviceTypes & HMServiceType.AirPurifier) == HMServiceType.AirPurifier) - arr.Add (HMServiceType.AirPurifier.GetConstant ()); - if ((serviceTypes & HMServiceType.VentilationFan) == HMServiceType.VentilationFan) - arr.Add (HMServiceType.VentilationFan.GetConstant ()); - if ((serviceTypes & HMServiceType.FilterMaintenance) == HMServiceType.FilterMaintenance) - arr.Add (HMServiceType.FilterMaintenance.GetConstant ()); - if ((serviceTypes & HMServiceType.HeaterCooler) == HMServiceType.HeaterCooler) - arr.Add (HMServiceType.HeaterCooler.GetConstant ()); - if ((serviceTypes & HMServiceType.HumidifierDehumidifier) == HMServiceType.HumidifierDehumidifier) - arr.Add (HMServiceType.HumidifierDehumidifier.GetConstant ()); - if ((serviceTypes & HMServiceType.Slats) == HMServiceType.Slats) - arr.Add (HMServiceType.Slats.GetConstant ()); - - return GetServices (arr.ToArray ()); - } - - class ServiceTypeList : List { - public new void Add (T? item) - { - if (item is not null) - base.Add (item); - } + return GetServices (serviceTypes.ToArray ()); } #if !NET diff --git a/src/PassKit/PKPaymentRequest.cs b/src/PassKit/PKPaymentRequest.cs index 4717be7e90..5b2017a7a5 100644 --- a/src/PassKit/PKPaymentRequest.cs +++ b/src/PassKit/PKPaymentRequest.cs @@ -10,38 +10,14 @@ namespace PassKit { static public PKContactFields GetValue (NSSet set) { - var fields = PKContactFields.None; if (set is null) - return fields; - - foreach (PKContactFields value in Enum.GetValues (typeof (PKContactFields))) { - var constant = value.GetConstant (); - // None does not have an associated native value and Contains would throw an ANE - if ((constant is not null) && set.Contains (constant)) - fields |= value; - } - return fields; + return PKContactFields.None; + return PKContactFieldsExtensions.ToFlags (set.ToArray ()); } static public NSSet GetSet (PKContactFields values) { - var set = new NSMutableSet (); - if (values == PKContactFields.None) - return set; - -#if NET - foreach (var value in Enum.GetValues ()) { -#else - foreach (PKContactFields value in Enum.GetValues (typeof (PKContactFields))) { -#endif - if (values.HasFlag (value)) { - var constant = value.GetConstant (); - // None does not have an associated native value and Contains would throw an ANE - if (constant is not null) - set.Add (constant); - } - } - return set; + return new NSMutableSet (values.ToArray ()); } } diff --git a/src/bgen/Enums.cs b/src/bgen/Enums.cs index b4b62c14ba..5195bd425c 100644 --- a/src/bgen/Enums.cs +++ b/src/bgen/Enums.cs @@ -45,7 +45,8 @@ public partial class Generator { // - call/emit PrintPlatformAttributes on the type void GenerateEnum (Type type) { - if (AttributeManager.HasAttribute (type)) + var isFlagsEnum = AttributeManager.HasAttribute (type); + if (isFlagsEnum) print ("[Flags]"); var native = AttributeManager.GetCustomAttribute (type); @@ -238,6 +239,57 @@ public partial class Generator { print ("return {0}.{1};", type.Name, default_symbol.Item1.Name); indent--; print ("}"); + + if (isFlagsEnum) { + print ($"public static NSString[] ToArray (this {type.Name} value)"); + print ("{"); + indent++; + print ("var rv = new global::System.Collections.Generic.List ();"); + foreach (var kvp in fields) { + print ($"if (value.HasFlag ({type.Name}.{kvp.Key.Name}) && {kvp.Value.SymbolName} != IntPtr.Zero)"); + indent++; + print ($"rv.Add ((NSString) Runtime.GetNSObject ({kvp.Value.SymbolName})!);"); + indent--; + } + print ($"// In order to be forward-compatible, any unknown values are ignored."); + print ("return rv.ToArray ();"); + indent--; + print ("}"); + print (""); + + print ($"public static {type.Name} ToFlags (global::System.Collections.Generic.IEnumerable{(nullable ? "?" : "")} constants)"); + print ("{"); + indent++; + print ($"var rv = default ({type.Name});"); + print ($"if (constants is null)"); + indent++; + print ("return rv;"); + indent--; + print ($"foreach (var constant in constants) {{"); + indent++; + var first = true; + if (nullable) { + print ("if (constant is null)"); + indent++; + print ("continue;"); + indent--; + first = false; + } + foreach (var kvp in fields) { + print ($"{(first ? "if" : "else if")} (constant.IsEqualTo ({kvp.Value.SymbolName}))"); + first = false; + indent++; + print ($"rv |= {type.Name}.{kvp.Key.Name};"); + indent--; + } + print ($"// In order to be forward-compatible, any unknown values are ignored."); + indent--; + print ("}"); + print ("return rv;"); + indent--; + print ("}"); + print (""); + } } if ((fields.Count > 0) || (error is not null) || (null_field is not null)) { diff --git a/tests/monotouch-test/AVFoundation/CaptureMetadataOutputTest.cs b/tests/monotouch-test/AVFoundation/CaptureMetadataOutputTest.cs index a411cb9b09..2c36678041 100644 --- a/tests/monotouch-test/AVFoundation/CaptureMetadataOutputTest.cs +++ b/tests/monotouch-test/AVFoundation/CaptureMetadataOutputTest.cs @@ -51,21 +51,12 @@ namespace MonoTouchFixtures.AVFoundation { [Test] public void Flags () { - // we can only only work with [Weak]AvailableMetadataObjectTypes on an instance of AVCaptureMetadataOutput - // so we use reflection to test the internal of the flags/enum/constants conversions - // the previous tests ensure what we need is not removed by the linker - var t = typeof (AVMetadataObject); - var array_to_enum = t.GetMethod ("ArrayToEnum", BindingFlags.Static | BindingFlags.NonPublic); - Assert.NotNull (array_to_enum, "ArrayToEnum"); - - var enum_to_array = t.GetMethod ("EnumToArray", BindingFlags.Static | BindingFlags.NonPublic); - Assert.NotNull (enum_to_array, "EnumToArray"); - // single var flags = AVMetadataObjectType.Face; - var result = (AVMetadataObjectType) array_to_enum.Invoke (null, new [] { new NSString [] { flags.GetConstant () } }); + var result = AVMetadataObjectTypeExtensions.ToFlags (new NSString [] { flags.GetConstant () }); Assert.AreEqual (flags, result, "a2e 1"); - var back = (NSString []) enum_to_array.Invoke (null, new object [] { result }); + + var back = result.ToArray (); Assert.That (back.Length, Is.EqualTo (1), "l 1"); Assert.That (back [0], Is.EqualTo (flags.GetConstant ()), "e2a 1"); @@ -78,14 +69,21 @@ namespace MonoTouchFixtures.AVFoundation { AVMetadataObjectType.DogBody.GetConstant (), AVMetadataObjectType.HumanBody.GetConstant () }; - result = (AVMetadataObjectType) array_to_enum.Invoke (null, new [] { array }); + result = AVMetadataObjectTypeExtensions.ToFlags (array); Assert.AreEqual (flags, result, "a2e 3"); - back = (NSString []) enum_to_array.Invoke (null, new object [] { result }); + back = result.ToArray (); Assert.That (back.Length, Is.EqualTo (3), "l 3"); Assert.That (back [0], Is.EqualTo (array [0]), "e2a 3a"); Assert.That (back [1], Is.EqualTo (array [1]), "e2a 3b"); Assert.That (back [2], Is.EqualTo (array [2]), "e2a 3c"); } + + var all = (AVMetadataObjectType) ulong.MaxValue; + var someArray = all.ToArray (); // converting all flags to an array will only return strings for flags that exist in the current OS. + Assert.That (someArray.Length, Is.GreaterThan (1), "some back"); + var someFlags = AVMetadataObjectTypeExtensions.ToFlags (someArray); + Assert.That (someFlags, Is.Not.EqualTo (AVMetadataObjectType.None), "Some, but not None"); + Assert.That (someFlags, Is.Not.EqualTo (all), "Some, but not all"); } [Test]