Zařazování výjimek v Xamarin.iOS a Xamarin.Mac
Spravovaný kód i Objective-C podpora výjimek modulu runtime (klauzule try/catch/finally).
Jejich implementace se ale liší, což znamená, že knihovny modulu runtime (Mono runtime nebo CoreCLR a Objective-C knihovny runtime) mají problémy, když musí zpracovat výjimky a pak spustit kód napsaný v jiných jazycích.
Tento dokument vysvětluje problémy, ke kterým může dojít, a možná řešení.
Obsahuje také ukázkový projekt, zařazování výjimek, které lze použít k testování různých scénářů a jejich řešení.
Problém
K problému dochází v případě, že dojde k vyvolání výjimky a při odvíjení zásobníku dojde k chybě, která neodpovídá typu vyvolané výjimky.
Typickým příkladem tohoto problému je, když nativní rozhraní API vyvolá Objective-C výjimku a pak Objective-C se tato výjimka musí nějak zpracovat, když proces odvíjení zásobníku dosáhne spravovaného rámce.
U starších projektů Xamarinu (pre-.NET) je výchozí akcí nic dělat.
U výše uvedené ukázky to znamená, že modul runtime uvolní Objective-C spravované rámce. Tato akce je problematická, protože Objective-C modul runtime neví, jak uvolnit spravované rámce, například nespustí žádné catch
klauzule ani finally
klauzule v tomto rámci.
Poškozený kód
Vezměme si následující příklad kódu:
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Tento kód vyvolá Objective-C výjimku NSInvalidArgumentException v nativním kódu:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
Trasování zásobníku bude vypadat přibližně takto:
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 ()
Rámce 0–3 jsou nativní rámce a zásobník v Objective-C modulu runtime může tyto rámce uvolnit. Konkrétně se spustí jakákoli Objective-C@catch
klauzule nebo @finally
klauzule.
Objective-C Unwinder zásobníku však nedokáže správně uvolnit spravované rámce (rámce 4–6): Objective-C odvíjení zásobníku zruší spravované rámce, ale neprovede žádnou logiku spravované výjimky (například catch
klauzule finally).
To znamená, že tyto výjimky obvykle není možné zachytit následujícím způsobem:
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
Důvodem je to, že Objective-C zásobník unwinder neví o spravované catch
klauzuli a ani tato klauzule se finally
nespustí.
Pokud je výše uvedený vzorek kódu účinný, je to proto, že Objective-C má metodu oznámení o neošetřených Objective-C výjimkách, NSSetUncaughtExceptionHandler
které používá Xamarin.iOS a Xamarin.Mac, a v tomto okamžiku se pokusí převést všechny Objective-C výjimky na spravované výjimky.
Scénáře
Scénář 1 – zachycení Objective-C výjimek pomocí spravované obslužné rutiny catch
V následujícím scénáři je možné zachytit Objective-C výjimky pomocí spravovaných catch
obslužných rutin:
- Vyvolá Objective-C se výjimka.
- Modul Objective-C runtime provede zásobník (ale nerozvíjí ho), hledá nativní
@catch
obslužnou rutinu, která dokáže zpracovat výjimku. - Modul Objective-C runtime nenajde žádné
@catch
obslužné rutiny, voláníNSGetUncaughtExceptionHandler
a vyvolá obslužnou rutinu nainstalovanou Xamarin.iOS/Xamarin.Mac. - Obslužná rutina Xamarin.iOS/Xamarin.Mac převede Objective-C výjimku na spravovanou výjimku a vyvolá ji. Objective-C Vzhledem k tomu, že modul runtime nezvolil zásobník (jen ho prošel), je aktuální rámec stejný jako v případě, že došlo k Objective-C vyvolání výjimky.
K dalšímu problému dochází tady, protože modul runtime Mono neví, jak správně uvolnit Objective-C snímky.
Při volání zpětného volání nezachycených Objective-C výjimek Xamarin.iOS je zásobník podobný tomuto:
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]
V této části jsou jedinými spravovanými snímky rámce 8–10, ale spravovaná výjimka se vyvolá v rámci 0. To znamená, že modul runtime Mono musí uvolnit nativní rámce 0–7, což způsobí problém, který je ekvivalentem výše uvedeného problému: přestože modul runtime Mono uvolní nativní rámce, neprovede žádné Objective-C@catch
klauzule ani @finally
klauzule.
Příklad kódu:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
@finally
A klauzule se nespustí, protože modul runtime Mono, který tento rámec uvolní, o něm neví.
Variantou je vyvolání spravované výjimky ve spravovaném kódu a následné odvíjení nativních rámců, aby se dostala k první spravované catch
klauzuli:
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.");
}
}
}
Spravovaná UIApplication:Main
metoda bude volat nativní UIApplicationMain
metodu a pak iOS provede spoustu nativního spuštění kódu, než nakonec zavolá spravovanou AppDelegate:FinishedLaunching
metodu, s stále mnoha nativními snímky v zásobníku při vyvolání spravované výjimky:
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[])
Rámce 0–1 a 27–30 se spravují, zatímco všechny rámce mezi těmito snímky jsou nativní.
Pokud mono odvíjeje přes tyto rámce, nespustí se žádná Objective-C@catch
klauzule nebo @finally
klauzule.
Scénář 2 – nejde zachytit Objective-C výjimky
V následujícím scénáři není možné zachytit Objective-C výjimky pomocí spravovaných catch
obslužných rutin, protože výjimka Objective-C byla zpracována jiným způsobem:
- Vyvolá Objective-C se výjimka.
- Modul Objective-C runtime provede zásobník (ale nerozvíjí ho), hledá nativní
@catch
obslužnou rutinu, která dokáže zpracovat výjimku. - Modul Objective-C runtime najde obslužnou rutinu
@catch
, rozbalí zásobník a spustí obslužnou rutinu@catch
.
Tento scénář se běžně vyskytuje v aplikacích Xamarin.iOS, protože v hlavním vlákně je obvykle kód podobný tomuto:
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
To znamená, že v hlavním vlákně neexistuje nikdy neošetřená Objective-C výjimka, a proto se naše zpětná volání, která převádí Objective-C výjimky na spravované výjimky, se nikdy nevolá.
To je také běžné při ladění aplikací Xamarin.Mac ve starší verzi macOS než Xamarin.Mac podporuje, protože kontrola většiny objektů uživatelského rozhraní v ladicím programu se pokusí načíst vlastnosti, které odpovídají selektorům, které na prováděcí platformě neexistují (protože Xamarin.Mac zahrnuje podporu pro vyšší verzi macOS). Volání takových selektorů vyvolá výjimku NSInvalidArgumentException
(Nerozpoznaný selektor odeslaný do ...), což nakonec způsobí chybové ukončení procesu.
Pokud chcete shrnout, když modul Objective-C runtime nebo mono runtime uvolní rámce, které nejsou naprogramované tak, aby zpracovávaly, můžou vést k nedefinovaným chováním, jako jsou chybové ukončení, nevracení paměti a další typy nepředvídatelných (chybných) chování.
Řešení
V Xamarin.iOS 10 a Xamarin.Mac 2.10 jsme přidali podporu pro zachytávání spravovaných i Objective-C výjimek na libovolné hranici nativní pro správu a pro převod této výjimky na druhý typ.
V pseudokódu vypadá přibližně takto:
[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);
static void DoSomething (NSObject obj)
{
objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}
Volání nespravovaného kódu pro objc_msgSend je zachyceno a tento kód se místo toho volá:
void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
@try {
objc_msgSend (obj, sel);
} @catch (NSException *ex) {
convert_to_and_throw_managed_exception (ex);
}
}
A něco podobného se provádí pro reverzní případ (zařazování spravovaných výjimek na Objective-C výjimky).
Zachytávání výjimek na hranici nativní pro správu není nákladově bezplatné, takže u starších projektů Xamarinu (pre-.NET) není ve výchozím nastavení vždy povolená:
- Xamarin.iOS/tvOS: Zachycení Objective-C výjimek je v simulátoru povolené.
- Xamarin.watchOS: Průsečík se vynucuje ve všech případech, protože nechat Objective-C modul runtime uvolnit spravované rámce zmást systém uvolňování paměti a buď se zablokuje, nebo se chybově ukončí.
- Xamarin.Mac: Zachycení Objective-C výjimek je povolené pro sestavení ladění.
V .NET je ve výchozím nastavení povolené zařazování spravovaných výjimek na Objective-C výjimky.
Část Příznaky v čase sestavení vysvětluje, jak povolit zachycení, když není ve výchozím nastavení povolená (nebo zakázat zachytávání, když je výchozí).
Událost
Existují dvě události, které jsou vyvolány po zachycení výjimky: Runtime.MarshalManagedException
a Runtime.MarshalObjectiveCException
.
Obě události se předávají EventArgs
objekt, který obsahuje původní výjimku, která byla vyvolána ( Exception
vlastnost), a ExceptionMode
vlastnost definují, jak má být výjimka zařazována.
ExceptionMode
Vlastnost lze změnit v obslužné rutině události změnit chování podle jakéhokoli vlastního zpracování provedeného v obslužné rutině. Jedním z příkladů by bylo přerušení procesu, pokud dojde k určité výjimce.
ExceptionMode
Změna vlastnosti platí pro jednu událost, nemá vliv na žádné výjimky zachycené v budoucnu.
Při zařazování spravovaných výjimek do nativního kódu jsou k dispozici následující režimy:
Default
: Výchozí hodnota se liší podle platformy. Je vždyThrowObjectiveCException
v .NET. U starších projektů Xamarinu jeThrowObjectiveCException
to v režimu spolupráce (watchOS) aUnwindNativeCode
v opačném případě (iOS / watchOS / macOS). Výchozí nastavení se může v budoucnu změnit.UnwindNativeCode
: Toto je předchozí (nedefinované) chování. To není k dispozici při použití GC v režimu spolupráce (což je jediná možnost v watchOS, takže to není platná možnost pro watchOS), ani při použití CoreCLR, ale je to výchozí možnost pro všechny ostatní platformy ve starších projektech Xamarin.ThrowObjectiveCException
: Převede spravovanou Objective-C výjimku na výjimku a vyvolá Objective-C výjimku. Toto je výchozí nastavení v .NET a ve watchOS ve starších projektech Xamarin.Abort
: Přerušte proces.Disable
: Zakáže zachytávání výjimek, takže nemá smysl nastavit tuto hodnotu v obslužné rutině události, ale jakmile je událost vyvolána, je příliš pozdě ji zakázat. V každém případě, pokud je nastavena, bude se chovat jakoUnwindNativeCode
.
Při zařazování výjimek do spravovaného Objective-C kódu jsou k dispozici následující režimy:
Default
: Výchozí hodnota se liší podle platformy. Je vždyThrowManagedException
v .NET. U starších projektů Xamarinu jeThrowManagedException
to v režimu spolupráce (watchOS) aUnwindManagedCode
v opačném případě (iOS / tvOS / macOS). Výchozí nastavení se může v budoucnu změnit.UnwindManagedCode
: Toto je předchozí (nedefinované) chování. To není k dispozici při použití GC v režimu spolupráce (což je jediný platný režim GC ve watchOS, takže to není platná možnost pro watchOS), ani při použití CoreCLR, ale je to výchozí pro všechny ostatní platformy ve starších projektech Xamarin.ThrowManagedException
: Převeďte Objective-C výjimku na spravovanou výjimku a vyvoláte spravovanou výjimku. Toto je výchozí nastavení v .NET a ve watchOS ve starších projektech Xamarin.Abort
: Přerušte proces.Disable
: Zakáže zachycování výjimek, takže nemá smysl nastavit tuto hodnotu v obslužné rutině události, ale jakmile je událost vyvolána, je příliš pozdě ji zakázat. V každém případě v případě nastavení proces přeruší.
Pokud se tedy chcete podívat, kdy se zařadí výjimka, můžete to udělat takto:
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);
};
Příznaky času sestavení
Pro aplikace Xamarin.iOS (pro aplikace Xamarin.iOS) a mmp (pro aplikace Xamarin.Mac) je možné předat následující možnosti, které určí, jestli je povolené zachycení výjimek, a nastavit výchozí akci, která by měla nastat:
--marshal-managed-exceptions=
default
unwindnativecode
throwobjectivecexception
abort
disable
--marshal-objectivec-exceptions=
default
unwindmanagedcode
throwmanagedexception
abort
disable
disable
S výjimkou těchto hodnot jsou stejné jako ExceptionMode
hodnoty, které se předávají událostem a MarshalObjectiveCException
událostemMarshalManagedException
.
Tato disable
možnost většinou zakáže zachycování, s výjimkou případů, kdy nezachytí žádné režijní náklady na spuštění. U těchto výjimek jsou stále vyvolány události zařazování, přičemž výchozí režim je výchozím režimem pro prováděcí platformu.
Omezení
Při pokusu objc_msgSend
o zachycení Objective-C výjimek zachytáváme volání nespravovaných volání pouze pro řadu funkcí. To znamená, že volání nedefinovaného chování P/Invoke jiné funkce jazyka C, které pak vyvolá všechny Objective-C výjimky, stále narazí na staré a nedefinované chování (v budoucnu to může být vylepšeno).