diff --git a/src/CoreFoundation/CFMessagePort.cs b/src/CoreFoundation/CFMessagePort.cs index 43aa87de939..75d0a8bea6f 100644 --- a/src/CoreFoundation/CFMessagePort.cs +++ b/src/CoreFoundation/CFMessagePort.cs @@ -10,6 +10,7 @@ #nullable enable using System.Collections.Generic; +using System.Threading; using dispatch_queue_t = System.IntPtr; @@ -17,42 +18,59 @@ namespace CoreFoundation { // untyped enum from CFMessagePort.h // used as a return value of type SInt32 (always 4 bytes) - /// This enumeration contains status codes for . - /// To be added. + /// Specifies the result of sending a message with . public enum CFMessagePortSendRequestStatus { /// The message was sent, and any expected reply was received. Success = 0, + /// The port timed out before the message could be sent. SendTimeout = -1, + /// The port timed out before the response was received. ReceiveTimeout = -2, + /// The port became invalid before the message was sent. IsInvalid = -3, + /// An error occurred. TransportError = -4, + /// The port became invalid after the message was sent, but before a response was received. BecameInvalidError = -5, } - internal class CFMessagePortContext { - - public Func? Retain { get; set; } - - public Action? Release { get; set; } - - public Func? CopyDescription { get; set; } - } - - /// A communication channel between multiple threads on the local device. - /// - /// + /// Provides local interprocess communication through named message ports. + /// + /// Create a local port with to receive messages, and create a remote port with to send messages to a named local port. + /// A local port must be scheduled by calling and adding the returned source to a run loop, or by calling . + /// [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("tvos")] public class CFMessagePort : NativeObject { + sealed class MessagePortContext { + int retainCount = 1; + + public CFMessagePortCallBack Callback { get; } + + public MessagePortContext (CFMessagePortCallBack callback) + { + Callback = callback; + } + + public void Retain () + { + Interlocked.Increment (ref retainCount); + } + + public void Release (GCHandle handle) + { + if (Interlocked.Decrement (ref retainCount) == 0) + handle.Free (); + } + } - // CFMessagePortContext [StructLayout (LayoutKind.Sequential)] unsafe struct ContextProxy { /* CFIndex */ @@ -63,77 +81,69 @@ unsafe struct ContextProxy { public delegate* unmanaged copyDescription; } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Handles a message received by a local message port. + /// The application-defined message identifier. + /// The message data. + /// The data to return to the sender. + /// The object is only valid for the duration of the callback. Copy it if it must be retained after the callback returns. public delegate NSData CFMessagePortCallBack (int type, NSData data); - static Dictionary outputHandles = new Dictionary (Runtime.IntPtrEqualityComparer); + // Remote ports pass null as the native invalidation callback's info argument, and multiple + // managed wrappers may share a native port, so invalidation callbacks must be keyed by handle. + static Dictionary invalidationHandles = new Dictionary (Runtime.IntPtrEqualityComparer); - static Dictionary invalidationHandles = new Dictionary (Runtime.IntPtrEqualityComparer); - - static Dictionary messagePortContexts = new Dictionary (Runtime.IntPtrEqualityComparer); - - IntPtr contextHandle; - - /// Returns a Boolean value that indicates whether a current instance of CFMessagePort object represents a remote port. - /// Boolean value. - /// Property returns true if CFMessagePort is remote. + /// Gets a value that indicates whether this instance represents a remote port. + /// for a remote port; for a local port. public bool IsRemote { get { return CFMessagePortIsRemote (GetCheckedHandle ()) != 0; } } - /// The registered name of message port. - /// String representation of message port's name. - ///  Property returns null if port have no name. + /// Gets or sets the registered name of the message port. + /// The registered port name, or if the port is unnamed. + /// The value being assigned is . + /// The setter does not report whether the name was changed. Use when the result is needed. Changing the name does not make an already scheduled unnamed local port able to receive messages. public string? Name { get { return CFString.FromHandle (CFMessagePortGetName (GetCheckedHandle ())); } set { - var n = CFString.CreateNative (value); - try { - CFMessagePortSetName (GetCheckedHandle (), n); - } finally { - CFString.ReleaseNative (n); - } + if (value is null) + ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (value)); + TrySetName (value); } } - /// Returns a boolean value that indicates whether a CFMessagePort object is valid. - /// Boolean value. - /// Property indicates whether message port can send or receive messages. - public bool IsValid { - get { - return CFMessagePortIsValid (GetCheckedHandle ()) != 0; + /// Attempts to change the registered name of this local message port. + /// The new port name. + /// if the name was changed; otherwise, . + /// is . + /// This method returns for remote ports, duplicate names, invalid names, and native registration failures. + public bool TrySetName (string name) + { + if (name is null) + ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (name)); + + var n = CFString.CreateNative (name); + try { + return CFMessagePortSetName (GetCheckedHandle (), n) != 0; + } finally { + CFString.ReleaseNative (n); } } - internal CFMessagePortContext? Context { + /// Gets a value that indicates whether the port can send or receive messages. + /// if the port is valid; otherwise, . + public bool IsValid { get { - CFMessagePortContext? result; - ContextProxy context = new ContextProxy (); - unsafe { - CFMessagePortGetContext (GetCheckedHandle (), &context); - } - - if (context.info == IntPtr.Zero) - return null; - - lock (messagePortContexts) - messagePortContexts.TryGetValue (context.info, out result); - - return result; + return CFMessagePortIsValid (GetCheckedHandle ()) != 0; } } - /// Gets or sets the invalidation callback method for a CFMessagePort object. - /// Delegate - /// Set null value to remove callback. Callback will be fired on message on port invalidation. + /// Gets or sets the callback invoked when the message port becomes invalid. + /// The invalidation callback, or if no callback is installed. + /// Assigning a new callback replaces the previous callback. Assign to remove it. If the port is already invalid when a callback is assigned, the callback is invoked synchronously. public Action? InvalidationCallback { get { lock (invalidationHandles) { @@ -142,15 +152,17 @@ public Action? InvalidationCallback { } } set { + var handle = GetCheckedHandle (); lock (invalidationHandles) { if (value is null) - invalidationHandles [GetCheckedHandle ()] = null; + invalidationHandles.Remove (handle); else - invalidationHandles.Add (GetCheckedHandle (), value); + invalidationHandles [handle] = value; } unsafe { - CFMessagePortSetInvalidationCallBack (Handle, &MessagePortInvalidationCallback); + delegate* unmanaged callback = value is null ? null : &MessagePortInvalidationCallback; + CFMessagePortSetInvalidationCallBack (handle, callback); } } } @@ -161,30 +173,6 @@ internal CFMessagePort (NativeHandle handle, bool owns) { } - /// - protected override void Dispose (bool disposing) - { - if (Handle != IntPtr.Zero) { - - lock (outputHandles) - outputHandles.Remove (Handle); - - lock (invalidationHandles) { - if (invalidationHandles.ContainsKey (Handle)) - invalidationHandles.Remove (Handle); - } - - lock (messagePortContexts) { - if (messagePortContexts.ContainsKey (contextHandle)) - messagePortContexts.Remove (contextHandle); - } - - contextHandle = IntPtr.Zero; - } - - base.Dispose (disposing); - } - [DllImport (Constants.CoreFoundationLibrary)] static unsafe extern /* CFMessagePortRef */ IntPtr CFMessagePortCreateLocal (/* CFAllocatorRef */ IntPtr allocator, /* CFStringRef */ IntPtr name, delegate* unmanaged callout, /* CFMessagePortContext */ ContextProxy* context, byte* shouldFreeInfo); @@ -209,9 +197,6 @@ protected override void Dispose (bool disposing) [DllImport (Constants.CoreFoundationLibrary)] static extern /* CFStringRef */ IntPtr CFMessagePortGetName (/* CFMessagePortRef */ IntPtr ms); - [DllImport (Constants.CoreFoundationLibrary)] - unsafe static extern void CFMessagePortGetContext (/* CFMessagePortRef */ IntPtr ms, /* CFMessagePortContext* */ ContextProxy* context); - [DllImport (Constants.CoreFoundationLibrary)] static extern /* Boolean */ byte CFMessagePortIsValid (/* CFMessagePortRef */ IntPtr ms); @@ -221,46 +206,28 @@ protected override void Dispose (bool disposing) [DllImport (Constants.CoreFoundationLibrary)] static unsafe extern void CFMessagePortSetInvalidationCallBack (/* CFMessagePortRef */ IntPtr ms, delegate* unmanaged callout); - [DllImport (Constants.CoreFoundationLibrary)] - static extern IntPtr CFMessagePortGetInvalidationCallBack (/* CFMessagePortRef */ IntPtr ms); - - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Creates a local message port that receives messages. + /// The name to register, or to create an unnamed port. + /// The callback that handles received messages. + /// The allocator to use, or to use the default allocator. + /// A local message port, or if the port could not be created. + /// is . + /// If another local port with the same name already exists in the process, Core Foundation returns that port and continues to use its original callback. public static CFMessagePort? CreateLocalPort (string? name, CFMessagePortCallBack callback, CFAllocator? allocator = null) { if (callback is null) ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (callback)); - return CreateLocalPort (allocator, name, callback, context: null); - } - - internal static CFMessagePort? CreateLocalPort (CFAllocator? allocator, string? name, CFMessagePortCallBack callback, CFMessagePortContext? context) - { var n = CFString.CreateNative (name); byte shouldFreeInfo = 0; - var contextProxy = new ContextProxy (); - - // a GCHandle is needed because we do not have an handle before calling CFMessagePortCreateLocal - // and that will call the RetainProxy. So using this (short-lived) GCHandle allow us to find back the - // original context defined by developer - var shortHandle = GCHandle.Alloc (contextProxy); - - if (context is not null) { - unsafe { - if (context.Retain is not null) - contextProxy.retain = &RetainProxy; - if (context.Release is not null) - contextProxy.release = &ReleaseProxy; - if (context.CopyDescription is not null) - contextProxy.copyDescription = &CopyDescriptionProxy; - } - contextProxy.info = (IntPtr) shortHandle; - lock (messagePortContexts) - messagePortContexts.Add (contextProxy.info, context); + var context = new MessagePortContext (callback); + var contextHandle = GCHandle.Alloc (context); + var contextProxy = new ContextProxy { + info = GCHandle.ToIntPtr (contextHandle), + }; + unsafe { + contextProxy.retain = &RetainProxy; + contextProxy.release = &ReleaseProxy; } try { @@ -270,33 +237,13 @@ protected override void Dispose (bool disposing) GC.KeepAlive (allocator); } - // TODO handle should free info if (portHandle == IntPtr.Zero) return null; - var result = new CFMessagePort (portHandle, true); - - lock (outputHandles) - outputHandles.Add (portHandle, callback); - - if (context is not null) { - lock (messagePortContexts) { - messagePortContexts.Remove (contextProxy.info); - unsafe { - CFMessagePortGetContext (portHandle, &contextProxy); - } - messagePortContexts.Add (contextProxy.info, context); - } - - result.contextHandle = contextProxy.info; - } - - return result; + return new CFMessagePort (portHandle, true); } finally { CFString.ReleaseNative (n); - - // we won't need short GCHandle after the Create call - shortHandle.Free (); + context.Release (contextHandle); } } @@ -306,61 +253,31 @@ protected override void Dispose (bool disposing) [UnmanagedCallersOnly] static IntPtr RetainProxy (IntPtr info) { - INativeObject? result = null; - CFMessagePortContext? context; - - lock (messagePortContexts) { - messagePortContexts.TryGetValue (info, out context); - } - - if (context?.Retain is not null) - result = context.Retain (); - - return result.GetHandle (); + var context = GCHandle.FromIntPtr (info).Target as MessagePortContext; + context?.Retain (); + return info; } [UnmanagedCallersOnly] static void ReleaseProxy (IntPtr info) { - CFMessagePortContext? context; - - lock (messagePortContexts) - messagePortContexts.TryGetValue (info, out context); - - if (context?.Release is not null) - context.Release (); - } - - [UnmanagedCallersOnly] - static IntPtr CopyDescriptionProxy (IntPtr info) - { - NSString? result = null; - CFMessagePortContext? context; - - lock (messagePortContexts) - messagePortContexts.TryGetValue (info, out context); - - if (context?.CopyDescription is not null) - result = context.CopyDescription (); - -#pragma warning disable RBI0014 - return result.GetHandle (); -#pragma warning restore RBI0014 + var handle = GCHandle.FromIntPtr (info); + var context = handle.Target as MessagePortContext; + context?.Release (handle); } [UnmanagedCallersOnly] static IntPtr MessagePortCallback (IntPtr local, int msgid, IntPtr data, IntPtr info) { - CFMessagePortCallBack callback; - - lock (outputHandles) - callback = outputHandles [local]; - - if (callback is null) + var context = GCHandle.FromIntPtr (info).Target as MessagePortContext; + if (context is null) return IntPtr.Zero; - using (var managedData = Runtime.GetNSObject (data)!) { - var result = callback.Invoke (msgid, managedData); + using (var managedData = Runtime.GetNSObject (data)) { + if (managedData is null) + return IntPtr.Zero; + + var result = context.Callback.Invoke (msgid, managedData); // System will release returned CFData result?.DangerousRetain (); #pragma warning disable RBI0014 @@ -374,18 +291,19 @@ static void MessagePortInvalidationCallback (IntPtr messagePort, IntPtr info) { Action? callback; - lock (invalidationHandles) + lock (invalidationHandles) { invalidationHandles.TryGetValue (messagePort, out callback); + invalidationHandles.Remove (messagePort); + } - if (callback is not null) - callback.Invoke (); + callback?.Invoke (); } - /// To be added. - /// To be added. - /// Deprecated. - /// To be added. - /// To be added. + /// Creates a remote message port for sending messages to a named local port. + /// The allocator to use, or to use the default allocator. + /// The name of the local port. + /// A remote message port, or if no valid local port with the specified name is available. + /// is . public static CFMessagePort? CreateRemotePort (CFAllocator? allocator, string name) { if (name is null) @@ -401,50 +319,51 @@ static void MessagePortInvalidationCallback (IntPtr messagePort, IntPtr info) } } - /// Invalidating a message port prevents the port from ever sending or receiving any more messages.  - /// The message port is not deallocated after invalidation, however  property is set to be true. + /// Invalidates the message port so that it can no longer send or receive messages. + /// Invalidation is permanent and does not dispose the managed object. After this method returns, is . public void Invalidate () { CFMessagePortInvalidate (GetCheckedHandle ()); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// Sends a message to the port. - /// To be added. - /// To be added. + /// Sends a message through a remote message port. + /// The application-defined message identifier. + /// The message data, or to send an empty message. + /// The maximum number of seconds to wait while sending the message. + /// The maximum number of seconds to wait for a reply. + /// The run loop mode in which to wait for a reply, or if no reply is expected. + /// On return, the reply data, or if no data was returned. + /// A value that describes whether the message was sent and, if requested, whether a reply was received. + /// This method is intended for remote ports. When is , the method returns after sending and does not wait for a reply. public CFMessagePortSendRequestStatus SendRequest (int msgid, NSData? data, double sendTimeout, double rcvTimeout, NSString? replyMode, out NSData? returnData) { CFMessagePortSendRequestStatus result; - IntPtr returnDataHandle; + IntPtr returnDataHandle = IntPtr.Zero; unsafe { result = CFMessagePortSendRequest (GetCheckedHandle (), msgid, data.GetHandle (), sendTimeout, rcvTimeout, replyMode.GetHandle (), &returnDataHandle); GC.KeepAlive (data); GC.KeepAlive (replyMode); } - returnData = Runtime.GetINativeObject (returnDataHandle, false); + // Apple's documentation says ownership of returnData follows the Create Rule. + returnData = Runtime.GetINativeObject (returnDataHandle, true); return result; } - /// Creates a CFRunLoopSource object for a CFMessagePort object. - /// The new CFRunLoopSource object for listening port - /// Method returns loop which is not added to any run loop. Use  to activate the loop. + /// Creates a run loop source that delivers messages to this local port. + /// A new run loop source that has not yet been added to a run loop. + /// Add the returned source to a run loop with . A port cannot use both a run loop source and a dispatch queue. public CFRunLoopSource CreateRunLoopSource () { // note: order is currently ignored by CFMessagePort object run loop sources. Pass 0 for this value. - var runLoopHandle = CFMessagePortCreateRunLoopSource (IntPtr.Zero, Handle, 0); - return new CFRunLoopSource (runLoopHandle, false); + var runLoopHandle = CFMessagePortCreateRunLoopSource (IntPtr.Zero, GetCheckedHandle (), 0); + return new CFRunLoopSource (runLoopHandle, true); } - /// To be added. - /// Schedules message port’s callbacks on the specified dispatch queue. - /// To be added. + /// Schedules this local port's callbacks on a dispatch queue. + /// The dispatch queue to use, or to stop using the current queue. + /// A port cannot use both a dispatch queue and a run loop source. Calling this method on a remote or invalid port has no effect. public void SetDispatchQueue (DispatchQueue? queue) { CFMessagePortSetDispatchQueue (GetCheckedHandle (), queue.GetHandle ()); diff --git a/tests/cecil-tests/HandleSafety.KnownFailures.cs b/tests/cecil-tests/HandleSafety.KnownFailures.cs index f28c5d9be69..00783b63d60 100644 --- a/tests/cecil-tests/HandleSafety.KnownFailures.cs +++ b/tests/cecil-tests/HandleSafety.KnownFailures.cs @@ -8,9 +8,7 @@ public partial class HandleSafetyTest { "AudioUnit.AUScheduledAudioFileRegion.GetAudioFileRegion ()", "AudioUnit.SamplerInstrumentData.ToStruct ()", "CoreFoundation.CFDataBuffer.get_Handle ()", - "CoreFoundation.CFMessagePort.CopyDescriptionProxy (System.IntPtr)", "CoreFoundation.CFMessagePort.MessagePortCallback (System.IntPtr, System.Int32, System.IntPtr, System.IntPtr)", - "CoreFoundation.CFMessagePort.RetainProxy (System.IntPtr)", "CoreFoundation.CFMutableString.Transform (CoreFoundation.CFRange&, CoreFoundation.CFStringTransform, System.Boolean)", "CoreFoundation.CFMutableString.Transform (CoreFoundation.CFStringTransform, System.Boolean)", "CoreFoundation.CFSocketSignature..ctor (System.Net.Sockets.AddressFamily, System.Net.Sockets.SocketType, System.Net.Sockets.ProtocolType, CoreFoundation.CFSocketAddress)", diff --git a/tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-R2R-size.txt b/tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-R2R-size.txt index 15b6ac5e731..0c9db5d7c35 100644 --- a/tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-R2R-size.txt +++ b/tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-R2R-size.txt @@ -1,9 +1,9 @@ -AppBundleSize: 333,112,974 bytes (325,305.6 KB = 317.7 MB) +AppBundleSize: 333,102,819 bytes (325,295.7 KB = 317.7 MB) # The following list of files and their sizes is just informational / for review, and isn't used in the test: Contents/Info.plist: - 726 bytes (0.7 KB = 0.0 MB) + 747 bytes (0.7 KB = 0.0 MB) Contents/MacOS/SizeTestApp: - 7,438,728 bytes (7,264.4 KB = 7.1 MB) + 7,439,304 bytes (7,264.9 KB = 7.1 MB) Contents/MonoBundle/_Microsoft.macOS.TypeMaps.dll: 2,560 bytes (2.5 KB = 0.0 MB) Contents/MonoBundle/.xamarin/osx-arm64/_Microsoft.macOS.TypeMap.dll: @@ -31,7 +31,7 @@ Contents/MonoBundle/.xamarin/osx-arm64/Microsoft.Extensions.Options.dll: Contents/MonoBundle/.xamarin/osx-arm64/Microsoft.Extensions.Primitives.dll: 90,408 bytes (88.3 KB = 0.1 MB) Contents/MonoBundle/.xamarin/osx-arm64/Microsoft.macOS.dll: - 74,894,848 bytes (73,139.5 KB = 71.4 MB) + 74,889,216 bytes (73,134.0 KB = 71.4 MB) Contents/MonoBundle/.xamarin/osx-arm64/Microsoft.VisualBasic.Core.dll: 1,337,128 bytes (1,305.8 KB = 1.3 MB) Contents/MonoBundle/.xamarin/osx-arm64/Microsoft.VisualBasic.dll: @@ -401,7 +401,7 @@ Contents/MonoBundle/.xamarin/osx-x64/Microsoft.Extensions.Options.dll: Contents/MonoBundle/.xamarin/osx-x64/Microsoft.Extensions.Primitives.dll: 83,240 bytes (81.3 KB = 0.1 MB) Contents/MonoBundle/.xamarin/osx-x64/Microsoft.macOS.dll: - 64,058,368 bytes (62,557.0 KB = 61.1 MB) + 64,053,248 bytes (62,552.0 KB = 61.1 MB) Contents/MonoBundle/.xamarin/osx-x64/Microsoft.VisualBasic.Core.dll: 1,191,720 bytes (1,163.8 KB = 1.1 MB) Contents/MonoBundle/.xamarin/osx-x64/Microsoft.VisualBasic.dll: diff --git a/tests/monotouch-test/CoreFoundation/CFMessagePortTest.cs b/tests/monotouch-test/CoreFoundation/CFMessagePortTest.cs new file mode 100644 index 00000000000..1e1713f317c --- /dev/null +++ b/tests/monotouch-test/CoreFoundation/CFMessagePortTest.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; + +namespace MonoTouchFixtures.CoreFoundation { + + [TestFixture] + [Preserve (AllMembers = true)] + public class CFMessagePortTest { + static string CreatePortName () + { + return $"com.microsoft.dotnet.macios.cfmessageport.{Guid.NewGuid ():N}"; + } + + [Test] + public void CreateAndInvalidate () + { + using var port = CFMessagePort.CreateLocalPort (null, (type, data) => new NSData ()); + + Assert.That (port, Is.Not.Null, "Port"); + Assert.That (port.IsRemote, Is.False, "IsRemote"); + Assert.That (port.IsValid, Is.True, "IsValid"); + Assert.That (port.Name, Is.Null, "Name"); + Assert.Throws (() => port.Name = null, "Set null name"); + var name = CreatePortName (); + Assert.That (port.TrySetName (name), Is.True, "TrySetName"); + Assert.That (port.Name, Is.EqualTo (name), "Changed name"); + + var firstCallbackCount = 0; + var secondCallbackCount = 0; + port.InvalidationCallback = () => firstCallbackCount++; + port.InvalidationCallback = () => secondCallbackCount++; + Assert.That (port.InvalidationCallback, Is.Not.Null, "InvalidationCallback"); + + port.Invalidate (); + + Assert.That (port.IsValid, Is.False, "IsValid after invalidation"); + Assert.That (firstCallbackCount, Is.Zero, "First callback"); + Assert.That (secondCallbackCount, Is.EqualTo (1), "Second callback"); + Assert.That (port.InvalidationCallback, Is.Null, "InvalidationCallback after invalidation"); + } + + [Test] + public void DuplicateLocalPortUsesOriginalCallback () + { + var name = CreatePortName (); + var callbackCount = 0; + var invalidationCallbackCount = 0; + var first = CFMessagePort.CreateLocalPort (name, (type, data) => { + Interlocked.Increment (ref callbackCount); + return new NSData (); + }); + using var second = CFMessagePort.CreateLocalPort (name, (type, data) => throw new InvalidOperationException ()); + using var remote = CFMessagePort.CreateRemotePort (null, name); + + Assert.That (first, Is.Not.Null, "First"); + Assert.That (second, Is.Not.Null, "Second"); + Assert.That (remote, Is.Not.Null, "Remote"); + Assert.That (second.Handle, Is.EqualTo (first.Handle), "Handle"); + + using var source = first.CreateRunLoopSource (); + second.InvalidationCallback = () => invalidationCallbackCount++; + var runLoop = CFRunLoop.Current; + runLoop.AddSource (source, CFRunLoop.ModeDefault); + first.Dispose (); + try { + var status = remote.SendRequest (1, null, 5, 5, CFRunLoop.ModeDefault, out var response); + response?.Dispose (); + Assert.That (status, Is.EqualTo (CFMessagePortSendRequestStatus.Success), "Status"); + Assert.That (callbackCount, Is.EqualTo (1), "Callback count"); + } finally { + second.Invalidate (); + runLoop.RemoveSource (source, CFRunLoop.ModeDefault); + } + Assert.That (invalidationCallbackCount, Is.EqualTo (1), "Invalidation callback count"); + } + + [Test] + public void SendRequest () + { + var name = CreatePortName (); + var requestBytes = new byte [] { 1, 2, 3 }; + var responseBytes = new byte [] { 4, 5, 6 }; + var callbackCount = 0; + var receivedType = 0; + byte [] receivedData = null; + using var local = CFMessagePort.CreateLocalPort (name, (type, data) => { + Interlocked.Increment (ref callbackCount); + receivedType = type; + receivedData = data.ToArray (); + return NSData.FromArray (responseBytes); + }); + using var remote = CFMessagePort.CreateRemotePort (null, name); + using var queue = new DispatchQueue ("CFMessagePortTest.SendRequest"); + using var request = NSData.FromArray (requestBytes); + + Assert.That (local, Is.Not.Null, "Local"); + Assert.That (remote, Is.Not.Null, "Remote"); + Assert.That (remote.IsRemote, Is.True, "IsRemote"); + Assert.That (remote.TrySetName (CreatePortName ()), Is.False, "TrySetName remote"); + + local.SetDispatchQueue (queue); + var status = remote.SendRequest (42, request, 5, 5, CFRunLoop.ModeDefault, out var response); + using (response) { + Assert.That (status, Is.EqualTo (CFMessagePortSendRequestStatus.Success), "Status"); + Assert.That (response, Is.Not.Null, "Response"); + Assert.That (response.ToArray (), Is.EqualTo (responseBytes), "Response data"); + Assert.That (TestRuntime.CFGetRetainCount (response.Handle), Is.EqualTo ((nint) 1), "Response retain count"); + } + Assert.That (callbackCount, Is.EqualTo (1), "Callback count"); + Assert.That (receivedType, Is.EqualTo (42), "Message identifier"); + Assert.That (receivedData, Is.EqualTo (requestBytes), "Request data"); + + var invalidationCallbackCount = 0; + remote.InvalidationCallback = () => invalidationCallbackCount++; + remote.Invalidate (); + Assert.That (invalidationCallbackCount, Is.EqualTo (1), "Remote invalidation callback count"); + status = remote.SendRequest (43, null, 0, 0, CFRunLoop.ModeDefault, out response); + Assert.That (status, Is.EqualTo (CFMessagePortSendRequestStatus.IsInvalid), "Invalid status"); + Assert.That (response, Is.Null, "Invalid response"); + local.Invalidate (); + } + + [Test] + public void CreateRunLoopSourceOwnership () + { + using var local = CFMessagePort.CreateLocalPort (null, (type, data) => new NSData ()); + using var source = local.CreateRunLoopSource (); + + Assert.That (source.Handle, Is.Not.EqualTo (NativeHandle.Zero), "Handle"); + Assert.That (TestRuntime.CFGetRetainCount (source.Handle), Is.EqualTo ((nint) 2), "Retain count"); + + local.Invalidate (); + } + } +} diff --git a/tests/xtro-sharpie/api-annotations-dotnet/common-CoreFoundation.ignore b/tests/xtro-sharpie/api-annotations-dotnet/common-CoreFoundation.ignore index d1bf028da99..56e72d8b46b 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/common-CoreFoundation.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/common-CoreFoundation.ignore @@ -1,3 +1,7 @@ +# These CFMessagePort getters don't do anything useful for us. +!missing-pinvoke! CFMessagePortGetContext is not bound +!missing-pinvoke! CFMessagePortGetInvalidationCallBack is not bound + ## we already expose the NSURLFileProtection* constants !missing-field! kCFURLVolumeSupportsFileProtectionKey not bound