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:
- Zgłaszany Objective-C jest wyjątek.
- Ś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. - Środowisko Objective-C uruchomieniowe nie znajduje żadnych
@catch
procedur obsługi, wywołujeNSGetUncaughtExceptionHandler
metodę i wywołuje program obsługi zainstalowany przez platformę Xamarin.iOS/Xamarin.Mac. - 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:
- Zgłaszany Objective-C jest wyjątek.
- Ś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. - Ś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 jestThrowObjectiveCException
to, czy GC jest w trybie współpracy (watchOS) iUnwindNativeCode
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ę jakoUnwindNativeCode
.
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 toThrowManagedException
, czy GC jest w trybie współpracy (watchOS) iUnwindManagedCode
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
disable
Z 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).