カクタスソフトウェア
カクタスソフトウェア
サウンド MIDI マルチメディア アプリケーション

変数の型

ブリッタブル(blittable)変数

2つのシステムが異なるデータ表現を採用している場合でも、中にはまったく同じ形式の変数が存在します。形式が同じなので当然マーシャリングの必要がありません。この様に形式が同じでマーシャリングの必要が無い変数をブリッタブル変数と呼びます。

以下の変数タイプはマネージ環境とアンマネージ環境で表現が同一です。

システムタイプ C/C++ C# 意味
SByte char   sbyte 符号付き8ビット整数
Byte unsigned char BYTE byte 符号なし8ビット整数
Int16 short   short 符号付き16ビット整数
UInt16 unsigned short WORD ushort 符号なし16ビット整数
Int32 long   int 符号付き32ビット整数
Uint32 unsigned long DWORD uint 符号なし32ビット整数
Int64 longlong   long 符号付き64ビット整数
Uint64 unsigned longlong   ulong 符号なし64ビット整数
IntPtr     IntPtr 符号付きポインター
UIntPtr     UIntPtr 符号なしポインター
Single float   float 単精度実数
Double double   double 倍精度実数

ブリッタブル変数を要素とする1次元配列はブリッタブルです。しかし可変長の配列はブリッタブルではありません。ブリッタブル変数への参照はブリッタブルではありません。

非ブリッタブル変数

非ブリッタブル変数はマーシャリングを必要とします。以下に非ブリッタブル変数の例を示します。

システムタイプ C# 意味
Array array 配列
Boolean bool Boolean型
Char char Unicode文字
Class class クラス
Object object オブジェクト
String string 文字列

ブリッタブル型の引数について

引数がブリッタブル型の場合、マーシャラーはマーシャリングを行いません。引数が値型の場合、変数のコピーがスタックに設定されます。引数が参照型の場合は、変数への参照がスタックに設定されます。このとき関数の実行が終了するまで変数の位置が移動させられることがないよう固定されます。

ここで参照型の引数について、以下の点に注意して下さい。

変数への参照が直接相手側に伝えられるため、たとえ方向属性に[Out]が指定されていなくても、呼び出し先で変数を変更することが可能です。ただし関数呼び出しが異なるスレッドに渡る場合など不具合が起こる可能性があり、この操作は常に安全とは言えません。

相手側の関数は、[In]属性としてマーシャリングされた引数を変更すべきではありません。変更が必要なときは必ず[Out]属性を指定して下さい。

ブリッタブル変数で参照渡しされる例として、すべてのメンバー変数がブリッタブル型で構成されたクラスがあります。クラスは常に参照として扱われるため、このクラスを構造体としてマーシャリングした場合、構造体への参照として扱われます。詳しくは後のセクションを参照して下さい。

変数の対応

このセクションでは個々の変数型について利用法や注意点について解説します。複数のC/C++の型がC#側では一つの型に対応するため、見出しをC/C++側の型で構成しました。マーシャリングが必要な変数はもちろん、ブリッタブル変数であってもC/C++とC#の書式の違いからコーディングに注意が必要な場合があります。

以下に変数型を示します。

文化の違い

個々の変数について解説する前に従来からのアンマネージ環境とマネージ環境では変数の扱いに文化的とも言える違いがあるので紹介します。

C/C++では構造体を定義する場合などunsigned属性の変数が多く用いられます。例えばWAVEHDR、WAVEFORMATEX、WAVEOUTCAPSなどのメンバーはほとんどがWORDやDWORDによって構成されています。これは負にならないことが明らかなものはunsigned属性で定義するという方針の様に思われます。以下にWAVEFORMATEXの定義を示します。

typedef struct tWAVEFORMATEX
{
    WORD        wFormatTag;         /* format type */
    WORD        nChannels;          /* number of channels (i.e. mono, stereo...) */
    DWORD       nSamplesPerSec;     /* sample rate */
    DWORD       nAvgBytesPerSec;    /* for buffer estimation */
    WORD        nBlockAlign;        /* block size of data */
    WORD        wBitsPerSample;     /* number of bits per sample of mono data */
    WORD        cbSize;             /* the count in bytes of the size of */
                                    /* extra information (after cbSize) */
} WAVEFORMATEX, *PWAVEFORMATEX, NEAR *NPWAVEFORMATEX, FAR *LPWAVEFORMATEX;

しかしC#ではunsignedでなければならないはっきりした理由がある場合を除きsignedで定義する方針の様に思われます。例えばWaveFormat構造体は以下の様に定義されています。

    public struct WaveFormat
    {
        public WaveFormat();
        public int              AverageBytesPerSecond   { get; set; }
        public short            BitsPerSample           { get; set; }
        public short            BlockAlign              { get; set; }
        public short            Channels                { get; set; }
        public WaveFormatTag    FormatTag               { get; set; }
        public int              SamplesPerSecond        { get; set; }
        public override string  ToString();
    }

C/C++で定義された構造体をC#で使うために移植する場合、正確に変数の属性を反映させても良いですが、この文化の違いを意識してC#のスタイルで実装しても良いかも知れません。またこうした方がunsignedへのキャストが減るため合理的です。

さらにC/C++では構造体の名前はすべて大文字で表しますが、C#では単語の頭だけを大文字にしたしスタイルで表現されます。C#ではタグ名などをすべて大文字で表現する習慣はありません。

HANDLE

HANDLEはデバイスを管理するコントロールユニットへのポインターです。C#にはHANDLEに相当する変数がありませんので、汎用ポインターであるIntPtrで対応します。C/C++では関数の種類に応じて専用のHANDLEを定義することが一般的です。HANDLEの定義にはDECLARE_HANDLEマクロが用いられるので、変数がHANDLE属性かどうかすぐ見分けることができます。DECLARE_HANDLEで定義されている変数タイプは、IntPtrで受けると覚えておけば良いでしょう。以下にいくつかの例を示します。

/* windef.h */
DECLARE_HANDLE(HWND);
DECLARE_HANDLE(HBITMAP);
DECLARE_HANDLE(HDC);
DECLARE_HANDLE(HFONT);
DECLARE_HANDLE(HINSTANCE);
...
/* waveform audio data types */
DECLARE_HANDLE(HWAVEIN);
DECLARE_HANDLE(HWAVEOUT);
/* MMIO data types */
DECLARE_HANDLE(HMMIO);

以下にサンプルを示します。

MMRESULT midiOutShortMsg(HMIDIOUT hmo, DWORD dwMsg);
int midiOutShortMsg(IntPtr hmo, uint dwMsg);

LPVOID

C/C++にはポインター属性の変数型が多数ありますが、C#では場面に応じて使い分ける習慣がありません。以下にいくつか例を示しますが、これらはすべてIntPtrで表現することができます。

IntPtrは一般的なポインターであり、使い方としてC/C++のLPVOIDに似ています。IntPtrが示す内容はプログラムの文脈で異なり、これを管理するのはプログラマーの責任です。IntPtrはより適切な型にキャストして用います。IntPtrが示す内容にマーシャラーが何らかのマーシャリングを行うことはありません。

C/C++からC#へ正式に移植するには、対応するクラスやインターフェースを定義しなければならないケースがあります。しかし変数への参照をIntPtrで受けておけば、それらが未定義でもとりあえずコンパイルを通すことができます。

HRESULT

HRESULTはCOM関連の関数の戻り値として幅広く用いられています。エラーコードの定義も豊富でC#プログラムの中でもHRESULTとしてコーディングしたいところです。しかしC#にはtypedef機能が無いため以下の様に列挙型として定義するしかありません。

public class MyClass
{
    public enum HRESULT : int
    {
        S_OK    = 0x0,
        S_FALSE = 0x1,
        // エラーコードについては多すぎて書ききれない!

しかしHRESULTのエラーコードは複数のファイルに渡って非常に多くの定義があり、これらを統合し完全な定義を得ることは困難です。そこでHRESULTを戻す関数はintを戻すと定義し、必要に応じてエラーコードを定義するようにします。以下にいくつか例を示します。この例では併せて成功・失敗を判断する関数も定義しています。

public class MyClass
{
    public static bool Succeeded(int hr)
    {
        return (hr >= 0) ? true : false;
    }

    public static bool Failed(int hr)
    {
        return (hr < 0) ? true : false;
    }

    public const int    s_ok = 0x0;
    public const int    s_false = 0x1;

    public const int    e_unexpected    = unchecked((int)0x8000ffff);
    public const int    e_notimpl       = unchecked((int)0x80004001);
    public const int    e_outofmemory   = unchecked((int)0x8007000e);
    ...

MMRESULT

MMRESULTはマルチメディア関連のAPIで用いられる戻り値のタイプですが、扱いはHRESULTと同様です。関数はintを戻すと定義します。

BOOL

C/C++のBOOLは柔軟な運用がなされています。数値、ポインターなどあらゆる変数を対象に、値がゼロかゼロでないかによってFALSEかTRUEとして評価されます。変数としてのBOOLは組み込み変数ではなくintとして定義されています。一方C#のboolは組み込み変数で、bool変数どうしの代入しか許されません。これらの違いからBOOLを扱う場合はマーシャリングが必要です。

マーシャラーに対して[MarshalAs(UnmanagedType.Bool)]という指示をします。以下にBOOL変数を含む例を示します。

関数の引数

int MyFunc(BOOL bHoge);
int MyFunc([MarshalAs(UnmanagedType.Bool)] bool hoge);

構造体のメンバー

typedef struct tMYSTRUCT {
    BOOL    bHoge;
} MYSTRUCT;
public struct MyStruct
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool     hoge;
}

文字列

Ansi文字列

LPSTRはAnsi文字列へのポインターです。C#では文字列にstring変数を用います。マーシャラーに対して[MarshalAs(UnmanagedType.LPStr)]という指示を行います。

int MyFunc(LPSTR pHoge);
int MyFunc([MarshalAs(UnmanagedType.LPStr)] string hoge);
構造体で文字列へのポインターが用いられることは比較的少ないと思われます。C/C++の構造体に文字列を含める場合、固定長の文字配列を用意することが一般的です。詳しくは文字配列を参照して下さい。

Unicode文字列

LPWSTRはUnicode文字列へのポインターです。マーシャラーに対しては[MarshalAs(UnmanagedType.LPWStr)]という指示をします。

int MyFunc(LPWSTR pHoge);
int MyFunc([MarshalAs(UnmanagedType.LPWStr)] string hoge);

Unicode文字列へのポインターを示す変数には以下のものがあります。これらはすべてstringに対応します。

BSTR

BSTRもUnicode文字列へのポインターですが、LPWSTRのグループと管理方法が異なります。LPWSTRは単なるUnicode文字列へのポインターですが、BSTRは文字列の前段部分に文字列の長さを示すパラメータが含まれています。マーシャラーに対しては[MarshalAs(UnmanagedType.BStr)]という指示をします。

int MyFunc(BSTR hoge);
int MyFunc([MarshalAs(UnmanagedType.BStr)] string hoge);

デフォルトの動作

一般に文字列へのポインターはC#ではstring型に変換されます。確実にマーシャリングを行うにはMarshalAsディレクティブを指定しますが、使われる環境に応じてデフォルトの動作が決められており、期待する動作がデフォルトで良いときはマーシャラーへの指示を省略することができます。

以下に環境とデフォルトの動作を示します。

環境 デフォルトの動作
プラットフォーム呼び出し UnmanagedType.LPTStr
構造体 規定の動作はないため必ず指定
インターフェイス UnmanagedType.BStr

UnmanagedType.LPTStrは環境に応じて、AnsiであればUnmanagedType.LPStrに、UnicodeであればUnmanagedType.LPWStrに対応します。

VARIANT

VARIANTとはどんな変数も収容できる巨大なunionから成る構造体です。一方C#のobjectはすべての型の源流にあたるもので、どんな変数も代入することができます。VARIANTとobjectは本来全く異質なものですが、マーシャラーはこの2つの仲立ちをして値を代入します。

マーシャラーへの指示に[MarshalAs(UnmanagedType.Struct)]を用います。以下のコードは[In]属性のVARIANTへの参照を引数とする例です。

int MyFunc(/* [in] */ VARIANT *pHoge);
int MyFunc([In, MarshalAs(UnmanagedType.Struct)] ref object hoge);

GUID

GUID(Globally Unique Identifier)はCOMで用いられる16バイトの識別子です。C#では専用の変数Guidが用意されています。Guid変数によってC++側の変数がGUIDであることが分かるので、MarshalAs指定は必要ありません。

GUIDはC/C++側では構造体として定義されています。そのため一般的な構造体としてマーシャリングすることも可能です。しかし対応するマネージ型の変数が用意されている場合は、できるだけそちらを使うようにして下さい。
int MyFunc(/* [in] */ const GUID *pHoge);
int MyFunc([In] ref Guid hoge);

GUIDを表す型には以下のものがあります。これらはすべてGuidに対応します。

REFCLSIDの様にREFが付くものはGUID定数への参照を意味するので、利用方法は以下のようになります。

int MyFunc(/* [in] */ REFCLSID pHoge);
int MyFunc([In] ref Guid hoge);
Guidの様に変更してはいけない型の場合、方向属性に[Out]を指定してはいけません。

配列

Cスタイルの1次元配列

構造体のメンバーに配列を含める場合、マーシャラーへ[MarshalAs(UnmanagedType.ByValArray, SizeConst=N)]という指示を行います。配列の長さを示すためSizeConstディレクティブが必要です。Nは配列の要素数を示す固定値です。以下は32バイトのバッファを確保する例です。

typedef struct tMYSTRUCT
{
    char    hoge[32];
} MYSTRUCT;
public struct MyStruct
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)]
    public byte[]   hoge;
}

通常、プラットフォーム呼び出しで用いる配列は参照として相手側に伝えられます。配列への参照は[MarshalAs(UnmanagedType.LPArray)]という指示を用います。マネージ配列には配列の要素数が記録されています。マーシャラーは配列から要素数を知ることができるので、ここではSizeConstディレクティブは必要ありません。

// 関数テンプレート
int MyFunc(int* pHoge);

// 関数呼び出し
Foo()
{
    int hoge[32];
    MyFunc(hoge);
// 関数テンプレート
int MyFunc([MarshalAs(UnmanagedType.LPArray)] int[] hoge);

// 関数呼び出し
Foo()
{
    int[] hoge = new int[32];
    MyFunc(hoge);
配列の要素数を指定するディレクティブとしてSizeParamIndexがあります。これはアンマネージ環境からマネージ環境の関数をコールするときに使います。アンマネージ配列への参照は単なるポインターに過ぎず、配列がいくつの要素から成るのかマーシャラーは知ることができません。多くの場合、アンマネージ関数は配列への参照に加え配列の要素数を引数として相手側に伝えます。SizeParamIndexはこの要素数を格納した引数が関数の何番目にあたるのかを指定するものです。これによってマーシャラーは配列の要素数を知ることができます。一方マネージ配列では、マーシャラーは配列から要素数を知ることができるのでSizeParamIndexの指定は必要ありません。

配列の要素がブリッタブル型の場合、マーシャラーへはUnmanagedType.ByValArrayやUnmanagedType.LPArrayの様な配列を示す指示だけで構いません。しかし配列の要素がBoolean型や文字列型の様に非ブリッタブルの場合には、ArraySubTypeディレクティブで配列要素のマーシャリングについて指示する必要があります。以下はLPSTR型配列を引数とする関数の例です。なお引数の初期化などのコードは省略しています。

// 関数テンプレート
int MyFunc(LPSTR* pHoge);

// 関数呼び出し
Foo()
{
    LPSTR hoge[32];
    MyFunc(hoge);
// 関数テンプレート
int MyFunc(
    [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStr)] string[] hoge);

// 関数呼び出し
Foo()
{
    string[] hoge = new string[32];
    MyFunc(hoge);

固定長の文字配列

固定長の文字配列が用いられるのは関数の引数と構造体のメンバーですが、このケースではそれぞれ対応が異なります。

文字配列が構造体のメンバーの場合、C#側でstring型として表現することができます。マーシャラーへの指示は[MarshalAs(UnmanagedType.ByValTStr)]を用います。文字列の長さを示すためにSizeConstディレクティブを指定します。

typedef struct tMYSTRUCT
{
    char    hoge[32];
} MYSTRUCT;
public struct MyStruct
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
    public string   hoge;
}

固定長の文字配列を関数の引数として用いるときは、バッファを相手側に渡し文字列を代入してもらうという使い方が一般的です。

一般的な変数はMarshalAsを付けてマーシャリングを行いますが、この例だけは特別で、変数の表現にStringBuilderクラスを用います。StringBuilderは必要とする最大のバッファサイズを指定してnewします。相手側の関数はこのバッファサイズを越えない範囲で文字列を代入することができます。

マーシャラーはデフォルトでStringBuilder型の変数に、[In,Out]双方向のマーシャリングを行います。

// 関数テンプレート
int MyFunc(LPSTR pHoge);

// 関数呼び出し
Foo()
{
    char hoge[32];
    MyFunc(hoge);
// 関数テンプレート
int MyFunc(StringBuilder hoge);

// 関数呼び出し
MyFunc()
{
    StringBuilder hoge = new StringBuilder(32);
    MyFunc(hoge);

文字列をstring型あるいはStringBuilder型で受けた場合、表現できる最大の文字数はバッファサイズ-1になることに注意して下さい。これは文字の終わりを示すNULL文字が必ず最後に付加されるためです。

string型は、ファイル名・デバイス名・フィルター名・ジャンル名などの識別を行う、比較的長い文字列に対して使うべきです。1バイト〜4バイト程度でIDの識別に使われる文字列には使うべきではありません。例えば3バイトのエリアで”ID3”の識別子を持つ変数をstring型で受けると、”ID”しか表現できず正しい処理ができません。このケースでは3バイトのbyte配列とするべきでしょう。

多次元配列

マーシャラーは2次元以上の複雑な配列は支援しません。2次元以上の配列を必要とするときは、1次元配列として解釈し直したものを用います。以下に例を示します。

typedef struct tMYSTRUCT
{
    double  hoge[4][3][2];
} MYSTRUCT;

この例ではSizeConstは4*3*2=24となります。C#側で多次元配列としてアクセスするにはヘルパー関数を定義するなどの工夫が必要です。

public struct MyStruct
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=24)]
    public double[] hoge;
}

デリゲート

例えばタイマー関数のようにコールバック関数を登録すると必要に応じて登録した関数を呼び出してくれるものがあります。以下はtimeSetEventの定義と使い方を簡単に示したものです。

TIMECALLBACKで定義された関数仕様のTimerCallback関数を定義し、timeSetEventに渡します。timeSetEventが実行されるとTimerCallback関数が100mSec毎に呼び出されます。

// 関数テンプレート
typedef void (CALLBACK TIMECALLBACK)(
    UINT            uTimerID,
    UINT            nMsg,
    DWORD_PTR       dwUser,
    DWORD_PTR       dwParam1,
    DWORD_PTR       dwParam2);
typedef TIMECALLBACK FAR *LPTIMECALLBACK;

WINMMAPI MMRESULT WINAPI timeSetEvent(
    UINT            uDelay,
    UINT            uResolution,
    LPTIMECALLBACK  lpTimeProc,
    DWORD_PTR       dwUser,
    UINT            fuEvent);

// 関数呼び出し
void CALLBACK TimerCallback(
    UINT            uTimerID,
    UINT            nMsg,
    DWORD_PTR       dwUser,
    DWORD_PTR       dwParam1,
    DWORD_PTR       dwParam2)
{
    // TODO
}

MyFunc()
{
    UINT uTimerID = timeSetEvent(
        100,
        100,
        TimerCallback,
        0,
        TIME_PERIODIC);
}

C/C++のコールバック関数をC#に移植するにはdelegateという仕組みを用います。以下にTIMECALLBACKのC#版を示します。

delegateを用いてコールバック関数のテンプレートを定義します。TimeCallbackはC/C++のTIMECALLBACKを移植したものです。MyTimerCallbackはテンプレートに沿って作成されたコールバック関数です。コールバック関数本体はstaticでなければなりません。timeSetEventの引数としてTimeCallback型デリゲートのインスタンスを作成します。引数は呼び出される関数MyTimerCallbackです。

public delegate void TimeCallback(
    int             timerID,
    int             msg,
    IntPtr          instance,
    IntPtr          param1,
    IntPtr          param2);

[DllImport("winmm.dll")]
public static extern int timeSetEvent(
    int             uDelay,
    int             uResolution,
    TimeCallback    lpTimeProc,
    int             dwUser,
    TimeEvent       fuEvent);

static void MyTimerCallback(
    int             timerID,
    int             msg,
    IntPtr          instance,
    IntPtr          param1,
    IntPtr          param2)
{
    // TODO
}
マネージ環境から見るとtimeSetEventはプラットフォーム呼び出しになります。プラットフォーム呼び出しを行う方法は後で詳しく説明するのでここでは触れません。
public partial class MainForm : Form
{
    private TimeCallback timeCallback = new TimeCallback(MyTimerCallback);

    void MyFunc()
    {
        int timerID = timeSetEvent(
            100,
            100,
            timeCallback,
            0,
            TimeEvent.Periodic);

デリゲートのインスタンスはデリゲートの運用が終了するまで保存しなければなりません。上記の例ではtimeCallbackがそれにあたります。コールバック関数が呼び出されている間はtimeCallbackがガベージコレクタに回収されないよう変数のスコープに注意して下さい。上記の例ではMainFormのメンバーとすることで変数を保存しています。ガベージコレクタに回収された後、コールバック関数が呼び出されるとプログラムがクラッシュします。

専用フラグ

APIの動作を指定するために特別なフラグを必要とする場合があります。例えば上記のtimeSetEventの5番目の引数などがそうです。以下にMMSystem.hの定義を示します。

#define TIME_ONESHOT    0x0000   /* program timer for single event */
#define TIME_PERIODIC   0x0001   /* program for continuous periodic event */

これらの数値をC#側で使えるようにするため以下のような列挙型を定義します。

public enum TimeEvent
{
    OneShot     = 0x0000,       // program timer for single event
    Periodic    = 0x0001,       // program for continuous periodic event
}

高次の参照

マーシャラーは[In]属性・[Out]属性共にポインターへのポインターの様な高次の参照をサポートしません。高次の参照が必要な時は、とりあえずIntPtrで受け、後にプログラムの中で解釈することになります。

以下の例は、構造体へのポインターへのポインターの例です。

typedef struct tPOINT
{
    int     x;
    int     y;
} POINT;

int MyFunc(/* [out] */ POINT **ppHoge);
public struct Point
{
    int     x;
    int     y;
}

int MyFunc([Out] out IntPtr hoge);
構造体をクラスとして実装すれば、構造体への参照への参照を当該クラスへの参照として表現することができます。詳しくはセクション「クラスの利用」を参照して下さい。

ドキュメントの先頭へ

カクタスソフトウェア 技術協力 資料室 資料室の広場 SourceForge.jp お問い合わせ