Marshaling wyjątków na platformie Xamarin.iOS i Xamarin.Mac

Zarówno kod zarządzany, jak i Objective-C obsługują wyjątki środowiska uruchomieniowego (klauzule try/catch/finally).

Jednak ich implementacje są różne, co oznacza, że biblioteki uruchomieniowe (środowisko uruchomieniowe Mono lub CoreCLR i Objective-C biblioteki środowiska uruchomieniowego) mają problemy, gdy muszą obsługiwać wyjątki, a następnie uruchamiać kod napisany w innych językach.

W tym dokumencie opisano problemy, które mogą wystąpić, oraz możliwe rozwiązania.

Zawiera również przykładowy projekt , Marshaling wyjątków, który może służyć do testowania różnych scenariuszy i ich rozwiązań.

Problem

Problem występuje, gdy zgłaszany jest wyjątek, a podczas odwijania stosu występuje ramka, która nie jest zgodna z typem wyjątku, który został zgłoszony.

Typowym przykładem tego problemu jest zgłoszenie wyjątku przez natywny interfejs API Objective-C , a następnie obsłużenie tego Objective-C wyjątku, gdy proces odwijania stosu osiągnie zarządzaną ramkę.

W przypadku starszych projektów platformy Xamarin (pre-.NET) domyślną akcją jest nic nie robić. W przypadku powyższego przykładu oznacza to, że pozwala to na odwijanie Objective-C ramek zarządzanych przez środowisko uruchomieniowe. Ta akcja jest problematyczna, ponieważ Objective-C środowisko uruchomieniowe nie wie, jak odwijać zarządzane ramki, na przykład nie będzie wykonywać żadnych catch klauzul ani finally ani w tej ramce.

Uszkodzony kod

Rozważmy następujący przykład kodu:

var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero); 

Ten kod zgłosi wyjątek Objective-C NSInvalidArgumentException w kodzie natywnym:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

Ślad stosu będzie podobny do następującego:

0   CoreFoundation          __exceptionPreprocess + 194
1   libobjc.A.dylib         objc_exception_throw + 52
2   CoreFoundation          -[__NSDictionaryM setObject:forKey:] + 1015
3   libobjc.A.dylib         objc_msgSend + 102
4   TestApp                 ObjCRuntime.Messaging.void_objc_msgSend_IntPtr_IntPtr (intptr,intptr,intptr,intptr)
5   TestApp                 Foundation.NSMutableDictionary.LowlevelSetObject (intptr,intptr)
6   TestApp                 ExceptionMarshaling.Exceptions.ThrowObjectiveCException ()

Ramki 0–3 są ramkami natywnymi, a odwijarka stosu Objective-C w środowisku uruchomieniowym może odwinąć te ramki. W szczególności spowoduje wykonanie dowolnych Objective-C@catch klauzul lub @finally .

Jednak odwijanie Objective-C stosu nie jest w stanie prawidłowo odwinąć zarządzanych ramek (ramki 4–6): Objective-C odwijanie stosu odwije zarządzane ramki, ale nie wykona żadnej logiki wyjątków zarządzanych (takich jak catch lub "finally klauzule).

Oznacza to, że zwykle nie można przechwycić tych wyjątków w następujący sposób:

try {
    var dict = new NSMutableDictionary ();
    dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
    Console.WriteLine (ex);
} finally {
    Console.WriteLine ("finally");
}

Wynika to z faktu, że odwijacz Objective-C stosu nie wie o klauzuli zarządzanej catch i nie zostanie wykonana żadna klauzula finally .

Gdy powyższy przykład kodu jest skuteczny, jest to spowodowane Objective-C tym, że ma metodę powiadamiania o nieobsługiwanych Objective-C wyjątkach, NSSetUncaughtExceptionHandler, których używa program Xamarin.iOS i Xamarin.Mac, a w tym momencie próbuje przekonwertować wyjątki Objective-C na wyjątki zarządzane.

Scenariusze

Scenariusz 1 — przechwytywanie Objective-C wyjątków za pomocą zarządzanego programu obsługi catch

W poniższym scenariuszu można przechwytywać Objective-C wyjątki przy użyciu zarządzanych catch programów obsługi:

  1. Zgłaszany Objective-C jest wyjątek.
  2. Środowisko Objective-C uruchomieniowe przeprowadzi stos (ale nie odwije go), szukając natywnej @catch procedury obsługi, która może obsłużyć wyjątek.
  3. Środowisko Objective-C uruchomieniowe nie znajduje żadnych @catch procedur obsługi, wywołuje NSGetUncaughtExceptionHandlermetodę i wywołuje program obsługi zainstalowany przez platformę Xamarin.iOS/Xamarin.Mac.
  4. Program obsługi platformy Xamarin.iOS/Xamarin.Mac przekonwertuje Objective-C wyjątek na wyjątek zarządzany i zgłosi go. Objective-C Ponieważ środowisko uruchomieniowe nie odwijało stosu (chodziło tylko po nim), bieżąca ramka jest taka sama jak w przypadku zgłoszenia wyjątkuObjective-C.

W tym miejscu występuje inny problem, ponieważ środowisko uruchomieniowe Mono nie wie, jak prawidłowo odwijać Objective-C ramki.

Po wywołaniu wywołania zwrotnego nieuchwyconego Objective-C wyjątku platformy Xamarin.iOS stos jest podobny do następującego:

 0 libxamarin-debug.dylib   exception_handler(exc=name: "NSInvalidArgumentException" - reason: "*** setObjectForKey: key cannot be nil")
 1 CoreFoundation           __handleUncaughtException + 809
 2 libobjc.A.dylib          _objc_terminate() + 100
 3 libc++abi.dylib          std::__terminate(void (*)()) + 14
 4 libc++abi.dylib          __cxa_throw + 122
 5 libobjc.A.dylib          objc_exception_throw + 337
 6 CoreFoundation           -[__NSDictionaryM setObject:forKey:] + 1015
 7 libxamarin-debug.dylib   xamarin_dyn_objc_msgSend + 102
 8 TestApp                  ObjCRuntime.Messaging.void_objc_msgSend_IntPtr_IntPtr (intptr,intptr,intptr,intptr)
 9 TestApp                  Foundation.NSMutableDictionary.LowlevelSetObject (intptr,intptr) [0x00000]
10 TestApp                  ExceptionMarshaling.Exceptions.ThrowObjectiveCException () [0x00013]

W tym miejscu jedynymi zarządzanymi ramkami są ramki 8-10, ale wyjątek zarządzany jest zgłaszany w ramce 0. Oznacza to, że środowisko uruchomieniowe Mono musi odwinąć ramki natywne 0–7, co powoduje problem odpowiadający omówionemu powyżej problemowi: chociaż środowisko uruchomieniowe Mono odwinie ramek natywnych, nie wykona żadnych Objective-C@catch klauzul ani @finally .

Przykładowy kod:

-(id) setObject: (id) object forKey: (id) key
{
    @try {
        if (key == nil)
            [NSException raise: @"NSInvalidArgumentException"];
    } @finally {
        NSLog (@"This won't be executed");
    }
}

Klauzula @finally nie zostanie wykonana, ponieważ środowisko uruchomieniowe Mono, które odwija tę ramkę, nie wie o tym.

Odmianą tej metody jest zgłaszanie wyjątku zarządzanego w kodzie zarządzanym, a następnie odwijanie ramek natywnych w celu przejścia do pierwszej klauzuli zarządzanej catch :

class AppDelegate : UIApplicationDelegate {
    public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
    {
        throw new Exception ("An exception");
    }
    static void Main (string [] args)
    {
        try {
            UIApplication.Main (args, null, typeof (AppDelegate));
        } catch (Exception ex) {
            Console.WriteLine ("Managed exception caught.");
        }
    }
}

Metoda zarządzana UIApplication:Main wywoła metodę natywną UIApplicationMain , a następnie system iOS wykona wiele natywnych wykonywania kodu przed ostatecznie wywołaniem metody zarządzanej AppDelegate:FinishedLaunching , przy czym wiele ramek natywnych na stosie zostanie zgłoszonych:

 0: TestApp                 ExceptionMarshaling.IOS.AppDelegate:FinishedLaunching (UIKit.UIApplication,Foundation.NSDictionary)
 1: TestApp                 (wrapper runtime-invoke) <Module>:runtime_invoke_bool__this___object_object (object,intptr,intptr,intptr) 
 2: libmonosgen-2.0.dylib   mono_jit_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
 3: libmonosgen-2.0.dylib   do_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
 4: libmonosgen-2.0.dylib   mono_runtime_invoke [inlined] mono_runtime_invoke_checked(method=<unavailable>, obj=<unavailable>, params=<unavailable>, error=0xbff45758)
 5: libmonosgen-2.0.dylib   mono_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>)
 6: libxamarin-debug.dylib  xamarin_invoke_trampoline(type=<unavailable>, self=<unavailable>, sel="application:didFinishLaunchingWithOptions:", iterator=<unavailable>), context=<unavailable>)
 7: libxamarin-debug.dylib  xamarin_arch_trampoline(state=0xbff45ad4)
 8: libxamarin-debug.dylib  xamarin_i386_common_trampoline
 9: UIKit                   -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:]
10: UIKit                   -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:]
11: UIKit                   -[UIApplication _runWithMainScene:transitionContext:completion:]
12: UIKit                   __84-[UIApplication _handleApplicationActivationWithScene:transitionContext:completion:]_block_invoke.3124
13: UIKit                   -[UIApplication workspaceDidEndTransaction:]
14: FrontBoardServices      __37-[FBSWorkspace clientEndTransaction:]_block_invoke_2
15: FrontBoardServices      __40-[FBSWorkspace _performDelegateCallOut:]_block_invoke
16: FrontBoardServices      __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__
17: FrontBoardServices      -[FBSSerialQueue _performNext]
18: FrontBoardServices      -[FBSSerialQueue _performNextFromRunLoopSource]
19: FrontBoardServices      FBSSerialQueueRunLoopSourceHandler
20: CoreFoundation          __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
21: CoreFoundation          __CFRunLoopDoSources0
22: CoreFoundation          __CFRunLoopRun
23: CoreFoundation          CFRunLoopRunSpecific
24: CoreFoundation          CFRunLoopRunInMode
25: UIKit                   -[UIApplication _run]
26: UIKit                   UIApplicationMain
27: TestApp                 (wrapper managed-to-native) UIKit.UIApplication:UIApplicationMain (int,string[],intptr,intptr)
28: TestApp                 UIKit.UIApplication:Main (string[],intptr,intptr)
29: TestApp                 UIKit.UIApplication:Main (string[],string,string)
30: TestApp                 ExceptionMarshaling.IOS.Application:Main (string[])

Ramki 0-1 i 27-30 są zarządzane, podczas gdy wszystkie ramki między nimi są natywne. Jeśli mono rozwinie się przez te ramki, nie Objective-C@catch zostaną wykonane żadne klauzule lub @finally .

Scenariusz 2 — nie można przechwycić Objective-C wyjątków

W poniższym scenariuszu nie można przechwycić Objective-C wyjątków przy użyciu programów obsługi zarządzanejcatch, ponieważ Objective-C wyjątek został obsłużony w inny sposób:

  1. Zgłaszany Objective-C jest wyjątek.
  2. Środowisko Objective-C uruchomieniowe przeprowadzi stos (ale nie odwije go), szukając natywnej @catch procedury obsługi, która może obsłużyć wyjątek.
  3. Środowisko Objective-C uruchomieniowe znajduje procedurę @catch obsługi, odwija stos i rozpoczyna wykonywanie @catch procedury obsługi.

Ten scenariusz jest często spotykany w aplikacjach platformy Xamarin.iOS, ponieważ w głównym wątku zwykle występuje kod podobny do następującego:

void UIApplicationMain ()
{
    @try {
        while (true) {
            ExecuteRunLoop ();
        }
    } @catch (NSException *ex) {
        NSLog (@"An unhandled exception occured: %@", exc);
        abort ();
    }
}

Oznacza to, że w głównym wątku nigdy nie istnieje nieobsługiwany Objective-C wyjątek, a zatem nasze wywołanie zwrotne, które konwertuje Objective-C wyjątki na wyjątki zarządzane, nigdy nie jest wywoływane.

Jest to również typowe w przypadku debugowania aplikacji Xamarin.Mac we wcześniejszej wersji systemu macOS niż obsługiwane przez platformę Xamarin.Mac, ponieważ inspekcja większości obiektów interfejsu użytkownika w debugerze spróbuje pobrać właściwości odpowiadające selektorom, które nie istnieją na platformie wykonawczej (ponieważ platforma Xamarin.Mac obejmuje obsługę nowszej wersji systemu macOS). Wywołanie takich selektorów spowoduje zgłoszenie NSInvalidArgumentException ("Nierozpoznany selektor wysłany do ..."), co ostatecznie powoduje awarię procesu.

Podsumowując, jeśli Objective-C środowisko uruchomieniowe lub środowisko uruchomieniowe Mono nie są programowane do obsługi, może prowadzić do niezdefiniowanych zachowań, takich jak awarie, przecieki pamięci i inne typy nieprzewidywalnych (błędnych)zachowań.

Rozwiązanie

W środowiskach Xamarin.iOS 10 i Xamarin.Mac 2.10 dodaliśmy obsługę przechwytywania zarówno zarządzanych, jak i Objective-C wyjątków na dowolnej granicy natywnej zarządzanej oraz konwertowania tego wyjątku na inny typ.

W przykładzie przykładowym wygląda to następująco:

[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);

static void DoSomething (NSObject obj)
{
    objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}

Przechwycono metodę P/Invoke w celu objc_msgSend, a ten kod jest wywoływany:

void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
    @try {
        objc_msgSend (obj, sel);
    } @catch (NSException *ex) {
        convert_to_and_throw_managed_exception (ex);
    }
}

I coś podobnego jest wykonywane w przypadku odwrotnego przypadku (marshaling zarządzanych wyjątków do Objective-C wyjątków).

Przechwytywanie wyjątków na granicy natywnej zarządzanej nie jest bezpłatne, więc w przypadku starszych projektów platformy Xamarin (pre-.NET) nie zawsze jest ona domyślnie włączona:

  • Xamarin.iOS/tvOS: przechwytywanie wyjątków Objective-C jest włączone w symulatorze.
  • Xamarin.watchOS: przechwytywanie jest wymuszane we wszystkich przypadkach, ponieważ umożliwienie Objective-C środowiska uruchomieniowego odwijania zarządzanych ramek spowoduje pomylenie modułu odśmiecania pamięci i zawieszenie lub awarię.
  • Xamarin.Mac: przechwytywanie wyjątków Objective-C jest włączone dla kompilacji debugowania.

Na platformie .NET przeprowadzanie marshalingu wyjątków zarządzanych do Objective-C wyjątków jest zawsze domyślnie włączone.

W sekcji Flagi czasu kompilacji wyjaśniono, jak włączyć przechwytywanie, gdy nie jest włączone domyślnie (lub wyłączyć przechwytywanie, gdy jest to ustawienie domyślne).

Zdarzenia

Istnieją dwa zdarzenia, które są zgłaszane po przechwyceniu wyjątku: Runtime.MarshalManagedException i Runtime.MarshalObjectiveCException.

Oba zdarzenia są przekazywane EventArgs obiekt, który zawiera oryginalny wyjątek, który został zgłoszony ( Exception właściwość) i ExceptionMode właściwość do zdefiniowania sposobu marshalingu wyjątku.

Właściwość ExceptionMode można zmienić w procedurze obsługi zdarzeń, aby zmienić zachowanie zgodnie z dowolnym niestandardowym przetwarzaniem wykonanym w procedurze obsługi. Przykładem może być przerwanie procesu w przypadku wystąpienia określonego wyjątku.

ExceptionMode Zmiana właściwości dotyczy pojedynczego zdarzenia, ale nie ma wpływu na żadne wyjątki przechwycone w przyszłości.

Podczas marshalingu wyjątków zarządzanych do kodu natywnego są dostępne następujące tryby:

  • Default: Wartość domyślna różni się w zależności od platformy. ThrowObjectiveCException Jest zawsze na platformie .NET. W przypadku starszych projektów Xamarin jest ThrowObjectiveCException to, czy GC jest w trybie współpracy (watchOS) i UnwindNativeCode w przeciwnym razie (iOS / watchOS / macOS). Wartość domyślna może ulec zmianie w przyszłości.
  • UnwindNativeCode: To jest poprzednie (niezdefiniowane) zachowanie. Nie jest to dostępne w przypadku korzystania z GC w trybie współpracy (która jest jedyną opcją w systemie watchOS; w związku z tym nie jest to prawidłowa opcja w systemie watchOS), ani w przypadku korzystania z coreCLR, ale jest to opcja domyślna dla wszystkich innych platform w starszych projektach platformY Xamarin.
  • ThrowObjectiveCException: przekonwertuj wyjątek zarządzany na Objective-C wyjątek i zgłosić Objective-C wyjątek. Jest to ustawienie domyślne na platformie .NET i w systemie watchOS w starszych projektach platformy Xamarin.
  • Abort: przerwać proces.
  • Disable: wyłącza przechwytywanie wyjątków, więc nie ma sensu ustawiać tej wartości w procedurze obsługi zdarzeń, ale po wystąpieniu zdarzenia jest za późno, aby go wyłączyć. W każdym razie, jeśli zostanie ustawiona, będzie ona zachowywać się jako UnwindNativeCode.

Podczas marshalingu Objective-C wyjątków do kodu zarządzanego dostępne są następujące tryby:

  • Default: Wartość domyślna różni się w zależności od platformy. ThrowManagedException Jest zawsze na platformie .NET. W przypadku starszych projektów Xamarin jest to ThrowManagedException , czy GC jest w trybie współpracy (watchOS) i UnwindManagedCode w przeciwnym razie (iOS / tvOS / macOS). Wartość domyślna może ulec zmianie w przyszłości.
  • UnwindManagedCode: To jest poprzednie (niezdefiniowane) zachowanie. Nie jest to dostępne w przypadku korzystania z GC w trybie współpracy (który jest jedynym prawidłowym trybem GC w systemie watchOS; w związku z tym nie jest to prawidłowa opcja w systemie watchOS), ani w przypadku korzystania z coreCLR, ale jest to ustawienie domyślne dla wszystkich innych platform w starszych projektach platformY Xamarin.
  • ThrowManagedException: Przekonwertuj Objective-C wyjątek na wyjątek zarządzany i zgłosić wyjątek zarządzany. Jest to ustawienie domyślne na platformie .NET i w systemie watchOS w starszych projektach platformy Xamarin.
  • Abort: przerwać proces.
  • Disable: wyłącza przechwytywanie wyjątków, więc nie ma sensu ustawiać tej wartości w procedurze obsługi zdarzeń, ale po wystąpieniu zdarzenia jest za późno, aby ją wyłączyć. W każdym przypadku, jeśli zostanie ustawiona, proces zostanie przerwany.

Dlatego, aby zobaczyć za każdym razem, gdy wyjątek jest marshaled, możesz to zrobić:

Runtime.MarshalManagedException += (object sender, MarshalManagedExceptionEventArgs args) =>
{
    Console.WriteLine ("Marshaling managed exception");
    Console.WriteLine ("    Exception: {0}", args.Exception);
    Console.WriteLine ("    Mode: {0}", args.ExceptionMode);
    
};
Runtime.MarshalObjectiveCException += (object sender, MarshalObjectiveCExceptionEventArgs args) =>
{
    Console.WriteLine ("Marshaling Objective-C exception");
    Console.WriteLine ("    Exception: {0}", args.Exception);
    Console.WriteLine ("    Mode: {0}", args.ExceptionMode);
};

Flagi czasu kompilacji

Można przekazać następujące opcje do aplikacji mtouch (dla aplikacji platformy Xamarin.iOS) i mmp (dla aplikacji Xamarin.Mac), które określą, czy włączono przechwytywanie wyjątków, i ustaw domyślną akcję, która powinna wystąpić:

  • --marshal-managed-exceptions=

    • default
    • unwindnativecode
    • throwobjectivecexception
    • abort
    • disable
  • --marshal-objectivec-exceptions=

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

disableZ wyjątkiem wartości , te wartości są identyczne z ExceptionMode wartościami przekazywanymi do zdarzeń MarshalManagedException i MarshalObjectiveCException .

Opcja disable spowoduje wyłączenie przechwytywania, z wyjątkiem tego, że nadal będziemy przechwytywać wyjątki, gdy nie dodaje żadnych obciążeń związanych z wykonywaniem. Zdarzenia marshalingu są nadal wywoływane dla tych wyjątków, a tryb domyślny jest trybem domyślnym dla platformy wykonawczej.

Ograniczenia

Przechwycimy tylko funkcję P/Invoke do objc_msgSend rodziny funkcji podczas próby przechwycenia Objective-C wyjątków. Oznacza to, że funkcja P/Invoke do innej funkcji języka C, która następnie zgłasza wszelkie Objective-C wyjątki, nadal będzie działać w starym i niezdefiniowanym zachowaniu (może to zostać ulepszone w przyszłości).