C# へのネイティブ DLL の取り込み - 構造体編

ここまでで,C# から C で作成した DLL のメソッドへの変数・文字列の渡し方を見てきたが,構造体を引数として渡すことも実用上考えられる.ここでは,構造体を渡す方法をしらべたのでまとめる.

1. C で定義した構造体を C# でも定義する.

[StructLayout(LayoutKind.Explicit)]
struct SAMPLE {
  [FieldOffset(0)]
  public int width;
  [FieldOffset(4)]
  public int height;
  [FieldOffset(8)]
  public byte* buf;
}

上のサンプルでは,"LayoutKind.Explicit" 属性によってメンバの配置を明示的に指定している.この方法をとった場合,各要素のバイト位置を "FieldOffset(x)" で指定しないといけない.

2. C で定義されている関数を C# で呼んでやる.

[DllImport("MyDll.dll", CallingConvention = CallingConvenction.Cdecl)]
unsafe private extern static void FillSample(ImageData *data);

3. C# のコードを書く.

例1

undafe public void test(int *size);


unsafe public void test(int *size) 
{
  byte[] data = new byte[10]
  fixed (byte *p = data) {

    SAMPLE o = new SAMPLE();
    FillSample(&o);

  }

}

例2

public unsafe int Read(byte[] buffer, int index, int count)
{
  int bytesRead = 0;
  fixed (byte* bytePointer = buffer)
  {
    ReadFile(fileHandle, bytePointer + index, count, &bytesRead, 0);
  }
  return bytesRead;
}

ここで,unsafe というキーワードは C# にてポインタを使用する際に必要となるもので,C# では unsafe が宣言されたコンテキストでなければポインタを使うことができない.まとめると....

  • メソッドやブロック内でポインタを使用したいときには,unsafe キーワードをつける.
  • unsafe を使うためには,コンパイル時にコンパイラに明示的に宣言しないといけない.Visual Studio の場合は,プロジェクトのプロパティをひらいて「ビルド」⇒「全般」の「アンセーフコードの許可」にチェックを入れる.

また,unsafe コンテキストでは下記の機能が利用可能になる.

fixed keyword

 最初の例「SAMPLE」を考えると,構造体の中にはポインタ変数 *buf がある.ポインタ変数でアクセスするということはデータの領域が連続していないといけないが,.NETFrameWorkでは効率化を実現するためにガーベッジコレクションのプログラムがバッファの位置を動かしてしまうかもしれない.C の関数では,バッファの位置は当然ながら連続したものとして考えて処理されるであろうから,確保した配列 data に関してすくなくとも C の関数に呼ばれている間は配列の値が同じメモリ状の位置に固定されていないといけない.この機能を提供するのが fixed keyword である.fixed コンテキスト内では,指定されたポインタのメモリ上の位置がコンテキスト内では不変であることが保証される.

C# へのネイティブ DLL の取り込み - 値渡し,参照渡し引数編

仕事で C# を使うようになったため C# 関連の勉強を開始.既存の資産が C である場合が多いので,C の DLL を取り込む方法を調べた.

C# 側での記述

簡単な例 (参照渡しパラメータなしの Win32API)

 DllImport 属性 (System.Runtime.InteropServices) を利用し,その関数が外部にあることを宣言する.例えば,下記の Win32API を用いる場合,,,

BOOL MessageBeep(
    UINT uType   // サウンドの形式
);

C# 側では下記のように宣言する.
DllImport 属性をつけた Win32 API や DLL 関数の宣言では,関数の実体が外部にあることを表す extern 修飾子と静的なメンバであることを表す static を必ず指定する.
(Windows の DLL と .NET Framework とでは型の管理方法が異なるため,実際には型の相互変換(マーシャリング)が行われる.)

class BeepTest
{

  // DLL "user32.dll" から持ってくる関数であることを宣言.
  [DllImport("user32.dll")]
  extern static bool MessageBeep(uint uType)

  public void test() {
    // 値渡しのパラメータを渡して呼んであげる.
    MessageBeep(0xFFFFFFFF);
  }

  static void Main() {
    BeepTest test = new BeepTest();
    test.test();
  }

}

参照渡しのパラメータを含む DLL の場合

 参照渡しのパラメータを含む関数を宣言するには,参照渡しになっているパラメータを ref パラメータとして宣言する.たとえば,例として下記のような関数を考えてみる.ここで,下記の関数は数字を受け取り,受け取った値に対して10追加して返す関数とする.

BOOL Add10(
    int* num // 参照渡しのパラメータ.関数呼び出し時に初期値の指定が必要で,関数が戻ってきたときの値も必要.
);

上記のような参照渡しのパラメータを含む DLL 関数の場合,C# 側では下記のように宣言する.

// 呼び出し側から初期値を与える必要がない場合は,"ref" を "out" として宣言することもできる.

class AddTest 
{

  [DllImport("MyDll.dll")]
  extern static bool Add10(ref uint num);

  public void test() {
    int num = 3;
    Console.WriteLine("Original Number : " + num);
    Add10(ref num); // 関数呼び出し側でも参照変数であることを明示.
    Console.WriteLine("Modified Number : " + num);
    Console.ReadLine();
  }

  static void Main() {
    test();
  }

}


一方で,関数の提供側である C のコードは下記のように記述する.

// MyDll.h

extern "C" {
  __declspec(dllexport) void Add10(int* num);
}

// MyDll.cpp

void Add10(int* num) {

  *num = *num + 10;
  return;

}

Calling Convention

Calling Convention (呼出規約)

 プログラミングにおける呼出規約はサブルーチンを呼び出す際の標準的な手法を指している.サブルーチンにデータを渡し,戻るべきアドレス(リターンアドレス)を記録し,サブルーチンからデータを受け取るための規則.一つのプログラムでは,同一の呼出規約を守る必要がある.

cdecl

 インテル x86 ベースのシステム上の C, C++ では cdecl 呼出規約が用いられることが多い.cdecl では関数の引数は右から左の順でスタックに積まれる.関数の戻り値は EAX に格納される.呼び出された側の関数では EAX, ECX, EDX のレジスタの元の値を保存することなく使用してよい.呼び出し側の関数では必要ならば呼び出す前にそれらのレジスタをスタック上などに保存する.例えば,以下の C プログラムの関数呼び出しは,,,

 int function(int, int, int);
 int a, b, c, x;
 ...
 x = function(a, b, c);

以下のような機械語を生成する.

 push c
 push b
 push a
 call function
 add esp, 12 ;スタック上の引数を除去
 mov x, eax

Visual Studio 2013 - 構成プロパティ

構成プロパティ

出力ディレクトリ

  • コンパイルを通して生成された実行ファイル・DLL等を置く場所

中間ディレクトリ

  • コンパイル途中で生成され,リンクしてない obj ファイルを置く場所

上記の設定は,下記のように変更可能

該当するプロジェクト上で右クリック -> プロパティを -> 構成プロパティ

  • > 出力ディレクトリ
  • > 中間ディレクトリ

C# 属性

属性:クラス,メンバに追加情報を与えるもの.

 C++などの既存の言語では,追加情報を定義する場合,言語仕様自体を拡張し,新たにコンパイラを作り直す必要があった.C#では自分で属性を定義し,クラスやメンバに付加することができる.すなわち,ライブラリて提供されている属性や自作した属性を用いることで,コンパイラに対する指示を行ったり,クラスの利用者に対する情報を残すことができる.

属性の一例としては,,,

  • 条件コンパイルなどの,コンパイラへの指示に使う.
  • 作者情報などをメタデータとしてプログラムに埋め込む.
  • リフレクションを使用して,プログラム実行時に属性情報を取り出す.

属性の使い方
属性は以下のようにして,クラスやメンバの前につけて使う.

[属性名(属性パラメータ)]
メンバーの定義

下記のように使うと,DEBUG_PRINTというシンボルが定義されている時のみメソッドが実行される.

[Conditional("DEBUG_PRINT")]
static void DebugOutput(double[] array) {
  printf("Debug Mode")
}

定義済み属性

標準ライブラリによって提供されている定義済み属性

属性の対象

属性をつける場所によって対象が変わるが,つける位置によってあいまいになることがある.この曖昧さを取り除くため,明示的に属性の対象を指定する構文がある.

[属性の対象:属性名(属性のオプション)]
[method: DllImport("msvcrt.dll")]
[return: MarsharlAs(UnmanagedType.I4)]
public static extern int puts([param: MarshalAs(UnmanagedType.LPStr)] string m)

C# 実行時型情報

メタデータ:クラス名,メンバ名,それらのアクセスレベル等の情報.

リフレクション:プログラムの実行時にメタデータを取り出すための機能.

実行時型情報:C#では,クラスのインスタンスから実行時に型情報を取得したり,リフレクションを利用して型情報からメタデータを取得する機能がある.このようにして実行時に得られる型情報のこと.

実行時型情報をサポートしない言語では,実行時に不要なメンバの名前等の情報は削除される.一方,C#のような実行時型情報をサポートする言語では,実行には直接的に不要なこれらの情報も引き出せるようになっている.

class City {
  string name;
  int population;
}

// Type 取得
Type t = Type.GetType("SolutionName.OuterClass1+OuterClass2+Rect");

// 取得した Type のインスタンスを生成
object o = Activator.CreateInstance(t);

o.GetField("name").SetValue(o, "Tokushima");
o.GetField("population").SetValue(o, 10000);

string name = o.GetField("name").GetValue(o);
int population = o.GetField("population").GetValue(o);

C++ コンパイラがデフォルトで提供するメンバ関数の無効化

オブジェクトのコピーを抑制するには,クラス型のコピーコンストラクタと代入演算子を無効化する必要がある.
C++11では,コンパイラがデフォルトで提供するメンバ関数を利用させないようにすることを明示的に宣言できる手段がある.具体的には,メンバ関数の宣言に "=delete" を付加する.

class SlamSystem {

public:
// コピーコンストラクタの無効化
SlamSystem(const SlamSystem&) = delete;

// 代入演算子の無効化
SlamSystem& operator=(const SlamSystem&) = delete;

}