Xamarin.iOS 및 Xamarin.Mac의 예외 마샬링
관리 코드와 Objective-C 런타임 예외(try/catch/finally 절)를 모두 지원합니다.
그러나 해당 구현은 서로 다릅니다. 즉, 런타임 라이브러리(Mono 런타임 또는 CoreCLR 및 Objective-C 런타임 라이브러리)는 예외를 처리한 다음 다른 언어로 작성된 코드를 실행해야 할 때 문제가 발생합니다.
이 문서에서는 발생할 수 있는 문제 및 가능한 해결에 대해 설명합니다.
또한 다양한 시나리오와 해당 솔루션을 테스트하는 데 사용할 수 있는 샘플 프로젝트 인 예외 마샬링도 포함되어 있습니다.
문제
이 문제는 예외가 throw될 때 발생하며 스택 해제 중에 throw된 예외 유형과 일치하지 않는 프레임이 발견됩니다.
이 문제의 일반적인 예는 네이티브 API가 예외를 Objective-C throw한 다음 스택 해제 프로세스가 관리되는 프레임에 도달할 때 해당 Objective-C 예외를 처리해야 하는 경우입니다.
레거시 Xamarin 프로젝트(pre-.NET)의 경우 기본 작업은 아무 작업도 수행하지 않는 것입니다.
위의 샘플의 경우 이는 런타임이 관리되는 프레임을 Objective-C 해제하도록 하는 것을 의미합니다. 이 작업은 런타임에서 관리되는 프레임을 Objective-C 해제하는 방법을 모르기 때문에 문제가 됩니다. 예를 들어 해당 프레임에서 절이나 아무 절도 catch
finally
실행하지 않습니다.
끊어진 코드
다음 코드 예제를 생각해보세요.
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
이 코드는 네이티브 코드에서 Objective-C NSInvalidArgumentException을 throw합니다.
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-6Objective-C)을 제대로 해제할 수 없습니다. 스택 해제는 관리되는 프레임을 해제하지만 관리되는 예외 논리(예: catch
'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 하면 처리되지 않은 Objective-C 예외NSSetUncaughtExceptionHandler
( Xamarin.iOS 및 Xamarin.Mac에서 사용)에 대한 알림을 받는 메서드가 있으며, 이때 예외를 관리되는 예외로 변환 Objective-C 하려고 하기 때문입니다.
시나리오
시나리오 1 - 관리형 catch 처리기를 사용하여 예외 catch Objective-C
다음 시나리오에서는 관리 catch
되는 처리기를 사용하여 예외를 catch Objective-C 할 수 있습니다.
- Objective-C 예외가 throw됩니다.
- 런타임은 Objective-C 예외를 처리할 수 있는 네이티브
@catch
처리기를 찾아 스택을 안내하지만 해제하지는 않습니다. - 런타임은 Objective-C 처리기, 호출
NSGetUncaughtExceptionHandler
을 찾을 수@catch
없으며 Xamarin.iOS/Xamarin.Mac에서 설치한 처리기를 호출합니다. - Xamarin.iOS/Xamarin.Mac의 처리기는 예외를 Objective-C 관리되는 예외로 변환하고 throw합니다. 런타임이 스택을 Objective-C 해제하지 않았기 때문에(단지 걸어서) 현재 프레임은 예외가 throw된 위치 Objective-C 와 동일합니다.
Mono 런타임에서 프레임을 제대로 해제 Objective-C 하는 방법을 모르기 때문에 또 다른 문제가 발생합니다.
Xamarin.iOS의 catch 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에서 throw됩니다. 즉, Mono 런타임은 네이티브 프레임 0-7을 해제해야 하므로 위에서 설명한 문제와 동일한 문제가 발생합니다. 단, Mono 런타임은 네이티브 프레임을 해제하지만 어떠한 절이나 절도 Objective-C@catch
@finally
실행하지 않습니다.
코드 예제:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
또한 이 프레임을 @finally
해제하는 Mono 런타임이 해당 프레임에 대해 모르기 때문에 절이 실행되지 않습니다.
이러한 변형은 관리 코드에서 관리되는 예외를 throw한 다음 네이티브 프레임을 해제하여 첫 번째 관리 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
메서드를 호출한 다음, 관리되는 예외가 throw될 때 스택에 여전히 많은 네이티브 프레임을 사용하여 관리 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가 이러한 프레임을 통해 해제하는 경우 no Objective-C@catch
또는 @finally
절이 실행되지 않습니다.
시나리오 2 - 예외를 catch Objective-C 할 수 없음
다음 시나리오에서는 예외가 다른 방식으로 처리되었으므로 관리 catch
되는 처리기를 사용하여 예외를 Objective-C catch Objective-C 할 수 없습니다.
- Objective-C 예외가 throw됩니다.
- 런타임은 Objective-C 예외를 처리할 수 있는 네이티브
@catch
처리기를 찾아 스택을 안내하지만 해제하지는 않습니다. - 런타임은 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 예외를 모두 catch하고 해당 예외를 다른 형식으로 변환하기 위한 지원을 추가했습니다.
의사 코드에서는 다음과 같이 표시됩니다.
[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 비슷한 작업이 수행됩니다.
관리되는 네이티브 경계에서 예외를 catch하는 것은 비용이 들지 않으므로 레거시 Xamarin 프로젝트(pre-.NET)의 경우 기본적으로 항상 사용하도록 설정되지는 않습니다.
- Xamarin.iOS/tvOS: 시뮬레이터에서 예외 가로채기 Objective-C 를 사용할 수 있습니다.
- Xamarin.watchOS: 런타임에서 관리되는 프레임을 해제하면 Objective-C 가비지 수집기가 혼동되고 중단되거나 충돌하기 때문에 모든 경우에 가로채기가 적용됩니다.
- Xamarin.Mac: 디버그 빌드에 대해 예외 차단 Objective-C 이 사용됩니다.
.NET에서 관리되는 예외를 예외로 마샬링하는 Objective-C 것은 항상 기본적으로 사용하도록 설정됩니다.
빌드 시간 플래그 섹션에서는 기본적으로 사용하도록 설정되지 않은 경우 가로채기를 사용하도록 설정하거나 기본값인 경우 가로채기를 사용하지 않도록 설정하는 방법을 설명합니다.
이벤트
예외가 가로채 Runtime.MarshalManagedException
면 발생하는 두 가지 이벤트가 있습니다 Runtime.MarshalObjectiveCException
.
두 이벤트 모두 throw된 EventArgs
원래 예외( Exception
속성)를 포함하는 개체와 ExceptionMode
예외를 마샬링하는 방법을 정의하는 속성을 전달합니다.
ExceptionMode
이벤트 처리기에서 속성을 변경하여 처리기에서 수행된 사용자 지정 처리에 따라 동작을 변경할 수 있습니다. 한 가지 예는 특정 예외가 발생하는 경우 프로세스를 중단하는 것입니다.
ExceptionMode
속성을 변경하면 단일 이벤트에 적용되며 나중에 가로채는 예외에는 영향을 주지 않습니다.
관리되는 예외를 네이티브 코드로 마샬링할 때 사용할 수 있는 모드는 다음과 같습니다.
Default
: 기본값은 플랫폼에 따라 다릅니다. 항상ThrowObjectiveCException
.NET에 있습니다. 레거시 Xamarin 프로젝트의ThrowObjectiveCException
경우 GC가 watchOS(협조 모드)이고UnwindNativeCode
, 그렇지 않으면(iOS/watchOS/ macOS)입니다. 기본값은 나중에 변경될 수 있습니다.UnwindNativeCode
: 이전(정의되지 않은) 동작입니다. 협조 모드에서 GC를 사용할 때는 사용할 수 없습니다(watchOS에서 유일한 옵션이므로 watchOS에서 유효한 옵션은 아님) 또는 CoreCLR을 사용할 때는 사용할 수 없지만 레거시 Xamarin 프로젝트의 다른 모든 플랫폼에 대한 기본 옵션입니다.ThrowObjectiveCException
: 관리되는 예외를 Objective-C 예외로 변환하고 예외를 throw합니다 Objective-C . 이는 레거시 Xamarin 프로젝트의 .NET 및 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 관리되는 예외로 변환하고 관리되는 예외를 throw합니다. 이는 레거시 Xamarin 프로젝트의 .NET 및 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);
};
빌드 시간 플래그
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
옵션은 실행 오버헤드를 추가하지 않을 때 예외를 가로채는 것을 제외하고 대부분 가로채기를 사용하지 않도록 설정합니다. 마샬링 이벤트는 이러한 예외에 대해 계속 발생하며, 기본 모드는 실행 중인 플랫폼의 기본 모드입니다.
제한 사항
예외를 catch Objective-C 하려고 할 때 함수 패밀리에 objc_msgSend
대한 P/Invokes만 가로챌 수 있습니다. 즉, 예외를 throw Objective-C 하는 다른 C 함수에 대한 P/Invoke는 여전히 이전 및 정의되지 않은 동작으로 실행됩니다(나중에 개선될 수 있음).