using System; using System.Diagnostics; #if !__WATCHOS__ using System.Drawing; #endif using System.Runtime.InteropServices; using System.Threading; using CoreGraphics; using Foundation; using ObjCRuntime; #if !__WATCHOS__ using SpriteKit; #endif #if !MONOMAC using UIKit; #endif using NUnit.Framework; using MonoTests.System.Net.Http; namespace MonoTouchFixtures.ObjCRuntime { static class AssociatedObjects { public enum AssociationPolicy { // uintptr_t Assign = 0, RetainNonAtomic = 1, CopyNonAtomic = 3, Retain = 0x301, Copy = 0x303, } [DllImport (Messaging.LIBOBJC_DYLIB)] static extern void objc_setAssociatedObject (IntPtr obj, IntPtr key, IntPtr value, IntPtr policy); public static void SetAssociatedObject (this NSObject self, IntPtr key, NSObject value, AssociationPolicy policy) { objc_setAssociatedObject (self.Handle, key, value.Handle, new IntPtr ((int) policy)); } public static IntPtr GetAssociatedPointer (this NSObject self, IntPtr key) { throw new NotImplementedException (); } public static void RemoveAssociatedObjects (this NSObject self) { throw new NotImplementedException (); } } [TestFixture] [Preserve (AllMembers = true)] public class RuntimeTest { static bool connectMethodTestDone; [Test] public void ConnectMethodTest () { if (connectMethodTestDone) Assert.Ignore ("This test can only be executed once, it modifies global state."); if (!Runtime.DynamicRegistrationSupported) Assert.Ignore ("This test requires support for dynamic registration."); connectMethodTestDone = true; // Bug 20013. This should not throw a KeyNotFoundException. Runtime.ConnectMethod (typeof (ConnectMethodClass).GetMethod ("Method"), new Selector ("method")); } class ConnectMethodClass : NSObject { public void Method () { } } [Test] public void GetNSObject_IntPtrZero () { Assert.Null (Runtime.GetNSObject (IntPtr.Zero)); } [Test] public void RegisterAssembly_null () { Assert.Throws (() => Runtime.RegisterAssembly (null)); } #if __IOS__ && !__MACCATALYST__ [Test] public void StartWWAN () { Assert.Throws (delegate { Runtime.StartWWAN (null); }, "null"); Assert.Throws (delegate { Runtime.StartWWAN (new Uri ("ftp://www.xamarin.com")); }, "ftp"); Runtime.StartWWAN (new Uri ("http://www.xamarin.com")); } #endif // __IOS__ && !__MACCATALYST__ [Test] public void GetNSObject_Subclass () { using (var c = new NSHttpCookie ("name", "value")) { // we want to ensure we get the NSMutableDictionary even if we ask for (the base) NSDictionary var d = Runtime.GetNSObject (Messaging.IntPtr_objc_msgSend (c.Handle, Selector.GetHandle ("properties"))); Assert.That (d, Is.TypeOf (), "NSMutableDictionary"); } } #if !__WATCHOS__ [Test] public void GetNSObject_Different_Class () { TestRuntime.AssertXcodeVersion (5, 0); TestRuntime.AssertSystemVersion (PlatformName.MacOSX, 10, 9, throwIfOtherPlatform: false); IntPtr class_ptr = Class.GetHandle ("SKPhysicsBody"); var size = new CGSize (3, 2); using (var body = Runtime.GetNSObject (Messaging.IntPtr_objc_msgSend_CGSize (class_ptr, Selector.GetHandle ("bodyWithRectangleOfSize:"), size))) { // This would normally return a PKPhysicsBody which is not a subclass but answers the same selectors // as a SKPhysicsBody. That's an issue since we can't register PKPhysicsBody (Apple won't like it since // it's a private type) and the non-generic version of GetNSObject (and bindings) would throw an // InvalidaCastException (since it's Class will resolve to NSObject) // note: that's the internal PhysicKit shared by UIKit and SpriteKit Assert.That (body, Is.TypeOf (), "SKPhysicsBody"); } } #endif // !__WATCHOS__ [Test] public void GetNSObject_Posing_Class () { TestRuntime.AssertXcodeVersion (5, 0); TestRuntime.AssertSystemVersion (PlatformName.MacOSX, 10, 9, throwIfOtherPlatform: false); NSUrlSession session = NSUrlSession.SharedSession; using (var request = new NSUrlRequest (new NSUrl (NetworkResources.MicrosoftUrl))) { // In iOS 8 the native CreateDownloadTask function returns an instance of a // __NSCFLocalDownloadTask, which does not derive from // NSUrlSessionDownloadTask (which is the documented/expected return type from // CreateDownloadTask). But __NSCFLocalDownloadTask does override // isKindOfClass: to return true if asked about NSUrlSessionDownloadTask. // In other words it's posing as a NSUrlSessionDownloadTask. using (var o = session.CreateDownloadTask (request)) { } } } [Test] public void UsableUntilDead () { // The test can be inconclusive once in a while. // 100 times in a row is a bit too much though. for (int i = 0; i < 100; i++) { if (UsableUntilDeadImpl ()) return; } Assert.Inconclusive ("Failed to collect the notification object at least once in 100 runs."); } public bool UsableUntilDeadImpl () { // This test ensure that the main thread can send messages to a garbage collected object, // until the final 'release' message for the managed reference has been sent // (on the main thread). var notifierHandle = IntPtr.Zero; // bool isDeallocated = false; Action deallocated = () => { //Console.WriteLine ("Final release!"); // isDeallocated = true; }; ManualResetEvent isCollected = new ManualResetEvent (false); Action collected = () => { //Console.WriteLine ("Garbage collected!"); isCollected.Set (); }; bool isNotified = false; Action notified = () => { //Console.WriteLine ("Notified"); isNotified = true; }; // Create an object whose handle we store in a local variable. We do not // store the object itself, since we want the object to be garbage collected. var t = new Thread (() => { var obj = new Notifier (collected, notified); ReleaseNotifier.NotifyWhenDeallocated (obj, deallocated); notifierHandle = obj.Handle; }) { IsBackground = true, }; t.Start (); t.Join (); // Now we have a handle to an object that may be garbage collected at any time. int counter = 0; do { GC.Collect (); GC.WaitForPendingFinalizers (); } while (counter++ < 10 && !isCollected.WaitOne (10)); // Now we have a handle to a garbage collected object. if (!isCollected.WaitOne (0)) { // Objects may randomly not end up collected (at least in Boehm), because // other objects may happen to contain a random value pointing to the // object we're waiting to become freed. return false; } // Send a message to the collected object. Messaging.void_objc_msgSend (notifierHandle, Selector.GetHandle ("notify")); Assert.IsTrue (isNotified, "notified"); // We're done. Cleanup. NSRunLoop.Main.RunUntil (NSDate.Now.AddSeconds (0.1)); // Don't verify cleanup, it's not consistent. // And in any case it's not what this test is about. // Assert.IsTrue (isDeallocated, "released"); return true; } class Notifier : NSObject { Action collected; Action notified; public Notifier (Action collected, Action notified) { this.collected = collected; this.notified = notified; } ~Notifier () { collected (); } [Export ("notify")] void Notify () { Console.WriteLine ("{0} Notified", DateTime.Now.ToString ()); notified (); } } class Level1 : NSObject {} // we need two levels of subclassing, since the XI will override 'release' on the first one, and we need to override it as well. class ReleaseNotifier : Level1 { Action deallocated; bool enabled; ReleaseNotifier (Action deallocated) { this.deallocated = deallocated; } [Export ("release")] new void Release () { if (enabled) deallocated (); Messaging.objc_super super; super.Handle = Handle; super.SuperHandle = ClassHandle; Messaging.void_objc_msgSendSuper (ref super, Selector.GetHandle ("release")); } public static void NotifyWhenDeallocated (NSObject obj, Action deallocated) { // Add an associated object which will be 'released'd when the obj // to watch is deallocated. When 'release' is sent, then invoke // the 'deallocated' callback. var notifier = new ReleaseNotifier (deallocated); obj.SetAssociatedObject (IntPtr.Zero, notifier, AssociatedObjects.AssociationPolicy.Retain); notifier.Dispose (); // remove any managed references. notifier.enabled = true; // notify on the next 'release' message. } } [Test] public void FinalizationRaceCondition () { #if __WATCHOS__ if (Runtime.Arch == Arch.DEVICE) Assert.Ignore ("This test uses too much memory for the watch."); #endif NSDictionary dict = null; var thread = new Thread (() => { dict = new NSMutableDictionary(); dict["Hello"] = new NSString(@"World"); dict["Bye"] = new NSString(@"Bye"); }) { IsBackground = true }; thread.Start (); thread.Join (); var getter1 = new Func ((key) => dict [key] as NSString); var getter2 = new Func ((key) => dict [key] as NSString); var broken = 0; var count = 0; thread = new Thread (() => { var watch = new Stopwatch (); watch.Start (); while (broken == 0 && watch.ElapsedMilliseconds < 10000) { // try getting using Systen.String key string hello = getter1("Hello"); if (hello == null) broken = 1; string bye = getter1("Bye"); if (bye == null) broken = 2; // try getting using NSString key string nHello = getter2(new NSString(@"Hello")); string nBye = getter2(new NSString(@"Bye")); if (nHello == null) broken = 3; if (nBye == null) broken = 4; count++; } }) { IsBackground = true, }; thread.Start (); while (!thread.Join (1)) NSRunLoop.Main.RunUntil (NSDate.Now.AddSeconds (0.1)); Assert.AreEqual (0, broken, string.Format ("broken after {0} iterations", count)); } [Test] public void ConnectMethod () { if (!Runtime.DynamicRegistrationSupported) Assert.Ignore ("This test requires support for dynamic registration."); var minfo = typeof (RuntimeTest).GetMethod ("ConnectMethod"); Assert.Throws (() => Runtime.ConnectMethod (null, new Selector ("")), "1"); Assert.Throws (() => Runtime.ConnectMethod (minfo, null), "2"); Assert.Throws (() => Runtime.ConnectMethod (null, minfo, new ExportAttribute ("foo")), "3"); Assert.Throws (() => Runtime.ConnectMethod (typeof (RuntimeTest), null, new ExportAttribute ("foo")), "4"); Assert.Throws (() => Runtime.ConnectMethod (typeof (RuntimeTest), minfo, (ExportAttribute) null), "5"); Assert.Throws (() => Runtime.ConnectMethod (typeof (RuntimeTest), minfo, new Selector ("foo")), "6"); Assert.Throws (() => Runtime.ConnectMethod (typeof (A), minfo, new Selector ("foo")), "7"); } static bool connectMethod1Done; [Test] public void ConnectMethod1 () { if (!Runtime.DynamicRegistrationSupported) Assert.Ignore ("This test requires support for dynamic registration."); if (connectMethod1Done) Assert.Ignore ("This is a one-shot test. Restart to run again."); connectMethod1Done = true; Runtime.ConnectMethod (typeof (A).GetMethod ("Test"), new Selector ("test1")); using (var a = new A ()) Messaging.void_objc_msgSend (a.Handle, Selector.GetHandle ("test1")); } static bool connectMethod2Done; [Test] public void ConnectMethod2 () { if (!Runtime.DynamicRegistrationSupported) Assert.Ignore ("This test requires support for dynamic registration."); if (connectMethod2Done) Assert.Ignore ("This is a one-shot test. Restart to run again."); connectMethod2Done = true; // the method is not declared on the type we're connecting to,but a completely different type. Runtime.ConnectMethod (typeof (A), typeof (RuntimeTest).GetMethod ("Test2"), new Selector ("test2")); Messaging.void_objc_msgSend (Class.GetHandle (typeof (A)), Selector.GetHandle ("test2")); Assert.IsTrue (calledTest2); } static bool calledTest2; public static void Test2 () { calledTest2 = true; } static bool connectMethod3Done; [Test] public void ConnectMethod3 () { if (!Runtime.DynamicRegistrationSupported) Assert.Ignore ("This test requires support for dynamic registration."); if (connectMethod3Done) Assert.Ignore ("This is a one-shot test. Restart to run again."); connectMethod3Done = true; Runtime.ConnectMethod (typeof (NSString), typeof (RuntimeTest).GetMethod ("Test3"), new Selector ("test3")); Messaging.void_objc_msgSend (Class.GetHandle (typeof (NSString)), Selector.GetHandle ("test3")); Assert.IsTrue (calledTest3); } static bool calledTest3; public static void Test3 () { calledTest3 = true; } class A : NSObject { public void Test() { Console.WriteLine ("Tested!"); } } [Test] public void GetINativeObjectTest () { // create a string and try to get it. IntPtr strptr = IntPtr.Zero; IntPtr valueptr; NSString obj; try { strptr = Marshal.StringToHGlobalAuto ("value"); valueptr = Messaging.IntPtr_objc_msgSend_IntPtr (Class.GetHandle (typeof (NSString)), Selector.GetHandle ("stringWithUTF8String:"), strptr); Assert.Throws (() => Runtime.GetINativeObject (valueptr, false), "INativeObject"); valueptr = Messaging.IntPtr_objc_msgSend_IntPtr (Class.GetHandle (typeof (NSString)), Selector.GetHandle ("stringWithUTF8String:"), strptr); obj = Runtime.GetINativeObject (valueptr, false) as NSString; Assert.AreEqual ("value", (string) obj, "NSObject"); valueptr = Messaging.IntPtr_objc_msgSend_IntPtr (Class.GetHandle (typeof (NSString)), Selector.GetHandle ("stringWithUTF8String:"), strptr); obj = Runtime.GetINativeObject (valueptr, false) as NSString; Assert.AreEqual ("value", (string) obj, "NSString"); valueptr = Messaging.IntPtr_objc_msgSend_IntPtr (Class.GetHandle (typeof (NSString)), Selector.GetHandle ("stringWithUTF8String:"), strptr); var nscopying = Runtime.GetINativeObject (valueptr, false); Assert.NotNull (nscopying, "INSCopying"); } finally { Marshal.FreeHGlobal (strptr); } } [Test] #if NET [Ignore ("https://github.com/dotnet/runtime/issues/32543")] #endif public void NSAutoreleasePoolInThreadPool () { var count = 100; var counter = new CountdownEvent (count); var obj = new NSObject (); for (int i = 0; i < count; i++) { ThreadPool.QueueUserWorkItem ((v) => { obj.DangerousRetain ().DangerousAutorelease (); counter.Signal (); }); } Assert.IsTrue (counter.Wait (TimeSpan.FromSeconds (5)), "timed out"); // there is a race condition here: we don't necessarily know when the // threadpool's autorelease pools are freed (in theory we can have X // threadpool threads stopped just after the 'counter.Signal' line, // before unwinding to the autorelease pool's dispose frame). So // assert that the retain count has decreased, but not that it has // decreased to 1 var max_iterations = 100; var iterations = 0; while ((long) obj.RetainCount > (long) count / 2 && iterations++ < max_iterations) { Thread.Sleep (100); } Assert.That (obj.RetainCount, Is.Not.GreaterThan ((nuint) (count / 2)), "RC. Iterations: " + iterations); obj.Dispose (); } class ResurrectedObjectsDisposedTestClass : NSObject { [Export ("invokeMe:wait:")] static bool InvokeMe (NSObject obj, int invokerWait) { Thread.Sleep (invokerWait); return obj.Handle != IntPtr.Zero; } } [Test] #if !MONOMAC // Failing with 10 broken [TestCase (typeof (NSObject))] #endif [TestCase (typeof (ResurrectedObjectsDisposedTestClass))] public void ResurrectedObjectsDisposedTest (Type type) { #if __WATCHOS__ if (Runtime.Arch == Arch.DEVICE) Assert.Ignore ("This test uses too much memory for the watch."); #endif var invokerClassHandle = Class.GetHandle (typeof (ResurrectedObjectsDisposedTestClass)); // Create a number of native objects with no managed wrappers. // We create more than one to try to minimize the random effects // of the GC (a random value somewhere can keep a single object // from being collected, but the chances of X random values keeping // X objects from being collected are much lower). // Also the consequences if the GC doesn't collect an object is // that the test unexpectedly succeeds. // 10 objects seem to be a fine number that will cause pretty much // all test executions to fail. var objects = new IntPtr [10]; for (int i = 0; i < objects.Length; i++) objects [i] = Messaging.IntPtr_objc_msgSend (Messaging.IntPtr_objc_msgSend (Class.GetHandle (type), Selector.GetHandle ("alloc")), Selector.GetHandle ("init")); // Create a thread that creates managed wrappers for all of the above native objects. // We do this on a separate thread so that the GC finds no pointers to the managed // objects in any thread. var t1 = new Thread (() => { for (int i = 0; i < objects.Length; i++) Messaging.bool_objc_msgSend_IntPtr_int (invokerClassHandle, Selector.GetHandle ("invokeMe:wait:"), objects [i], 0); }); t1.Start (); t1.Join (); // Collect all those managed wrappers, and make sure their finalizers are executed. GC.Collect (); GC.WaitForPendingFinalizers (); // Now all the managed wrappers should be on NSObject's drain queue on the main thread. // Spin up a thread for every native object, and invoke a managed method // that will fetch the managed wrapper and then wait for a while (500ms), // before verifying that the managed object hasn't been disposed while waiting. var invokerWait = 500; var threads = new Thread [objects.Length]; var brokenCount = 0; var countdown = new CountdownEvent (threads.Length); for (int t = 0; t < threads.Length; t++) { var tt = t; var thread = new Thread (() => { var ok = Messaging.bool_objc_msgSend_IntPtr_int (invokerClassHandle, Selector.GetHandle ("invokeMe:wait:"), objects [tt], invokerWait); if (!ok) { //Console.WriteLine ("Broken #{0}: 0x{1}", tt, objects [tt].ToString ("x")); Interlocked.Increment (ref brokenCount); } countdown.Signal (); }); thread.Start (); } // Now all those threads should be waiting. // Run the runloop on the main thread, which will drain the managed wrappers // on NSObject's drain queue. while (!countdown.IsSet) { NSRunLoop.Current.RunUntil ((NSDate) DateTime.Now.AddSeconds (0.1)); } // Release all the native objects we created for (int i = 0; i < objects.Length; i++) Messaging.void_objc_msgSend (objects [i], Selector.GetHandle ("release")); Assert.AreEqual (0, brokenCount, "broken count"); } [Test] public void MX8029 () { var handle = Messaging.IntPtr_objc_msgSend (Messaging.IntPtr_objc_msgSend (Class.GetHandle (typeof (Dummy)), Selector.GetHandle ("alloc")), Selector.GetHandle ("init")); try { try { Messaging.void_objc_msgSend_IntPtr (Class.GetHandle (typeof (Dummy)), Selector.GetHandle ("doSomethingElse:"), handle); Assert.Fail ("Expected an MX8029 exception (A)"); } catch (RuntimeException mex) { Assert.AreEqual (8029, mex.Code, "Exception code (A)"); var failure = mex.ToString (); Assert.That (failure, Does.Contain ("Failed to marshal the Objective-C object"), "Failed to marshal (A)"); Assert.That (failure, Does.Contain ("Additional information:"), "Additional information: (A)"); Assert.That (failure, Does.Contain ("Selector: doSomethingElse:"), "Selector (A)"); Assert.That (failure, Does.Contain ("DoSomethingElse"), "DoSomethingElse (A)"); } try { Messaging.void_objc_msgSend_IntPtr (handle, Selector.GetHandle ("doSomething:"), handle); Assert.Fail ("Expected an MX8034 exception (B)"); } catch (RuntimeException mex) { Assert.That (mex.Code, Is.EqualTo (8034).Or.EqualTo (8027), "Exception code (B)"); var failure = mex.ToString (); Assert.That (failure, Does.Contain ("Failed to marshal the Objective-C object"), "Failed to marshal (B)"); Assert.That (failure, Does.Contain ("Additional information:"), "Additional information: (B)"); Assert.That (failure, Does.Contain ("Selector: doSomething:"), "Selector (B)"); Assert.That (failure, Does.Contain ("DoSomething"), "DoSomething (B)"); } } finally { Messaging.void_objc_msgSend (handle, Selector.GetHandle ("release")); } } #if DYNAMIC_REGISTRAR [Test] public void MX8029_b () { try { using (var arr = new NSArray ()) { Messaging.void_objc_msgSend_IntPtr (Class.GetHandle (typeof (Dummy)), Selector.GetHandle ("setIntArray:"), arr.Handle); Assert.Fail ("An exception should have been thrown"); } } catch (RuntimeException re) { Assert.AreEqual (8029, re.Code, "Code"); Assert.AreEqual (@"Unable to marshal the array parameter #1 whose managed type is 'System.Int32[]' to managed. Additional information: Selector: setIntArray: Method: MonoTouchFixtures.ObjCRuntime.RuntimeTest/Dummy:SetIntArray (int[]) ", re.Message, "Message"); var inner = (RuntimeException)re.InnerException; Assert.AreEqual (8031, inner.Code, "Inner Code"); Assert.AreEqual ("Unable to convert from an NSArray to a managed array of System.Int32.", inner.Message, "Inner Message"); } } [Test] public void MX8033 () { try { Messaging.IntPtr_objc_msgSend (Class.GetHandle (typeof (Dummy)), Selector.GetHandle ("intArray")); Assert.Fail ("An exception should have been thrown"); } catch (RuntimeException re) { Assert.AreEqual (8033, re.Code, "Code"); Assert.AreEqual (@"Unable to marshal the return value of type 'System.Int32[]' to Objective-C. Additional information: Selector: intArray Method: MonoTouchFixtures.ObjCRuntime.RuntimeTest/Dummy:GetIntArray () ", re.Message, "Message"); var inner = (RuntimeException) re.InnerException; Assert.AreEqual (8032, inner.Code, "Inner Code"); Assert.AreEqual ("Unable to convert from a managed array of System.Int32 to an NSArray.", inner.Message, "Inner Message"); } } #endif // does not have an IntPtr constructor class Dummy : NSObject { [Export ("initWithFoo:")] public Dummy (int foo) { // Prevent the default ctor from being exported. } [Export ("doSomething:")] public void DoSomething (Dummy dummy) { } [Export ("doSomethingElse:")] public static void DoSomethingElse (Dummy dummy) { } #if DYNAMIC_REGISTRAR // This function makes the static registrar show an error, so it's only built when using the dynamic registrar. [Export ("intArray")] static int[] GetIntArray () { return new int [] { }; } // This function makes the static registrar show an error, so it's only built when using the dynamic registrar. [Export ("setIntArray:")] static void SetIntArray (int[] array) { Assert.Fail ("This method should never be called."); } #endif } [Test] public void GetINativeObject_ForcedType () { using (var str = new NSString ("hello world")) { NSDate date; Assert.Throws (() => Runtime.GetINativeObject (str.Handle, false), "EX1"); // Assert.Throws (() => Runtime.GetINativeObject (str.Handle, false, false), "EX2"); // // Making a string quack like a date. // This is a big hack, but hopefully it works well enough for this particular test case. // Just don't inspect the date variable in a debugger (it will most likely crash the app). date = Runtime.GetINativeObject (str.Handle, true, false); Assert.AreEqual (date.Handle, str.Handle, "Same native pointer"); date.Dispose (); date = null; } } } }