Xamarin.iOS と Xamarin.Mac での例外マーシャリング

マネージド コードと Objective-C ランタイム例外 (try/catch/finally 句) の両方がサポートされています。

ただし、実装は異なります。つまり、ランタイム ライブラリ (Mono ランタイムまたは CoreCLR と Objective-C ランタイム ライブラリ) には、例外を処理し、他の言語で記述されたコードを実行する必要がある場合に問題があります。

このドキュメントでは、発生する可能性がある問題と考えられる解決策について説明します。

また、さまざまなシナリオとそのソリューションをテストするために使用できるサンプル プロジェクトである 例外マーシャリングも含まれています。

問題

この問題は、例外がスローされ、スタック アンワインド中に、スローされた例外の種類と一致しないフレームが発生したときに発生します。

この問題の一般的な例は、ネイティブ API が例外を Objective-C スローし、スタック アンワインド プロセスが Objective-C マネージド フレームに到達したときに何らかの方法でその例外を処理する必要がある場合です。

従来の Xamarin プロジェクト (pre-.NET) の場合、既定のアクションでは何も実行されません。 上記のサンプルでは、これはランタイムがマネージド フレームを Objective-C アンワインドすることを意味します。 ランタイムはマネージド フレームをObjective-Cアンワインドする方法を知らないため、このアクションは問題です。たとえば、そのフレームで 句や finally 句は実行catchされません。

壊れたコード

次のコード例について考えてみます。

var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero); 

このコードは、ネイティブ コードで Objective-C NSInvalidArgumentException をスローします。

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

スタック トレースは次のようになります。

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 ()

フレーム 0 から 3 はネイティブ フレームであり、ランタイムのスタック アンワインダーはそれらのフレームを Objective-C アンワインド できます 。 特に、任意 Objective-C@catch の 句または @finally 句を実行します。

ただし、Objective-Cスタック アンワインダーはマネージド フレーム (フレーム 4 から 6) Objective-C を適切にアンワインドできません。スタック アンワインダーはマネージド フレームをアンワインドしますが、マネージド例外ロジック (または 'finally 句などcatch) は実行しません。

つまり、通常、次の方法でこれらの例外をキャッチすることはできません。

try {
    var dict = new NSMutableDictionary ();
    dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
    Console.WriteLine (ex);
} finally {
    Console.WriteLine ("finally");
}

これは、スタック アンワインダーが Objective-C マネージド catch 句について認識せず、 句も実行されないためです finally

上記のコード サンプルが有効な場合は、ハンドルされない例外の通知を受けObjective-C取るメソッドがあり、NSSetUncaughtExceptionHandlerXamarin.iOS と Xamarin.Mac で使用され、その時点で例外をObjective-Cマネージド例外に変換しようとするためです。Objective-C

シナリオ

シナリオ 1 - Objective-C マネージド catch ハンドラーを使用して例外をキャッチする

次のシナリオでは、マネージド catch ハンドラーを使用して例外をキャッチObjective-Cできます。

  1. 例外 Objective-C がスローされます。
  2. ランタイムは Objective-C スタックをウォークし (ただしアンワインドしません)、例外を処理できるネイティブ @catch ハンドラーを探します。
  3. ランタイムは Objective-C ハンドラーを見つけ @catch ず、 を呼び出 NSGetUncaughtExceptionHandlerし、Xamarin.iOS/Xamarin.Mac によってインストールされたハンドラーを呼び出します。
  4. Xamarin.iOS/Xamarin.Mac のハンドラーは、例外を Objective-C マネージド例外に変換してスローします。 ランタイムはスタックを Objective-C アンワインドしなかったため (ウォークのみ)、現在のフレームは例外がスローされた場所 Objective-C と同じです。

Mono ランタイムではフレームを適切にアンワインド Objective-C する方法が認識されないため、ここでもう 1 つの問題が発生します。

Xamarin.iOS のキャッチ Objective-C されない例外コールバックが呼び出されると、スタックは次のようになります。

 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]

ここでは、マネージド フレームはフレーム 8 から 10 のみですが、マネージド例外はフレーム 0 でスローされます。 つまり、Mono ランタイムはネイティブ フレーム 0 から 7 をアンワインドする必要があります。これにより、上記の問題と同等の問題が発生します。Mono ランタイムはネイティブ フレームをアンワインドしますが、または @finally 句は実行Objective-C@catchしません。

コード例:

-(id) setObject: (id) object forKey: (id) key
{
    @try {
        if (key == nil)
            [NSException raise: @"NSInvalidArgumentException"];
    } @finally {
        NSLog (@"This won't be executed");
    }
}

また、このフレームを @finally アンワインドする Mono ランタイムでは認識されないため、 句は実行されません。

この違いは、マネージド コードでマネージド例外をスローし、ネイティブ フレームを使用してアンワインドして最初のマネージド 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.");
        }
    }
}

マネージド UIApplication:Main メソッドはネイティブ UIApplicationMain メソッドを呼び出し、最終的にマネージド AppDelegate:FinishedLaunching メソッドを呼び出す前に、iOS は多くのネイティブ コード実行を実行します。マネージド例外がスローされても、スタック上のネイティブ フレームは多数あります。

 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[])

フレーム 0 から 1 と 27 から 30 は管理されますが、その間のすべてのフレームはネイティブです。 Mono がこれらのフレームを通じてアンワインドする場合、 Objective-C@catch 句または @finally 句は実行されません。

シナリオ 2 - 例外をキャッチ Objective-C できない

次のシナリオでは、例外が別の方法で処理されたため、マネージド catch ハンドラーを使用して例外をObjective-CキャッチObjective-Cすることはできません

  1. 例外 Objective-C がスローされます。
  2. ランタイムは Objective-C スタックをウォークし (ただしアンワインドしません)、例外を処理できるネイティブ @catch ハンドラーを探します。
  3. ランタイムは Objective-C ハンドラーを @catch 検索し、スタックをアンワインドして、ハンドラーの実行を @catch 開始します。

このシナリオは Xamarin.iOS アプリでよく見られます。メイン スレッドには通常、次のようなコードがあるためです。

void UIApplicationMain ()
{
    @try {
        while (true) {
            ExecuteRunLoop ();
        }
    } @catch (NSException *ex) {
        NSLog (@"An unhandled exception occured: %@", exc);
        abort ();
    }
}

つまり、メイン スレッドでは実際にはハンドルObjective-Cされない例外は発生しません。したがって、例外をマネージド例外に変換Objective-Cするコールバックは呼び出されません。

これは、Xamarin.Mac がサポートする以前の macOS バージョンで Xamarin.Mac アプリをデバッグする場合にも一般的です。デバッガーでほとんどの UI オブジェクトを検査すると、実行中のプラットフォームに存在しないセレクターに対応するプロパティがフェッチされます (Xamarin.Mac にはより高い macOS バージョンのサポートが含まれているため)。 このようなセレクターを呼び出すと、("認識されないセレクターが ...." に送信されます) がスロー NSInvalidArgumentException され、最終的にプロセスがクラッシュします。

要約すると、 Objective-C ランタイムまたは Mono ランタイム アンワインド フレームを処理するようにプログラムされていないと、クラッシュ、メモリ リーク、その他の種類の予期しない (誤った) 動作など、未定義の動作が発生する可能性があります。

解決策

Xamarin.iOS 10 と Xamarin.Mac 2.10 では、マネージドネイティブ境界でマネージドと Objective-C 例外の両方をキャッチし、その例外を他の種類に変換するためのサポートを追加しました。

擬似コードでは、次のようになります。

[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);

static void DoSomething (NSObject obj)
{
    objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}

objc_msgSendへの P/Invoke がインターセプトされ、代わりにこのコードが呼び出されます。

void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
    @try {
        objc_msgSend (obj, sel);
    } @catch (NSException *ex) {
        convert_to_and_throw_managed_exception (ex);
    }
}

また、逆のケース (マネージド例外を例外にマーシャリングする) に対して同様の処理が Objective-C 行われます。

マネージド ネイティブ境界で例外をキャッチすることはコストフリーではないので、従来の Xamarin プロジェクト (pre-.NET) の場合、既定では常に有効になっているとは限りません。

  • Xamarin.iOS/tvOS: シミュレーターで例外の Objective-C インターセプトが有効になります。
  • Xamarin.watchOS: ランタイム アンワインド マネージド フレームを使用すると Objective-C ガベージ コレクターが混乱し、ハングまたはクラッシュするため、すべてのケースでインターセプトが適用されます。
  • Xamarin.Mac: デバッグ ビルドで例外の Objective-C インターセプトが有効になります。

.NET では、マネージド例外を例外にマーシャリングすることは、常に Objective-C 既定で有効になっています。

[ ビルド時フラグ ] セクションでは、既定で有効になっていない場合にインターセプトを有効にする方法 (または、既定の場合はインターセプトを無効にする) 方法について説明します。

イベント

例外がインターセプト Runtime.MarshalManagedException されると発生するイベントは、 と Runtime.MarshalObjectiveCExceptionの 2 つあります。

両方のイベントには、スローされた EventArgs 元の例外 ( Exception プロパティ) を含むオブジェクトと ExceptionMode 、例外のマーシャリング方法を定義するプロパティが渡されます。

ExceptionModeイベント ハンドラーで プロパティを変更して、ハンドラーで実行されるカスタム処理に従って動作を変更できます。 1 つの例として、特定の例外が発生した場合にプロセスを中止します。

プロパティの変更は ExceptionMode 単一のイベントに適用され、今後インターセプトされる例外には影響しません。

マネージド例外をネイティブ コードにマーシャリングする場合は、次のモードを使用できます。

  • Default: 既定値はプラットフォームによって異なります。 これは常に ThrowObjectiveCException .NET にあります。 従来の Xamarin プロジェクトの場合は、 ThrowObjectiveCException GC が協調モード (watchOS) UnwindNativeCode 、それ以外の場合は (iOS/watchOS/macOS) です。 既定値は将来変更される可能性があります。
  • UnwindNativeCode: これは以前の (未定義の) 動作です。 これは、協調モードで GC を使用する場合 (watchOS では唯一のオプションであるため、watchOS では有効なオプションではありません)、CoreCLR を使用する場合は使用できませんが、従来の Xamarin プロジェクト内の他のすべてのプラットフォームでは既定のオプションです。
  • ThrowObjectiveCException: マネージド例外を例外に Objective-C 変換し、例外をスローします Objective-C 。 これは、.NET の既定値であり、従来の Xamarin プロジェクトの watchOS ではです。
  • Abort: プロセスを中止します。
  • Disable: 例外インターセプトを無効にするため、イベント ハンドラーでこの値を設定しても意味がありませんが、イベントが発生すると、無効にするには遅すぎます。 いずれの場合も、設定されている場合は として UnwindNativeCode動作します。

マネージド コードに例外をマーシャリングする場合は、次の Objective-C モードを使用できます。

  • Default: 既定値はプラットフォームによって異なります。 これは常に ThrowManagedException .NET にあります。 従来の Xamarin プロジェクトの場合は、 ThrowManagedException GC が協調モード (watchOS) UnwindManagedCode 、それ以外の場合は (iOS/tvOS/macOS) です。 既定値は将来変更される可能性があります。
  • UnwindManagedCode: これは以前の (未定義の) 動作です。 これは、協調モードで GC を使用する場合 (watchOS で唯一有効な GC モードであるため、watchOS では有効なオプションではありません)、CoreCLR を使用する場合は使用できませんが、従来の Xamarin プロジェクト内の他のすべてのプラットフォームでは既定値です。
  • ThrowManagedException: 例外を Objective-C マネージド例外に変換し、マネージド例外をスローします。 これは、.NET の既定値であり、従来の Xamarin プロジェクトの watchOS ではです。
  • Abort: プロセスを中止します。
  • Disable: 例外インターセプトを無効にするため、イベント ハンドラーでこの値を設定しても意味がありませんが、イベントが発生すると、無効にするには遅すぎます。 いずれの場合も、設定されている場合は、プロセスが中止されます。

そのため、例外がマーシャリングされるたびに表示するには、次の操作を行います。

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);
};

Build-Time フラグ

次のオプションを mtouch (Xamarin.iOS アプリの場合) と mmp (Xamarin.Mac アプリの場合) に渡すことができます。これにより、例外インターセプトが有効になっているかどうかを判断し、発生する既定のアクションを設定できます。

  • --marshal-managed-exceptions=

    • default
    • unwindnativecode
    • throwobjectivecexception
    • abort
    • disable
  • --marshal-objectivec-exceptions=

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

disable除き、これらの値は、 イベントと イベントにMarshalManagedException渡される値とMarshalObjectiveCException同じですExceptionMode

オプションはdisable、ほとんどの場合、インターセプトを無効にします。ただし、実行オーバーヘッドが追加されない場合でも例外をインターセプトします。 これらの例外に対してマーシャリング イベントは引き続き発生し、既定のモードは実行中のプラットフォームの既定のモードです。

制限事項

例外をキャッチObjective-Cする場合にのみ、関数ファミリにobjc_msgSend対して P/Invokes をインターセプトします。 つまり、別の C 関数に対する P/Invoke は例外を Objective-C スローしますが、引き続き古い未定義の動作に実行されます (これは今後改善される可能性があります)。