18

私は何度も呼び出す必要があるソフトウェアを c# で作成しており、多くのスレッドで c++ アンマネージ dll の関数を呼び出しています。

次のような 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" 
        }
    }

}

私が「やりたい...」と書いたのは、C++ 関数が各ラウンドで「変数」も更新する必要があるためです。

私の質問は:

毎回参照を渡すのを避けるために、C++ と C# の間でメモリを共有することは可能ですか? それはただの時間の無駄ですか?

メモリマップファイルについて読みました。彼らは私を助けることができますか?しかし、より適切な解決策を知っていますか?
どうもありがとうございました。

4

3 に答える 3

24

仕組みがわかれば、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# から呼び出すことです。

最後になりましたが、C++ を CLI インターフェイスでいつでも使用できます。ただし、私自身は必要な場合にのみ行っています。専門用語は好きではありませんが、非常に複雑なデータの場合は、必ず考慮する必要があります。

編集

完全を期すために、正しい呼び出し規約を DllImport に追加しました。DllImport 属性で使用される既定の呼び出し規則は Winapi (Windows では __stdcall に変換されます) であるのに対し、C/C++ での既定の呼び出し規則 (コンパイラ オプションを変更しない限り) は __cdecl であることに注意してください。

于 2012-11-09T09:34:42.430 に答える
4

あなたができる最善のことは、C++ コードを定義することです (これは管理されていないと思います)。次に、C++/CLI でラッパーを作成します。ラッパーは C# へのインターフェイスを提供し、マーチャリング (管理されていない領域から管理されている領域へのデータの移動) を処理できる場所です。

于 2012-11-07T17:54:04.393 に答える
1

C# と C++ の両方のコードが必要とする変数/データへの参照を渡す必要を回避する 1 つの方法は、ネイティブ DLL から 2 つの関数をエクスポートすることです。作業を行う関数に加えて、両方の関数と同じ .cpp ファイルで定義されているファイル スコープの静的ポインターに参照を渡して格納できるようにする別の関数を提供して、両方にアクセスできるようにします。

前述のように、メモリ マップ ファイルを使用することもできます (この場合、ディスクに書き込む必要がないため、永続化されていない可能性があります)。

于 2012-11-13T01:59:42.563 に答える