Skip to content

Commit

Permalink
Propagate C++ exceptions back to C#.
Browse files Browse the repository at this point in the history
  • Loading branch information
kring committed Jan 24, 2025
1 parent 5cc7921 commit b5da340
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 8 deletions.
34 changes: 34 additions & 0 deletions Reinterop~/CSharpReinteropException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.CodeAnalysis;

namespace Reinterop
{
/// <summary>
/// Inserts the "ReinteropException" class into an assembly as it is compiled.
/// This is intended to be used from a RegisterPostInitializationOutput
/// callback on an IIncrementalGenerator.
/// </summary>
internal class CSharpReinteropException
{
public static void Generate(GeneratorExecutionContext context)
{
context.AddSource("ReinteropException", Source);
}

public const string Source =
"""
namespace Reinterop
{
[Reinterop]
internal class ReinteropException : System.Exception
{
public ReinteropException(string message) : base(message) {}
internal static void ExposeToCPP()
{
ReinteropException e = new ReinteropException("message");
}
}
}
""";
}
}
36 changes: 32 additions & 4 deletions Reinterop~/MethodsImplementedInCpp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
var parameterList = parameters.Select(parameter => $"{parameter.InteropType.GetFullyQualifiedName()} {parameter.ParameterName}");
var callParameterList = parameters.Where(parameter => parameter.CallSiteName.Length > 0).Select(parameter => parameter.Type.GetConversionFromInteropType(context, parameter.CallSiteName));

string parameterListString = string.Join(", ", parameterList);
string parameterListString = string.Join(", ", parameterList.Concat(new[] {"void** reinteropException"}));
string callParameterListString = string.Join(", ", callParameterList);

string implementation;
Expand All @@ -320,23 +320,44 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
""";
}

string returnDefault = "";
if (interopReturnType != CppType.Void)
{
if (interopReturnType.Flags.HasFlag(CppTypeFlags.Pointer))
returnDefault = "return nullptr;";
else
returnDefault = $$"""return {{interopReturnType.GetFullyQualifiedName()}}();""";
}

result.CppImplementationInvoker.Functions.Add(new(
Content:
$$"""
#if defined(_WIN32)
__declspec(dllexport)
#endif
{{interopReturnType.GetFullyQualifiedName()}} {{name}}({{parameterListString}}) {
{{GenerationUtility.JoinAndIndent(new[] { getCallTarget }, " ")}}
{{new[] { implementation }.JoinAndIndent(" ")}}
try {
{{GenerationUtility.JoinAndIndent(new[] { getCallTarget }, " ")}}
{{new[] { implementation }.JoinAndIndent(" ")}}
} catch (::DotNet::Reinterop::ReinteropException& e) {
*reinteropException = ::DotNet::Reinterop::ObjectHandle(e.GetDotNetException().GetHandle()).Release();
{{returnDefault}}
} catch (std::exception& e) {
*reinteropException = ::DotNet::Reinterop::ReinteropException(::DotNet::System::String(e.what())).GetHandle().Release();
{{returnDefault}}
} catch (...) {
*reinteropException = ::DotNet::Reinterop::ReinteropException(::DotNet::System::String("An unknown native exception occurred.")).GetHandle().Release();
{{returnDefault}}
}
}
""",
TypeDefinitionsReferenced: new[]
{
wrapperType,
implType,
returnType,
objectHandleType
objectHandleType,
CppReinteropException.GetCppType(context)
}.Concat(parameters.Select(parameter => parameter.Type))
.Concat(parameters.Select(parameter => parameter.InteropType)),
AdditionalIncludes: hasStructRewrite ? new[] { "<utility>" } : null // for std::move
Expand Down Expand Up @@ -375,6 +396,10 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
csInteropReturnType = CSharpType.FromSymbol(context, returnType.Kind == InteropTypeKind.Nullable ? context.Compilation.GetSpecialType(SpecialType.System_Byte) : context.Compilation.GetSpecialType(SpecialType.System_Void));
}

// Add a parameter in which to receive an exception from the C++ side.
CSharpType exceptionPtr = CSharpType.FromSymbol(context, context.Compilation.GetSpecialType(SpecialType.System_IntPtr)).AsPointer();
csParametersInterop = csParametersInterop.Concat(new[] { (Name: "reinteropException", CallName: "&reinteropException", Type: exceptionPtr, IsParams: false) });

List<string> csImplementationLines = new List<string>();
if (hasStructRewrite)
{
Expand All @@ -386,6 +411,8 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
else
csImplementationLines.Add($"var result = {name}({string.Join(", ", csParametersInterop.Select(parameter => parameter.Type.GetConversionToInteropType(parameter.CallName)))});");

csImplementationLines.Add("if (reinteropException != IntPtr.Zero) throw (System.Exception)Reinterop.ObjectHandleUtility.GetObjectAndFreeHandle(reinteropException);");

if (csReturnType.SpecialType != SpecialType.System_Void)
{
if (hasStructRewrite)
Expand Down Expand Up @@ -434,6 +461,7 @@ private static void GenerateMethod(CppGenerationContext context, TypeToGenerate
unsafe
{
{{GenerationUtility.JoinAndIndent(new[] { implementationCheck }, " ")}}
System.IntPtr reinteropException = System.IntPtr.Zero;
{{csImplementationLines.JoinAndIndent(" ")}}
}
}
Expand Down
1 change: 1 addition & 0 deletions Reinterop~/RoslynSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public void Execute(GeneratorExecutionContext context)
CSharpReinteropAttribute.Generate(context);
CSharpReinteropNativeImplementationAttribute.Generate(context);
CSharpObjectHandleUtility.Generate(context);
CSharpReinteropException.Generate(context);

// Create a new Compilation with the CSharpObjectHandleUtility created above.
// Newer versions of Roslyn make this easy, but not the one in Unity.
Expand Down
3 changes: 2 additions & 1 deletion Runtime/TestReinterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace CesiumForUnity
[ReinteropNativeImplementation("CesiumForUnityNative::TestReinteropImpl", "TestReinteropImpl.h", true)]
internal partial class TestReinterop
{
public partial bool CallThrowAnExceptionFromCpp();
public partial bool CallThrowAnExceptionFromCppAndCatchIt();
public partial bool CallThrowAnExceptionFromCppAndDontCatchIt();

public static void ThrowAnException()
{
Expand Down
14 changes: 13 additions & 1 deletion Tests/TestReinterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ public class TestReinterop
public void TestADotNetExceptionCanBeCaughtInCpp()
{
CesiumForUnity.TestReinterop o = new CesiumForUnity.TestReinterop();
Assert.IsTrue(o.CallThrowAnExceptionFromCpp());
Assert.IsTrue(o.CallThrowAnExceptionFromCppAndCatchIt());
}

[Test]
public void TestADotNetExceptionCanPropagateThroughCpp()
{
CesiumForUnity.TestReinterop o = new CesiumForUnity.TestReinterop();
try {
o.CallThrowAnExceptionFromCppAndDontCatchIt();
} catch (System.Exception e)
{
Assert.AreEqual("Test Exception!", e.Message);
}
}
}
8 changes: 7 additions & 1 deletion native~/Runtime/src/TestReinteropImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace CesiumForUnityNative {

bool TestReinteropImpl::CallThrowAnExceptionFromCpp(
bool TestReinteropImpl::CallThrowAnExceptionFromCppAndCatchIt(
const DotNet::CesiumForUnity::TestReinterop& instance) {
try {
instance.ThrowAnException();
Expand All @@ -15,4 +15,10 @@ bool TestReinteropImpl::CallThrowAnExceptionFromCpp(
return false;
}

bool TestReinteropImpl::CallThrowAnExceptionFromCppAndDontCatchIt(
const DotNet::CesiumForUnity::TestReinterop& instance) {
instance.ThrowAnException();
return false;
}

} // namespace CesiumForUnityNative
4 changes: 3 additions & 1 deletion native~/Runtime/src/TestReinteropImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ namespace CesiumForUnityNative {

class TestReinteropImpl {
public:
static bool CallThrowAnExceptionFromCpp(
static bool CallThrowAnExceptionFromCppAndCatchIt(
const DotNet::CesiumForUnity::TestReinterop& instance);
static bool CallThrowAnExceptionFromCppAndDontCatchIt(
const DotNet::CesiumForUnity::TestReinterop& instance);
};

Expand Down

0 comments on commit b5da340

Please sign in to comment.