Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Le code managé et Objective-C prennent en charge les exceptions d'exécution (clauses try/catch/finally).
Toutefois, leurs implémentations sont différentes, ce qui signifie que les bibliothèques d’exécution (runtimes MonoVM/CoreCLR et les bibliothèques d’exécution Objective-C) présentent des problèmes lorsqu’elles rencontrent des exceptions à partir d’autres runtimes.
Cet article explique les problèmes qui peuvent se produire et les solutions possibles.
Il inclut également un exemple de projet, Exception Marshaling, qui peut être utilisé pour tester différents scénarios et leurs solutions.
Problème
Le problème survient lorsqu'une exception est levée, et qu'au cours du déroulement de la pile, on rencontre une trame qui ne correspond pas au type d'exception levée.
Un exemple classique de ce problème est lorsqu'une API native lève une exception Objective-C, puis que l'exception Objective-C doit être d'une certaine manière gérée lorsque le processus de déroulement de la pile atteint une trame managée.
Dans le passé (pre-.NET), l’action par défaut était de ne rien faire. Pour l’exemple ci-dessus, cela signifie que le runtime Objective-C déroule les trames gérées. Cette action est problématique, car le runtime Objective-C ne parvient pas à dérouler les trames managées ; par exemple, il n’exécute pas de clauses catch ou finally managées, ce qui entraîne des bogues extrêmement difficiles à trouver.
Code rompu
Considérez l’exemple de code suivant :
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Ce code va lancer une exception NSInvalidArgumentException Objective-C dans le code natif :
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
Et la trace de pile sera quelque chose comme ceci :
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 ()
Les images 0-3 sont des images natives, et le déroulement de la pile dans le runtime Objective-C peut décompresser ces images. En particulier, il exécutera toutes les clauses Objective-C @catch ou @finally.
Toutefois, le déroulement de la pile Objective-C n’est pas capable de déroulier correctement les trames managées (images 4-6) : le déroulement de la pile Objective-C déroulant les images managées, mais n’exécute pas de logique d’exception managée (par exemple catch , ou finally des clauses).
Cela signifie qu’il n’est généralement pas possible d’intercepter ces exceptions de la manière suivante :
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
Cela est dû au fait que le dérouleur de pile Objective-C ne connaît pas la clause managée catch, et que la clause finally ne sera pas exécutée.
Lorsque l’exemple de code ci-dessus est efficace, c’est parce que Objective-C a une méthode d’être avertie d’exceptions non gérées Objective-C, NSSetUncaughtExceptionHandlerque les kits SDK .NET utilisent, et à ce stade tente de convertir les exceptions Objective-C en exceptions gérées.
Scénarios
Scénario 1 : intercepter des exceptions Objective-C avec un gestionnaire d'interception géré
Dans le scénario suivant, il est possible de capturer des exceptions Objective-C à l’aide de gestionnaires managés catch :
- Une exception Objective-C est levée.
- Le runtime Objective-C parcourt la pile (mais ne la déroule pas), en recherchant un gestionnaire natif
@catchpour gérer l'exception. - Le runtime Objective-C ne trouve pas de
@catchgestionnaires, appelleNSGetUncaughtExceptionHandleret invoque le gestionnaire installé par le SDK .NET. - Le gestionnaire des kits de développement logiciel (SDK) .NET convertit l’exception Objective-C en exception managée, puis la lève. Étant donné que le runtime Objective-C n’a pas déroulé la pile (uniquement l’a parcourue), l’image actuelle est identique à celle où l’exception Objective-C a été levée.
Un autre problème se produit ici, car le runtime Mono ne sait pas comment déroulage Objective-C images correctement.
Lorsque le rappel d’exception des kits sdk .NET n’a pas été lancé Objective-C est appelé, la pile est semblable à ceci :
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]
Ici, les seules trames managées sont les trames 8 à 10, mais l’exception managée est levée dans la trame 0. Cela signifie que le runtime Mono doit dérouler les cadres natifs 0-7, ce qui provoque un problème équivalent au problème décrit ci-dessus : bien que le runtime Mono déroule les cadres natifs, il n’exécute aucune des clauses Objective-C @catch ou @finally.
Exemple de code :
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
Et la @finally clause ne sera pas exécutée, car le runtime Mono qui déroulait cette trame ne le sait pas.
Une variante de ceci consiste à lever une exception managée dans le code managé, puis à décompresser les trames natives pour accéder à la première clause managée 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.");
}
}
}
La méthode managée UIApplication:Main appelle la méthode native UIApplicationMain , puis iOS effectue un grand nombre d’exécutions de code natif avant d’appeler la méthode managée AppDelegate:FinishedLaunching , avec encore de nombreuses images natives sur la pile lorsque l’exception managée est levée :
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[])
Les images 0-1 et 27-30 sont gérées, tandis que toutes les images entre elles sont natives.
Si Mono déroule ces cadres, aucunes clauses Objective-C @catch ou @finally ne sont exécutées.
Important
Seul le runtime MonoVM prend en charge le déroulement des images natives pendant la gestion des exceptions gérées. Le runtime CoreCLR abandonne simplement le processus lors de cette situation (le runtime CoreCLR est utilisé pour les applications macOS, ainsi que lorsque NativeAOT est activé sur n’importe quelle plateforme).
Scénario 2 : impossible d’intercepter les exceptions Objective-C
Dans le scénario suivant, il n’est pas possible d’intercepter Objective-C exceptions à l’aide de gestionnaires managés catch , car l’exception Objective-C a été gérée d’une autre manière :
- Une exception Objective-C est levée.
- Le runtime Objective-C parcourt la pile (mais ne la déroule pas), afin de trouver un gestionnaire natif
@catchcapable de gérer l'exception. - Le runtime Objective-C recherche un
@catchgestionnaire, décompresse la pile et commence à exécuter le@catchgestionnaire.
Ce scénario est généralement trouvé dans .NET pour les applications iOS, car sur le thread principal, il existe généralement du code comme suit :
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
Cela signifie que sur le thread principal, il n’y a jamais vraiment d’exception Objective-C non gérée, et par conséquent, notre rappel qui convertit Objective-C exceptions en exceptions managées n’est jamais appelé.
Cela est également courant lors du débogage d’applications macOS sur une version antérieure de macOS, car l’inspection de la plupart des objets d’interface utilisateur dans le débogueur tente d’extraire les propriétés qui correspondent aux sélecteurs qui n’existent pas sur la plateforme en cours d’exécution. L’appel de ces sélecteurs lève un NSInvalidArgumentException « sélecteur non reconnu envoyé à ... », ce qui entraîne finalement le blocage du processus.
Pour résumer, le fait d'avoir le runtime Objective-C ou le runtime Mono tenter de désenrouler des trames qu'ils ne sont pas programmés pour gérer peut entraîner des comportements non définis, tels que des crashs, des fuites de mémoire et d'autres types de comportements imprévisibles (incorrects).
Conseil / Astuce
Pour les applications macOS et Mac Catalyst (mais pas iOS ou tvOS), il est possible de configurer la boucle d'interface utilisateur pour qu'elle ne capture pas toutes les exceptions, en définissant la propriété NSApplicationCrashOnExceptions de l'application sur true:
var defs = new NSDictionary ((NSString) "NSApplicationCrashOnExceptions", NSNumber.FromBoolean (true));
NSUserDefaults.StandardUserDefaults.RegisterDefaults (defs);
Toutefois, notez que cette propriété n’est pas documentée par Apple. Par conséquent, le comportement peut changer à l’avenir.
Solution
Nous prenons en charge l'interception des exceptions gérées et Objective-C sur n'importe quelle interface managée-native, afin de convertir cette exception en un autre type.
Dans le pseudo-code, il ressemble à ceci :
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"));
}
}
L'appel à P/Invoke objc_msgSend est intercepté, et ce code est exécuté à la place :
void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
@try {
objc_msgSend (obj, sel);
} @catch (NSException *ex) {
convert_to_and_throw_managed_exception (ex);
}
}
Et quelque chose de similaire est fait pour le cas inverse (transfert des exceptions managées vers les exceptions Objective-C).
Dans .NET, le marshaling des exceptions gérées pour Objective-C exceptions est toujours activé par défaut.
La section Indicateurs de génération explique comment désactiver l’interception lorsqu’il s’agit de la valeur par défaut.
Événements
Il existe deux événements déclenchés une fois qu’une exception est interceptée : Runtime.MarshalManagedException et Runtime.MarshalObjectiveCException.
Les deux événements sont passés à un EventArgs objet qui contient l’exception d’origine levée (propriété Exception ) et une ExceptionMode propriété pour définir la façon dont l’exception doit être marshalée.
La ExceptionMode propriété peut être modifiée dans le gestionnaire d’événements pour modifier le comportement en fonction du traitement personnalisé effectué dans le gestionnaire. Un exemple consisterait à abandonner le processus si une certaine exception se produit.
La modification de la ExceptionMode propriété s’applique à l’événement unique, elle n’affecte aucune exception interceptée ultérieurement.
Les modes suivants sont disponibles lors du marshaling d’exceptions managées vers du code natif :
-
Default: Actuellement, c’est toujoursThrowObjectiveCException. La valeur par défaut peut changer à l’avenir. -
UnwindNativeCode: Cela n’est pas disponible lors de l’utilisation de CoreCLR (CoreCLR ne prend pas en charge le déroulement du code natif, donc il interrompt le processus). -
ThrowObjectiveCException: convertissez l’exception managée en exception Objective-C et lèvez l’exception Objective-C. Il s’agit de la valeur par défaut dans .NET. -
Abort: abandonner le processus. -
Disable: désactive l’interception des exceptions. Il n’est pas judicieux de définir cette valeur dans le gestionnaire d’événements (une fois que l’événement est déclenché, il est trop tard pour désactiver l’interception de l’exception). Dans tous les cas, s’il est défini, il se comporte commeUnwindNativeCode.
Les modes suivants sont disponibles pour le transfert des exceptions Objective-C vers le code géré :
-
Default: Actuellement, c'est toujoursThrowManagedExceptiondans .NET. La valeur par défaut peut changer à l’avenir. -
UnwindManagedCode: Il s’agit du comportement précédent (non défini). -
ThrowManagedException: convertissez l’exception Objective-C en exception managée et lèvez l’exception managée. Il s’agit de la valeur par défaut dans .NET. -
Abort: abandonner le processus. -
Disable: désactive l’interception des exceptions. Il n’est pas judicieux de définir cette valeur dans le gestionnaire d’événements (une fois que l’événement est déclenché, il est trop tard pour désactiver l’interception de l’exception). Dans tous les cas, s’il est défini, il se comporte commeUnwindManagedCode.
Ainsi, pour voir chaque fois qu’une exception est marshalée, vous pouvez procéder comme suit :
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);
};
/// ...
}
}
Conseil / Astuce
Dans l’idéal, les Objective-C exceptions ne doivent pas se produire dans une application bien conçue (Apple les considère comme bien plus exceptionnelles que les exceptions gérées : « évitez de lever des exceptions [Objective-C] dans une application que vous distribuez aux utilisateurs »). Une façon d’y parvenir consisterait à ajouter un gestionnaire d’événements pour l’événement Runtime.MarshalObjectiveCException qui journaliserait toutes les exceptions marshalées Objective-C à l’aide de la télémétrie (pour les builds de débogage/locales peut également définir le mode d’exception sur « Abort » ainsi) pour détecter toutes ces exceptions afin de les corriger/éviter.
Drapeaux de Build-Time
Il est possible de définir les propriétés MSBuild suivantes, qui déterminent si l’interception des exceptions est activée et définissez l’action par défaut qui doit se produire :
- MarshalManagedExceptionMode : « default », « unwindnativecode », « throwobjectivecexception », « abort », « disable ».
- MarshalObjectiveCExceptionMode : « default », « unwindmanagedcode », « throwmanagedexception », « abort », « disable ».
Exemple:
<PropertyGroup>
<MarshalManagedExceptionMode>throwobjectivecexception</MarshalManagedExceptionMode>
<MarshalObjectiveCExceptionMode>throwmanagedexception</MarshalObjectiveCExceptionMode>
</PropertyGroup>
À l’exception de disable, ces valeurs sont identiques aux ExceptionMode valeurs transmises aux événements MarshalManagedException et MarshalObjectiveCException .
L’option disable désactive principalement l’interception, sauf que nous interceptons toujours les exceptions lorsqu’elle n’ajoute aucune surcharge d’exécution. Les événements de marshaling sont toujours déclenchés pour ces exceptions, le mode par défaut étant le mode par défaut de la plateforme en cours d’exécution.
Limites
Nous interceptons uniquement les appels P/Invoke de la famille objc_msgSend de fonctions dans le cadre de la capture des exceptions Objective-C. Cela signifie qu’une fonction P/Invoke vers une autre fonction C, qui lève ensuite n'importe quelle exception Objective-C, rencontrera toujours le comportement ancien et indéfini (cela peut être amélioré à l’avenir).