.NET TIPS

Win32 APIやDLL関数に構造体を渡すには?

泉 祐介
2003/05/09

 TIPS:Win32 APIやDLL関数を呼び出すには? では、.NETのプログラムからWin32 APIやDLLファイルにある関数を呼び出すための基本的な手順を解説している。ここではそれらDLL関数に渡すパラメータが「構造体」である場合の取り扱い方法について解説する。

 .NET FrameworkからWin32 APIやDLL関数を呼び出す場合に、パラメータや戻り値に構造体が使用されているときは、その構造体と等価なものをC#で改めて定義しなければならない。Win32 APIで使用する構造体は、TIPS:Win32 APIやDLL関数を呼び出すには? で解説している定数と同様に、プラットフォームSDKのヘッダ・ファイルにその定義がある。また、構造体に関してはMSDNのドキュメント類にも定義が記述されている。

 例えば、Win32 APIでしばしば使用される構造体の1つにPOINT構造体がある。これは名前のとおり、点の座標を表現するために使用される構造体であり、C言語による定義では次のようになっている(念のため、本稿の後半でC言語の構造体について簡単に解説しておいたので、C言語にあまりなじみのない読者はそちらを参照されたい)。

typedef struct tagPOINT {
  LONG x;
  LONG y;
} POINT;

 通常の型と同様、構造体に関しても、関数をエクスポートしているDLL側と.NET Framework側との間で型の相互変換が生じるが、この相互変換処理は、メモリ上における各メンバのオフセット(相対位置)に基づいて行われる。このため、Win32 APIやDLL関数に渡す構造体を定義するときは、各メンバのオフセットをDLL側と一致させる必要がある。

 ところが.NET Frameworkでは、通常、アクセスに最適な配置となるように、CLRがメモリ上における構造体の各メンバの配置を決定する。つまり、単純に同じ構造体をC#などで宣言しただけでは、各メンバのオフセットをDLL側と確実に一致させることは不可能である(事実、通常のC#の構造体をWin32 APIやDLL関数に渡そうとすると例外が発生する)。

 従って、Win32 APIやDLL関数に渡す構造体を定義する場合は、そのメンバの配置方法を変更する必要がある。これを実現するには、構造体に対してStructLayout属性(System.Runtime.InteropServices名前空間)を設定する。このとき、配置方法としてLayoutKind列挙体(System.Runtime.InteropServices名前空間)のメンバのいずれかを指定する。C言語などの構造体と同様に、メンバが宣言された順に配置されるようにするには、LayoutKind.Sequentialという値を指定すればよい。

 これらの話を踏まえて、前出のPOINT構造体と等価なものをC#で定義すると次のようになる。

[StructLayout(LayoutKind.Sequential)]
struct POINT {
  public int x;
  public int y;
}

 フィールド(C言語の世界ではメンバ変数と呼ばれる)のスコープはすべてpublicにしておかなければならない。というのは、これらの構造体に対しては、フィールドを直接操作する必要があるからだ。また当然のことながら、各フィールドの型は、対応する.NET Frameworkの型に改める必要がある(具体的な型の対応についてはTIPS:Win32 APIやDLL関数を呼び出すには? を参照)。ここで取り上げているPOINT構造体の場合は、いずれのメンバ変数もLONG型であり、C#ではそれに対応するint型を指定すればよい。

 また、フィールドを宣言する順序は、元のC言語での定義と完全に一致させる必要がある。先にも述べたとおり、各メンバのメモリ上における位置をDLL側と一致させなければならないためだ。もし、異なる順序で宣言してしまうと、それらのメンバがDLL側とは異なる位置に配置されてしまい、DLL側での配置と一致しなくなる。その結果、プログラムが実行できたとしても、予期せぬ動作を引き起こすことにもなるので注意が必要だ。

 構造体を利用した具体的な例を挙げておこう。以下のサンプル・プログラムは、ユーザーに1点の座標と長方形の左上隅、右下隅の座標を入力させ、指定した点が長方形の内側にあるか外側にあるかを出力するプログラムである。ここでは、長方形の内側にあるか外側にあるかの判定に、PtInRectというWin32 APIを利用している。また、ここまでに説明したPOINT構造体以外にRECT構造体も同様の方法で定義している。

// ptinrect.cs

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
struct POINT {
  public int x;
  public int y;
}

[StructLayout(LayoutKind.Sequential)]
struct RECT {
  public int left;
  public int top;
  public int right;
  public int bottom;
}

class PointInRect {
  [DllImport("user32.dll")]
  private static extern bool PtInRect(ref RECT r, POINT p);

  private static int ReadValue(string prompt) {
    Console.Write(prompt + " = ");
    return Int32.Parse(Console.ReadLine());
  }

  private static void Main() {
    RECT r;
    POINT p;
    p.x      = ReadValue("点のx座標");
    p.y      = ReadValue("点のy座標");
    r.left   = ReadValue("長方形の左上隅のx座標");
    r.top    = ReadValue("長方形の左上隅のy座標");
    r.right  = ReadValue("長方形の右下隅のx座標");
    r.bottom = ReadValue("長方形の右下隅のy座標");
    Console.WriteLine(PtInRect(ref r, p) ? "内側" : "外側");
  }
}

// コンパイル方法: csc ptinrect.cs
指定した点が長方形の内側にあるか外側にあるかを判定するプログラム(ptinrect.cs)

 なお、DllImport属性(System.Runtime.InteropServices名前空間)を利用したWin32 APIの呼び出し手順については、TIPS:Win32 APIやDLL関数を呼び出すには? を参照にしていただきたい。

LayoutKind.Explicitと共用体

 LayoutKind列挙体(構造体のメンバの配置方法を表す列挙体)の値の1つに、LayoutKind.Explicitというものがある。この値をStructLayout属性(構造体のメンバの配置方法を設定する属性)のパラメータとして指定すると、各メンバを配置するオフセットをプログラマーが明示的に設定できるようになる。例えば、最初に取り上げたPOINT構造体は、このLayoutKind.Explicitを使って次のように定義することも可能である。

[StructLayout(LayoutKind.Explicit)]
struct POINT {
  [FieldOffset(0)] public int x;
  [FieldOffset(4)] public int y;
}

 この例のように、メンバを配置するオフセットはFieldOffset属性(System.Runtime.InteropServices名前空間)により設定する。

 このとき、異なるメンバに対して同じオフセットを指定することも可能である。従って、これを利用すればC言語の共用体を.NET Frameworkの構造体で記述することも可能になる。例えば、INPUT構造体をC#で定義すると次のようになる(MOUSEINPUT、KEYBOARDINPUT、HARDWAREINPUTの各構造体は別個に定義する必要がある)。

[StructLayout(LayoutKind.Explicit)]
struct INPUT {
  [FieldOffset(0)] public uint type;
  [FieldOffset(4)] public MOUSEINPUT mi;
  [FieldOffset(4)] public KEYBOARDINPUT ki;
  [FieldOffset(4)] public HARDWAREINPUT hi;
}

補足:C言語における構造体の宣言

 C言語で構造体を定義する場合、次のように記述するのが最も基本的な方法である。

struct tagPOINT {
  LONG x;
  LONG y;
};

 この構文はC#での構造体やクラスを定義するときのものとよく似ている。そもそもC#の言語仕様は、C++(C言語を拡張して制定された言語)を基にして策定されたためである。

 ところで、C言語でこの型を参照するとき、つまり変数を宣言する場合や、パラメータや戻り値の型としてこの構造体を指定する場合には、単にtagPOINTと記述するのではなく、struct tagPOINTと記述する必要がある。しかし、この型を参照するたびにstruct tagPOINTと記述するのは可読性に欠けるので、typedefというキーワードを利用して型の別名を定義することが一般に行われている。例えば、次のような定義文を記述すれば、単にPOINTと書くだけでこの構造体を参照できる。

typedef struct tagPOINT POINT;

 C言語では、構造体の定義をtypedef文の内部に記述することも可能である。実際にtagPOINT構造体をtypedef文の内部で定義すると、本稿の最初でPOINT構造体の定義として示した定義文と同じになる。

typedef struct tagPOINT {
  LONG x;
  LONG y;
} POINT;

 ちなみに、typedef文の内部で構造体を定義する場合は、structキーワードの直後に記述する名前(この例だとtagPOINT)は省略可能である。End of Article

カテゴリ:クラス・ライブラリ 処理対象:Win32 API
使用ライブラリ:StructLayout属性(System.Runtime.InteropServices名前空間)
使用ライブラリ:LayoutKind列挙体(System.Runtime.InteropServices名前空間)
使用ライブラリ:FieldOffset属性(System.Runtime.InteropServices名前空間)
使用ライブラリ:DllImport属性(System.Runtime.InteropServices名前空間)
関連TIPS:Win32 APIやDLL関数を呼び出すには?
 
この記事と関連性の高い別の.NET TIPS
Win32 APIやDLL関数を呼び出すには?
実行ファイルからアプリケーションのアイコンを取得するには?
DateTimeとDateTimeOffsetの違いとは?[C#、VB]
列挙体の値を列挙するには?
列挙体の名前を列挙するには?
このリストは、(株)デジタルアドバンテージが開発した
自動関連記事探索システム Jigsaw(ジグソー) により自動抽出したものです。
generated by

「.NET TIPS」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間