Sdílet prostřednictvím


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, NSSetUncaughtExceptionHandlerkteré 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:

  1. Vyvolá Objective-C se výjimka.
  2. Modul Objective-C runtime provede zásobník (ale nerozvíjí ho), hledá nativní @catch obslužnou rutinu, která dokáže zpracovat výjimku.
  3. Modul Objective-C runtime nenajde žádné @catch obslužné rutiny, volání NSGetUncaughtExceptionHandlera vyvolá obslužnou rutinu nainstalovanou Xamarin.iOS/Xamarin.Mac.
  4. 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:

  1. Vyvolá Objective-C se výjimka.
  2. Modul Objective-C runtime provede zásobník (ale nerozvíjí ho), hledá nativní @catch obslužnou rutinu, která dokáže zpracovat výjimku.
  3. 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ždy ThrowObjectiveCException v .NET. U starších projektů Xamarinu je ThrowObjectiveCException to v režimu spolupráce (watchOS) a UnwindNativeCode 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 jako UnwindNativeCode.

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ždy ThrowManagedException v .NET. U starších projektů Xamarinu je ThrowManagedException to v režimu spolupráce (watchOS) a UnwindManagedCode 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

disableS 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).