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

如何在C++中安全处理C#类实例?求非C++/CLI方案

C#调用C++函数安全传递类实例(只读场景)

问题核心

需要从C#调用C函数并传入C#类实例,C仅做只读处理;此前用GCHandle.Alloc锁定结构体可行,但锁定引用类型(类实例)报错,需无需C++/CLI的方案。

现有尝试的问题分析

你给出的四种调用方式存在以下问题:

  1. 手动复制到非托管内存:可行但需注意字符串指针的生命周期——你当前调用完CppExecute就释放内存,若C++在函数外持有Name指针会导致野指针。
  2. GCHandle.Normal获取句柄:得到的是GC句柄而非对象内存地址,C++无法直接解析,完全不可行。
  3. 直接传递类实例:CLR会自动封送并临时pin住对象(直到调用结束),若C++仅在函数内只读访问且布局匹配,是可行的,但存在GC移动风险(虽然调用期间CLR会处理)。
  4. GCHandle.Pinned锁定类实例:非blittable的引用类型(含字符串等非blittable成员)无法被pin,因此报错。

推荐方案与代码示例

方案一:用值类型结构体替代类(最安全)

将C#的AppParameter改为结构体(值类型),blittable成员的结构体可直接被pin,封送逻辑简单可靠。

C#代码

public class DummyClass
{
    [DllImport("example.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void CppExecute(IntPtr parameter);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct AppParameter // 改为结构体
    {
        public Guid Id;
        public int Value;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string Name;
    }

    public AppParameter Parameter { get; set; } = new AppParameter
    {
        Id = Guid.NewGuid(),
        Value = 0,
        Name = "ABC"
    };

    public void Execute()
    {
        // 手动pin结构体,避免GC移动
        var handle = GCHandle.Alloc(Parameter, GCHandleType.Pinned);
        try
        {
            CppExecute(handle.AddrOfPinnedObject());
        }
        finally
        {
            handle.Free(); // 必须释放句柄
        }

        // 也可直接传递结构体,CLR自动处理封送(内部会临时pin)
        // 需修改DllImport参数为AppParameter:
        // [DllImport("example.dll", CallingConvention = CallingConvention.StdCall)]
        // public static extern void CppExecute(AppParameter parameter);
        // CppExecute(Parameter);
    }
}

C++代码

#include <Windows.h>
#include <cstring>

typedef struct _AppParameter
{
    GUID Id;
    int Value;
    const wchar_t* Name;
} AppParameter;

extern "C" __declspec(dllexport) void __stdcall CppExecute(AppParameter* parameter)
{
    // 严格只读访问,不要修改成员
    // 若需保存Name,复制到本地缓冲区,避免依赖C#内存
    wchar_t localName[256];
    wcsncpy_s(localName, parameter->Name, _countof(localName) - 1);
    localName[_countof(localName) - 1] = L'\0'; // 确保字符串终止

    // 处理逻辑...
}

方案二:手动复制类成员到非托管内存(兼容现有类结构)

若必须保留C#类结构,手动将成员复制到非托管内存,完全控制内存生命周期。

C#代码

public class DummyClass
{
    [DllImport("example.dll", CallingConvention = CallingConvention.StdCall)]
    public static extern void CppExecute(IntPtr parameter);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public class AppParameter
    {
        public Guid Id { get; init; } = Guid.NewGuid();
        public int Value { get; init; } = 0;
        public string Name { get; init; } = "ABC";
    }

    public AppParameter Parameter { get; set; } = new AppParameter();

    public void Execute()
    {
        // 计算非托管结构体大小:Guid(16字节) + int(4字节) + IntPtr(8/4字节)
        int structSize = Marshal.SizeOf<Guid>() + sizeof(int) + IntPtr.Size;
        IntPtr paramPtr = Marshal.AllocHGlobal(structSize);
        
        try
        {
            // 逐个复制成员到非托管内存
            IntPtr currentPtr = paramPtr;
            // 复制Guid
            Marshal.StructureToPtr(Parameter.Id, currentPtr, false);
            currentPtr = new IntPtr(currentPtr.ToInt64() + Marshal.SizeOf<Guid>());
            // 复制Value
            Marshal.WriteInt32(currentPtr, Parameter.Value);
            currentPtr = new IntPtr(currentPtr.ToInt64() + sizeof(int));
            // 复制Name到非托管字符串内存
            IntPtr namePtr = Marshal.StringToHGlobalUni(Parameter.Name);
            Marshal.WriteIntPtr(currentPtr, namePtr);

            // 调用C++函数
            CppExecute(paramPtr);

            // 释放字符串内存
            Marshal.FreeHGlobal(namePtr);
        }
        finally
        {
            // 释放结构体内存,防止泄漏
            Marshal.FreeHGlobal(paramPtr);
        }
    }
}

C++代码

同方案一,注意不要持有Name指针超过函数调用时长。

最佳实践

  • 优先用结构体:值类型的blittable结构体封送效率高,GC不会移动,安全可控。
  • 严格只读约束:C++中绝对不要修改传入的数据,不要持有任何来自C#的内存指针(如字符串)超过函数调用周期。
  • 布局严格匹配:C#和C++的结构体/类必须使用相同的布局顺序、字符集、成员大小,避免内存偏移导致访问错误。
  • 手动封送需清理:所有手动分配的非托管内存必须在finally块中释放,防止内存泄漏。
  • 避免pin非blittable引用类型:非blittable的类无法被GCHandle.Pinned锁定,会抛出异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 20:57:01