Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise the AppDomain.UnhandledException event from Objective-C bridge #102730

Open
rolfbjarne opened this issue May 27, 2024 · 10 comments
Open

Raise the AppDomain.UnhandledException event from Objective-C bridge #102730

rolfbjarne opened this issue May 27, 2024 · 10 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.InteropServices
Milestone

Comments

@rolfbjarne
Copy link
Member

rolfbjarne commented May 27, 2024

Background and motivation

The ObjectiveCMarshal.Initialize method takes a callback that's called if an exception is supposed to be thrown when returning from managed code to native code.

This works fine, but if we determine that the exception is truly unhandled, there doesn't seem to be a way for us to invoke the AppDomain.UnhandledException event before terminating the process.

This event is often used by customers when logging app crashes, so the fact that we don't always raise it becomes a problem (xamarin/xamarin-macios#15252). We have other events (our own) we raise, but if we could raise the event everybody else uses that would be preferrable.

Note that we'd need the same for Mono. Looks like we can use mono_unhandled_exception for Mono.

API Proposal

namespace System.Runtime.InteropServices.ObjectiveC;

public static class ObjectiveCMarshal 
{
	// This raises the AppDomain.UnhandledException event
    public void OnUnhandledException (Exception exception);
}

API Usage

The method would be called when we detect that there are no Objective-C exception handlers nor any managed exception handlers on the stack.

Currently this happens:

  • The ObjectiveCMarshal unhandled exception propagation handler is called.
  • We convert the managed exception into an Objective-C exception and throw the Objective-C exception.
  • Objective-C doesn't find any Objective-C exception handlers, and calls our Objective-C unhandled exception callback
  • In our Objective-C unhandled exception callback, we abort the process. This is where we'd like to raise the AppDomain.UnhandledException event, before aborting. Note that we still have access to the original managed exception at this point.

It would also be nice if we could tell the debugger about these unhandled exception as well, but I don't have any idea how that would look.

Alternative Designs

No response

Risks

No response

@rolfbjarne rolfbjarne added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label May 27, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label May 27, 2024
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label May 27, 2024
@rolfbjarne
Copy link
Member Author

CC @AaronRobinsonMSFT

@teo-tsirpanis teo-tsirpanis added area-System.Runtime.InteropServices and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels May 27, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

@AaronRobinsonMSFT
Copy link
Member

@rolfbjarne Would #101560 help with this issue? If not, is this a .NET 9 ask?

@rolfbjarne
Copy link
Member Author

@rolfbjarne Would #101560 help with this issue?

No, I don't think so, that's a proposal to support ignoring unhandled exceptions, while our exceptions can't be ignored, the process will terminate soon after no matter what the handler does.

The fatal error handler would eventually be called, but getting the unhandled managed exception (to log it for instance) after the error handled has been called due to a SIGABRT wouldn't be easy, so it's not really a good alternative.

If not, is this a .NET 9 ask?

Yes, please!

@janvorli
Copy link
Member

Maybe that in the Objective-C unhandled exception callback, we can unwind to the first managed frame and rethrow the managed exception from there. That way, the exception that was not handled by the Objective-C stuff would continue flowing into the managed caller of that code and possibly get even handled in the managed code or hit the standard unhandled exception path that would also raise the AppDomain.UnhandledException event.
There are some unknowns to me though that can complicate things. For example, I am not sure if objective-C frames can be unwound using the libunwind.

@rolfbjarne
Copy link
Member Author

Maybe that in the Objective-C unhandled exception callback, we can unwind to the first managed frame and rethrow the managed exception from there.

There isn't necessarily any managed frames up the stack (in fact probably won't be any).

Also note that there may be both managed and Objective-C exception handled up the stack... because Apple does something like this sometimes:

void someFunction ()
{
    @try {
        callUserCode ();
    } @catch (NSException *ex) {
        terminateDueToUnhandledObjectiveCException (ex);
    }
}

Stack trace: https://gist.github.com/rolfbjarne/5ab15ce73bc6476d4cee83cff387dd30

frame 0 is our unhandled Objective-C exception callback.
frame 6 is (probably) something like the someFunction from above.
frame 20 has an Objective-C catch clause.
frames 26-23 are managed frames.

In any case, it doesn't sound like a good idea to try to recover and continue executing from this stack trace:

    frame #2: 0x000000018005fbe4 libobjc.A.dylib`_objc_terminate() + 112
    frame #3: 0x000000018028e150 libc++abi.dylib`std::__terminate(void (*)()) + 12
    frame #4: 0x000000018028e100 libc++abi.dylib`std::terminate() + 52
    frame #5: 0x00000001800842d4 libobjc.A.dylib`objc_terminate + 12

That way, the exception that was not handled by the Objective-C stuff would continue flowing into the managed caller of that code and possibly get even handled in the managed code or hit the standard unhandled exception path that would also raise the AppDomain.UnhandledException event.

We end up with a kind of chicken-and-egg problem here: exceptions are only truly unhandled if there are neither Objective-C exception handlers nor managed exception handlers up the stack.

  1. We convert Objective-C exceptions into managed exceptions after calling Objective-C selectors that throw Objective-C exceptions.
  2. We convert managed exceptions into Objective-C exceptions when returning to native code and a managed exception occurred.

This means that (almost) all unhandled managed exceptions will actually be converted into Objective-C exceptions at some point.

We reach the end when the there are no Objective-C exception handlers on the stack, and the unhandled Objective-C exception callback is called. At this point we can inspect the Objective-C exception, and determine whether it originated from a managed exception (and this is where we want to call AppDomain.UnhandledException).

@janvorli
Copy link
Member

Ok, I can see I have not considered the cases when objective-C thread is reverse-pinvoking managed code or when there is an objective-C host. What I was talking about was a case when managed code app would call objective-C method.
I have also not expected that objective C would call its unhandled exception handler down the call chain from the std::terminate in what I assume is a termination handler. I am not sure if it would be safe to execute any managed code (the AppDomain.UnhandledException) on that thread from that state at all.

rolfbjarne added a commit to xamarin/xamarin-macios that referenced this issue May 29, 2024
…xception. (#20656)

Call mono_unhandled_exception to raise AppDomain.UnhandledException when
managed exceptions are unhandled.

Partial fix for #15252 (for MonoVM, still pending for CoreCLR, which
needs dotnet/runtime#102730 fixed first).
@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jun 5, 2024

@VSadov Do you have any thoughts on adding an additional API to #101560? The issue here is attempting to trigger the runtime's UnhandledException infrastructure on demand. It seems like it fits nicely with the aforementioned API. The API itself is likely uninteresting, but the semantics definitely are.

namespace System.Runtime.ExceptionServices
{
    public static class ExceptionHandling
    {
        /// <summary>
        /// Triggers the runtime's UnhandledException infrastructure.
        /// </summary>
        /// <param name="exception">Exception to trigger</exception>
        /// <remarks>
        /// QUESTION: What happens on return?
        /// </remarks>
        public static void TriggerUnhandledExceptionHandlers(Exception exception);
    }
}

/cc @jkotas

@stephentoub stephentoub added this to the Future milestone Jul 19, 2024
@jeffschwMSFT jeffschwMSFT removed the untriaged New issue has not been triaged by the area owner label Jul 24, 2024
@rolfbjarne
Copy link
Member Author

@AaronRobinsonMSFT @jkotas any updates on this?

Is there still time to get this in .NET 9?

@jkotas
Copy link
Member

jkotas commented Sep 13, 2024

.NET 9 is done. We should address this scenario as part of #101560 in .NET 10.

@jkotas jkotas modified the milestones: Future, 10.0.0 Sep 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.InteropServices
Projects
Status: No status
Development

No branches or pull requests

7 participants