Search code examples
c#c++multithreadingfunction-pointersclr

C# function pointers causes access violation upon calling from C++


In the following bit of code, I'm using C# function pointers to omit using a delegate type, for faster performance. The code works fine when I use a delegate type marshalled as a function pointer, no exception no issues. However, when changing it from a delegate to a function pointer it sometimes throws an access violation.

Code is modified to remove namespaces and prefixes not relevant to the question

public static class Engine
{
    // Causing issues with sometimes throwing access violation
    [DllImport("engine", CallingConvention = CallingConvention.Cdecl)]
    public static unsafe extern UInt32 EventScheduler_ScheduleLocalEvent(
        int priority, UInt64 delay, delegate* unmanaged[Cdecl]<nint, void> callback, nint obj);

    // Works fine every time, but is much slower seems due to delegate instatiations
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void EventScheduler_ScheduleLocalEvent_callbackDelegate(nint obj);
    [DllImport("engine", CallingConvention = CallingConvention.Cdecl)]
    public static unsafe extern UInt32 EventScheduler_ScheduleLocalEvent(
        int priority,
        UInt64 delay,
        [MarshalAs(UnmanagedType.FunctionPtr)] EventScheduler_ScheduleLocalEvent_callbackDelegate callback,
        nint obj);

}

public static class EventScheduler
{
    public delegate void EventCallback();

    // attribute is only present in the function pointer senario
    [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl ) }, EntryPoint = "ScheduleLocalEvent_EventCallbackFunction")]
    private static void ScheduleLocalEvent_EventCallbackFunction(nint handlePtr)
    {
        Thread.BeginThreadAffinity();
        GCHandle handle = GCHandle.FromIntPtr(handlePtr);
        var cb          = (EventScheduler.EventCallback)handle.Target!;
        cb();
        handle.Free();
        Thread.EndThreadAffinity();
    }
    public static UInt32 ScheduleLocalEvent(int priority, SimulationTime delayTime, EventCallback eventCallback)
    {
        GCHandle handle  = GCHandle.Alloc(eventCallback, GCHandleType.Normal);
        IntPtr handlePtr = GCHandle.ToIntPtr(handle);
        unsafe
        {
            return Engine.EventScheduler_ScheduleLocalEvent(
                priority, delayTime, &ScheduleLocalEvent_EventCallbackFunction, handlePtr);
        }
    }

}

On the C++ side

#define API extern "C" __declspec(dllexport)

API uint32_t EventScheduler_ScheduleLocalEvent(int priority, uint64_t delay, void(__cdecl* callback)(void* obj), void* obj)
{
    ERSAssert(Ers::Core::InsideSubModel());
    Core::SyncManagerBase& syncManager = Ers::Core::GetSyncManagerBase();
    return syncManager.ScheduleEvent(priority, delay, [callback, obj]() { callback(obj); });
}

The following output is given when the program is run.

AWealthOfRows.exe (process 39016) exited with code -1073741819.

With the following output

`' AWealthOfRows.exe' (Win32): Loaded
'C:\Projects\Testing\Enterprise_Resource_Simulator\bindings\CSharp\AWealthOfRows\bin\Debug\net8.0\AWealthOfRows.exe'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Windows\System32\ntdll.dll'. 
'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\kernel32.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\KernelBase.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\user32.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\win32u.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\gdi32.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\gdi32full.dll'. 
'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\msvcp_win.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\ucrtbase.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\shell32.dll'. 
'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\advapi32.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\msvcrt.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\sechost.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\bcrypt.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\rpcrt4.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\imm32.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Program
Files\dotnet\host\fxr\9.0.0-preview.3.24172.9\hostfxr.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\hostpolicy.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\coreclr.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Windows\System32\ole32.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Windows\System32\combase.dll'.
'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\oleaut32.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\bcryptprimitives.dll'.  The thread 41728
has exited with code 0 (0x0). 'AWealthOfRows.exe' (Win32): Loaded
'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Private.CoreLib.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\clrjit.dll'. 
'AWealthOfRows.exe' (CoreCLR: DefaultDomain): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Private.CoreLib.dll'.
'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\kernel.appcore.dll'.  'AWealthOfRows.exe'
(Win32): Loaded
'C:\Projects\Testing\Enterprise_Resource_Simulator\bindings\CSharp\AWealthOfRows\bin\Debug\net8.0\AWealthOfRows.dll'.
The thread 41464 has exited with code 0 (0x0). 'AWealthOfRows.exe'
(CoreCLR: clrhost): Loaded
'C:\Projects\Testing\Enterprise_Resource_Simulator\bindings\CSharp\AWealthOfRows\bin\Debug\net8.0\AWealthOfRows.dll'.
Symbols loaded. 'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Runtime.dll'. 
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Runtime.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Windows\System32\icu.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program Files\Microsoft Visual
Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\HotReload\Microsoft.Extensions.DotNetDeltaApplier.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'c:\program
files\microsoft visual
studio\2022\community\common7\ide\commonextensions\microsoft\hotreload\Microsoft.Extensions.DotNetDeltaApplier.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.IO.Pipes.dll'. 
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.IO.Pipes.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Linq.dll'. 
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Linq.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Collections.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Collections.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Console.dll'. 
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Console.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Threading.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Threading.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Runtime.InteropServices.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Runtime.InteropServices.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Threading.Overlapped.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Threading.Overlapped.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Security.AccessControl.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Security.AccessControl.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Security.Principal.Windows.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Security.Principal.Windows.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Windows\System32\ntmarta.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Security.Claims.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Security.Claims.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Runtime.Loader.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Runtime.Loader.dll'.
'AWealthOfRows.exe' (Win32): Loaded
'C:\Projects\Testing\Enterprise_Resource_Simulator\bindings\CSharp\AWealthOfRows\bin\Debug\net8.0\Ers.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Collections.Concurrent.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded
'C:\Projects\Testing\Enterprise_Resource_Simulator\bindings\CSharp\AWealthOfRows\bin\Debug\net8.0\Ers.dll'.
Symbols loaded. 'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded
'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Collections.Concurrent.dll'.
'AWealthOfRows.exe' (Win32): Loaded
'C:\Projects\Testing\Enterprise_Resource_Simulator\bindings\CSharp\AWealthOfRows\bin\Debug\net8.0\ers-engine.dll'.
Symbols loaded. 'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\setupapi.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\winmm.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\version.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\vcruntime140d.dll'. 
'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\msvcp140d.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Windows\System32\vcruntime140_1d.dll'.  'AWealthOfRows.exe'
(Win32): Loaded 'C:\Windows\System32\ucrtbased.dll'. 
'AWealthOfRows.exe' (Win32): Loaded
'C:\Windows\System32\cfgmgr32.dll'.  'AWealthOfRows.exe' (Win32):
Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Diagnostics.Process.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Diagnostics.Process.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.ComponentModel.Primitives.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.ComponentModel.Primitives.dll'.
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Memory.dll'. 
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Memory.dll'. 
'AWealthOfRows.exe' (Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Text.Encoding.Extensions.dll'.
The thread 20540 has exited with code 0 (0x0). The thread 23312 has
exited with code 0 (0x0). 'AWealthOfRows.exe' (CoreCLR: clrhost):
Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Text.Encoding.Extensions.dll'.
The thread 9224 has exited with code 0 (0x0). 'AWealthOfRows.exe'
(Win32): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Threading.Thread.dll'.
'AWealthOfRows.exe' (CoreCLR: clrhost): Loaded 'C:\Program
Files\dotnet\shared\Microsoft.NETCore.App\8.0.5\System.Threading.Thread.dll'.
Exception thrown at 0x000001F303570860 in AWealthOfRows.exe:
0xC0000005: Access violation reading location 0x000001F30385FB32. The
Common Language Runtime cannot stop at this exception. Common causes
include: incorrect COM interop marshalling and memory corruption. To
investigate further use native-only debugging. Unhandled exception at
0x000001F303570860 in AWealthOfRows.exe: 0xC0000005: Access violation
reading location 0x000001F30385FB32. The Common Language Runtime
cannot stop at this exception. Common causes include: incorrect COM
interop marshalling and memory corruption. To investigate further use
native-only debugging. The thread '.NET Counter Poller' (35516) has
exited with code 3221225477 (0xc0000005). The thread '.NET TP Gate'
(24688) has exited with code 3221225477 (0xc0000005). The thread 33416
has exited with code 3221225477 (0xc0000005). The thread '.NET
Finalizer' (38504) has exited with code 3221225477 (0xc0000005). The
thread '.NET EventPipe' (23352) has exited with code 3221225477
(0xc0000005). The thread 36944 has exited with code 3221225477
(0xc0000005). The thread 30968 has exited with code 3221225477
(0xc0000005). The thread 3560 has exited with code 3221225477
(0xc0000005). The thread 41012 has exited with code 3221225477
(0xc0000005). The thread 13568 has exited with code 3221225477
(0xc0000005). The thread 22992 has exited with code 3221225477
(0xc0000005). The thread '.NET TP Worker' (38436) has exited with code
3221225477 (0xc0000005). The thread 41284 has exited with code
3221225477 (0xc0000005). The thread 40972 has exited with code
3221225477 (0xc0000005). The thread 40980 has exited with code
3221225477 (0xc0000005). The thread 41972 has exited with code
3221225477 (0xc0000005). The thread 39600 has exited with code
3221225477 (0xc0000005). The thread 41096 has exited with code
3221225477 (0xc0000005). The thread 41968 has exited with code
3221225477 (0xc0000005). The thread 7472 has exited with code
3221225477 (0xc0000005). The thread '.NET ThreadPool IO' (36056) has
exited with code 3221225477 (0xc0000005). The thread 16184 has exited
with code 3221225477 (0xc0000005). The thread 11736 has exited with
code 3221225477 (0xc0000005). The thread '.NET TP Worker' (33156) has
exited with code 3221225477 (0xc0000005). The thread '.NET TP Worker'
(5368) has exited with code 3221225477 (0xc0000005). The thread '.NET
ThreadPool IO' (22304) has exited with code 3221225477 (0xc0000005).
The thread '.NET TP Worker' (42112) has exited with code 3221225477
(0xc0000005). The thread 40516 has exited with code 3221225477
(0xc0000005). The thread '.NET Tiered Compilation Worker' (2424) has
exited with code 3221225477 (0xc0000005). The program '[39016]
AWealthOfRows.exe' has exited with code 3221225477 (0xc0000005)
'Access violation'. `

I have tried debugging, but when the issue occurs. In this case C++ calls the callback, but before it enters the function's scope the exception is thrown, but immediately returns with an error code.

For the sake of debugging, I've tried switching the calling convention from cdecl to stdcall to see if it has to do with stack corruption.

My assumptions are that:

  • static functions marked with [UnmanagedCallersOnly] have a static address, especially when an entrypoint is defined.
  • Thread.BeginThreadAffinity() followed by Thread.EndThreadAffinity() ensures that an unmanaged thread can successfully start executing managed code.

Good to know:

  • The function is being invoked by a thread not created in C#, there are many threads calling this function

Looking at the documentation for function pointers here I can't seem to figure out if I am not following the standard or if there is just an issue with C#'s implementation.


Solution

  • It turns out, changing the garbage collector modes fixed the issue permanently after testing extensively. This behavior is the same for .net8.0 and the .net9.0(preview) released a week ago.

    The issue is raised here in the official .net repo

    My solution for now is to add these settings to within the PropertyGroup tags in your .csproj file before this is fixed in a later version of .net

    <ServerGarbageCollection>true</ServerGarbageCollection>
    <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
    

    You can read more about them here