C#からClipboardを操作する

最近 C# から Clipboard をゴニョゴニョするプログラムを作る必要があったため,いろいろと調べてみました(VS2005sp1 + .NET Framework 2.0).


.NET Framework では,Clipboard クラス (System.Windows.Forms) として Clipboard に対する操作がまとめられており,簡単にデータの設定/取得を行うことができます.
Clipboard クラスを使用したサンプルは,DOBON.NET: VB.NET, C#, 無料ソフトウェア... にて紹介されています.
例:クリップボードにファイルをコピーまたは切り取りをする、クリップボードからファイルを取得する、貼り付ける: .NET Tips: C#, VB.NET


しかし,Clipboard に対するイベントは Clipboard クラスに定義されていないため,Clipboard の監視を行いたい場合は,API を使って自分で何とかするしかありません.調べてみると,なんと Clipboardの監視はできない? − Insider.NET − @IT にコードが載っています.ありがたや(^_^;

また,英語ですが Using the Clipboard の "Creating a Clipboard Viewer Window" という項目でも,API ベースではありますが Clipboard Viewer のサンプルコードが紹介されており,参考になります(MSDN ライブラリの方では日本語資料があるのかな?).

今回は,テキストデータや画像データだけではなく,Officeアプリケーション上でコピーした内容も扱えるようにしようと考えました.


string[] IDataObject.GetFormats(); で,変換可能な形式を取得してみると...
Power Point の描画オブジェクトやクリップアートをコピーすると,

Office Drawing Shape Format
PNG+Office Art
JFIF+Office Art
GIF+Office Art
Object Descriptor
PNG
JFIF
GIF
MetaFilePict
EnhancedMetafile
PowerPoint 8.0 Internal Slides
ActiveClipboard
System.Drawing.Bitmap
Bitmap

Power Point のスライドをコピーすると,

Embedded Object
Object Descriptor
EnhancedMetafile
MetaFilePict
PNG
JFIF
GIF
Link Source
Link Source Descriptor
PowerPoint 8.0 Internal Slides
ActiveClipboard

Excelのセルをコピーすると,

1回目:Clipboardを空にしてくる
2回目:
EnhancedMetafile
MetaFilePict
System.Drawing.Bitmap
Bitmap
Biff8
Biff5
BIFF4
Biff3
Biff
SymbolicLink
Wk1
DataInterchangeFormat
XML Spreadsheet
HTML Format
System.String
UnicodeText
Text
Csv
Rich Text Format
Embed Source
Object Descriptor
Link Source
Link Source Descriptor
Link
Format129

Excelのグラフをコピーすると,

EnhancedMetafile
Embed Source
Object Descriptor
Link Source
Link Source Descriptor
Link
(上記の内容で,2回更新される)

(なんだか,見慣れない名前のフォーマットがいくつかあります)


例えば,Clipboardの内容をOffice描画オブジェクトとして扱いたいのであれば,

IDataObject data = Clipboard.GetDataObject();
if (data.GetDataPresent("Office Drawing Shape Format"))
{
    MemoryStream memory = (MemoryStream)data.GetData("Office Drawing Shape Format");    // 取得

    Clipboard.SetText("ダミーテキスト");

    Clipboard.SetData("Office Drawing Shape Format", memory);    // 設定
}

のように,MemoryStreamとして取り扱うことができます(キャストする型はGetTypeで調べてください).


さて,これらをどう扱っていこうかなぁ...( ´Д`)

ただ,問題がありまして,DataFormats.EnhancedMetafile を指定して GetData すると null が返ってきます(Microsoft Power Point のオブジェクトをコピーしたとき等).場合によっては,例外が発生します.

IDataObject data = Clipboard.GetDataObject();
if (data.GetDataPresent(DataFormats.EnhancedMetafile))
{
    object obj = data.GetData(DataFormats.EnhancedMetafile);    // nullが返ってくる
}


MSDN 曰く,

クリップボードでメタファイル形式を使用する場合には、特別な配慮が必要な場合があります。DataObject クラスの現在の実装における制限により、.NET Framework で使用されるメタファイル形式は、旧メタファイル形式を使用するアプリケーションでは認識されない場合があります。この場合は、Win32 クリップボード アプリケーション プログラミング インターフェイス (API: Application Programming Interface) で相互運用する必要があります。詳細については、http://support.microsoft.com にある Microsoft サポート技術情報の文書 323530 (「Metafiles on Clipboard Are Not Visible to All Applications」) を参照してください。

だそうな.

この件については,先ほどの DOBON.NET: VB.NET, C#, 無料ソフトウェア... でも紹介されています.


ならば,ということで,上記2つのページを参考に,Clipboard への Metafile の設定/取得メソッドを実装してみたところ,"取得"のほうは(おそらく)うまく行きましたが(試しに Metafile.GetThumbnailImage を用いてサムネイルを表示させると,正しく表示される,ぐらいしか確認はしていませんが),"設定"のほうはうまく行きませんでした.


まず,PRB: Metafiles on Clipboard Are Not Visible to All Applications に載っているサンプル通りに実装すると,

    hEMF2 = CopyEnhMetaFile(hEMF, new IntPtr(0));

が,"hEMF2 == IntPtr.Zero" を返してきます.
また,"hEMF" をダイレクトに(hEMF2 = hEMF; みたいにして)SetClipboardData 関数へ渡すと,今度は SetClipboardData 関数が NULL を返してきます.


さらに,Clipboard クラスの SetData メソッドを使用しても,Officeアプリケーション側からは"認識できないファイル",または"Windows メタファイル"とか名前が出ても実際は貼り付けられない等の状態になってしまいます.

Metafile.Saveメソッドを用いて,MemoryStreamに格納しようとしても,encoder が null だよと例外が飛んでくる.

ぬぬぬ...(`皿´)

Excelを操作するときのIndexは...

ExcelのRowやColumnを操作するときは,indexが1から始まるのをすっかり忘れていて,実行してエラーが出るまで気づかずコーディングしていた...orz
1からなんだよね.0じゃないんだったそうでした.

using Excel = Microsoft.Office.Interop.Excel;

// ...

using (ComRef<Excel.Range> xlRange_i = new ComRef<Excel.Range>(xlAreas.Obj.get_Item(i)))
{
    List<List<string>> data = new List<List<string>>();
    for (int row = 1; row <= xlRange_i.Obj.Row; ++row)
    {
        List<string> line = new List<string>();
        for (int col = 1; col <= xlRange_i.Obj.Column; ++col)
        {
            using (ComRef<Excel.Range> xlCell = new ComRef<Excel.Range>( (Excel.Range)xlRange_i.Obj[row, col]) )
            // ...

ComRefについては,http://d.hatena.ne.jp/KrdLab/20070102/1167666799をご覧ください.

C#でExcelを操作する

C#からExcelを操作する必要が生じたため,その辺をWebで調べてみました.
一番基本となる情報はこれらではないでしょうか.

COMといえば,"Release"なのですが,このサイトのサンプルには見あたりません.
調べてみると,やはり自分でやった方が良いようです(ガベージコレクタが回収するのはいつかわかりませんし).

いくつかの情報源で,「明示的にCOMオブジェクトを解放しないと,Excelプロセスが残る」とあったのですが,私の環境では,明示的に解放しなくてもプロセスは消滅していました.
(OS: WindowsXP Professional sp2, Visual Studio 2005)
だからといって,明示的に解放しなくても良い,ということにはならないと思いますが...(調査してみないと)


総合すると,

  • COMのMicrosoft Excel 11.0 Object Libraryを使用する
  • 省略可能なパラメータには,System.Reflection.Missing.ValueかType.Missingを使用する
  • COMなので参照カウントに注意する

ということです.Microsoftのサイトにあるサンプルそのままではいけないということか.
しかし,先に挙げたサイトのサンプルを見るとわかるのですが,try-finallyの深いネストができあがってしまいます(仕方がないのですが...).


とりあえず応急処置として,ComRefなるものを作成し,ほんの少しだけネストを解消してみました.

public sealed class ComRef<T> : IDisposable where T : class
{
    private T _obj;

    public T Obj
    {
        get { return this._obj; }
    }

    public ComRef(T obj)
    {
        this._obj = obj;
    }

    ~ComRef()
    {
        Dispose(false);
    }

    void IDisposable.Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            // managed objects
        }

        // unmanaged objects
        if (this._obj != null)
        {
            if (System.Runtime.InteropServices.Marshal.IsComObject(this._obj))
            {
                System.Runtime.InteropServices.Marshal.ReleaseComObject(this._obj);
            }
            this._obj = null;
        }
    }
}


じゃんぬ様のサイトより,
http://jeanne.wankuma.com/tips/programing/releasecom.html
このサンプルコードを,ComRefを使って書き直してみると,以下のようになります.

ComRef<Excel.Application> xlApplication = null;

using (xlApplication = new ComRef<Excel.Application>(new Excel.Application()))
{
    xlApplication.Obj.DisplayAlerts = false;

    using (ComRef<Excel.Workbooks> xlBooks = new ComRef<Excel.Workbooks>(xlApplication.Obj.Workbooks))
    using (ComRef<Excel.Workbook> xlBook = new ComRef<Excel.Workbook>(xlBooks.Obj.Add(string.Empty)))
    {
        using (ComRef<Excel.Sheets> xlSheets = new ComRef<Excel.Sheets>(xlBook.Obj.Worksheets))
        using (ComRef<Excel.Worksheet> xlSheet = new ComRef<Excel.Worksheet>( (Excel.Worksheet)xlSheets.Obj[1]) )
        using (ComRef<Excel.Range> xlCells = new ComRef<Excel.Range>(xlSheet.Obj.Cells))
        using (ComRef<Excel.Range> xlRange = new ComRef<Excel.Range>( (Excel.Range)xlCells.Obj[6, 4]) )
        {
            xlApplication.Obj.Visible = true;

            System.Threading.Thread.Sleep(1000);

            xlRange.Obj.Value2 = "あと 1 秒で終了します";

            System.Threading.Thread.Sleep(1000);
        }

        xlBook.Obj.Close(Missing.Value, Missing.Value, Missing.Value);
    }

    xlApplication.Obj.Quit();
}

うーん,".Obj"が嫌だなぁ...


しかし今回の内容,2年くらい前の話題のようで...
自分の鈍さに自己嫌悪...

Serialization

今開発中のプログラムで,シリアライズ機能が必要になったため,MSDNを中心に調べてテストしてみました.
シリアル化:http://msdn2.microsoft.com/ja-jp/library/7ay27kt9(VS.80).aspx
SerializableAttribute:http://msdn2.microsoft.com/ja-jp/library/system.serializableattribute(VS.80).aspx
ISerializable:http://msdn2.microsoft.com/ja-jp/library/system.runtime.serialization.iserializable(VS.80).aspx


シリアル化の定義
http://msdn2.microsoft.com/ja-jp/library/72hyey7b(VS.80).aspxより抜粋

シリアル化は、オブジェクトの状態をストレージ メディアに保存するプロセスとして定義されています。
このプロセスの間に、オブジェクトのパブリック フィールドとプライベート フィールド、およびクラス名が、バイトのストリームに変換され、データ ストリームに書き込まれます。
その後、シリアル化されたオブジェクトを逆シリアル化すると、元のオブジェクトの正確なクローンが作成されます。


シリアル化するためには,その対象となるクラスにSerializable属性をつける必要があります.
またその方法には,BinaryFormatter,SoapFormatter,XMLSerializerの3つがあります.

BinaryFormatter
http://msdn2.microsoft.com/ja-jp/library/system.runtime.serialization.formatters.binary.binaryformatter(VS.80).aspx
シリアル化は,各メンバについて,private,publicに関係なく行われる.
.NET Framework Version1.1と2.0とで,互換性がある.


SoapFormatter
http://msdn2.microsoft.com/ja-jp/library/system.runtime.serialization.formatters.soap.soapformatter(VS.80).aspx
シリアル化は,各メンバについて,private,publicに関係なく行われる.
.NET Framework Version1.1と2.0とで,互換性がない.


XMLSerializer
http://msdn2.microsoft.com/ja-jp/library/system.xml.serialization.xmlserializer(VS.80).aspx
シリアル化は,publicフィールドとプロパティだけ.
propertyの場合,set,getの両方が定義されている必要がある.


バイナリシリアル化はオブジェクトを維持するために使用し,XMLシリアル化はデータの共有(外部とやりとり可能なpublicだけなので)に使用します.
また,バージョントレラントなシリアル化(VTS : Version Tolerant Serialization)をサポートしているようです.知りませんでした.
VTS:http://msdn2.microsoft.com/ja-jp/library/ms229752(VS.80).aspx
要は,古いバージョンの保存ファイル(シリアル化されたオブジェクトのファイル)があっても,気にせずにシリアライズ対象のクラスを変更できるということです(ファイルに保存されていないメンバは無視されるので).
なお,BinaryFormatterであれば,VTS機能の全てがサポートされています.

C# 3.0

今更ながら,C#の次期バージョン3.0について少し知りたくなたので,Webで情報をあさってみました.


手っ取り早く全体を把握するには,「Overview of C# 3.0」を読むのが良さそうです(というか,日本語版があることに,後で気がついた...orz).
ざっと目を通した限りでは,型推論の強化(varですって!),ユーザによるクラスメソッドの拡張(メソッドを後から追加しているように見せかけることができる),ラムダ式(やったー),初期化記法の拡張,匿名型,SQLみたいな式が書ける等々...


他にも,紹介記事がいろいろあります(1年前の話題ですし...orz).目を通しておかないと.


う〜ん,世の中の流れに追いつけていません.もっと努力します.

はまった...

.NET FrameworkのRichTextBoxコントロールで,

string wd = "検索ワード";
int spos = 0;
int fpos = -1;
while (-1 < (fpos = this.richTextBox.Find(wd, spos, RichTextBoxFinds.WholeWord)))
{
    // いろいろ...
    spos = fpos + wd.Length;
}

とやると,なぜか無限ループになってしまいました.
どうやらこれが原因らしいです.
Find メソッド、開始パラメーターが、最後のパラメーターと等しい場合リッチテキスト ボックス コントロールの文字列全体を検索します。

public int Find (string str, int start, int end, RichTextBoxFinds options)

この関数のendにLength+1を指定して,無事解決しました.( ´Д`)

P/Invoke時におけるマーシャラの動作(マーシャリング)

P/Invoke において,データをC#側からアンマネージ側へ渡す,またはアンマネージ側から受け取るとき,どのような動作をしているのかについて調べてみました.

まず P/Invoke について,MSDN の中で参考になりそうなページを挙げます.
クラス、構造体、および共用体のマーシャリング
文字列のマーシャリング
型の配列のマーシャリング
各種のマーシャリング
プラットフォーム呼び出しのデータ型


これらのドキュメントを読んでいると,値型,参照型,blittable 型,非 blittable 型といった,型に関する用語が出てきます.
以下にまとめてみました (blittable の語源だけはわかりませんでしたが...).

値型 (値型)

値型とは,System.ValueType から派生する型のことであり,インスタンスがスタック上に割り当てられます.
また値型の変数は,参照型の変数のようにオブジェクトへの参照を持つのではなく,変数がデータを直接格納します(newする必要がないということ).
値型には,

があります.

その他参考:http://msdn2.microsoft.com/ja-jp/library/34yytbws(VS.80).aspx

参照型

参照型は,値型以外の型のことであり,インスタンスがヒープ上に割り当てられます.
また参照型の変数は,ヒープ上のオブジェクトへの参照を格納します.
参照型には,

があります.
さらに自己記述型は,配列型とクラス型の2つに分けられます.
クラス型とは,ユーザー定義のクラス,ボックス化された値型,およびデリゲートのことを指します.

その他参考

http://msdn2.microsoft.com/ja-jp/library/3ewxz6et(VS.80).aspx
http://msdn2.microsoft.com/ja-jp/library/zcx1eb1e(VS.80).aspx
http://msdn2.microsoft.com/ja-jp/library/2hf02550(VS.80).aspx


また,上記のような分類とは別に,マーシャリングの観点から以下のような分類ができます.

Blittable 型

blittable 型とは,マネージメモリ上でも,アンマネージメモリ上でも表現が共通している型のことをいいます.
blittable 型の一覧は,下記ページに記載されています.
http://msdn2.microsoft.com/ja-jp/library/75dwhxf7(VS.80).aspx

また,これらの一次元配列や,blittable 型のみを含む書式指定された (StructLayout 属性で Sequential またはExplicit と指定された) 値型およびクラス型も blittable 型です (多次元配列は異なるようです).

非 Blittable 型

非 blittable 型とは,マネージメモリ上とアンマネージメモリ上とでは表現が異なる,または曖昧 (条件によって同じであったり異なったりする) である型のことをいいます.
非 blittable 型をマネージコードとアンマネージコードの間でマーシャリングするときは,データ表現を変換する必要が生じることがあります.
http://msdn2.microsoft.com/ja-jp/library/75dwhxf7(VS.80).aspx

In/Out 属性

P/Invoke 宣言でパラメータの方向属性を指定するものに,InAttribute,OutAttribute があります.
http://msdn2.microsoft.com/ja-jp/library/77e6taeh(VS.80).aspx

以下,「呼び出し先」はアンマネージDLLの関数,「呼び出しもと」はC#側のことを指しています.

  • InAttribute

http://msdn2.microsoft.com/ja-jp/library/system.runtime.interopservices.inattribute(VS.80).aspx
MSDNからの引用です.

呼び出し元から呼び出し先へデータをマーシャリングするが、逆方向にはマーシャリングしないことを示します。
...
InAttribute と OutAttribute を組み合わせると、配列型および書式付きの非 blittable 型に適用する場合に、特に役立ちます。
両方の属性を適用した場合にだけ、呼び出し元は、呼び出し先がこれらの型に対して加えた変更を参照できます。
これらの型はマーシャリング中にコピーを要求するため、InAttribute と OutAttribute を使用して、不要なコピーを減らすことができます。

  • OutAttribute

http://msdn2.microsoft.com/ja-jp/library/system.runtime.interopservices.outattribute(VS.80).aspx
MSDNからの引用です.

呼び出し先から呼び出し元へ、データをマーシャリングすることを示します。
...
参照渡しの値型または参照型に OutAttribute を適用することによって、In/Out の動作を Out のみの動作に変更できます。
これは、C# で out キーワードを使用することに相当します。
たとえば、値渡しの配列 (既定で In のみのパラメータとしてマーシャリングされる) は、Out のみに変更できます。
ただし、相互運用マーシャラは固定を使用するため、要素またはフィールドがすべて blittable の型の場合、この動作は期待されるセマンティクスを提供しないことがあります。
データを呼び出し先に渡すかどうかが問題にならない場合、非 blittable 型については Out のみのマーシャリングの方がパフォーマンスに優れています。


マーシャラは,パラメータの型やその修飾子に基づいて方向を決定します.
マーシャラの既定の動作を変更する場合に,方向属性を指定するようですね.

ref/out 修飾子

通常はオブジェクトが参照渡しされますが,ref 修飾子を使用すると引数が参照渡しされます (引数として渡された変数そのものがメソッドの中に入っていくイメージ).
そのため,メソッド内部で引数に加えられた変更は,制御が呼び出し元のメソッドに戻された時点で変数に反映されます (メソッド内部で,変数の参照するオブジェクトを別のオブジェクトにする等).
ただし,ref パラメータとして渡される引数は,事前に初期化されている必要があります.

  • 補足

ref 修飾子と類似するものに,out 修飾子があります.
out 修飾子は,メソッド内部で引数にオブジェクトへの参照が格納されることを保証します.
引数として渡す変数は,事前に初期化する必要がありませんが,呼び出し先のメソッド内部では,制御が呼び出し元へ戻る前に値を代入しなければなりません.

コピーと固定 (http://msdn2.microsoft.com/ja-jp/library/23acw07k(VS.80).aspx)

いよいよ本題です.

値型の値渡し (値型をそのまま渡した場合)


アンマネージ側:

extern "C" {
__declspec(dllexport) int __stdcall Test_Int(int n)
{
    return -1 * n;
}

struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct(BlittableStruct bs)
{
    bs.a = -1;
    bs.b = -1;
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_Int")]
public static extern int Test_Int(int n);

[StructLayout(LayoutKind.Sequential)]
public struct BlittableStruct
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct")]
public static extern void Test_BlittableStruct1(BlittableStruct bs);

動作説明
データが直接スタックに渡される (コピー).
何も指定しないと,マーシャラの既定動作は In である.
呼び出し先の引数の形式は XXX x でなければならない.

値型の参照渡し (ref 修飾子を付けた場合)


アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_Int_Ptr(int* p)
{
    *p = -1;
}

struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct_Ptr(BlittableStruct* p)
{
    p->a = -1;
    p->b = -1;
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_Int_Ptr")]
public static extern void Test_Int_Ptr(ref int n);

[StructLayout(LayoutKind.Sequential)]
public struct BlittableStruct
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_Ptr")]
public static extern void Test_BlittableStruct_Ptr1(ref BlittableStruct bs);

...

int n = 0;
Test_Int_Ptr(ref n);

BlittableStruct bs;
bs.a = 9;
bs.b = 9;
Test_BlittableStruct_Ptr1(ref bs);

動作説明
アンマネージヒープ上にオブジェクトがコピーされ,そのオブジェクトへのポインタがスタックに渡される.
何も指定しないと,マーシャラの既定動作は In/Out である.
呼び出し先の引数の形式は XXX* p でなければならない.
補足
呼び出し先の引数の形式が XXX** pp であっても参照渡しは成功するが,アンマネージ側に渡される値は,アンマネージ側の意図する値ではない (pp がオブジェクトを指すポインタへのポインタではなく,オブジェクトへのポインタになってしまう).
【例】
アンマネージ側:

extern "C" {
struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};
__declspec(dllexport) void __stdcall Test_BlittableStruct_PtrPtr(BlittableStruct** pp)
{
    (*pp) = new BlittableStruct();
    (*pp)->a = -1;
    (*pp)->b = -1;
}
}

マネージ側:

[StructLayout(LayoutKind.Sequential)]
public struct BlittableStruct
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_PtrPtr")]
public static extern void Test_BlittableStruct_PtrPtr1(ref BlittableStruct bs);
...
BlittableStruct bs;
bs.a = 9;
bs.b = 9;
Test_BlittableStruct_PtrPtr1(ref bs);

// この呼び出し後,bs.aはアンマネージ側でnewされたオブジェクトのアドレスが格納され,bs.bは9のままである.
// イミディエイトウィンドウで確認すると,
//   *((BlittableStruct*)bs.a)
//   {Invoker.Tester1.BlittableStruct}
//   a: 0xffffffff    // ← -1
//   b: 0xffffffff    // ← -1
// のように,bs.aの指すオブジェクトに,アンマネージ側の変更が反映されている.

参照型の値渡し (参照型をそのまま渡した場合)

blittable 型と非 blittable 型とで,動作が変わります.
例 (blittable 型)
アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_Blittable_Array(int* p)    // 配列のサイズも渡すべき
{
    p[0] = -1;    // 当然のことながら,オーバーランには十分注意する必要がある
}

struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct_Ptr(BlittableStruct* p)
{
    p->a = -1;
    p->b = -1;
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint="Test_Blittable_Array")]
public static extern int Test_Blittable_Array(int arr);

[StructLayout(LayoutKind.Sequential)]
public class BlittableStruct2
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_Ptr")]
public static extern void Test_BlittableStruct_Ptr2(BlittableStruct2 bs);
...
int arr = new int[10];
...
Test_Blittable_Array(arr);
// ↑この呼び出しで,arrが直接変更されてしまう.

BlittableStruct2 bs2 = new BlittableStruct2();
Test_BlittableStruct_Ptr2(bs2);
// ↑この呼び出しで,bs2が直接変更されてしまう.

例 (非 blittable 型)
アンマネージ側:

extern "C" {
struct __declspec(dllexport) NonBlittableStruct
{
    char* str;
};

__declspec(dllexport) void __stdcall Test_NonBlittableStruct_Ptr(NonBlittableStruct* p)
{
    p->str = "hoge hoge!";
}
}

マネージ側:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public class NonBlittableStruct2
{
    public string str;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_NonBlittableStruct_Ptr")]
public static extern void Test_NonBlittableStruct_Ptr2(NonBlittableStruct2 bs);

...

NonBlittableStruct2 nbs2 = new NonBlittableStruct2();
nbs2.str = "Let's Go!";
Test_NonBlittableStruct_Ptr2(nbs2);
// ↑blittableの時とは異なり,nbs2が直接変更されることはない.
// [Out]を指定すると,マーシャリングにより変更が反映される

動作説明

  • blittable 型:固定され,ポインタがスタックへ渡される
  • 非 blittable 型:アンマネージ側へコピーされ,アンマネージヒープ上のオブジェクトへのポインタがスタックへ渡される

何も指定しないとマーシャラの既定動作はInである.
しかし,blittable 型の場合,変更が呼び出し元のオブジェクトへ反映されてしまう危険性がある (In/Out のようにみえるが,属性的には In のみであるため,おかしい).
もしアンマネージ側のコードが変更を加えるものであれば,明示的に Out 属性を指定しなければならない.
なお非 blittable 型の場合,明示的に Out 属性を指定しないと反映されない.
また,呼び出し先の形式は XXX* p でなければならない.
値型の参照渡しの時と同様,XXX** pでも呼び出しは成功するが,正しくない.

参照型の参照渡し (ref 修飾子を付けた場合)


アンマネージ側:

extern "C" {
struct __declspec(dllexport) BlittableStruct
{
    int a;
    int b;
};

__declspec(dllexport) void __stdcall Test_BlittableStruct_PtrPtr(BlittableStruct** pp)
{
    (*pp) = new BlittableStruct();
    (*pp)->a = -1;
    (*pp)->b = -1;
}
}

マネージ側:

[StructLayout(LayoutKind.Sequential)]
public class BlittableStruct2
{
    public int a;
    public int b;
}

[DllImport("Unmanaged.dll", EntryPoint = "Test_BlittableStruct_PtrPtr")]
public static extern void Test_BlittableStruct_PtrPtr2(ref BlittableStruct2 bs);
...
BlittableStruct2 bs2 = new BlittableStruct2();
Test_BlittableStruct_PtrPtr2(ref bs2);

動作説明
呼び出し先へは,「アンマネージヒープ上へコピーされたオブジェクトへのポインタ」を指すポインタが渡される.
呼び出し先から戻るときは,アンマネージヒープ上のオブジェクトの内容を,マネージヒープ上のオブジェクトにコピーすることで,アンマネージ側の変更を反映させる.
何も指定しないとマーシャラの既定動作はIn/Outである.
呼び出し先の形式は XXX** pp でなければならない.

System.String と System.Text.StringBuilder

通常の動作とは異なる部分だけを説明します.
System.String の値渡し

アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_AnsiString(char* str)            // const char* str とすべき,サイズを渡すべき
{
    printf("str = %s\n", str);
}

__declspec(dllexport) void __stdcall Test_UnicodeString(wchar_t* str)    // const wchar_t* str とすべき,サイズを渡すべき
{
    str[0] = 'Z';    // このコードは危険!!
}
}

マネージ側:

[DllImport("Unmanaged.dll", CharSet = CharSet.Ansi)]
public static extern void Test_AnsiString([In,Out] string str);

[DllImport("Unmanaged.dll", EntryPoint = "Test_UnicodeString", CharSet = CharSet.Unicode)]
public static extern void Test_Val_UnicodeString(string str);
...
string str = "abc";
Test_AnsiString(str);
Test_Val_UnicodeString(str);    // この呼び出しは危険!

動作説明
CharSet.Unicode として値渡しするときに限り,内部バッファのポインタが呼び出し先へと渡される.
しかし,呼び出し先で内部バッファを変更してしまうと,呼び出し元へ戻ってきた後の String オブジェクトの動作がおかしくなってしまう (String オブジェクトは不変オブジェクト).
呼び出し先で変更を加えたいときは,適切なサイズを確保したStringBuilderオブジェクトを値渡しすべき.
CharSet.Ansi として値渡しするときは,呼び出し側へコピーが渡され,かつ Out 属性を指定してもアンマネージ側で加えられた変更は反映されない (String オブジェクトは不変オブジェクトなので).


System.String の参照渡し

アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_UnicodeString_Ptr(wchar_t** pstr)
{
    (*pstr)[0] = 'Z';
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_UnicodeString_Ptr", CharSet = CharSet.Unicode)]
public static extern void Test_Ref_UnicodeString_Ptr(ref string str);
...
string str = "abc";
Test_Ref_UnicodeString_Ptr(ref str);
// str == "Zbc",呼び出し前/後で,strの指すオブジェクトが変化する(ポインタで確認)

動作説明
呼び出し先から制御が戻ってきたとき,アンマネージヒープ上のオブジェクトの内容をマネージヒープ上のオブジェクトにコピーするのではなく,新しい String オブジェクトを生成し,そこにコピーする.
参照渡しした変数は,新しいオブジェクトへの参照を持つことになる (イミディエイトウィンドウで String オブジェクトのポインタを確認すると,呼び出しの前後で変化している).


System.Text.StringBuilder の値渡し

アンマネージ側:

extern "C" {
__declspec(dllexport) void __stdcall Test_UnicodeString(wchar_t* str)
{
    str[0] = 'Z';
}
}

マネージ側:

[DllImport("Unmanaged.dll", EntryPoint = "Test_UnicodeString", CharSet = CharSet.Auto)]
public static extern void Test_Val_UnicodeStringBuilder(StringBuilder sb);
...
StringBuilder sb = new StringBuilder(100);
Test_Val_UnicodeStringBuilder(sb);
// ↑sb[0]に'Z'が入る.

動作説明
StringBuilder オブジェクトの内部バッファへのポインタが,呼び出し側へ渡される.
通常,参照型の値渡しは In ですが,StringBuilder は常に In/Out となる (例外規則).

補足

アンマネージ側から,何かしらのポインタを受け取る場合,C# 側では IntPtr として受け取り,Marshal.PtrToStructure で変換する必要があります.