在C#和C++之间共享变量

18

我正在使用C#编写一个软件,需要多次调用并且由多个线程调用一个未托管的C++动态链接库中的函数。

我有一个类似以下代码的C++文件:

// "variables" which consist in some simple variables (int, double) 
//  and in some complex variables (structs containing arrays of structs)


extern "C"
{
     __declspec(dllexport) int function1()
    {
        // some work depending on random and on the "variables"
    }
}

并且一个像这样的C#类

public class class1
{
    //  "variables" <--- the "same" as the C++ file's ones 
    //  Dll import <--- ok

    public void method1()
    {
        int [] result;

        for(int i=0; i<many_times; i++)
        {
            result = new int[number_of_parallel_tasks];                

            Parallel.For(0, number_of_parallel_tasks, delegate(int j)
            { 
                 // I would like to do  result[j] = function1()  
            });

            //  choose best result
            //  then update "variables" 
        }
    }

}

我写了"I would like to do ...",因为每一轮C++函数都需要更新"variables"。

我的问题是:

是否可能在C++和C#之间共享内存以避免每次传递引用?这只是浪费时间吗?

我读过内存映射文件。它们能帮到我吗?不过,你知道更合适的解决方案吗?
非常感谢。


1
我赞同了你的问题,但你应该明确“变量”的性质才能得到一个好答案。 - Sebastian Cabot
3个回答

25

一旦了解其工作原理,使用P/Invoke在C#和C++之间共享内存没有问题。建议阅读MSDN关于封送的文章。您还可以阅读有关使用unsafe关键字和修复内存的文章。

以下是一个示例,假设您的变量可以描述为简单的结构体:

在C ++中声明您的函数如下:

#pragma pack(1)
typedef struct VARIABLES
{
/*
Use simple variables, avoid pointers
If you need to use arrays use fixed size ones
*/
}variables_t;
#pragma pack()
extern "C"
{
     __declspec(dllexport) int function1(void * variables)
    {
        // some work depending on random and on the "variables"
    }
}

在 C# 中可以这样做:

[StructLayout(LayoutKind.Sequential, Pack=1)]
struct variables_t
{
/*
Place the exact same definition as in C++
remember that long long in c++ is long in c#
use MarshalAs for fixed size arrays
*/
};

[DllExport("YourDll.dll", CallingConvention=CallingConvention.Cdecl)]
static extern int function(ref variables_t variables); 

并且在你的类中:

variables_t variables = new variables_t();
//Initialize variables here
for(int i=0; i<many_times; i++)
{
    int[] result = new int[number_of_parallel_tasks];
    Parallel.For(0, number_of_parallel_tasks, delegate(int j)
    { 
          result[j] = function1(ref variables)  
    });

    //  choose best result
    //  then update "variables" 
}
你可以使用更复杂的场景,比如在C ++中分配和释放结构体,在获取数据时使用其他形式的编组,比如构建自己的类以直接读写非托管内存。但是,如果你可以使用一个简单的结构体来保存变量,上述方法是最简单的方法。 编辑:处理更复杂数据的指针 因此,我的观点是上面的示例是在C#和C++之间“共享”数据的正确方式,如果它是简单的数据,例如包含基本类型或固定大小的基本类型数组的结构体。 话虽如此,实际上你可以用C#访问内存的方式有很少限制。要了解更多信息,请查看unsafe关键字、fixed关键字和GCHandle结构。即使有非常复杂的包含其他结构体数组等的数据结构,你仍然需要进行更复杂的任务。 在上述情况下,我建议将更新“变量”的逻辑移动到C++中,并在C++中添加一个类似以下的函数:
extern "C"
{
     __declspec(dllexport) void updateVariables(int bestResult)
    {
        // update the variables
    }
}

我仍建议不要使用全局变量,因此我提出以下方案。 在C++中:

typedef struct MYVERYCOMPLEXDATA
{
/*
Some very complex data structure
*/
}variables_t;
extern "C"
{
     __declspec(dllexport) variables_t * AllocVariables()
    {
        // Alloc the variables;
    }
     __declspec(dllexport) void ReleaseVariables(variables_t * variables)
    {
        // Free the variables;
    }
     __declspec(dllexport) int function1(variables_t const * variables)
    {
        // Do some work depending on variables;
    }
    __declspec(dllexport) void updateVariables(variables_t * variables, int bestResult)
    {
       // update the variables
    }
};
在C#中:
[DllExport("YourDll.dll", CallingConvention=CallingConvention.Cdecl)]
static extern IntPtr AllocVariables();
[DllExport("YourDll.dll", CallingConvention=CallingConvention.Cdecl)]
static extern void ReleaseVariables(IntPtr variables); 
[DllExport("YourDll.dll", CallingConvention=CallingConvention.Cdecl)]
static extern int function1(IntPtr variables); 
[DllExport("YourDll.dll", CallingConvention=CallingConvention.Cdecl)]
static extern void updateVariables(IntPtr variables, int bestResult); 

如果你仍然想要在C#中保留你的逻辑,你需要像下面这样做: 创建一个类来保存从C++返回的内存,并编写自己的内存访问逻辑。使用复制语义将数据公开给C#。我的意思是如下所示, 假设你在C++中有这样一个结构:

#pragma pack(1)
typedef struct SUBSTRUCT
{
int subInt;
double subDouble;
}subvar_t;
typedef struct COMPLEXDATA
{
int int0;
double double0;
int subdata_length;
subvar_t * subdata;
}variables_t;
#pragma pack()

在C#中,你可以像这样进行操作

[DllImport("kernel32.dll")]
static extern void CopyMemory(IntPtr dst, IntPtr src, uint size);

[StructLayout((LayoutKind.Sequential, Pack=1)]
struct variable_t
{    
    public int int0;
    public double double0;
    public int subdata_length;
    private IntPtr subdata;
    public SubData[] subdata
    {
        get
        {
             SubData[] ret = new SubData[subdata_length];
             GCHandle gcH = GCHandle.Alloc(ret, GCHandleType.Pinned);
             CopyMemory(gcH.AddrOfPinnedObject(), subdata, (uint)Marshal.SizeOf(typeof(SubData))*subdata_length);
             gcH.Free();
             return ret;
        }
        set
        {
             if(value == null || value.Length == 0)
             {
                 subdata_length = 0;
                 subdata = IntPtr.Zero;
             }else
             {
                 GCHandle gcH = GCHandle.Alloc(value, GCHandleType.Pinned);
                 subdata_length = value.Length;
                 if(subdata != IntPtr.Zero)
                     Marshal.FreeHGlobal(subdata);
                 subdata = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SubData))*subdata_length);
                 CopyMemory(subdata, gcH.AddrOfPinnedObject(),(uint)Marshal.SizeOf(typeof(SubData))*subdata_length);
                 gcH.Free();
             }
        }
    }
};
[StructLayout((LayoutKind.Sequential, Pack=1)]
sturct SubData
{
    public int subInt;
    public double subDouble;
};
在上面的示例中,结构仍然可以像在第一个示例中那样传递。当然,这只是关于如何处理带有结构数组和结构数组的复杂数据的概述。正如您所看到的,您需要大量复制以保护自己免受内存损坏。此外,如果内存是通过C ++分配的,则如果使用FreeHGlobal来释放它,情况将非常糟糕。
如果您想避免复制内存并仍然保持C#内部的逻辑,则可以编写本机内存包装器,并为所需内容编写访问器。例如,您将拥有一种方法来直接设置或获取第N个数组成员的subInt-这样,您将仅保留到您访问的内容的副本。
另一个选择是编写特定的C ++函数来处理您的困难数据,并根据您的逻辑从C#调用它们。
最后但并非最不重要的是,您始终可以使用具有CLI界面的C ++。但是,我自己只有在必须时才会这样做-我不喜欢这种术语,但对于非常复杂的数据,您确实必须考虑它。
编辑
我添加了正确的DllImport调用约定以完整性。请注意,默认DllImport属性使用的调用约定是Winapi(在Windows上转换为__stdcall),而C / C ++中的默认调用约定(除非更改编译器选项)是__cdecl。

谢谢您提供这么清晰的答案。我已经点赞了,但是我还不太满意。也许在我的问题中,我没有明确说明我已经尝试过使用P/invoke,但是以这种方式(如果我没有弄错)我必须在每次迭代中传递所有变量(好的,引用)。我想知道是否有一种避免这种情况的方法。 - 888
1
@mcdg 将结构体的引用传递没有任何问题。这样做并不会带来太大的开销。在许多情况下,使用某种结构体或类来保存状态是更好的实践方式。这样你就可以进行多线程操作,并使函数可重入,而使用全局变量则会限制你的可能性。然而,由于你已经在问题中更新了“变量”的规范,我将在答案中添加一些处理更复杂情况的指针。 - Sebastian Cabot

4

最好的做法是定义你的C++代码(我想它是非托管的)。 然后在C++/CLI中编写一个包装器。这个包装器将为你提供与C#的接口,并且也是处理数据传输(从非托管区域转移到托管区域)的地方。


1
为什么他们要使用C++/CLI来创建包装器?你完全可以用C#轻松地编写包装器。 - Security Hound
1
C++/CLI在处理所有细节方面要灵活一些,特别是如果您想在托管代码和非托管代码之间切换。结合.NET和C++的C++/CLI,如果您至少不习惯C++,那么并不是“易于上手”的(但这只是我的个人意见)。那么坚持使用C#可能是一个更明智的选择。 - PapaAtHome
我稍微修改了一下问题,以更好地阐述自己。 - 888
1
你的C代码将使用“非托管”代码和内存(本地代码),而你的C#代码将使用“托管”代码和内存(.NET CLR系统)。这两者不能混合使用或交叉引用。你需要编写转换代码来实现它。 - PapaAtHome
1
如果可以将您的C代码重新编译为“托管”的C++代码,则无需转换代码。这是您可以尝试的另一件事情。 - PapaAtHome
谢谢您的提示,但在这种情况下,出于性能考虑,我将保持 dll 为非托管状态。 - 888

1

避免在C#和C++代码中传递变量/数据的引用的一种方法是从本地DLL中导出两个函数。除了执行工作的函数之外,还提供另一个函数,允许将引用传递并存储到文件范围静态指针中,该指针在与两个函数相同的.cpp文件中定义,因此可以被两者访问。

正如您所提到的,您还可以使用内存映射文件(在这种情况下,它可以是非持久性的,因为它永远不需要写入磁盘)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接