barus's diary

とても真面目なblogですにゃ

C#からC++作成DLLへ構造体の配列を受け渡し#VS2019

C#からC++作成DLLへ構造体の配列を渡す例を参考に

VisualStudioを使いこなす。

 

以下は、ある数字10000までの素数C++のDLLで計算させて

C#側に結果を返している。

f:id:hatakeka:20200506105051p:plain

概要。

この際、以下の構造体を受け渡しを行うことにした。

f:id:hatakeka:20200506073335p:plain

 

仕組みとして、

構造体の受け渡しに、構造体のメモリサイズをC#が決めてC++

やり取りを行う。C++側はvoidのポインタでメモリサイズはルーズ。

C#側で制約している感じだ。

 

なので、C#側でサイズを認識出来ないと渡すのは困難

以下の場合の構造体は無理だった。

f:id:hatakeka:20200506072740p:plain

 

プロジェクトを作成する。

VisualStudio2019を起動し、新しいプロジェクトの作成する。

 

f:id:hatakeka:20200506071807p:plain

C#のプロジェクトを作成する。

VisualStudioのバージョンで詳細は異なります。

それっぽいのを見つけてください。現時点(VS2019)は以下。

f:id:hatakeka:20200506073806p:plain

プロジェクトの作成するフォルダを適当に入力する。

f:id:hatakeka:20200506074006p:plain

 

初期画面。

f:id:hatakeka:20200506074134p:plain

 

これに、VC++で作成するDLLプロジェクトを追記する。

ソリューションエクスプローラー上で右クリック>追加>新しいプロジェクト

f:id:hatakeka:20200506074258p:plain

VC++のDLLプロジェクト

※それっぽいのを見つけてください。現時点(VS2019)は以下。

 

f:id:hatakeka:20200506074745p:plain

 

プロジェクト名を入力し、作成ボタンを押すと

Windowsデスクトッププロジェクトが立ち上がるので

ダイナミックリンクライブラリ(dll)を選択。

※それっぽいのを見つけて操作して下さい。現時点(VS2019)は以下。

f:id:hatakeka:20200506075047p:plain

 

テスト用として、コンソールプロジェクトも追加しておく。

f:id:hatakeka:20200506075451p:plain

 

ソリューションエクスプローラーに3つのプロジェクトが追加された。

f:id:hatakeka:20200506080551p:plain




ここで、何もいじらずにビルドしてみる。

ソリューションエクスプローラー上で、Cntrlキー押しながらマウスでプロジェクトを選択し、右クリック>選択範囲のリビルドを実行する。

 

f:id:hatakeka:20200506081531p:plain

 

f:id:hatakeka:20200506081656p:plain



 

プロジェクトを追加しただけなのでビルドエラーは起きてないはずだ。

 

現在の、フォルダ構成は以下となっている。DLLと実行ファイルEXEは同じフォルダにしないと動かないため、出力先を変更する。

f:id:hatakeka:20200506075832p:plain

 

C#側

ソリューションエクスプローラーの、C#のプロジェクト(CsharpAndVCDLL_Samp)を右クリック>プロパティ>ビルドタブ>出力パスを

以下のよう変更する。

 

bin\Debug\

..\Debug\

※それっぽいのを見つけて操作して下さい。現時点(VS2019)は以下

 

f:id:hatakeka:20200506080504p:plain

C++

の出力ディレクトリを変更する。

ソリューションエクスプローラーの、C++のプロジェクト(VCDLL)を右クリック>プロパティ>構成プロパティ>全般>出力ディスク

以下のよう変更する。

 

$(SolutionDir)$(Configuration)\

$(SolutionDir)..\Debug\

※それっぽいのを見つけて操作して下さい。現時点(VS2019)は以下

f:id:hatakeka:20200506080833p:plain


C++側のテストコンソール

の出力ディレクトリも同様に変更する。

 

ソリューションエクスプローラーの、C++のプロジェクト(TestCOM)を右クリック>プロパティ>構成プロパティ>全般>出力ディスク

以下のよう変更する。

 

$(SolutionDir)$(Configuration)\

$(SolutionDir)..\Debug\

 

これで、再度、全体ビルドする。

f:id:hatakeka:20200506081656p:plain

 

出力先がDebugフォルダに作成されるようになった。

 

CsharpAndVCDLL_Samp.exe ←C#プロジェクトより作成

TestCom.exe ←C++プロジェクトより作成

VCDLL.dll ←C++プロジェクトより作成

 

f:id:hatakeka:20200506081913p:plain

 

 

※ここまではソースは一切いじっていない。

 

C#側の画面を作成

ソリューションエクスプローラー画面のC#プロジェクトCsharpAndVCDLL_Samp

右クリック>追加>新しいフォルダでUser作成。

f:id:hatakeka:20200506082659p:plain

ソリューションエクスプローラー画面のC#プロジェクトCsharpAndVCDLL_Samp

のUserフォルダ上で、右クリック>追加>ユーザーコントロールを選択

 

f:id:hatakeka:20200506082432p:plain

 

こんな感じ。

f:id:hatakeka:20200506083132p:plain

 

このUserControl1上にペタペタと、ツールボックスからButton,TextBox、DataGridViewの3つを張り付ける。

(※Form1上に張り付けてもよい。UserControl1は不整合が起きるとすぐに編集できなくなったりして不安定なので、まったくの初心者には難しいかも)

f:id:hatakeka:20200506083055p:plain

DataGridViewの列を編集する。以下の赤丸をクリック。

f:id:hatakeka:20200506083518p:plain

列の編集

f:id:hatakeka:20200506083612p:plain

f:id:hatakeka:20200506083957p:plain

「追加」ボタンで

名前とヘッダーテキストに「No」「prime」「tag」と入力すると以下のようになる「OK」ボタンで閉じる。

f:id:hatakeka:20200506083824p:plain

 

画面は以下のようになる。

f:id:hatakeka:20200506084112p:plain

画面サイズが変更した際に、自動でDataGridViewのサイズも変わるようにプロパティのAnchorを以下のように変更する。

f:id:hatakeka:20200506085105p:plain



ここでビルドする。

 

ソリューションエクスプローラー画面のC#プロジェクトCsharpAndVCDLL_Samp

右クリック>ビルド

  

すると、以下のようにツールボックスに作成したUserControl1が表示されるのでForm1画面に張り付ける。

f:id:hatakeka:20200506084312p:plain

 

ソリューションエクスプローラー画面のC#プロジェクトCsharpAndVCDLL_Samp 

のForm1を表示させて、ツールボックスのUserControl1をForm1に張り付ける。

f:id:hatakeka:20200506154523p:plain

 

画面サイズが変更した際に、自動でUserControl1のサイズも変わるようにプロパティのAnchorを以下のように変更する。

f:id:hatakeka:20200506085008p:plain

 

こんな感じ。

お好みで、下部分のツールボックスより、StatusStripを付加した。

f:id:hatakeka:20200506085346p:plain

これで一応、見た目の完成。

 

C#側のDLLとの中継を一括で行うクラスを作ることにします。

 

ソリューションエクスプローラー画面のC#プロジェクトCsharpAndVCDLL_Samp

右クリック>追加>新しいフォルダでdllフォルダ作成。

 

ソリューションエクスプローラー画面のC#プロジェクトCsharpAndVCDLL_Samp

右クリック>追加>クラスで、クラス名をCallDLLで作成する 

f:id:hatakeka:20200506093557p:plain

C#側のソース

CallDLL.cs

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

// DllImportに必要
using System.Runtime.InteropServices;

namespace CsharpAndVCDLL_Samp
{
[StructLayout(LayoutKind.Sequential)]
public struct Point3D
{
  public double X;
  public double Y; 
  public double Z;
}

[StructLayout(LayoutKind.Sequential)]
public struct Prime3D
{
  public Point3D p;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)]
  public string tag;
  public int prime;
};

class CallDLL
{
System.IntPtr m_ptr;


//整数numまでの素数をrltで返す
[DllImport("VCDLL.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void getPrime(int num, System.IntPtr pvoid);


public void CSharp_getPrime(int num, ref Prime3D rlt)
{

// アンマネージド配列のメモリ確保
  m_ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(Prime3D)) * num);

// C++に構造体の配列を渡す(ポインタを渡す)
  getPrime(num, m_ptr);

//構造体ポインタは、構造体の配列としては認識(マーシャリング)できないので、
//ポインタのオフセット値をずらしながら、1つずつ Prime3D構造体の値を取得します。
  for (int i = 0; rlt.Length > i; i++)
  {
    rlt[i] = (Prime3D)Marshal.PtrToStructure(m_ptr, typeof(Prime3D));
    m_ptr = (IntPtr)((int)m_ptr + Marshal.SizeOf(typeof(Prime3D)));
  }

}

public void Marshalfree()
{
  if (m_ptr != null)
  // アンマネージドのメモリを解放 
  Marshal.FreeCoTaskMem(m_ptr);

}


}
}

 

ここが肝となる。

 

構造体「Prime3D」はC++と同じ構造体にする。

(C++側は後述で出てくる。)

 

構造体の冒頭に「[StructLayout(LayoutKind.Sequential)]」をつける。

public string tag;

は、C++側の

char tag[10];

に相当する。今回サイズは10バイト固定と分かっているので

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)]

を先頭につけて、サイズを教えてあげないといけない。(みたい)

 

冒頭で述べたように、構造体「Prime3D」をC++側とやり取りするために「AllocCoTaskMem」にてメモリサイズを計算して確保している。

Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(Prime3D)) * num);

 

C++にポインタとして渡し、結果を参照渡しで取得する。
  getPrime(num, m_ptr);

 

//ポインタのオフセット値をずらしながら、1つずつ Prime3D 構造体の値を取得します。
  for (int i = 0; rlt.Length > i; i++)
  {
    rlt[i] = (Prime3D)Marshal.PtrToStructure(m_ptr, typeof(Prime3D));
    m_ptr = (IntPtr)((int)m_ptr + Marshal.SizeOf(typeof(Prime3D)));
  }

 

構造体の配列の場合はこうだが、intの配列の場合は以下のようして、マーシャルコピーMarshal.Copy(ptrInt, rlt, 0, num);という方法もある。

f:id:hatakeka:20200506100134p:plain

 

今回ソースには、メモリを解放という処理を明示的に含んでいないが


  // アンマネージドのメモリを解放 
  Marshal.FreeCoTaskMem(m_ptr);

 

使い終わったら解放してあげる。

 

フォルダを作成しクラスを作成すると、CallDDLL.csのnamespaceが、最初は、namespace CsharpAndVCDLL_Samp.dllとなっているはずなので以下のように変更する。

namespace CsharpAndVCDLL_Samp

 

 「素数」ボタンをWクリックでソースコード記載する。

f:id:hatakeka:20200506084112p:plain

UserControl1.cs

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace CsharpAndVCDLL_Samp.User
{
 public partial class UserControl1 : UserControl
  {
    public UserControl1()
    {
      InitializeComponent();
     }

  private void button1_Click(object sender, EventArgs e)
  {
     CallDLL callDLL = new CallDLL();

     Prime3D rlt;
     int num = int.Parse(textBox1.Text);
     rlt = new Prime3D[num];

     callDLL.CSharp_getPrime(num, ref rlt);

     TableAdd(rlt);//テーブルに追加
  }

 

  public void TableAdd(Prime3D rlt)
  {
    int i = 0;
    for (i = 0; rlt.Length > i; i++)
    {

    if (i < 60000)
    {
       dataGridView1.Rows.Add(i, rlt[i].prime, rlt[i].tag);
     }

  }
}//public void TableAdd(Prime3D rlt)

}//public partial class UserControl1 : UserControl
}//namespace CsharpAndVCDLL_Samp.User

 

  

C#側は、C++とのやり取りをCallDLLクラスで一括して行うことに決めたのでDLLを呼ぶ際はCallDLLを作成し

 CallDLL callDLL = new CallDLL();

処理を呼ぶ

callDLL.CSharp_getPrime(num, ref rlt);

受け取ったデータを、テーブルに追加している。

dataGridView1.Rows.Add(i, rlt[i].prime, rlt[i].tag);

 

ちなみに、Form1.cs にはソースコードの修正はない。

 

VCDLL側

クラスの追加する。DLL呼び出しではクラス毎渡せないので、メイン処理とそれを中継するクラスを作ると何かと便利かもしれない。

 

ClassPrime    ←処理クラス

ClassWork     ←中継クラス

 

ソリューションエクスプローラー画面のC#プロジェクトVCDLL

右クリック>追加>クラス>ClassPrimeを追加

 

f:id:hatakeka:20200506090025p:plain

 

ソリューションエクスプローラー画面のC#プロジェクトVCDLL

右クリック>追加>クラス>

クラス名をClassWork 

基底クラスをClassPrimeにする。

f:id:hatakeka:20200506090533p:plain

 

今現在は以下のようになっている。

f:id:hatakeka:20200506095011p:plain

 

dllmain.cpp

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。

#include "framework.h"

#include <string>

#include <iostream>

#include <fstream>

#include <vector>

#include <list>

#include "ClassWork.h"

#ifdef __cplusplus#define DLLEXPORT extern "C" __declspec(dllexport)

#else

#define DLLEXPORT __declspec(dllexport)

#endif


using namespace std;
ClassWork cwork;


//整数numまでの素数をrltで返す

DLLEXPORT void getPrime(int num, void* _rlt) {

   Prime3D* rlt = (Prime3D*)_rlt;
   cwork.getPrime(num, rlt);

}

DLLEXPORT  

このあたりは、いつも使う文言。

 

DLLは、クラス自身をコール出来ないっぽいのでClassWorkで中継してる。

voidのポインタで、サイズ不明を渡してキャストしてあげる。

Prime3D* rlt = (Prime3D*)_rlt;

 

ClassWork.h

#pragma once

#include "ClassPrime.h"

class ClassWork : public ClassPrime{
};

ClassPrimeを基底にしている。

ClassWork.cpp

#include "ClassWork.h"

とくに変更なし。

 

ClassPrime.h

#pragma once

#ifndef _Header_ClassPrime_H

#define _Header_ClassPrime_H
#include<Windows.h>

#include<stdio.h>

#include<stdlib.h>

#include<math.h>

 

typedef struct Point3D {

  double X;

  double Y;

  double Z;

};
typedef struct Prime3D {

  Point3D p;

  char tag[10];

  int prime;

};

 

class ClassPrime{

public: ClassPrime() {}

     //整数numまでの、素数がrltに入る

     void getPrime(int num, Prime3D* rlt);

};


#endif

 

 

構造体「Prime3D」はC#と同じ構造体にする。

C++側で特にC#にメモリサイズを教えるためにマーシャルを使って~

みたいな処理を加える必要はない。

 

 

基本はC++なんだというのが分かる。 

 

#ifndef _Header_ClassPrime_H

らへんは、いつも通り。

 

ClassPrime.cpp

 

#include "ClassPrime.h"

//素数

void ClassPrime::getPrime(int num, Prime3D* rlt) {

   int i, j, k;
   printf("%s %s", __FILE__, __func__);
   memset(rlt, 0x00, num * sizeof(Prime3D));
 

   rlt->prime = 2;

   sprintf_s(rlt->tag, 10, "prime");

   rlt++;
   for (i = 3; i <= num; i += 2) {

          k = 0;

         for (j = 3; j <= sqrt(i); j += 2) { if (i % j == 0) { k = 1; break; } }
         if (k == 0) {

             sprintf_s(rlt->tag, 10, "prime");

             rlt->prime = i; rlt++;

         } else {

            sprintf_s(rlt->tag, 10, "not prime");

            rlt++;

         }

   }//for

}

 

素数を計算している部分。

 

TestCOMで確認

以上で完成なわけだが、サイズもでかくなると、一発でうまくいかないだろう。その場合はTestCOMで動作確認する。

f:id:hatakeka:20200506102026p:plain

 

ソリューションエクスプローラー画面のC++プロジェクトTestCOM

右クリック>追加>既存の項目

f:id:hatakeka:20200506102159p:plain

 

 

f:id:hatakeka:20200506102400p:plain

ダイアログ画面より、

ClassPrime.cpp

ClassPrime.h

を選択して追加、「ClassPrime.h」をヘッダーフォルダに移動、以下のようになる。 

f:id:hatakeka:20200506102648p:plain

追加した、

ClassPrime.cpp

の完全パスを見てみると、CsharpAndVCDLL_Samp

のフォルダ先であることが分かる。なのでここで「ClassPrime.cpp」を編集すると、CsharpAndVCDLL_Samp

先の「ClassPrime.cpp」も同時に変更される。(同じファイル)

f:id:hatakeka:20200506103958p:plain

 

ソリューションエクスプローラー画面のC++プロジェクトTestCOM

右クリック>プロパティ

構成プロパティ>VC++ディレクトリ>インクルードディレクトリを以下のように追加変更する。「G:\plog\vs2019\CsharpAndVCDLL_Samp\VCDLL」がC++のDLLプロジェクトのフォルダ。

 

$(VC_IncludePath);$(WindowsSDK_IncludePath);

 ↓

$(SolutionDir)..\VCDLL;$(IncludePath)

 

※固定パスを避けて、相対パスorマクロにしておくのがベター。

 

f:id:hatakeka:20200506161112p:plain



 

TestCOM.cpp

// TestCOM.cpp : このファイルには 'main' 関数が含まれています。プログラム実行の開始と終了がそこで行われます。


#include <iostream>

#include "ClassPrime.h"
#include <vector>

#include <string>

#include <iostream>

#include <fstream>using namespace std;

 

void test_prime(int argc, char* argv);

 

int main(int argc, char** argv){

   std::cout << "Hello World!\n";
   test_prime(argc, argv);
}


//素数

void test_prime(int argc, char* argv) {
   ClassPrime clasprime;
   Prime3D rlt[100];

   clasprime.getPrime(100, rlt);
   for (int i = 0; 100 > i; i++) {

     printf("[%02d]%d\t%s \n", i, rlt[i].prime, rlt[i].tag);

   }
   cout << "end" << endl;

}

 

TestCOMはコンソール画面で

#include "ClassPrime.h"

でClassPrimeをインクルードして

clasprime.getPrime(100, rlt);

関数をテスト出来る。

f:id:hatakeka:20200506104645p:plain

 

 


以上。

 

参考URL:

http://www2u.biglobe.ne.jp/~kaduhiko/csharp_05.html

https://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=47240&forum=7