以前アップロードした「WPF 逆引き集」に乱丁があったので、
改訂して最新版をアップロードし直しました。
既にダウンロードされた方には見苦しいものを公開してしまい申し訳ありませんでした。
C/C++ による汎用 DLL を作成する その6
いよいよ最終回です。
今回はメンバにポインタを含む構造体を受け渡す方法について紹介します。
DLL 側で次のような関数を公開します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> #include <string.h> #include "SampleDll.h" typedef struct _SampleStruct2 { int length; double* data; } SampleStruct2, *PSampleStruct2; namespace Tips_Win32DLL { double g_dData[256]; void __stdcall Sample06(SampleStruct2* st) { printf("--<SampleDll:Sample06>---------------\r\n"); memset(st, 0, sizeof(SampleStruct2)); memset(g_dData, 0, sizeof(g_dData)); (*st).length = 10; (*st).data = g_dData; for (int i = 0; i < (*st).length; i++) { g_dData[i] = (i + 1) / 10.0; } printf("-------------------------------------\r\n"); } } |
構造体メンバにはアドレスのみを格納し、実体は別の場所にあるような場合を想定しています。
C# 側では構造体に IntPtr 構造体を含めて定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; class Program { /// <summary> /// DLL 側からメンバにポインタを含む構造体を受け取る関数のインポート例 /// </summary> /// <param name="st">受け渡す構造体の先頭アドレスを示すポインタを指定します。</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample06(IntPtr st); /// <summary> /// DLL との取り合いのために定義する構造体です。 /// LayoutKind.Sequential を指定することで、 /// C/C++ 同様、変数の宣言順通りにメモリに配置されるようにされます。 /// </summary> [StructLayout(LayoutKind.Sequential)] private struct SampleStruct2 { public int length; public IntPtr data; } static void Main(string[] args) { #region Sample06 var sample06_a = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SampleStruct2))); try { Sample06(sample06_a); var sample06_b = (SampleStruct2)Marshal.PtrToStructure(sample06_a, typeof(SampleStruct2)); for (var i = 0; i < sample06_b.length; i++) { var v = Marshal.ReadInt64(sample06_b.data, i * sizeof(double)); Console.WriteLine("data[{0}] = {1}", i, BitConverter.Int64BitsToDouble(v)); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } finally { // 必ずメモリを解放するようにする Marshal.FreeHGlobal(sample06_a); } #endregion Sample06 Console.ReadKey(); } } } |
DLL 側から構造体を返してもらうため、前回紹介したように Marshal.AllocHGlobal() メソッドを始めとした IntPtr 構造体を利用した方法を用います。取得した構造体メンバにもまた IntPtr 構造体が含まれているため、これを利用して値を取得します。
上記のサンプルでは、double 型の配列の先頭アドレスが IntPtr 構造体となっています。IntPtr 構造体から double 型の数値を取得するときは、一度 Int64 に変換し、これを BitConverter.Int64BitsToDouble() メソッドで double 型に変換します。
実行結果は次のようになります。
以上、全 6 回に渡って C# から C/C++ で作成した DLL を扱う方法についてまとめました。
ここで紹介した以外にも様々なパターンがあるかもしれませんが、基本は同じなのでだいたい対応できるようになったかなと思います。C# にはあまり似合わない泥臭いコードではありますが、C/C++ との窓口となるための重要な部分ですのでそうなって然るべきで、誤解のないようにきちんと押さえておくべきポイントかなと思います。
C/C++ による汎用 DLL を作成する その5
前回までで単純なメソッドを DLL から呼び出すことができるようになりました。
今回はメソッドに文字列型や構造体を含む場合について考えましょう。
C/C++ で次のような文字列を扱う関数を用意してみましょう。
1 2 3 4 5 6 |
void __stdcall Sample02(int a, char* str) { printf("--<SampleDll:Sample02>---------------\r\n"); printf("[%d] %s\r\n", a, str); printf("-------------------------------------\r\n"); } |
与えられた文字列 str を標準出力するだけの関数です。
これを C# から使えるようにするには次のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/// <summary> /// 文字列を引数に持つ関数のインポート例 /// </summary> /// <param name="a">4 バイト符号付き整数を指定します。</param> /// <param name="str">文字列を指定します。</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample02(int a, string str); static void Main(string[] args) { #region Sample02 var sample02_a = "string 型で文字列を渡すことができます。"; Sample02(2, sample02_a); #endregion Sample02 Console.ReadKey(); } |
C/C++ 側ではポインタ型でしたが、C# のほうではそのまま string 型のままで OK です。
実行結果は次のようになります。
このように文字列を渡すだけだと簡単です。
ただし、C/C++ 側の文字コードが Unicode の場合は
明示的に指定する必要がある場合があります。
1 2 |
[DllImport("Tips_Win32DLL.dll", CharSet = CharSet.Unicode)] private static extern void Sample02(int a, string str); |
ところで、文字列を C/C++ 側からもらうときは string 型ではなく、StringBuilder クラスを使う必要があります。
例えば次のような関数を DLL で用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> #include <string.h> #include "SampleDll.h" namespace Tips_Win32DLL { void __stdcall Sample03(int a, char* str) { printf("--<SampleDll:Sample03>---------------\r\n"); printf("[%d] %s\r\n", a, str); sprintf_s(str, 256, "DLL 側から文字列を返す場合は StringBuilder クラスを使用します。"); printf("-------------------------------------\r\n"); } } |
与えられた文字列バッファ str に対して sprintf_s() 関数で文字列をセットしています。これを C# から呼び出すときは、入力引数を StringBuilder クラスにする必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; using System.Text; class Program { /// DLL 側から文字列を受け取る関数のインポート例 /// </summary> /// <param name="a">4 バイト符号付き整数を指定します。</param> /// <param name="str">文字列を受け渡すバッファを指定します。</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample03(int a, StringBuilder str); static void Main(string[] args) { #region Sample03 var sample03_a = new System.Text.StringBuilder(256); sample03_a.Append("文字列のバッファを渡す場合は StringBuilder クラスで受け渡します。"); Sample03(3, sample03_a); Console.WriteLine(sample03_a); #endregion Sample03 Console.ReadKey(); } } } |
実行結果は次のようになります。
ここでは C# 側からも StringBuilder クラスで文字列を渡しています。
受け渡しどちらでもできるので、文字列型は StringBuilder クラスを使うようにしたほうがいいのかもしれません。
次に、構造体を扱う関数を考えてみましょう。
DLL 側では次のような関数を公開するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> #include <string.h> #include "SampleDll.h" typedef struct _SampleStruct { int index; char name[128]; int data[50]; } SampleStruct, *PSampleStruct; namespace Tips_Win32DLL { void __stdcall Sample04(SampleStruct st) { printf("--<SampleDll:Sample04>---------------\r\n"); printf("index = %d\r\n", st.index); printf("name = %s\r\n", st.name); printf("data[0] = %d, data[1] = %d, data[2] = %d, data[3] = %d\r\n", st.data[0], st.data[1], st.data[2], st.data[3]); printf("-------------------------------------\r\n"); } } |
C++ では構造体のサイズはコンパイル時に決定されますが、C# では実行時に決定されます。したがって、C# 側で構造体のサイズをあらかじめ指定しておく必要があります。この場合、構造体は固定長サイズとなるため、例えば配列などを定義する場合は異なるサイズの配列を後からインスタンス化するようなことができなくなります。C# 側のコードは次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; class Program { /// <summary> /// 構造体を引数に持つ関数のインポート例 /// </summary> /// <param name="st">DLL 側に渡す構造体を指定します</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample04(SampleStruct st); /// <summary> /// DLL との取り合いのために定義する構造体です。 /// LayoutKind.Sequential を指定することで、 /// C/C++ 同様、変数の宣言順通りにメモリに配置されるようにされます。 /// </summary> [StructLayout(LayoutKind.Sequential)] private struct SampleStruct { /// <summary> /// 4 バイト符号付整数 /// </summary> [MarshalAs(UnmanagedType.I4)] public int index; /// <summary> /// 固定長文字配列 (SizeConst は配列のサイズを示す) /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string name; /// <summary> /// 固定長配列 (SizeConst は配列の要素数を示す) /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)] public int[] data; } static void Main(string[] args) { #region Sample04 var sample04_a = new SampleStruct() { index = 4, name = "構造体サンプル", data = new int[50], }; sample04_a.data[0] = 11; sample04_a.data[1] = 22; sample04_a.data[2] = 33; Sample04(sample04_a); #endregion Sample04 Console.ReadKey(); } } } |
構造体を定義するとき、MarshalAs 属性を付加することで各フィールドのサイズをコンパイル時に決定させることができます。この場合、配列の長さは指定した長さでしかインスタンス化できません。
実行結果は次のようになります。
確かに構造体の値が DLL 側に渡されている様子がわかります。
今度は逆に構造体を返してもらいましょう。
DLL 側では次のような関数を公開するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> #include <string.h> #include "SampleDll.h" typedef struct _SampleStruct { int index; char name[128]; int data[50]; } SampleStruct, *PSampleStruct; namespace Tips_Win32DLL { void __stdcall Sample05(SampleStruct* st) { printf("--<SampleDll:Sample05>---------------\r\n"); //memset(st, 0, sizeof(SampleStruct)); (*st).index = 5; sprintf_s((*st).name, 128, "構造体ポインタサンプル"); (*st).data[0] = 11; (*st).data[1] = 22; (*st).data[2] = 33; printf("-------------------------------------\r\n"); } } |
C# 側では先ほどと同様に、構造体を定義するときに MarshalAs 属性を付けてサイズを固定化することに加え、関数の入力引数は SampleStruct 構造体ではなく IntPtr 構造体によるポインタを与えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; class Program { /// <summary> /// DLL 側から構造体を受け取る関数のインポート例 /// </summary> /// <param name="st">受け渡す構造体の先頭アドレスを示すポインタを指定します。</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample05(IntPtr st); /// <summary> /// DLL との取り合いのために定義する構造体です。 /// LayoutKind.Sequential を指定することで、 /// C/C++ 同様、変数の宣言順通りにメモリに配置されるようにされます。 /// </summary> [StructLayout(LayoutKind.Sequential)] private struct SampleStruct { /// <summary> /// 4 バイト符号付整数 /// </summary> [MarshalAs(UnmanagedType.I4)] public int index; /// <summary> /// 固定長文字配列 (SizeConst は配列のサイズを示す) /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string name; /// <summary> /// 固定長配列 (SizeConst は配列の要素数を示す) /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)] public int[] data; } static void Main(string[] args) { #region Sample05 var sample05_a = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SampleStruct))); try { Sample05(sample05_a); var sample05_b = (SampleStruct)Marshal.PtrToStructure(sample05_a, typeof(SampleStruct)); Console.WriteLine("index = " + sample05_b.index); Console.WriteLine("name = " + sample05_b.name); Console.WriteLine("data[0] = {0}, data[1] = {1}, data[2] = {2}, data[3] = {3}", sample05_b.data[0], sample05_b.data[1], sample05_b.data[2], sample05_b.data[3]); Console.WriteLine("DLL 側できちんと初期化していないと data[3] に値が現れることに注意する必要がある。"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } finally { // 必ずメモリを解放するようにする Marshal.FreeHGlobal(sample05_a); } #endregion Sample05 Console.ReadKey(); } } } |
Marshal.SizeOf() メソッドで SampleStruct 構造体のサイズを取得し、Marshal.AllocHGlobal() メソッドでそのサイズ分だけメモリ領域を確保し、その先頭アドレスをポインタ変数 sample05_a に格納しています。このとき、変数 sample05_a が用済みになった段階で必ず Marshal.FreeHGlobal() メソッドでメモリ領域を解放しないとメモリリークしてしまうので注意してください。
受け取ったポインタから SampleStruct 構造体の情報に構築し直すために、Marshal.PtrToStructure() メソッドを使用して変数 sample05_b に格納しています。
実行結果は次のようになります。
実行結果を見てわかるように、
構造体メンバの配列の要素 data[3] に謎の数値が代入されています。
これは、DLL 側の関数の中で渡されたポインタが示すメモリ領域を memset() 関数などでゼロクリアしていないことから起こる現象です。DLL 内で初期化処理をしないとこのようなことが起こり得るので、memset() 関数などによるゼロクリアは必ずするようにしたほうが良いでしょう。
だいぶ長くなってしまいました。
次回はおそらく最終回。メンバにポインタを含む構造体の受け渡しに関して紹介します。
C# で簡単に年齢計算をおこなう
年齢を計算するときに結構有名な方法として次の手順があります。
- 誕生日と年齢を計算する基準の日付を “YYYYMMDD” 形式で 8 桁の数値に変換する
- 変換した数値で { (基準の日付) – (誕生日) } ÷ 10000 を計算する
こうすると、整数部分が年齢になります。
うるう年とか面倒なことも考えずに済むので楽チンです。
なんでこの方法で年齢になるかは
実際の数値例をいくつかやってみるとすぐにわかります。
日付をまたいでいないときの桁下がりにちょっと感動したことを覚えています。
これを C# で計算させるときは例えば次のような感じで 1 行で書けます。
1 2 3 4 |
public int CalcAge(DateTime birthday, DateTime today) { return (int.Parse(today.ToString("yyyyMMdd")) - int.Parse(birthday.ToString("yyyyMMdd"))) / 10000; } |
int.Parse() メソッドを使っているので速度にこだわる場合にはオススメできませんが、
このメソッドのおかげでだいぶ簡単なコードに仕上がりました。
数万人分の年齢を一気に計算させるようなことでもない限りこれで十分かと思います。
おしまい。
RadarChart コントロール作りました
C/C++ による汎用 DLL を作成する その4
前回では次のような関数を公開する DLL を C/C++ で作成しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> #include "SampleDll.h" #define PI 3.1415926536 namespace Tips_Win32DLL { double __stdcall Sample01(int a) { printf("--<SampleDll:Sample01>---------------\r\n"); printf("a = %d\r\n", a); printf("-------------------------------------\r\n"); return PI; } } |
今回はこの DLL を C# から使う方法をまとめます。
とりあえず C# のコンソールアプリケーションプロジェクトを作成し、
次のようにコマンドプロンプトウィンドウが保持されるようにしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace Tips_Win32DllImport { using System; class Program { static void Main(string[] args) { Console.ReadKey(); } } } |
ここで一度ビルドしてアセンブリを生成しておきます。
さて、C# で作成される DLL アセンブリを使う場合は、
ソリューションエクスプローラー上の「参照設定」に
使用する DLL を追加するようにしていましたが、
C/C++ で作成された DLL ファイルを使う場合は方法が異なります。
まず、作成するアプリケーションファイルと同じディレクトリか、
環境変数に設定されているパス上に使用する DLL ファイルを置きます。
通常はプロジェクトファイルのあるディレクトリの中の
bin\Debug ディレクトリ、Release モードの場合は bin\Release ディレクトリですね。
その上で、次のようなコードを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; class Program { /// <summary> /// 最も基本的な関数のインポート例 /// </summary> /// <param name="a">4 バイト符号付き整数を指定します。</param> /// <returns>倍精度浮動小数を返します。</returns> [DllImport("Tips_Win32DLL.dll")] private static extern double Sample01(int a); static void Main(string[] args) { #region Sample01 var sample01_a = Sample01(1); Console.WriteLine(sample01_a); Console.WriteLine(); #endregion Sample01 Console.ReadKey(); } } } |
C/C++ による DLL を使用するときは、
使用する関数を static extern 修飾子を付けて宣言し、
System.Runtime.InteropServices.DllImport 属性を使って
使用する DLL ファイルを指定します。
このとき、DLL ファイルのパス指定が間違っていたり、
指定された場所に DLL ファイルが存在しないと
実行時に DllNotFoundException 例外が発生するので注意が必要です。
これで実行すると次のように出力されます。
DLL 内部で実装された関数内で printf による標準出力をおこなっているので、
その出力もコマンドプロンプトに表示されています。
確かに入力引数で渡した 1 という値が表示されています。
また、C# 側に戻した double 型の値もきちんと受け渡されていることがわかります。
このように単純な数値型であれば
DLL が公開している関数を簡単に導入できます。
DllImport 属性には DLL ファイルのパスを指定する以外に
次のような引数を与えることもできます。
名称 | 説明 |
---|---|
CallingConvention | エントリ ポイントの呼び出し規約を示します。 |
CharSet | 文字列パラメーターをメソッドにマーシャリングし、名前マングルを制御する方法を示します。 |
EntryPoint | 呼び出す DLL エントリ ポイントの名前または序数を指定します。 |
CallingConvention で呼び出し規約を明示的に指定できます。
デフォルトでは __stdcall となっているので、特に使用する必要はないと思います。
CharSet は文字列型を扱うときに使用するかもしれません。
EntryPoint は使用する DLL の関数の名前と
C# 上で使用する関数の名前を異なるものにしたいときに使用します。
これ以外にも細かい設定をおこなうための引数が用意されていますので、
興味がある方は DllImportAttribute クラスを調べてみてください。
次回は文字列型や構造体などの受け渡しの方法について紹介します。
WPF 学習用ドキュメント作りました
WPF の日本語ドキュメントもかなり増えてきて喜ばしいのですが、
エッセンシャル的な書籍が多い印象を受けています。
例えば「10 日でできる」とか「WPFで作る○○」とかいったような、
作業しながら読み進める学習本みたいなのは少ないというか
出版されてないんじゃないかな~ということで、
ないものは作ればいいじゃないの精神で自分で作ってみました。
PDF ファイルをこちらからダウンロードできます。
完全に独学で、C# も初めて触ってから 3 年も経っていないような
私が作ったものなので足りない部分は多々あると思いますが、
これから WPF+C# でアプリケーション開発を始めようと思う人たちにとっては
足がかりの一つになるんじゃないかなと思います。
また、こちらは公開するかどうか迷いましたが、
せっかくなのでこの機会に外へ出してみたいと思います。
「WPF 入門」というタイトルで、標準コントロールの使い方を中心に書いています。
かなり有名な方が同じような PDF を公開されているので
そちらのほうが情報源としては信頼性がありますが、
もうちょっと泥くさく、初級者に読みやすくしたつもりですので、
良ければ参考にしてください。
さらに、「WPF 逆引き集」というドキュメントも公開します。
こちらは「~~したいけどどうしたらいいの」というときに
リファレンスとして使用できる形態としてまとめています。
ドキュメント内にある Tips_xxxx というのは、
こちらのソリューションに含まれているプロジェクト名です。
学習するために「WPF実践」、
基本的な使い方を確認するために「WPF 入門」、
リファレンスとして「WPF 逆引き集」、
というような使い分けで参照していただければと思います。
最新情報を追っかけていると最近は UWP とか Xamarin とかがホットな話題になっていて、
WPF なんてもうオワコンなんじゃないかと錯覚してしまいますが、
XAML ファミリのアプリケーション開発の入門としてもWPF はとっつきやすいと思うので、
勉強して損はないはずです。
それにデスクトップアプリはまだまだ必要とされていますし、
未だに Windows7 がスタンダードな業界だっていくらでもあります。
「今更 WPF?時代遅れだよね~」なんて言われないと思います。
WPF はいいぞ!
— 2016/10/25 追記 —
「WPF 逆引き集」に乱丁があったので改訂して最新版のリンクに貼り直しました。
C/C++ による汎用 DLL を作成する その3
C++ で他の言語からでも利用可能な汎用 DLL を作成するには、次のことを守る必要があります。
- クラスではなく関数をエクスポートするようにする
クラスをエクスポートした場合、DLL 側のコンストラクタ/デストラクタを C# 側から直接呼び出せないため、DLL 側になんらかのヘルパが必要となるため。
また、クラスのメンバ関数はマングリングによって関数名が自動的に変更され、C# 側は常にその関数名に追従するようにメンテナンスすることが必要となるため。 - エクスポートする関数の呼び出し規約は __stdcall とする
Windows API のデファクトスタンダードであるため。 - __declspec(dllexport) は使用せず、モジュール定義ファイル (*.def) でエクスポートする関数を定義する
C++ では異なる名前空間上に同じ関数名が定義されたり、関数のオーバーロード機能によって同じ関数名でも機能が異なる関数が実装されたりします。このため、コンパイル後は定義した関数の名前が自動的に変更されるマングリングという処理がおこなわれてしまいます。したがって、DLL 化するときにも関数の名前が自動的に変更されるため、DLL を使う側が変更された名前を把握しなければならなくなります。しかし、マングリングによる命名規則はコンパイラに依存するため、完全に対応付けることは現実的ではありません。
そこで、関数宣言時に extern “C” を付けることで C++ の関数を C として扱うようにすることができます。つまりオーバーロード機能などがない関数となるため、マングリングによる処理がおこなわれなくなります。結果として、extern “C” を付けて DLL 化することで外部から同じ名前でアクセスできるようになる、というわけです。この場合、関数宣言時に __declspec(dllexport) も同時に付加する必要があります。
一方、extern “C” および __declspec(dllexport) を付けて宣言する代わりに、モジュール定義ファイル「SampleDll.def」に公開する関数名を並べることでも対応できます。どちらの場合でも、関数オーバーロードは使えないようです。
モジュール定義ファイルを使用した場合のコード例を以下に示します。
1 2 3 4 5 6 |
#pragma once namespace Tips_Win32DLL { double __stdcall Sample01(int a); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> #include "SampleDll.h" #define PI 3.1415926536 namespace Tips_Win32DLL { double __stdcall Sample01(int a) { printf("--<SampleDll:Sample01>---------------\r\n"); printf("a = %d\r\n", a); printf("-------------------------------------\r\n"); return PI; } } |
1 2 3 4 5 |
LIBRARY Tips_Win32DLL EXPORTS ; 公開する関数名をリストアップ Sample01 |
モジュール定義ファイルには名前空間を除いた関数名のみを記述します。コメントを書きたい場合は “//” ではなく、”;” を記述します。
ヘッダ、ソースファイルともに、エクスポートしたい関数は __stdcall を付けて宣言します。__stdcall は呼び出し規約と呼ばれるキーワードで、その他に次のような呼び出し規約があります。
キーワード | スタック維持の責任 | パラメータ渡し |
---|---|---|
__cdecl | 呼び出し元 | パラメータをスタックに逆の順序で右から左にプッシュする |
__clrcall | 適用なし | CLR 式スタックの順に左から右にパラメータを読み込む |
__stdcall | 呼出先 | パラメータをスタックに逆の順序で右から左にプッシュする |
__fastcall | 呼出先 | レジスタに格納されてからスタックにプッシュする |
__thiscall | 呼出先 | スタックにプッシュされる |
__vectorcall | 呼出先 | レジスタに格納されてからスタックに逆の順序で右から左にプッシュされる |
__cdecl | 呼出先 | パラメータをスタックに逆の順序で右から左にプッシュする |
呼び出し規約 __stdcall は Windows API で使用されているデファクトスタンダードであり、特別な理由がない限りこれを使用したほうが良いようです。
また、上記のコード例ではクラスを使用していません。というのは、C++ のクラスのメンバ関数の呼び出し規約が __thiscall であり、関数名が自動的に変更されるマングリング処理が働いてしまいます。
以上の作成方法からだいたい察しが付くと思いますが、DLL としてエクスポートできる関数には次のような制約条件があります。
- 公開する関数に関数オーバーロードは使えない
- 既にグローバルや別の名前空間で定義されている名前の関数はどちらも公開できない
- 公開する関数はクラスのメンバ関数にできない
これらの制約があることから、モジュール定義ファイルで公開する関数を管理したほうがメンテナンスしやすいのではないかと思います。また、公開する関数自体はクラスにすることはできませんが、内部実装に関してはクラスを使用することができるので、既にあるクラスが公開している関数を公開するためのラッパーを作ることで既存の資産を流用できます。
次回は作成した DLL を C# から呼び出す方法を紹介します。
C/C++ による汎用 DLL を作成する その2
今回は C++ による汎用 DLL を作成するための
Visual Studio のプロジェクトを作成します。
Visual Studio のバージョンによっては多少の違いはありますが、
概ねここで紹介するような方法でできると思います。
ちなみにここでは Visual Studio Professional 2013 Update 5 を使用します。
まず、Visual Studio で新しいプロジェクトを作成し、
そのテンプレートとして「Visual C++」→「Win32 プロジェクト」を選択します。
バージョンによっては「Win32 アプリケーション」という名前になっています。
このプロジェクトを追加するとき、
Win32 アプリケーションウィザードが自動的に開き、
プロジェクトの初期設定がおこなわれます。
このウィザード上で、アプリケーションの種別を「DLL」に、
追加のオプションを「空のプロジェクト」とし、
その他のチェックボックスをすべて外してプロジェクトを作成します。
プロジェクト追加直後はソースファイルやヘッダファイルが空のため、
適当な名前のソースファイルおよびヘッダファイルの組を追加します。
ここでは「SampleDll.h」、「SampleDll.cpp」を追加しています。
さらに、DLL として公開する関数名を定義するためにモジュール定義ファイルを追加します。
このファイルはソリューションエクスプローラで「ソースファイル」を右クリックして
「追加」→「新しい項目」メニューから「新しい項目の追加」ダイアログを開き、
「Visual C++」→「コード」→「モジュール定義 (.def)」を選択することで追加できます。
ここでは「SampleDll.def」を追加しています。
モジュール定義ファイルはリンカーへの入力として指定する必要があります。
ソリューションエクスプローラからプロジェクトのプロパティを開き、
「リンカー」→「入力」→「モジュール定義ファイル」の項目に SampleDll.def と記述します。
プロパティの設定は Debug 構成と Release 構成に分かれているため、
両方忘れずに設定してください。
また、プロジェクトのプロパティ設定にて、
「全般」→「プラットフォーム ツールセット」の項目は
必要に応じて WindowsXP 対応のものにしたほうが良いでしょう。
サポートが切れているとはいえ、まだまだ移行できていない業界は
珍しくないと思います。
以上で C/C++ による汎用 DLL 作成のための準備が整いました。
次回以降では実際のコードを実装していきます。
C/C++ による汎用 DLL を作成する その1
C# は JIT コンパイルが走って実行速度が落ちるので、
計算の肝の部分はやっぱり C/C++ で実行したい。
(というか秘伝のソースが C/C++ なのでこれを活用したいというのが本音)
そんなときは C/C++ で汎用 DLL を作って、
C# のほうでインポートしてやればいいです。
~2016/08/18 追記~~~
JIT コンパイルがどうのと書きましたが、そんなの関係ねぇですね。
C# は.NET Framework 上で動作するため、
メモリアクセスなどハードウェアに近い仕事が苦手です。
そこで C/C++ DLL を利用することで、
苦手なものを得意な人たちに任せることができるようになります。
また、シンプルに DLL を利用するメリットを享受できます。
つまり過去の資産を活用できたり、
複数のアプリケーションで同機能を共有できるので、
共通機能のメンテナンスコストだったり、
新規アプリケーションの設計コストだったりが削減できます。
~~~~~~~~~~~
DLL を作成するときの注意事項は次の 2 点。
- クラスではなく関数をエクスポートするようにする
- エクスポートする関数の呼び出し規約は __stdcall とする
詳細は次回以降で説明したいと思います。