From 7128d452fcc0d50c18f529367833ebc2bb8eda5b Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 19 Feb 2026 20:18:50 +0100 Subject: [PATCH 1/2] [Foundation] Add helper methods to NSArray to create managed arrays of strongly typed dictionaries. Add two internal convencience methods to NSArray: * DictionaryArrayFromHandleDropNullElements * NonNullDictionaryArrayFromHandleDropNullElements And update code in AudioToolbox, CFProxySupport, and CTFont to take advantage of these new methods. Also add a test for CTFont.GetVariationAxes. Contributes towards https://github.com/dotnet/macios/issues/17285. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/AudioToolbox/AudioToolbox.cs | 15 ++---- src/AudioUnit/AudioComponent.cs | 2 +- src/CoreFoundation/CFProxySupport.cs | 44 ++++------------- src/CoreText/CTFont.cs | 17 ++----- src/Foundation/NSArray.cs | 58 +++++++++++++++++++++++ src/VideoToolbox/VTVideoEncoder.cs | 8 +--- tests/monotouch-test/CoreText/FontTest.cs | 11 +++++ 7 files changed, 90 insertions(+), 65 deletions(-) diff --git a/src/AudioToolbox/AudioToolbox.cs b/src/AudioToolbox/AudioToolbox.cs index 2dd9149cd005..be3c0679dcb4 100644 --- a/src/AudioToolbox/AudioToolbox.cs +++ b/src/AudioToolbox/AudioToolbox.cs @@ -129,21 +129,14 @@ public static class SoundBank { if (url is null) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (url)); - InstrumentInfo []? result = null; IntPtr array = IntPtr.Zero; - OSStatus error; unsafe { - error = CopyInstrumentInfoFromSoundBank (url.Handle, &array); + var error = CopyInstrumentInfoFromSoundBank (url.Handle, &array); GC.KeepAlive (url); + if (error != 0) + return null; } - if (array != IntPtr.Zero) { - var dicts = NSArray.ArrayFromHandle (array); - result = new InstrumentInfo [dicts.Length]; - for (int i = 0; i < dicts.Length; i++) - result [i] = new InstrumentInfo (dicts [i]); - CFObject.CFRelease (array); - } - return (error != 0) ? null : result; + return NSArray.DictionaryArrayFromHandleDropNullElements (array, dict => new InstrumentInfo (dict), releaseHandle: true); } } } diff --git a/src/AudioUnit/AudioComponent.cs b/src/AudioUnit/AudioComponent.cs index 741ccb1eda9b..17021824a9c5 100644 --- a/src/AudioUnit/AudioComponent.cs +++ b/src/AudioUnit/AudioComponent.cs @@ -671,7 +671,7 @@ public AudioComponentInfo []? ComponentList { get { using var nameHandle = new TransientCFString (Name); var cHandle = AudioUnitExtensionCopyComponentList (nameHandle); - return NSArray.ArrayFromHandle (cHandle, h => new AudioComponentInfo (Runtime.GetNSObject (h)!), releaseHandle: true); + return NSArray.DictionaryArrayFromHandleDropNullElements (cHandle, h => new AudioComponentInfo (h), releaseHandle: true); } set { using var nameHandle = new TransientCFString (Name); diff --git a/src/CoreFoundation/CFProxySupport.cs b/src/CoreFoundation/CFProxySupport.cs index d52b9ffefbd1..02351a914125 100644 --- a/src/CoreFoundation/CFProxySupport.cs +++ b/src/CoreFoundation/CFProxySupport.cs @@ -263,7 +263,7 @@ public static partial class CFNetwork { /* CFStringRef __nonnull */ IntPtr proxyAutoConfigurationScript, /* CFURLRef __nonnull */ IntPtr targetURL, /* CFErrorRef __nullable * __nullable */ IntPtr* error); - static NSArray? CopyProxiesForAutoConfigurationScript (NSString proxyAutoConfigurationScript, NSUrl targetURL) + static IntPtr CopyProxiesForAutoConfigurationScript (NSString proxyAutoConfigurationScript, NSUrl targetURL) { IntPtr err; IntPtr native; @@ -272,7 +272,7 @@ public static partial class CFNetwork { GC.KeepAlive (proxyAutoConfigurationScript); GC.KeepAlive (targetURL); } - return native == IntPtr.Zero ? null : new NSArray (native); + return native; } /// JavaScript source to be executed to obtain a list of proxies to use. @@ -288,21 +288,9 @@ public static partial class CFNetwork { if (targetURL is null) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (targetURL)); - using (var array = CopyProxiesForAutoConfigurationScript (proxyAutoConfigurationScript, targetURL)) { - if (array is null) - return null; - - NSDictionary [] dictionaries = NSArray.ArrayFromHandle (array.Handle); - GC.KeepAlive (array); - if (dictionaries is null) - return null; - - CFProxy [] proxies = new CFProxy [dictionaries.Length]; - for (int i = 0; i < dictionaries.Length; i++) - proxies [i] = new CFProxy (dictionaries [i]); - - return proxies; - } + var array = CopyProxiesForAutoConfigurationScript (proxyAutoConfigurationScript, targetURL); + var proxies = NSArray.DictionaryArrayFromHandleDropNullElements (array, (dict) => new CFProxy (dict), releaseHandle: true); + return proxies; } /// @@ -325,12 +313,12 @@ public static partial class CFNetwork { /* CFURLRef __nonnull */ IntPtr url, /* CFDictionaryRef __nonnull */ IntPtr proxySettings); - static NSArray? CopyProxiesForURL (NSUrl url, NSDictionary proxySettings) + static IntPtr CopyProxiesForURL (NSUrl url, NSDictionary proxySettings) { IntPtr native = CFNetworkCopyProxiesForURL (url.Handle, proxySettings.Handle); GC.KeepAlive (url); GC.KeepAlive (proxySettings); - return native == IntPtr.Zero ? null : new NSArray (native); + return native; } /// The target URL to connect to. @@ -349,21 +337,9 @@ public static partial class CFNetwork { if (proxySettings is null) return null; - using (NSArray? array = CopyProxiesForURL (url, proxySettings.Dictionary)) { - if (array is null) - return null; - - NSDictionary [] dictionaries = NSArray.ArrayFromHandle (array.Handle); - GC.KeepAlive (array); - if (dictionaries is null) - return null; - - CFProxy [] proxies = new CFProxy [dictionaries.Length]; - for (int i = 0; i < dictionaries.Length; i++) - proxies [i] = new CFProxy (dictionaries [i]); - - return proxies; - } + var array = CopyProxiesForURL (url, proxySettings.Dictionary); + var proxies = NSArray.DictionaryArrayFromHandleDropNullElements (array, (dict) => new CFProxy (dict), releaseHandle: true); + return proxies; } /// The target Uri to connect to. diff --git a/src/CoreText/CTFont.cs b/src/CoreText/CTFont.cs index 9586fbc8a0f3..8d56556049c2 100644 --- a/src/CoreText/CTFont.cs +++ b/src/CoreText/CTFont.cs @@ -3475,10 +3475,7 @@ public nint GetLigatureCaretPositions (CGGlyph glyph, nfloat [] positions) public CTFontVariationAxes [] GetVariationAxes () { var cfArrayRef = CTFontCopyVariationAxes (Handle); - if (cfArrayRef == IntPtr.Zero) - return Array.Empty (); - return NSArray.ArrayFromHandle (cfArrayRef, - d => new CTFontVariationAxes (Runtime.GetNSObject (d)!), true); + return NSArray.NonNullDictionaryArrayFromHandleDropNullElements (cfArrayRef, d => new CTFontVariationAxes (d), true); } [DllImport (Constants.CoreTextLibrary)] @@ -3507,10 +3504,8 @@ public CTFontVariationAxes [] GetVariationAxes () public CTFontFeatures [] GetFeatures () { var cfArrayRef = CTFontCopyFeatures (Handle); - if (cfArrayRef == IntPtr.Zero) - return Array.Empty (); - return NSArray.ArrayFromHandle (cfArrayRef, - d => new CTFontFeatures (Runtime.GetNSObject (d)!), true); + return NSArray.NonNullDictionaryArrayFromHandleDropNullElements (cfArrayRef, + d => new CTFontFeatures (d), true); } [DllImport (Constants.CoreTextLibrary)] @@ -3523,10 +3518,8 @@ public CTFontFeatures [] GetFeatures () public CTFontFeatureSettings [] GetFeatureSettings () { var cfArrayRef = CTFontCopyFeatureSettings (Handle); - if (cfArrayRef == IntPtr.Zero) - return Array.Empty (); - return NSArray.ArrayFromHandle (cfArrayRef, - d => new CTFontFeatureSettings (Runtime.GetNSObject (d)!), true); + return NSArray.NonNullDictionaryArrayFromHandleDropNullElements (cfArrayRef, + d => new CTFontFeatureSettings (d), true); } #endregion diff --git a/src/Foundation/NSArray.cs b/src/Foundation/NSArray.cs index edc99558ebf8..410ba18c519a 100644 --- a/src/Foundation/NSArray.cs +++ b/src/Foundation/NSArray.cs @@ -623,6 +623,64 @@ public static T [] ArrayFromHandleFunc (NativeHandle handle, FuncCreates a managed array from a pointer to a native NSArray of NSDictionary objects, dropping null and NSNull elements. + /// The type of objects to create from the dictionaries. + /// The pointer to the native NSArray instance containing NSDictionary objects. + /// A factory function that creates an instance of type T from an NSDictionary. + /// Whether the native NSArray instance should be released before returning or not. Defaults to false. + /// A C# array with the values, or null if the handle is zero. Null and NSNull elements are excluded from the result. + /// + /// This method converts a native NSArray of NSDictionary objects into a managed array. Any null or NSNull elements in the source array are skipped, and the resulting array is resized accordingly. + /// +#nullable enable + internal static T []? DictionaryArrayFromHandleDropNullElements (NativeHandle handle, Func createObjectFromDictionary, bool releaseHandle = false) + { + if (handle == NativeHandle.Zero) + return null; + + try { + var count = GetCount (handle); + var ret = new T [count]; + nuint nextIndex = 0; + + for (nuint i = 0; i < count; i++) { + var val = GetAtIndex (handle, i); + if (val == IntPtr.Zero || val == NSNull.Null.Handle) + continue; + var dict = Runtime.GetNSObject (val); + if (dict is null) + continue; + ret [nextIndex++] = createObjectFromDictionary (dict); + } + + if (nextIndex != count) + Array.Resize (ref ret, (int) nextIndex); + + return ret; + } finally { + if (releaseHandle) + NSObject.DangerousRelease (handle); + } + } + + /// Creates a managed array from a pointer to a native NSArray of NSDictionary objects, dropping null and NSNull elements. Always returns a non-null array. + /// The type of objects to create from the dictionaries. + /// The pointer to the native NSArray instance containing NSDictionary objects. + /// A factory function that creates an instance of type T from an NSDictionary. + /// Whether the native NSArray instance should be released before returning or not. Defaults to false. + /// A C# array with the values. Returns an empty array if the handle is zero. Null and NSNull elements are excluded from the result. + /// + /// This method is a wrapper around that guarantees a non-null return value. If the handle is zero or null, an empty array is returned instead of null. + /// + internal static T [] NonNullDictionaryArrayFromHandleDropNullElements (NativeHandle handle, Func createObjectFromDictionary, bool releaseHandle = false) + { + var rv = DictionaryArrayFromHandleDropNullElements (handle, createObjectFromDictionary, releaseHandle); + if (rv is null) + return Array.Empty (); + return rv; + } +#nullable disable + /// Parameter type, determines the kind of array returned. /// Pointer (handle) to the unmanaged object. /// Method that can create objects of type T from a given IntPtr. diff --git a/src/VideoToolbox/VTVideoEncoder.cs b/src/VideoToolbox/VTVideoEncoder.cs index fcd701552ac4..45ebaf643191 100644 --- a/src/VideoToolbox/VTVideoEncoder.cs +++ b/src/VideoToolbox/VTVideoEncoder.cs @@ -35,13 +35,7 @@ public class VTVideoEncoder { return null; } - var dicts = NSArray.ArrayFromHandle (array); - var ret = new VTVideoEncoder [dicts.Length]; - int i = 0; - foreach (var dict in dicts) - ret [i++] = new VTVideoEncoder (dict); - CFObject.CFRelease (array); - return ret; + return NSArray.DictionaryArrayFromHandleDropNullElements (array, dict => new VTVideoEncoder (dict), releaseHandle: true); } /// To be added. diff --git a/tests/monotouch-test/CoreText/FontTest.cs b/tests/monotouch-test/CoreText/FontTest.cs index fc00ed2dd3b0..3d90d0211117 100644 --- a/tests/monotouch-test/CoreText/FontTest.cs +++ b/tests/monotouch-test/CoreText/FontTest.cs @@ -163,5 +163,16 @@ class AdaptiveImageProvider : NSObject, ICTAdaptiveImageProviding { return null; } } + + [Test] + public void GetVariationAxes () + { + using (var font = new CTFont ("HoeflerText-Regular", 10)) { + var axes = font.GetVariationAxes (); + Assert.IsNotNull (axes, "axes"); + // HoeflerText-Regular has no variation axes, so we expect an empty array + Assert.That (axes.Length, Is.EqualTo (0), "Length"); + } + } } } From 2f844b8af5fa30f22126f55cf9a18543e471e711 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 25 Feb 2026 18:03:19 +0100 Subject: [PATCH 2/2] Use new APIs --- src/Foundation/NSArray.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Foundation/NSArray.cs b/src/Foundation/NSArray.cs index e7c5b75fcf23..26815d1a9618 100644 --- a/src/Foundation/NSArray.cs +++ b/src/Foundation/NSArray.cs @@ -645,7 +645,7 @@ public static T [] ArrayFromHandleFunc (NativeHandle handle, Func (val); if (dict is null)