Pengecualian Marshaling di Xamarin.iOS dan Xamarin.Mac

Kode terkelola dan Objective-C memiliki dukungan untuk pengecualian runtime (klausul try/catch/finally).

Namun, implementasinya berbeda, yang berarti bahwa pustaka runtime (runtime Mono atau CoreCLR dan Objective-C pustaka runtime) memiliki masalah ketika mereka harus menangani pengecualian dan kemudian menjalankan kode yang ditulis dalam bahasa lain.

Dokumen ini menjelaskan masalah yang dapat terjadi, dan kemungkinan solusinya.

Ini juga termasuk proyek sampel, Exception Marshaling, yang dapat digunakan untuk menguji skenario yang berbeda dan solusinya.

Masalah

Masalah terjadi ketika pengecualian dilemparkan, dan selama melepas tumpukan bingkai ditemui yang tidak cocok dengan jenis pengecualian yang dilemparkan.

Contoh umum dari masalah ini adalah ketika API asli melemparkan Objective-C pengecualian, dan kemudian pengecualian tersebut Objective-C harus ditangani entah bagaimana ketika proses unwinding tumpukan mencapai bingkai terkelola.

Untuk proyek Xamarin warisan (pre-.NET), tindakan defaultnya adalah tidak melakukan apa-apa. Untuk sampel di atas, ini berarti membiarkan Objective-C runtime melepaskan bingkai terkelola. Tindakan ini bermasalah, karena Objective-C runtime tidak tahu cara melepas bingkai terkelola; misalnya, tindakan ini tidak akan menjalankan klausa atau finally apa pun catch dalam bingkai tersebut.

Kode rusak

Perhatikan contoh kode berikut:

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

Kode ini akan melempar Objective-C NSInvalidArgumentException dalam kode asli:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

Dan jejak tumpukan akan menjadi sesuatu seperti ini:

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

Bingkai 0-3 adalah bingkai asli, dan tumpukan unwinder dalam runtime dapat melepas bingkai tersebutObjective-C. Secara khusus, ini akan menjalankan klausul atau @finally apa pun.Objective-C@catch

Namun, Objective-C tumpukan unwinder tidak mampu melepaskan bingkai terkelola dengan benar (bingkai 4-6): Objective-C tumpukan unwinder akan melepas bingkai terkelola, tetapi tidak akan menjalankan logika pengecualian terkelola apa pun (seperti catch atau 'klausa akhirnya).

Yang berarti bahwa biasanya tidak mungkin untuk menangkap pengecualian ini dengan cara berikut:

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

Ini karena Objective-C tumpukan unwinder tidak tahu tentang klausa terkelola catch , dan klausa tidak akan finally dijalankan.

Ketika sampel kode di atas efektif, itu karena Objective-C memiliki metode untuk diberi tahu tentang pengecualian yang tidak tertanganiObjective-C, NSSetUncaughtExceptionHandler, yang digunakan Xamarin.iOS dan Xamarin.Mac, dan pada saat itu mencoba mengonversi pengecualian apa pun Objective-C ke pengecualian terkelola.

Skenario

Skenario 1 - menangkap Objective-C pengecualian dengan handler tangkapan terkelola

Dalam skenario berikut, dimungkinkan untuk menangkap Objective-C pengecualian menggunakan handler terkelola catch :

  1. Pengecualian Objective-C dilemparkan.
  2. Objective-C Runtime berjalan tumpukan (tetapi tidak melepasnya), mencari handler asli @catch yang dapat menangani pengecualian.
  3. Objective-C Runtime tidak menemukan @catch handler, panggilanNSGetUncaughtExceptionHandler, dan memanggil handler yang diinstal oleh Xamarin.iOS/Xamarin.Mac.
  4. Handler Xamarin.iOS/Xamarin.Mac akan mengonversi Objective-C pengecualian menjadi pengecualian terkelola, dan melemparkannya. Objective-C Karena runtime tidak melepas tumpukan (hanya berjalan), bingkai saat ini sama dengan tempat Objective-C pengecualian dilemparkan.

Masalah lain terjadi di sini, karena runtime Mono tidak tahu cara melepas Objective-C bingkai dengan benar.

Ketika panggilan balik pengecualian Xamarin.iOS yang tidak tertangkap Objective-C dipanggil, tumpukannya seperti ini:

 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]

Di sini, satu-satunya bingkai terkelola adalah bingkai 8-10, tetapi pengecualian terkelola dilemparkan dalam bingkai 0. Ini berarti bahwa runtime Mono harus melepas bingkai asli 0-7, yang menyebabkan masalah yang setara dengan masalah yang dibahas di atas: meskipun runtime Mono akan melepas bingkai asli, itu tidak akan menjalankan klausa atau @finally apa pun.Objective-C@catch

Contoh kode:

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

@finally Dan klausul tidak akan dijalankan karena runtime Mono yang melepas kembaran bingkai ini tidak tahu tentang hal itu.

Variasi dari ini adalah untuk melemparkan pengecualian terkelola dalam kode terkelola, lalu melepas penat melalui bingkai asli untuk sampai ke klausul terkelola catch pertama:

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

Metode terkelola UIApplication:Main akan memanggil metode asli UIApplicationMain , dan kemudian iOS akan melakukan banyak eksekusi kode asli sebelum akhirnya memanggil metode terkelola AppDelegate:FinishedLaunching , dengan masih banyak bingkai asli pada tumpukan ketika pengecualian terkelola dilemparkan:

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

Bingkai 0-1 dan 27-30 dikelola, sementara semua bingkai di antaranya asli. Jika Mono melepas lelah melalui bingkai ini, tidak ada Objective-C@catch atau @finally klausa yang akan dijalankan.

Skenario 2 - tidak dapat menangkap Objective-C pengecualian

Dalam skenario berikut, tidak dimungkinkan untuk menangkap Objective-C pengecualian menggunakan handler terkelola catch karena Objective-C pengecualian ditangani dengan cara lain:

  1. Pengecualian Objective-C dilemparkan.
  2. Objective-C Runtime berjalan tumpukan (tetapi tidak melepasnya), mencari handler asli @catch yang dapat menangani pengecualian.
  3. Objective-C Runtime menemukan @catch handler, melepas tumpukan, dan mulai mengeksekusi @catch handler.

Skenario ini biasanya ditemukan di aplikasi Xamarin.iOS, karena pada utas utama biasanya ada kode seperti ini:

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

Ini berarti bahwa pada utas utama tidak pernah ada pengecualian yang tidak tertangani Objective-C , dan dengan demikian panggilan balik kami yang mengonversi Objective-C pengecualian menjadi pengecualian terkelola tidak pernah dipanggil.

Ini juga umum ketika men-debug aplikasi Xamarin.Mac pada versi macOS sebelumnya daripada yang didukung Xamarin.Mac karena memeriksa sebagian besar objek UI dalam debugger akan mencoba mengambil properti yang sesuai dengan pemilih yang tidak ada di platform yang mengeksekusi (karena Xamarin.Mac menyertakan dukungan untuk versi macOS yang lebih tinggi). Memanggil pemilih tersebut akan melempar NSInvalidArgumentException ("Pemilih tidak dikenal yang dikirim ke ..."), yang akhirnya menyebabkan proses crash.

Untuk meringkas, memiliki Objective-C runtime atau bingkai unwind runtime Mono yang tidak diprogram untuk ditangani dapat menyebabkan perilaku yang tidak terdefinisi, seperti crash, kebocoran memori, dan jenis perilaku yang tidak dapat diprediksi (salah).

Solusi

Di Xamarin.iOS 10 dan Xamarin.Mac 2.10, kami telah menambahkan dukungan untuk menangkap batas terkelola dan Objective-C pengecualian pada batas asli terkelola, dan untuk mengonversi pengecualian tersebut ke jenis lainnya.

Dalam pseudo-code, terlihat seperti ini:

[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 untuk objc_msgSend dicegat, dan kode ini dipanggil sebagai gantinya:

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

Dan sesuatu yang serupa dilakukan untuk kasus terbalik (marshaling pengecualian terkelola untuk Objective-C pengecualian).

Menangkap pengecualian pada batas asli terkelola tidak bebas biaya, jadi untuk proyek Xamarin warisan (pre-.NET), itu tidak selalu diaktifkan secara default:

  • Xamarin.iOS/tvOS: intersepsi Objective-C pengecualian diaktifkan di simulator.
  • Xamarin.watchOS: intersepsi diberlakukan dalam semua kasus, karena membiarkan Objective-C runtime melepas bingkai terkelola akan membingungkan pengumpul sampah, dan membuatnya menggantung atau crash.
  • Xamarin.Mac: intersepsi Objective-C pengecualian diaktifkan untuk build debug.

Di .NET, marshaling pengecualian terkelola untuk Objective-C pengecualian selalu diaktifkan secara default.

Bagian Bendera waktu build menjelaskan cara mengaktifkan intersepsi saat tidak diaktifkan secara default (atau menonaktifkan intersepsi saat default).

Acara

Ada dua peristiwa yang dimunculkan setelah pengecualian disadap: Runtime.MarshalManagedException dan Runtime.MarshalObjectiveCException.

Kedua peristiwa dilewatkan objek EventArgs yang berisi pengecualian asli yang dilemparkan ( Exception properti), dan ExceptionMode properti untuk menentukan bagaimana pengecualian harus di-marshal.

Properti ExceptionMode dapat diubah dalam penanganan aktivitas untuk mengubah perilaku sesuai dengan pemrosesan kustom apa pun yang dilakukan di handler. Salah satu contohnya adalah membatalkan proses jika terjadi pengecualian tertentu.

Mengubah ExceptionMode properti berlaku untuk satu peristiwa, itu tidak memengaruhi pengecualian apa pun yang disadap di masa mendatang.

Mode berikut tersedia saat marshaling pengecualian terkelola ke kode asli:

  • Default: Default bervariasi menurut platform. Selalu ada ThrowObjectiveCException di .NET. Untuk proyek Xamarin warisan, itu ThrowObjectiveCException jika GC berada dalam mode kooperatif (watchOS), dan UnwindNativeCode sebaliknya (iOS / watchOS / macOS). Default dapat berubah di masa mendatang.
  • UnwindNativeCode: Ini adalah perilaku sebelumnya (tidak ditentukan). Ini tidak tersedia saat menggunakan GC dalam mode kooperatif (yang merupakan satu-satunya opsi di watchOS; dengan demikian, ini bukan opsi yang valid pada watchOS), atau saat menggunakan CoreCLR, tetapi ini adalah opsi default untuk semua platform lain dalam proyek Xamarin warisan.
  • ThrowObjectiveCException: Konversikan pengecualian terkelola menjadi Objective-C pengecualian dan berikan Objective-C pengecualian. Ini adalah default di .NET dan di watchOS dalam proyek Xamarin warisan.
  • Abort: Batalkan proses.
  • Disable: Menonaktifkan intersepsi pengecualian, sehingga tidak masuk akal untuk mengatur nilai ini di penanganan aktivitas, tetapi setelah peristiwa dinaikkan, terlambat untuk menonaktifkannya. Bagaimanapun, jika diatur, itu akan berperilaku sebagai UnwindNativeCode.

Mode berikut tersedia saat marshaling Objective-C pengecualian untuk kode terkelola:

  • Default: Default bervariasi menurut platform. Selalu ada ThrowManagedException di .NET. Untuk proyek Xamarin warisan, itu ThrowManagedException jika GC berada dalam mode kooperatif (watchOS), dan UnwindManagedCode sebaliknya (iOS / tvOS / macOS). Default dapat berubah di masa mendatang.
  • UnwindManagedCode: Ini adalah perilaku sebelumnya (tidak ditentukan). Ini tidak tersedia saat menggunakan GC dalam mode kooperatif (yang merupakan satu-satunya mode GC yang valid di watchOS; sehingga ini bukan opsi yang valid pada watchOS), atau saat menggunakan CoreCLR, tetapi ini adalah default untuk semua platform lain dalam proyek Xamarin warisan.
  • ThrowManagedException: Konversikan Objective-C pengecualian ke pengecualian terkelola dan berikan pengecualian terkelola. Ini adalah default di .NET dan di watchOS dalam proyek Xamarin warisan.
  • Abort: Batalkan proses.
  • Disable: Menonaktifkan intersepsi pengecualian, sehingga tidak masuk akal untuk mengatur nilai ini di penanganan aktivitas, tetapi setelah peristiwa dinaikkan, sudah terlambat untuk menonaktifkannya. Bagaimanapun jika ditetapkan, proses akan dibatalkan.

Jadi, untuk melihat setiap kali pengecualian dilunakkan, Anda dapat melakukan ini:

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

Bendera Build-Time

Dimungkinkan untuk meneruskan opsi berikut ke mtouch (untuk aplikasi Xamarin.iOS) dan mmp (untuk aplikasi Xamarin.Mac), yang akan menentukan apakah intersepsi pengecualian diaktifkan, dan mengatur tindakan default yang harus terjadi:

  • --marshal-managed-exceptions=

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

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

Kecuali untuk disable, nilai-nilai ini identik ExceptionMode dengan nilai yang diteruskan ke MarshalManagedException peristiwa dan MarshalObjectiveCException .

Opsi ini disable sebagian besar akan menonaktifkan intersepsi, kecuali kami masih akan mencegat pengecualian ketika tidak menambahkan overhead eksekusi apa pun. Peristiwa marshaling masih dinaikkan untuk pengecualian ini, dengan mode default menjadi mode default untuk platform yang mengeksekusi.

Batasan

Kami hanya mencegat P/Invokes ke objc_msgSend keluarga fungsi ketika mencoba menangkap Objective-C pengecualian. Ini berarti bahwa P/Invoke ke fungsi C lain, yang kemudian melemparkan pengecualian apa pun Objective-C , masih akan mengalami perilaku lama dan tidak terdefinisi (ini dapat ditingkatkan di masa depan).