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
Unmanaged Exception Interop on Unix #35017
Comments
I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label. |
CC. @janvorli, @jkotas, @jkoritzinsky |
You can do C++ exception interop manually today by doing try+catch on one side, remap the exception to the other system and throw on the other side. It is a boiler plate code, but it gives you a full control over what you can do. For example, we do that in the crossgen2 compiler here: https://github.com/dotnet/runtime/blob/master/src/coreclr/src/tools/crossgen2/jitinterface/jitinterface.h#L200 I think it would be a fine idea to explore how to reduce this boilerplate code, e.g. by having opt-in mechanism that allows you to attach a custom exception re-mapper to specific PInvokes/reverse PInvokes. Xamarin does it for Objective C exception interop, but it is specific to Objective C and depends on Mono embedding APIs: https://github.com/xamarin/xamarin-macios/blob/master/runtime/EXCEPTIONS.md . |
I'm updating the link of
Talking about propagating exception from unmanaged-managed boundary and removing the need for boilerplate, especially in the case of custom error handlers, I imagined having something like (tentative invented API) public class MyLibrary
{
static ErrorHandlerCallback? _errorHandler;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void ErrorHandlerCallback([MarshalAs(UnmanagedType.LPUTF8Str)]string message);
static MyLibrary()
{
_errorHandler = HandleError;
SetErrorHandler(_errorHandler);
}
public static void Foo()
{
NativeFoo();
}
static void HandleError(string message)
{
// Set managed exception from [UnmanagedFunctionPointer] handler, to be thrown late by the runtime
// NOTE: Tentative invented API
Marshal.SetManagedException(new Exception(message));
}
// NOTE: Tentative invented API
[DllImport("SharedLibrary", CheckExceptionOnExit = true, CallingConvention = CallingConvention.Cdecl)]
static extern void NativeFoo();
[DllImport("SharedLibrary", CallingConvention = CallingConvention.Cdecl)]
static extern void SetErrorHandler(ErrorHandlerCallback callback);
} And externally one would just |
You could make such wrapping yourself easily, there's no need to involve the runtime. There's an issue though that such exception storing wouldn't indicate a failure to native code which means you'd need to return some exit codes there. public static class Test
{
[ThreadStatic]
private static Exception exceptionStorage;
[StackTraceHidden]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RethrowNative()
{
Exception exception = exceptionStorage;
if (exception == null)
return;
exceptionStorage = null;
Rethrow(exception);
[StackTraceHidden]
static void Rethrow(Exception ex) => throw ex;
}
public static void Foo()
{
Bar(&Export);
RethrowNative();
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
static int Export()
{
try
{
Throw();
return 0;
}
catch (Exception ex)
{
exceptionStorage = ex;
return 1;
}
}
private static void Throw() => throw new Exception();
[DllImport("Library", CallingConvention = CallingConvention.Cdecl)]
static extern void Bar(delegate* unmanaged[Cdecl]<int> ptr);
} |
Where would |
Sorry, forget about that |
How would the runtime create this native part? |
@jkotas I'm confused by your question, in the sense the use case I was describing is really the classical .NET managed wrapper on an existing native library, hence native part is 3rd party or user made. Just look at the proposed API in the sample above with this in mind. |
@MichalPetryka You are absolutely correct but that still needs a bit of boilerplate, which is the calling of |
Ah sorry, I looked at your example again. It makes sense now.
If we were to do something here, I expect that it would be via LibraryImport source generator. |
It would be totally fine if interop improvements go first (or exclusively) to |
As in define the thread local in the runtime and expose getter/setter for it? It has miniscule benefit. I think the thread local can be generated by the LibraryImport source generator just fine. |
Ok, but how do you "store" the exception to that thread local if there is no getter/setter? |
The source generated code would store into and load from the thread local variable that is generated by the source generator and that is internal to your assembly, similar to how #35017 (comment) does it. |
I don't think the generator generates anything for |
The reverse PInvoke support in interop marshaling source generator is tracked by #63590. (The examples mentioned here so far were for PInvoke.) |
@jkotas But that example you linked still does some (little) boilerplate to be called after all P/Invoke calls. If you add some runtime support doing exception checking/throwing through the |
No, you would not be. The unmanaged side still needs to have the boiler plate code. There is no good way for the runtime to generate the unmanaged side of the boilerplate in a portable way (#97952 (comment)). |
Ah, the unmanaged part may still need some boiler plate, of course. I now understand that you often tried to grasp something about the unmanaged part, but no, I was always just referring to the boiler plate in the managed part. Again the question is: assuming I will have the right boiler plate in the unmanaged part, with this support in the For example one may imagine that I have function like this: static void HandleError(string message)
{
throw new Excpetion(message);
} Then I assign it to a delegate that gets passed to unamanged part: [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void ErrorHandlerCallback([MarshalAs(UnmanagedType.LPUTF8Str)]string message); Then the runtime/compiler in the trampoline it does something like: static void trampoline_HandleError(string message)
{
try
{
HandleError(message);
}
catch (Exception ex)
{
// ... Store the exception in a thread local variable, or in the stack somewhere
}
} Then I may have a [LibraryImport("SharedLibrary", CheckExceptionOnExit = true)]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
static partial void Foo(); So I can call this |
This assumes that #63590 is implemented. I would expect that there would be some gesture to trigger this behavior. It would not happen by default.
I would expect this to be more general. All you need is that the marshalling source generator calls your method right after the raw PInvoke. There are other patterns you may want to do - call a method right before the raw invoke, keep some state between before and after the PInvoke. It looks very similar to what the argument marshallers do, so we may want to reuse the marshaller concepts here. The new attribute would then point to the name of the marshaller, something like |
Ah, finally I got it! I didn't understand you had user made custom marshalling in mind. Then the Thank you for the replies and clarifications. I hope this feedback is seen as useful/productive, and I hope to see something materialize with this regard for .NET 9/10. |
There are a few issues/documentation that currently indicate unmanaged exception interop doesn't exist on Unix:
The latter of which indicates
However, I find this confusing as the System V ABI (https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI) does define this in
Section 6.2 - Unwind Library Interface
, as does the Itanium C++ ABI (https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html), both of which are used by code and more specifically C/C++ code on Unix platforms.It was mentioned that Windows would be made consistent and the support for propagating exceptions was going to be dropped, but that was latter dropped in favor of compatibility. Given that it should be possible to handle and even propagate exception information across the boundaries and it would make the cross-platform behavior consistent, is it worth taking another look at this?
The text was updated successfully, but these errors were encountered: