You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#函数指针从C++调用时触发访问冲突问题排查

函数指针替代委托与C++交互时偶发访问冲突问题排查

问题背景

为提升跨语言交互性能,在C#中尝试用函数指针替代委托类型与C++模块交互。委托封送版本运行稳定无异常,但改用函数指针后,偶发触发0xC0000005访问冲突错误。

代码片段

C# 代码

public static class Engine
{
    // 偶发访问冲突的函数指针版本
    [DllImport("engine", CallingConvention = CallingConvention.Cdecl)]
    public static unsafe extern UInt32 EventScheduler_ScheduleLocalEvent(
        int priority, UInt64 delay, delegate* unmanaged[Cdecl]<nint, void> callback, nint obj);

    // 运行正常但性能较差的委托版本
    [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();

    // 函数指针版本专用回调函数
    [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);
        }
    }
}

C++ 代码

#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); });
}

错误信息

运行时输出:

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

详细调试日志:

'AWealthOfRows.exe' (Win32): Loaded
...(省略部分加载信息)
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.
...(省略后续线程退出信息)
The program '[39016]
AWealthOfRows.exe' has exited with code 3221225477 (0xc0000005)
'Access violation'.

调试发现:异常触发在C++调用回调函数、进入C#回调作用域之前。曾尝试切换调用约定(cdecl/stdcall),未解决问题。

排查假设与已知条件

假设

  • 标记[UnmanagedCallersOnly]的静态函数地址稳定,尤其指定EntryPoint后
  • Thread.BeginThreadAffinity()/Thread.EndThreadAffinity()可保证非托管线程正常执行托管代码

已知条件

  • 调用该函数的线程为非C#创建的外部线程,且线程数量较多

问题原因分析

核心问题在于GCHandle生命周期管理错误:

  1. 委托版本中,CLR会自动维护委托的引用计数,确保非托管端不再使用前不会被GC回收;而函数指针版本完全依赖手动管理GCHandle。
  2. 当前代码在回调执行完成后立即调用handle.Free(),若C++端存在重复调度同一事件(如逻辑错误导致回调被多次触发),第二次调用时会访问已释放的GCHandle内存,触发访问冲突。
  3. 线程亲和性并非问题根源,Thread.BeginThreadAffinity()仅用于绑定托管代码到当前OS线程,与内存访问错误无关。

解决方案

1. 修正GCHandle释放时机

确保GCHandle在回调所有可能的调用完成后再释放,而非回调内部立即释放:

  • 修改C#回调函数,移除内部的handle.Free():
    [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
    private static void ScheduleLocalEvent_EventCallbackFunction(nint handlePtr)
    {
        Thread.BeginThreadAffinity();
        GCHandle handle = GCHandle.FromIntPtr(handlePtr);
        var cb = (EventScheduler.EventCallback)handle.Target!;
        try
        {
            cb();
        }
        finally
        {
            Thread.EndThreadAffinity();
        }
    }
    
  • 添加专门的GCHandle释放函数,让C++端在事件彻底完成后调用:
    [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
    private static void ReleaseEventCallbackHandle(nint handlePtr)
    {
        GCHandle handle = GCHandle.FromIntPtr(handlePtr);
        handle.Free();
    }
    
  • 修改C++导出函数,增加释放回调参数,并在事件完成后调用:
    API uint32_t EventScheduler_ScheduleLocalEvent(int priority, uint64_t delay, 
        void(__cdecl* callback)(void* obj), void(__cdecl* releaseHandle)(void* obj), void* obj)
    {
        ERSAssert(Ers::Core::InsideSubModel());
        Core::SyncManagerBase& syncManager = Ers::Core::GetSyncManagerBase();
        return syncManager.ScheduleEvent(priority, delay, [callback, releaseHandle, obj]() { 
            callback(obj); 
            releaseHandle(obj); // 事件完成后释放GCHandle
        });
    }
    
  • 更新C#调用逻辑,传递释放函数指针:
    private static unsafe delegate* unmanaged[Cdecl]<nint, void> _releasePtr = &ReleaseEventCallbackHandle;
    private static unsafe delegate* unmanaged[Cdecl]<nint, void> _callbackPtr = &ScheduleLocalEvent_EventCallbackFunction;
    
    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, _callbackPtr, _releasePtr, handlePtr);
        }
    }
    

2. 验证C++事件调度逻辑

排查C++端SyncManagerBase::ScheduleEvent是否存在重复触发同一事件的逻辑错误,确保每个事件仅被回调一次。

3. 缓存函数指针地址

虽然[UnmanagedCallersOnly]静态函数地址理论上稳定,但可提前缓存指针地址,避免极端情况下的地址变化:

private static unsafe delegate* unmanaged[Cdecl]<nint, void> _callbackPtr = &ScheduleLocalEvent_EventCallbackFunction;

总结

函数指针版本的性能优势建立在手动内存管理的基础上,必须严格保证GCHandle的存活时间覆盖回调的所有调用场景。委托版本的稳定性源于CLR自动管理引用计数,而函数指针版本需要开发者完全掌控资源生命周期。

内容的提问来源于stack exchange,提问作者Mark Oostveen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 20:17:31