Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Både hanterad kod och Objective-C har stöd för runtime-undantag (try/catch/finally-satser).
Implementeringarna skiljer sig dock åt, vilket innebär att körningsbiblioteken (MonoVM/CoreCLR-runtimes och Objective-C körningsbibliotek) har problem när de stöter på undantag från andra körningsmiljöer.
Den här artikeln förklarar de problem som kan uppstå och möjliga lösningar.
Det innehåller också ett exempelprojekt, Exception Marshaling, som kan användas för att testa olika scenarier och deras lösningar.
Problem
Problemet uppstår när ett undantag utlöses, och när stacken varvar ned påträffas en ram som inte matchar den typ av undantag som utlöstes.
Ett typiskt exempel på det här problemet är när ett internt API genererar ett Objective-C undantag och sedan att Objective-C undantag på något sätt måste hanteras när stackens avrullningsprocess når en hanterad ram.
Tidigare (pre-.NET) var standardåtgärden att inte göra någonting. För exemplet ovan skulle det innebära att låta Objective-C körningen varva ned hanterade ramar. Den här åtgärden är problematisk eftersom Objective-C-körningen inte vet hur hanterade ramar ska varva ned. Till exempel kommer den inte att köra några hanterade catch eller finally satser, vilket leder till imponerande svårt att hitta buggar.
Bruten kod
Ta följande kod som exempel:
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Den här koden genererar en Objective-C NSInvalidArgumentException i intern kod:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
Och stacktrace:n blir ungefär så här:
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 ()
Bildrutorna 0–3 är ursprungliga bildrutor, och stackavvecklaren i Objective-C-runtime kan avveckla dessa bildrutor. I synnerhet kommer den att köra alla Objective-C @catch eller @finally satser.
Den Objective-C-stackavvecklaren kan dock inte avveckla de hanterade ramarna (ramarna 4–6): Objective-C-stackavvecklaren kommer att avveckla de hanterade ramarna, men kör ingen logik för hanterade undantag (såsom catch eller finally satser).
Vilket innebär att det vanligtvis inte går att fånga dessa undantag på följande sätt:
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
Detta beror på att Objective-C stackupplindaren inte känner till den hanterade catch satsen, och inte heller kommer finally satsen att köras.
När kodexemplet ovan är effektivt beror det på att Objective-C har en metod för att meddelas om ohanterade Objective-C undantag, , NSSetUncaughtExceptionHandlersom .NET SDK:er använder, och vid den tidpunkten försöker konvertera alla Objective-C undantag till hanterade undantag.
Scenarier
Scenario 1 – fånga Objective-C undantag med en hanterad catch-hanterare
I följande scenario är det möjligt att fånga Objective-C undantag med hjälp av hanterade catch hanterare:
- Ett Objective-C undantag kastas.
- Den Objective-C körningen går över stacken (men varvar inte ned den) och letar efter en intern
@catchhanterare som kan hantera undantaget. - Objective-C-körningen hittar inga
@catchhanterare, anroparNSGetUncaughtExceptionHandleroch anropar hanteraren som har installerats av .NET SDK. - Hanteraren för .NET SDK:er konverterar Objective-C-undantaget till ett hanterat undantag och kastar det. Eftersom Objective-C-körningen inte varva ned stacken (gick bara igenom den) är den aktuella ramen samma som när det Objective-C undantaget utlöstes.
Ett annat problem uppstår här, eftersom Mono-runtime inte vet hur man avvecklar Objective-C frames korrekt.
När .NET SDK:ernas oupphörliga Objective-C undantagsåteranrop anropas är stacken så här:
0 TestApp 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 TestApp 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]
Här är de enda hanterade ramarna bildrutorna 8–10, men det hanterade undantaget genereras i bildruta 0. Det innebär att Mono runtime måste avtäcka de inbyggda ramarna 0–7, vilket medför ett problem liknande det som diskuterades ovan: även om Mono runtime avtäcker de inbyggda ramarna, kommer den inte att köra några Objective-C @catch eller @finally satser.
Kodexempel:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
@finally Och satsen kommer inte att köras eftersom Mono-körningen som varvar ned den här ramen inte känner till den.
En variant av detta är att utlösa ett hanterat undantag i hanterad kod och sedan avveckla genom inbyggda ramar för att komma till den första hanterade catch satsen.
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.");
}
}
}
Den hanterade UIApplication:Main metoden anropar den inbyggda UIApplicationMain metoden och sedan utför iOS en hel del intern kodkörning innan den hanterade AppDelegate:FinishedLaunching metoden anropas, med fortfarande många interna ramar i stacken när det hanterade undantaget genereras:
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: TestApp mono_jit_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
3: TestApp do_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
4: TestApp mono_runtime_invoke [inlined] mono_runtime_invoke_checked(method=<unavailable>, obj=<unavailable>, params=<unavailable>, error=0xbff45758)
5: TestApp mono_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>)
6: TestApp xamarin_invoke_trampoline(type=<unavailable>, self=<unavailable>, sel="application:didFinishLaunchingWithOptions:", iterator=<unavailable>), context=<unavailable>)
7: TestApp xamarin_arch_trampoline(state=0xbff45ad4)
8: TestApp 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[])
Bildrutorna 0-1 och 27-30 hanteras, medan alla bildrutor däremellan är inbyggda.
Om Mono varvar ned genom dessa ramar körs inga Objective-C @catch eller @finally satser.
Viktigt!
Endast MonoVM-körningen stöder avspolning av interna ramar under hantering av hanterade undantag. CoreCLR-körningen avbryter bara processen när den här situationen uppstår (CoreCLR-körningen används för macOS-appar samt när NativeAOT är aktiverat på valfri plattform).
Scenario 2 – Objective-C undantag kan inte fångas
I följande scenario går det inte att fånga Objective-C undantag med hanterade catch hanterare eftersom Objective-C-undantaget hanterades på ett annat sätt:
- Ett Objective-C-undantag kastas.
- Den Objective-C körningen går över stacken (men varvar inte ned den) och letar efter en intern
@catchhanterare som kan hantera undantaget. - Objective-C-körningen hittar en
@catchhanterare, varvar ner stacken och börjar köra@catchhanteraren.
Det här scenariot finns ofta i .NET för iOS-appar, eftersom det i huvudtråden vanligtvis finns kod som den här:
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
Det innebär att det i huvudtråden aldrig finns något ohanterat Objective-C undantag, och därför anropas aldrig vår motringning som konverterar Objective-C undantag till hanterade undantag.
Det här är också vanligt när du felsöker macOS-appar på en tidigare macOS-version än den senaste, eftersom de flesta gränssnittsobjekt i felsökningsprogrammet försöker hämta egenskaper som motsvarar väljare som inte finns på den körbara plattformen. Om du anropar sådana väljare utlöser du en NSInvalidArgumentException ("Okänd väljare som skickas till ..."), vilket så småningom gör att processen kraschar.
Sammanfattnings nog kan det leda till odefinierade beteenden, till exempel krascher, minnesläckor och andra typer av oförutsägbara (felaktiga) beteenden om du har antingen Objective-C körning eller mono-körningen som de inte är programmerade att hantera.
Tips/Råd
För macOS- och Mac Catalyst-appar (men inte iOS- eller tvOS)-appar är det möjligt att göra så att användargränssnittsloopen inte fångar alla undantag genom att ange NSApplicationCrashOnExceptions egenskapen för appen till true:
var defs = new NSDictionary ((NSString) "NSApplicationCrashOnExceptions", NSNumber.FromBoolean (true));
NSUserDefaults.StandardUserDefaults.RegisterDefaults (defs);
Observera dock att den här egenskapen inte dokumenteras av Apple, så beteendet kan ändras i framtiden.
Lösning
Vi har stöd för att fånga både hanterade och Objective-C undantag på alla hanterade interna gränser och för att konvertera undantaget till den andra typen.
I pseudokod ser det ut ungefär så här:
class MyClass {
[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);
static void DoSomething (NSObject obj)
{
objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}
}
P/Invoke till objc_msgSend fångas upp och den här koden anropas i stället:
void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
@try {
objc_msgSend (obj, sel);
} @catch (NSException *ex) {
convert_to_and_throw_managed_exception (ex);
}
}
Och något liknande görs för det omvända fallet (marshaling managed exceptions to Objective-C exceptions).
I .NET är det alltid aktiverat som standard att konvertera hanterade undantag till Objective-C undantag.
Avsnittet Skapa tidsflaggor förklarar hur du inaktiverar avlyssning när det är standard.
Evenemang
Det finns två händelser som aktiveras när ett undantag har avlyssnats: Runtime.MarshalManagedException och Runtime.MarshalObjectiveCException.
Båda händelserna skickas ett EventArgs-objekt som innehåller det ursprungliga undantaget som utlöstes (Exception-egenskapen) och en ExceptionMode-egenskap som definierar hur undantaget ska hanteras.
Egenskapen ExceptionMode kan ändras i händelsehanteraren för att ändra beteendet enligt all anpassad bearbetning som utförs i hanteraren. Ett exempel skulle vara att avbryta processen om ett visst undantag inträffar.
ExceptionMode Att ändra egenskapen gäller för den enskilda händelsen, det påverkar inte några undantag som fångas upp i framtiden.
Följande lägen är tillgängliga när hanterade undantag konverteras till intern kod:
-
Default: För närvarande är det alltidThrowObjectiveCException. Standardvärdet kan ändras i framtiden. -
UnwindNativeCode: Detta är inte tillgängligt när du använder CoreCLR (CoreCLR stöder inte avspolning av intern kod, det avbryter processen i stället). -
ThrowObjectiveCException: Konvertera det hanterade undantaget till ett Objective-C-undantag och kasta Objective-C-undantaget. Detta är standardvärdet i .NET. -
Abort: Avbryt processen. -
Disable: Inaktiverar undantagsavlyssning. Det är inte meningsfullt att ange det här värdet i händelsehanteraren (när händelsen har aktiverats är det för sent att inaktivera avlyssning av undantaget). I vilket fall som helst, om det anges, fungerar det somUnwindNativeCode.
Följande lägen är tillgängliga när du konverterar Objective-C undantag till hanterad kod:
-
Default: För närvarande är det alltidThrowManagedExceptioni .NET. Standardvärdet kan ändras i framtiden. -
UnwindManagedCode: Det här är det tidigare (odefinierade) beteendet. -
ThrowManagedException: Konvertera undantaget Objective-C till ett hanterat undantag och kasta det hanterade undantaget. Detta är standardvärdet i .NET. -
Abort: Avbryt processen. -
Disable: Inaktiverar undantagsavlyssning. Det är inte meningsfullt att ange det här värdet i händelsehanteraren (när händelsen har aktiverats är det för sent att inaktivera avlyssning av undantaget). I vilket fall som helst, om det anges, fungerar det somUnwindManagedCode.
Så om du vill se varje gång ett undantag hanteras kan du göra följande:
class MyApp {
static void Main (string args[])
{
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);
};
/// ...
}
}
Tips/Råd
Helst bör Objective-C undantag inte förekomma i en applikation som fungerar korrekt (Apple anser att de är betydligt mer exceptionella än hanterade undantag: "undvik att utlösa [Objective-C]-undantag i en app som du skickar till användare"). Ett sätt att åstadkomma detta är att lägga till en händelsehanterare för händelsen Runtime.MarshalObjectiveCException som skulle logga alla marskalkade Objective-C undantag med hjälp av telemetri (för felsökning/lokala versioner kanske också ställer in undantagsläget på "Avbryt" för att identifiera alla sådana undantag för att åtgärda/undvika dem.
Build-Time flaggor
Det är möjligt att ange följande MSBuild-egenskaper, som avgör om undantagsavlyssning är aktiverat och anger den standardåtgärd som ska utföras:
- MarshalManagedExceptionMode: "default", "unwindnativecode", "throwobjectivecexception", "abort", "disable".
- MarshalObjectiveCExceptionMode: "standard", "linda upp hanterad kod", "kast hanterat undantag", "avbryt", "inaktivera".
Exempel:
<PropertyGroup>
<MarshalManagedExceptionMode>throwobjectivecexception</MarshalManagedExceptionMode>
<MarshalObjectiveCExceptionMode>throwmanagedexception</MarshalObjectiveCExceptionMode>
</PropertyGroup>
disableFörutom är dessa värden identiska med de ExceptionMode värden som skickas till händelserna MarshalManagedException och MarshalObjectiveCException.
Alternativet disable inaktiverar mestadels avlyssning, förutom att vi fortfarande hanterar undantag när det inte medför någon extra kostnad för körning. Marshaling-händelserna aktiveras fortfarande för dessa undantag, där standardläget är det för den utförande plattformen.
Begränsningar
Vi fångar bara upp P/Invokes till objc_msgSend serien av funktioner när vi försöker hantera Objective-C undantag. Det innebär att en P/Invoke till en annan C-funktion, som sedan genererar eventuella Objective-C undantag, fortfarande kommer att stöta på det gamla och odefinierade beteendet (detta kan förbättras i framtiden).
Se även
- Undantagsmarskalkning (exempel)
- Hantera fel i [Objective-C]