Skip to content

Commit 735ac52

Browse files
authored
Merge pull request #628 from CesiumGS/delegate-drama
Fix two Reinterop problems with delegates
2 parents d6b5a18 + 47d9ca7 commit 735ac52

File tree

2 files changed

+71
-29
lines changed

2 files changed

+71
-29
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Change Log
22

3+
## ? - ?
4+
5+
##### Fixes :wrench:
6+
7+
- Exceptions thrown by delegates implemented in native code are now correctly propagated back to the managed caller. Previously, this scenario could cause crashes and undefined behavior.
8+
- Delegate wrappers for native functions now use `SafeHandle` to ensure the native function will not be destroyed by the garbage collector while it is running.
9+
310
## v1.19.0 - 2025-11-03
411

512
##### Additions :tada:

Reinterop~/CustomDelegateGenerator.cs

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private void GenerateDelegate(CppGenerationContext context, GeneratedResult resu
7272
"""
7373
));
7474

75-
// A a C# delegate type that wraps a std::function, and arrange for
75+
// A C# delegate type that wraps a std::function, and arranges for
7676
// the invoke and dispose to be implemented in C++.
7777
CSharpType csType = CSharpType.FromSymbol(context, item.Type);
7878

@@ -88,7 +88,7 @@ private void GenerateDelegate(CppGenerationContext context, GeneratedResult resu
8888
string disposeCallbackName = $"{csType.GetFullyQualifiedNamespace().Replace(".", "_")}_{item.Type.Name}{genericTypeHash}_DisposeCallback";
8989

9090
var invokeParameters = callbackParameters.Select(p => $"{p.CsType.GetFullyQualifiedName()} {p.Name}");
91-
var invokeInteropParameters = new[] { "IntPtr callbackFunction" }.Concat(callbackParameters.Select(p => $"{p.CsType.AsInteropTypeParameter().GetFullyQualifiedName()} {p.Name}"));
91+
var invokeInteropParameters = new[] { "ImplementationHandle callbackFunction" }.Concat(callbackParameters.Select(p => $"{p.CsType.AsInteropTypeParameter().GetFullyQualifiedName()} {p.Name}"));
9292
var callInvokeInteropParameters = new[] { "_callbackFunction" }.Concat(callbackParameters.Select(p => p.CsType.GetConversionToInteropType(p.Name)));
9393
var csReturnType = CSharpType.FromSymbol(context, invokeMethod.ReturnType);
9494

@@ -109,46 +109,57 @@ private void GenerateDelegate(CppGenerationContext context, GeneratedResult resu
109109
$$"""
110110
private class {{csType.Name}}{{genericTypeHash}}NativeFunction : System.IDisposable
111111
{
112-
private IntPtr _callbackFunction;
113-
114-
public {{csType.Name}}{{genericTypeHash}}NativeFunction(IntPtr callbackFunction)
112+
internal class ImplementationHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
115113
{
116-
_callbackFunction = callbackFunction;
117-
}
114+
public ImplementationHandle(IntPtr nativeImplementation) : base(true)
115+
{
116+
SetHandle(nativeImplementation);
117+
}
118118
119-
~{{csType.Name}}{{genericTypeHash}}NativeFunction()
120-
{
121-
Dispose(false);
119+
[System.Runtime.ConstrainedExecution.ReliabilityContract(System.Runtime.ConstrainedExecution.Consistency.WillNotCorruptState, System.Runtime.ConstrainedExecution.Cer.Success)]
120+
protected override bool ReleaseHandle()
121+
{
122+
{{disposeCallbackName}}(this.handle);
123+
return true;
124+
}
122125
}
123-
124-
public void Dispose()
126+
127+
[System.NonSerialized]
128+
private ImplementationHandle _callbackFunction;
129+
130+
public {{csType.Name}}{{genericTypeHash}}NativeFunction(IntPtr callbackFunction)
125131
{
126-
Dispose(true);
127-
GC.SuppressFinalize(this);
132+
_callbackFunction = new ImplementationHandle(callbackFunction);
128133
}
129134
130-
private void Dispose(bool disposing)
135+
public void Dispose()
131136
{
132-
if (_callbackFunction != IntPtr.Zero)
133-
{
134-
{{disposeCallbackName}}(_callbackFunction);
135-
_callbackFunction = IntPtr.Zero;
136-
}
137+
if (this._callbackFunction != null && !this._callbackFunction.IsInvalid)
138+
this._callbackFunction.Dispose();
139+
this._callbackFunction = null;
137140
}
138141
139-
public {{csReturnType.GetFullyQualifiedName()}} Invoke({{string.Join(", ", invokeParameters)}})
142+
public unsafe {{csReturnType.GetFullyQualifiedName()}} Invoke({{string.Join(", ", invokeParameters)}})
140143
{
141144
if (_callbackFunction == null)
142145
throw new System.ObjectDisposedException("{{csType.Name}}");
143-
144-
{{csResultImplementation}}{{invokeCallbackName}}({{string.Join(", ", callInvokeInteropParameters)}});
145-
{{csReturnImplementation}};
146+
147+
unsafe
148+
{
149+
System.IntPtr reinteropException = System.IntPtr.Zero;
150+
{{csResultImplementation}}{{invokeCallbackName}}({{string.Join(", ", callInvokeInteropParameters)}}, &reinteropException);
151+
if (reinteropException != System.IntPtr.Zero)
152+
{
153+
throw (System.Exception)Reinterop.ObjectHandleUtility.GetObjectAndFreeHandle(reinteropException);
154+
}
155+
{{csReturnImplementation}};
156+
}
146157
}
147158
148159
[System.Runtime.InteropServices.DllImport("{{context.NativeLibraryName}}", CallingConvention=System.Runtime.InteropServices.CallingConvention.Cdecl)]
149160
private static extern void {{disposeCallbackName}}(IntPtr callbackFunction);
150161
[System.Runtime.InteropServices.DllImport("{{context.NativeLibraryName}}", CallingConvention=System.Runtime.InteropServices.CallingConvention.Cdecl)]
151-
private static extern {{csReturnType.AsInteropTypeReturn().GetFullyQualifiedName()}} {{invokeCallbackName}}({{string.Join(", ", invokeInteropParameters)}});
162+
private static unsafe extern {{csReturnType.AsInteropTypeReturn().GetFullyQualifiedName()}} {{invokeCallbackName}}({{string.Join(", ", invokeInteropParameters)}}, IntPtr* reinteropException);
152163
}
153164
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
154165
private unsafe delegate IntPtr {{csBaseName}}Type(IntPtr callbackFunction);
@@ -165,12 +176,19 @@ private void Dispose(bool disposing)
165176
var interopParameters = new[] { (Name: "pCallbackFunction", CsType: CSharpType.FromSymbol(context, context.Compilation.GetSpecialType(SpecialType.System_IntPtr)), Type: CppType.VoidPointer, InteropType: CppType.VoidPointer) }.Concat(callbackParameters);
166177
var callParameters = callbackParameters.Select(p => p.Type.GetConversionFromInteropType(context, p.Name));
167178

179+
CppType interopReturnType = returnType.AsInteropType();
180+
168181
string resultImplementation = "";
169182
string returnImplementation = "return;";
183+
string returnDefault = "return;";
170184
if (invokeMethod.ReturnType.SpecialType != SpecialType.System_Void)
171185
{
172186
resultImplementation = "auto result = ";
173187
returnImplementation = $"return {returnType.GetConversionToInteropType(context, "result")};";
188+
if (interopReturnType.Flags.HasFlag(CppTypeFlags.Pointer))
189+
returnDefault = "return nullptr;";
190+
else
191+
returnDefault = $$"""return {{interopReturnType.GetFullyQualifiedName()}}();""";
174192
}
175193

176194
result.CppImplementationInvoker.Functions.Add(new(
@@ -179,12 +197,29 @@ private void Dispose(bool disposing)
179197
#if defined(_WIN32)
180198
__declspec(dllexport)
181199
#endif
182-
{{returnType.AsInteropType().GetFullyQualifiedName()}} {{invokeCallbackName}}({{string.Join(", ", interopParameters.Select(p => $"{p.InteropType.GetFullyQualifiedName()} {p.Name}"))}}) {
200+
{{interopReturnType.GetFullyQualifiedName()}} {{invokeCallbackName}}({{string.Join(", ", interopParameters.Select(p => $"{p.InteropType.GetFullyQualifiedName()} {p.Name}").Concat(new[] { "void** reinteropException" }))}}) {
183201
auto pFunc = reinterpret_cast<std::function<{{itemType.GetFullyQualifiedName()}}::FunctionSignature>*>(pCallbackFunction);
184-
{{resultImplementation}}(*pFunc)({{string.Join(", ", callParameters)}});
185-
{{returnImplementation}}
202+
try {
203+
{{resultImplementation}}(*pFunc)({{string.Join(", ", callParameters)}});
204+
{{returnImplementation}}
205+
} catch (::DotNet::Reinterop::ReinteropNativeException& e) {
206+
*reinteropException = ::DotNet::Reinterop::ObjectHandle(e.GetDotNetException().GetHandle()).Release();
207+
{{returnDefault}}
208+
} catch (std::exception& e) {
209+
*reinteropException = ::DotNet::Reinterop::ReinteropException(::DotNet::System::String(e.what())).GetHandle().Release();
210+
{{returnDefault}}
211+
} catch (...) {
212+
*reinteropException = ::DotNet::Reinterop::ReinteropException(::DotNet::System::String("An unknown native exception occurred.")).GetHandle().Release();
213+
{{returnDefault}}
214+
}
186215
}
187-
"""));
216+
""",
217+
TypeDefinitionsReferenced: new[]
218+
{
219+
CppReinteropException.GetCppType(context),
220+
CSharpReinteropException.GetCppWrapperType(context),
221+
CppType.FromCSharp(context, context.Compilation.GetSpecialType(SpecialType.System_String))
222+
}));
188223

189224
result.CppImplementationInvoker.Functions.Add(new(
190225
Content:

0 commit comments

Comments
 (0)