Microsoft .NET Compact Framework の P/Invoke とマーシャリング入門
Jon Box, Dan Fox
寄稿 :
Jonathan Wells
Microsoft Corporation
Jim Wilson
JW Hedgehog, Inc.
March 2003
日本語版最終更新日 2003 年 6 月 24 日
対象 :
Microsoft® .NET Compact Framework 1.0
Microsoft® Visual Studio® .NET 2003
概要 : .NET Compact Framework の Platform Invoke (P/Invoke) 機能の使用方法を調査します。
目次
はじめに
P/Invoke の使用
データのマーシャリング
まとめ
はじめに
開発者は、 場所、時間、および使用するデバイスを問わず情報にアクセスするという構想を実現するために、 堅牢で機能豊富なツールを必要とします。 Microsoft は、Microsoft .NET Compact Framework と Visual Studio .NET 2003 のスマート デバイス プロジェクトのリリースにより、 開発者が Microsoft .NET Framework に関する既存の知識を利用して、 アプリケーションをスマート デバイスに拡張することを容易にしました。
.NET Compact Framework は、 Pocket PC 2000、2002、および Microsoft® Windows® CE .NET 4.1 を搭載したデバイスを対象とし、 デスクトップ フレームワークのサブセットとして開発されました。 開発者は、Compact Framework を Visual Studio .NET 2003 のスマート デバイス プロジェクト サポートと組み合わせて使用することにより、 完全な .NET Framework のマネージ コードと同様の、 共通言語ランタイムで実行される Visual Basic や C# のマネージ コードを記述できるようになります。
ただし、.NET Compact Framework は .NET Framework のサブセットなので、 ユーザー入力、メッセージング、 および Microsoft® SQL? Server 2000 Windows CE Edition 2.0 を処理するいくつかの .NET Compact Framework 固有の型と、 名前空間全体にまたがる型のおよそ 25% しかサポートしていません。 このことは開発者にとって、 オペレーティング システム (Windows CE) API にドロップ ダウンする方法でのみアクセスできる機能が一部あることを意味します。 たとえば、Pocket PC 2002 プラットフォームに含まれている通知サービスは、 .NET Compact Framework に相当するマネージ機能がありません。 また、場合によってはアクセスする必要のあるさまざまなサードパーティ DLL やカスタム DLL があります。 たとえば、Pocket PC 2002 Phone Edition デバイスを使用して、電話をかけて通話記録を取得するための API などです。
幸運なことに、(デスクトップ フレームワークによく似た) .NET Compact Framework は、 Platform Invoke (P/Invoke) サービスをサポートしています。 マネージ コードはこのサービスにより、DLL に存在するアンマネージ関数を呼び出すことができます。 .NET Compact Framework は P/Invoke をサポートしていますが、 そのサポート方法は、完全な .NET Framework の方法とはやや異なります。 この資料と次回の資料で、 開発時に直面する類似点と相違点に注目しながら、 Compact Framework での P/Invoke の使用を調査します。 この資料は、 読者が完全な .NET Framework の基本的なレベルに精通していることを前提としています。
P/Invoke の使用
.NET Framework の場合と同様に、.NET Compact Framework での P/Invoke サービスの使用には、 宣言、呼び出し、およびエラー処理の 3 つの主要な手順を導入しています。 これらの手順を説明した後、.NET Compact Framework と完全な .NET Framework での P/Invoke サービスの相違点を見ていきます。
宣言
まず、デザイン時に、
呼び出すアンマネージ関数を .NET Compact Framework に指示する必要があります。
指示には、(モジュールともいう) DLL 名、(エントリ ポイントともいう) 関数名、および使用する呼び出し規約を含める必要があります。
たとえば、SHGetSpecialFolderPath 関数を呼び出して、
Windows CE 上のさまざまなシステム フォルダへのパスを返すには、
DllImportAttribute、
また VB の場合は DllImportAttribute か Declare
のいずれかのステートメントを使用して、関数を宣言します。
[VB]
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _ ByVal hwndOwner As Integer, _ ByVal lpszPath As String, _ ByVal nFolder As ceFolders, _ ByVal fCreate As Boolean) As Boolean
[C#]
[DllImport("coredll.dll", SetLastError=true)]
private static extern bool SHGetSpecialFolderPath(
int hwndOwner,
string lpszPath,
ceFolders nFolder,
bool fCreate);
どちらの場合も、宣言に、関数が存在する DLL の名前 (coredll.dll 、その中で Windows CE API の多くは Win32 API の kernel32.dll および user32.dll に類似したものがあります)、 および関数の名前 (SHGetSpecialFolderPath) を含めることに注意します。また、VB の関数を Private に設定しているので、関数を宣言しているクラス内からのみその関数を呼び出せます。アプリケーションが関数を呼び出す方法によって、関数を Public や Friend に設定する場合もあります。
注意 完全な .NET Framework ベースのアプリケーションと同様に、 アンマネージ API の宣言を Quilogy.CeApi などの適切な名前空間内のクラスにグループ化し、 アンマネージ API の関数を、 共有ラッパー メソッド経由で公開することが推奨事例です。
C# 宣言の場合 (関数は private に設定されているので隠ぺいされたままですが)、
関数が外部に実装されていて、
クラスのインスタンスを作成せずにその関数を呼び出せることを示すために、
static キーワードとextern キーワードが必要になります。
また、関数は、ceFolders 型の引数を必要とすることにも気が付きます。
実際、関数は、32 ビットの整数である CSIDL 値を要求して、
複数の Windows CE API 定数のいずれかに割り当てます。
このような場合、
以下のコードで示すように、
列挙には
(msdn.microsoft.com の API
ドキュメントで見つかります)
定数値を公開することが推奨事例です。
[VB]
Private Enum ceFolders As Integer
PROGRAMS = 2 ' \Windows\Start Menu\Programs
PERSONAL = 5 ' \My Documents
STARTUP = 7 ' \Windows\StartUp
STARTMENU = &HB ' \Windows\Start Menu
FONTS = &H14 ' \Windows\Fonts
FAVORITES = &H16 ' \Windows\Favorites
End Enum
.NET Framework の場合と同様に、Declareステートメントは、
関数が DLL とは別の名前を持つことを指定できるようにする Alias 句をサポートします。
これは、DLL の関数名が、コードで既に定義されているキーワードや別の関数と競合するときに役立ちます。
次に示すように、DllImportAttribute 属性も、
属性宣言に追加できる EntryPoint プロパティを使用して、
この機能をサポートします。
[DllImport("coredll.dll", EntryPoint="SHGetSpecialFolderPath")]
static extern bool GetFolderPath( //残りの宣言
注意 Alias 句や EntryPoint プロパティは、 それぞれ、"#155" のような "#num" の形式で、DLL 内の関数の序数を保持できます。
最後に、C# 宣言で、SetLastError プロパティを true に設定していることに注目します
(既定では、SetLastError プロパティは false です)。
これは、共通言語ランタイムが Windows CE GetLastError 関数を呼び出して、
返されるエラー値をキャッシュすることを指定するので、
他の関数はその値をオーバーライドしません。
したがって、Marshal.GetLastWin32Error を使用して、
安全にエラーを取得できます。
VB で DllImportAttribute を使用する場合と同様に、
Declare ステートメントでは、
この値に有効なのは true だとされています。
呼び出し
(通常、ユーティリティ クラスで) 呼び出す関数を正しく宣言した後、
そのクラスのメソッドに関数呼び出しをラップできます。
次に示すように、
使用する一般的な技法は、
クラスの Public Shared (または C# では、static) メソッドとして、
ラッパー関数を宣言することです。
Namespace Quilogy.CeApi
Public Class FileSystem
Private Sub New()
' クラスが作成されないようにします。
End Sub
Public Shared Function GetSpecialFolderPath( _
ByVal folder As ceFolders) As String
Dim sPath As String = New String(" "c, MAX_PATH)
Dim i As Integer
Dim ret As Boolean
Try
ret = SHGetSpecialFolderPath(0, sPath, folder, False)
Catch ex As Exception
HandleCeError(ex, "GetSpecialFolderPath")
Return Nothing
End Try
If Not ret Then
' API エラーが発生したので、エラー番号を取得します。
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"SHGetSpecialFolderPath returned False, " & _
"likely an invalid constant", errorNum), _
"GetSpecialFolderPath")
End If
Return sPath
End Function
' 他のメソッド。
End Class
End Namespace
この場合、SHGetSpecialFolderPath への呼び出しは、
Quilogy.CeApi.FileSystem クラスの GetSpecialFolderPath メソッドにラップされ、
DLL 関数への正しいパラメータの引き渡し、
およびすべてのエラー処理を担当します。
この例のように、クライアントが指定する必要があるラッパーの属性のみを公開することが推奨事例です。
残りは既定値の適切な値にできます。
クラスもプライベート コンストラクタを保持して、
クラスのインスタンスを作成しないようにしていることに注意します。
呼び出し側は、次のようにメソッドを呼び出します。
Imports Quilogy ... Dim docs As String docs = CeApi.FileSystem.GetSpecialFolderPath(ceApi.ceFolders.PERSONAL)
実行時にこのコードを JIT (Just-In-Time) コンパイルすると、
共通言語ランタイムの P/Invoke サービスが、
アセンブリのメタデータから Declare や DllImportAttribute 定義を取得し、
関数が含まれている DLL をメモリに配置して読み込み、
エントリ ポイント情報を使用して関数のアドレスを取得します。
すべて正常に完了した場合、関数が呼び出され、関数の引数がマーシャリングされ、
すべての戻り値が呼び出し側に返されます。
エラー処理
開発者は、記述したコードがランタイム エラーを生成するとは思ってはいませんが、P/Invoke を使用して DLL で呼び出された関数が、2 種類の別のエラーを生成する可能性があることに注意しておくことが重要になります。
1 つ目は、PInvoke サービス自体が生成する例外です。
この例外は、メソッドに渡された引数に無効なデータが含まれている場合、
または無効な引数で関数自体を宣言している場合に発生します。
この場合、NotSupportedException がスローされます。
この例外が発生した場合、
宣言を再検討して、
宣言が実際の DLL 定義と一致しているかどうか判断する必要があります。
また、P/Invoke は MissingMethodException をスローすることがあります。
MissingMethodException は、
その名前が示すように、
エントリ ポイントが見つからない場合に生成されます。
GetSpecialFolderPath メソッドでは、
これらの例外が Try Catch ブロックでトラップされ、
HandleCeError という別のカスタム メソッドに渡されます。
このメソッドは、渡された例外の型を調査して、
型 WinCeException のカスタム例外をスローします。
WinCeException は、
適切なメッセージを使用して ApplicationException から派生します。
以下のリストで、これらの両方を示します。
例外をラッピングするこの技法は、
エラー処理を集中管理し、(Windows CE エラー番号などの) カスタム メンバをカスタム例外クラスに追加できるので、
.NET Compact Framework では効果的です。
Private Shared Sub HandleCeError(ByVal ex As Exception, _
ByVal method As String)
' ここで、任意のログ記録を行います。
' 要求に応じて、例外を受け入れます。
If Not ExceptionsEnabled Then
Return
End If
If TypeOf ex Is NotSupportedException Then
' 引数が無効か、不適切に宣言されています。
Throw New WinCeException( _
"Bad arguments or incorrect declaration in " & method, 0, ex)
End If
If TypeOf ex Is MissingMethodException Then
' エントリ ポイントが見つかりません。
Throw New WinCeException( _
"Entry point not found in " & method, 0, ex)
End If
If TypeOf ex Is WinCeException Then
Throw ex
End If
' その他すべての例外
Throw New WinCeException( _
"Miscellaneous exception in " & method, 0, ex)
End Sub
Public Class WinCeException : Inherits ApplicationException
Public Sub New()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(message)
End Sub
Public Sub New(ByVal message As String, ByVal apiError As Integer)
MyBase.New(message)
Me.APIErrorNumber = apiError
End Sub
Public Sub New(ByVal message As String, _
ByVal apiError As Integer, ByVal innerexception As Exception)
MyBase.New(message, innerexception)
Me.APIErrorNumber = apiError
End Sub
Public APIErrorNumber As Integer = 0
End Class
HandleCeError メソッドは、
最初に ExceptionsEnabled という共有フィールドを調査し、
false の場合は単にメソッドから戻っていることがわかります。
呼び出し側は、この共有フィールドにより、クラスのメソッドから例外をスローするかどうかを指定できます。
注意 VS .NET ヘルプで説明しているように、 上記のコード リストで示したエラー文字列をそれぞれリソース ファイルに格納し、 サテライト アセンブリにパッケージ化して、 ResourceManager クラスを使用して動的に取得できます。
生成される可能性がある 2 つ目の種類のエラーは、DLL 関数自体から返されるエラーです。
GetSpecialFolderPath メソッドの場合、
このエラーは、SHGetSpecialFolderPath が False を返すとき
(たとえば、列挙に無効な定数やサポートされていない定数が含まれている場合) に発生します。
この場合、メソッドが GetLastWin32Error を使用してエラー番号を取得し、
オーバーロード コンストラクタを使用して、HandleCeError メソッドにエラー番号を渡します。
.NET Compact Framework の相違点
上記の宣言は、(モジュール名を除いて) 完全な .NET Framework の宣言と同様の .NET Compact Framework の宣言ですが、 わずかな相違点がいくつかあります。
- "常にすべてが Unicode"。
既定の文字セットは、(
ExactSpellingプロパティに応じて、P/Invoke が ANSI の "A"、 または Unicode の "W" のどちらを追加しているかにかかわらず) 文字列パラメータのマーシャリング動作、 および使用する正確なエントリ ポイント名を制御します。 完全な .NET Framework の場合、 その既定の文字セットを、DeclareステートメントのAnsi句、Auto句、またはUnicode句とDllImportAttributeのCharSetプロパティを使用して設定できます。 これは、完全な .NET Framework では既定値が ANSI ですが、.NET Compact Framework は Unicode のみをサポートするので、 結果としてCharSet.Unicode(および Unicode と同等のCharSet.Auto) 値のみが含まれ、Declareステートメントの句はサポートしません。 つまり、ExactSpellingプロパティもサポートされません。 したがって、.NET Compact Framework は、 常に Unicode 文字列へのポインタを渡すので、DLL 関数が ANSI 文字列を必要とする場合、 関数を呼び出す前に、DLL で変換を実行する必要があるか、ASCIIEncodingクラスのオーバーロードGetBytesメソッドを使用して、 文字列をバイト配列に変換する必要があります。 - "1 つの呼び出し規約"。
完全な .NET Framework は、
DllImportAttributeのCallingConventionプロパティで使用するCallingConvention列挙値を使用して、 (引数が関数に渡される順序や、スタックの整理を担当する機能などの問題を解決する) 3 つの異なる呼び出し規約をサポートしています。 ただし、.NET Compact Framework では、Winapi値 (既定のプラットフォーム規約) のみをサポートします。 この値は、C と C++ では Cdecl という 既定の呼び出し規約になります。 - "単一方向"。
DLL 関数はデータを .NET Compact Framework アプリケーションに返すことができるので、
値や参照でパラメータを DLL 関数に渡すことができますが、
.NET Compact Framework の P/Invoke は、
完全な .NET Framework が行うようなコールバックをサポートしません。
完全な .NET Framework の場合、DLL 関数に渡されるデリゲート (オブジェクト指向の関数ポインタ) を使用して、
コールバックをサポートします。
そして DLL 関数が関数の結果を使用して、
デリゲートのアドレスでマネージ アプリケーションを呼び出します。
コールバックの使用の一般的な例は、
EnumWindowsAPI 関数です。 この関数を使用して、すべての最上位レベル ウィンドウを列挙し、 ウィンドウのハンドルをコールバック関数に渡すことができます。 - "異なる例外"。
完全な .NET Framework では、
関数が見つからない場合、
または関数が不適切に宣言された場合は、
それぞれ、(通常)
EntryPointNotFoundExceptionとExecutionEngineExceptionがスローされます。 以前説明したように、 .NET Compact Framework では、MissingMethodException型とNotSupportedException型がスローされます。 - "Windows メッセージの処理"。
多くの場合、オペレーティング システム API を処理するとき、
ウィンドウへのハンドル (hwnd) を関数に渡すこと、
またはオペレーティング システムが送信するメッセージのカスタム処理を追加することが必要になります。
完全な .NET Framework の場合、
Formクラスは、 前者の要求を満たすHandleプロパティ、 および後者の要求を処理するDefWndProcメソッドをオーバーライドする機能を公開します。 実際、Control基本クラスがこれらのメンバを公開するので、ListBoxやButtonなど、Controlから派生するすべてのクラスがこの機能を使用できます。 .NET Compact Framework のFormクラスはどちらのメンバも保持していませんが、Microsoft.WindowsCE.Forms名前空間のMessageWindowクラスとMessageクラスが含まれています。MessageWindowクラス、 およびオーバーライドされたMessageWindowクラスのWndProcメソッドを継承して、Message形式の特定のメッセージを捕捉できます。 さらに、.NET Compact Framework ベースのアプリケーションは、MessageWindowクラスのSendMessageメソッドとPostMessageメソッドを使用して、 他のウィンドウにメッセージを送信できます。 たとえば、.NET Compact Framework ベースのアプリケーションが既に実行されているかどうかを判断するための確認を行っているときに、PostMessageを使用して、 ユーザーが作成したメッセージをすべての最上位レベル ウィンドウにブロードキャストできます。 Jonathan Wells が、 そのサンプル コードを smartdevices.microsoftdev.com (英語) に投稿しました。
データのマーシャリング
P/Invoke サービスは、呼び出し処理中に DLL 関数に渡されたパラメータ値のマーシャリングを担当します。 P/Invoke のこのコンポーネントは、通常、P/Invoke "マーシャラ" と呼ばれます。 ここでは、共通の型、文字列、構造体、整数以外の型、 および他のいくつかの問題点に対処するときのマーシャラの機能を説明します。
高速転送型
幸運にも、DLL 関数を呼び出すために使用する多くの型には、 .NET Compact Framework とアンマネージ コードの両方で共通の表記があります。 このような型は "高速転送型" といい、次の表で確認できます。
| Compact Framework 型 | Visual Basic キーワード | C# キーワード |
|---|---|---|
System.Byte
|
Byte
|
byte
|
System.SByte
|
なし
|
sbyte
|
System.Int16
|
Short
|
short
|
System.UInt16
|
なし
|
ushort
|
System.Int32
|
Integer
|
int
|
System.Int64
|
Long (ByRef のみ)
|
long (ref のみ)
|
System.UInt64
|
なし
|
ulong
|
System.IntPtr
|
なし
|
* using unsafe
|
つまり、マーシャラは、これらの型で定義されたパラメータをマネージ コードとアンマネージ コード間で変換するために、 特別な処理を実行する必要はありません。 拡大解釈すると、実際、マーシャラはこれらの型の 1 次元配列や、 さらにはこれらの型のみが含まれている構造体とクラスも変換する必要はありません。
この動作は、
完全な .NET Framework でも同様ですが、.NET Compact Framework には、
高速転送型として System.Char
(VB では Char、C# では char)、
System.String
(VB では String、C# では string)、
および System.Boolean
(VB では Boolean、C# では bool) も含まれています。
System.Char と System.String の場合、
これが含まれるのは、
上記で説明したように、.NET Compact Framework は Unicode のみをサポートしているので、
マーシャラは、常に、2 バイトの Unicode として前者をマーシャリングし、
Unicode 配列として後者をマーシャリングするという事実によるためです。
System.Boolean の場合、
完全な .NET Framework は 4 バイトの整数値を使用しますが、.NET Compact Framework マーシャラは 1 バイトの整数値を使用します。
警告System.Stringは、.NET Compact Framework の高速転送型ですが、 構造体やクラスなどの複合オブジェクト内で使用される場合は、高速転送型ではありません。 この状況については、次回の資料で詳しく調査します。
.NET Compact Framework アプリケーションでこれらの型の使用を続けることで、
読者にとってコーディングが簡単になることは明らかです。
高速転送型の使用についての別の簡単な例は、CeRunAppAtEvent 関数を呼び出すことです。
これにより、ActiveSync データの同期が完了すると、.NET Compact Framework アプリケーションが起動されます。
Public Enum ceEvents
NOTIFICATION_EVENT_NONE = 0
NOTIFICATION_EVENT_TIME_CHANGE = 1
NOTIFICATION_EVENT_SYNC_END = 2
NOTIFICATION_EVENT_DEVICE_CHANGE = 7
NOTIFICATION_EVENT_RS232_DETECTED = 9
NOTIFICATION_EVENT_RESTORE_END = 10
NOTIFICATION_EVENT_WAKEUP = 11
NOTIFICATION_EVENT_TZ_CHANGE = 12
End Enum
Public Class Environment
Private Sub New()
End Sub
<DllImport("coredll.dll", SetLastError:=True)> _
Private Shared Function CeRunAppAtEvent(ByVal appName As String, _
ByVal whichEvent As ceEvents) As Boolean
End Function
Public Shared Function ActivateAfterSync() As Boolean
Dim ret As Boolean
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_SYNC_END)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"ActivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "ActivateAfterSync")
Return False
End Try
End Function
Public Shared Function DeactivateAfterSync() As Boolean
Dim ret As Boolean = False
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_NONE)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"DeactivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "DeactivateAfterSync")
Return False
End Try
End Function
' 他の処理。
End Class
注意ActivateAfterSyncメソッドを呼び出した後、ActiveSync 同期が完了すると、 特別のコマンドライン パラメータを使用して、 アプリケーションのインスタンスが自動的に起動されます。 通常、コマンドラインを検出して、アプリケーションの既存のインスタンスをアクティブにするために、 スマート デバイス プロジェクトに新たなコードが追加されます。
この例では、SHGetSpecialFolderPath のように、
すべての引数と戻り値が高速転送型なので、
読者やマーシャラが追加の作業を行う必要はありません。
この場合、ceEvents 列挙値が、
元になる値型としてマーシャリングされることがわかります。
既定では、元になる値型は、高速転送型の System.Int32 です。
ただし、マーシャラは、32 ビット以下の戻り値の型でのみ機能することに注意する必要があります。
文字列の引き渡し
このように、.NET Compact Framework の文字列は高速転送型で、
Unicode 文字の NULL で終わる配列としてアンマネージ関数に示されます。
呼び出しを行うとき、System.String は参照型なので
(System.String はマネージ ヒープに割り当てられ、
そのアドレスは参照変数に格納されます)、System.String を値で渡す場合でも、
マーシャラは文字列へのポインタをアンマネージ関数に渡します。
これは、SHGetSpecialFolder と CeRunAppAtEvent
のどちらの場合でも同様です。
注意 .NET Compact Framework は、常に、参照型へのポインタを渡しますが、 参照 (VB ではByRef、C# ではref) による参照型の引き渡しはサポートしていません。
ただし、お気付きのように、SHGetSpecialFolder は固定長の文字列バッファを必要とします。
この場合、SHGetSpecialFolder はパスを格納できますが、
CeRunAppAtEvent は文字列を読み取るだけです。
SHGetSpecialFolder の場合、
固定長を次のように宣言します (c は、System.Char への変換を示します)。
Dim sPath As String = New String(" "c, MAX_PATH)
文字列へのポインタがアンマネージ関数に渡されるので、
アンマネージ コードは文字列を WCHAR *
(または TCHAR *、
または可能性のあるLPTSTR、 LPSTR) とみなし、
ポインタを使用して文字列を操作できます。
通常、関数が完了すると、呼び出し側は文字列を確認できます。
この動作は、完全な .NET Framework の動作とは大きく異なります。 完全な .NET Framework の場合、 マーシャラは文字セットを考慮する必要があります。 その結果、完全な .NET Framework は、 アンマネージ関数への値や参照による文字列の引き渡し、 およびアンマネージ関数がバッファの内容を変更できるようになることをサポートしません。
(多くの Win32 API は文字列バッファを必要とするので)
完全な .NET Framework でこの問題を解決するために代わりにできることは、
System.Text.StringBuilder オブジェクトを渡すことです。
その結果、マーシャラが、操作できるアンマネージ関数にポインタを渡します。
唯一の注意事項は、StringBuilder に戻り値用の十分な空き領域を割り当てる必要があることです。
それを行わなかった場合、
テキストがオーバーフローして、P/Invoke が例外をスローする原因となります。
また、.NET Compact Framework の場合とまったく同じ方法で StringBuilder を使用できるので、
SHGetSpecialFolderPath の宣言を以下のように変更する場合があります。
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _ ByVal hwndOwner As Integer, _ ByVal lpszPath As StringBuilder, _ ByVal nFolder As ceFolders, _ ByVal fCreate As Boolean) As Boolean
その場合、構文の呼び出しは以下のようになります。
Dim sPath As New StringBuilder(MAX_PATH) ret = SHGetSpecialFolderPath(0, sPath, folder, False)
実際、この特定の例の場合、VB の Declare ステートメントを使用すると、
ByVal String パラメータが out パラメータとしてマーシャリングされるので、
StringBuilder に変更する方がより効果的です。
新しい参照が返る前に関数が戻るとき、out パラメータを使用することで、
共通言語ランタイムに新しい String オブジェクトを作成させます。
DllImportAttribute は、この現象を発生させません。
固定長の文字列バッファは、初期化が容易になり、
完全な .NET Framework と一貫性を持つようになるので、
少なくとも、固定長の文字列バッファを処理するときは、StringBuilder を使用することをお勧めします。
構造体の引き渡し
上記で説明したように、
構造体に高速転送型が含まれている限り、
問題なくアンマネージ関数に構造体を渡すことができます。
たとえば、GlobalMemoryStatus Windows CE API は、
作成する MEMORYSTATUS 構造体へのポインタを渡して、
デバイスの物理メモリや仮想メモリに関する情報を返します。
構造体には、高速転送型
(.NET Compact Framework の場合、System.UInt32 という 32 ビットの符号なし整数に変換するアンマネージ DWORD 値)
のみが含まれているので、
以下のコードで示すように、
関数を容易に呼び出すことができます。
Private Structure MEMORY_STATUS
Public dwLength As UInt32
Public dwMemoryLoad As UInt32
Public dwTotalPhys As UInt32
Public dwAvailPhys As Integer
Public dwTotalPageFile As UInt32
Public dwAvailPageFile As UInt32
Public dwTotalVirtual As UInt32
Public dwAvailVirtual As UInt32
End Structure
<DllImport("coredll.dll", SetLastError:=True)> _
Private Shared Sub GlobalMemoryStatus(ByRef ms As MEMORY_STATUS)
End Sub
Public Shared Function GetAvailablePhysicalMemory() As String
Dim ms As New MEMORY_STATUS
Try
GlobalMemoryStatus(ms)
Dim avail As Double = CType(ms.dwAvailPhys, Double) / 1048.576
Dim sAvail As String = String.Format("{0:###,##}", avail)
Return sAvail
Catch ex As Exception
HandleCeError(ex, "GetAvailablePhysicalMemory")
Return Nothing
End Try
End Function
この場合、GetAvailablePhysicalMemory 関数が
MEMORY_STATUS 構造体のインスタンスを作成し、
GlobalMemoryStatus 関数を渡していることがわかります。
関数は (Windows CE では LPMEMORYSTATUS として定義されている)
構造体へのポインタを必要とするので、GlobalMemoryStatus の宣言は、
ms パラメータが ByRef であることを示します。
C# では、宣言と呼び出しの両方に ref キーワードを使用する必要があります。
また、構造体ではなく、クラスとして MEMORY_STATUS を宣言することで、
この関数を呼び出している場合があります。
クラスは参照型なので、参照としてパラメータを宣言する必要はありませんが、
マーシャラが、常に、参照型への 4 バイトのポインタをアンマネージ関数に渡すという事実に依存する場合があります。
参照型は、常に、マネージ コードに示された順でマーシャリングされることに注意することも重要です。
つまり、フィールドは、アンマネージ関数の要求どおりにメモリに配置されます。
したがって、完全な .NET Framework の場合と同様にサポートされていることですが、
StructLayoutAttribute と LayoutKind.Sequential
で構造体を修飾する必要はありません。
構造体 (およびクラス) をアンマネージ関数に渡すことができますが、
.NET Compact Framework マーシャラは、
アンマネージ関数から返される構造体へのポインタのマーシャリングをサポートしていません。
このような場合、Marshal クラスの PtrToStructure メソッドを使用して、
構造体を手動でマーシャリングする必要があります。
最後に、.NET Compact Framework のマーシャラと完全な .NET Framework のマーシャラ間の重大な相違点の 1 つは、
.NET Compact Framework マーシャラが、構造体内の複合オブジェクト (参照型) をマーシャリングできないことです。
つまり、構造体の任意のフィールドが、
上記で示した表に記載されている型以外の型 (文字列や文字列の配列など) を保持している場合、
構造体を完全にはマーシャリングできません。
これは、.NET Compact Framework が、完全な .NET Framework で使用する
MarshalAsAttribute をサポートせず、
データをマーシャリングする方法に関する明示的な指示をマーシャラに提供しないという事実によるためです。
ただし、次回の資料では、構造体に埋め込まれている文字列などの参照型を、
アンマネージ関数に渡す方法と、
アンマネージ関数から返す方法について調査します。
整数以外の型の引渡し
高速転送型の表に、 浮動小数点変数に関する記述がないことがわかります。 これらの型は (数字全体を表していない) 整数以外の型なので、.NET Compact Framework はこれらの型を値でマーシャリングできません。 ただし、これらの型を参照でマーシャリングできるので、 ラッパーまたは shim として動作するアンマネージ関数へのポインタとして、 これらの型を渡すことができます。
注意 このことは、INT64 へのポインタとしてマーシャリングされる 64 ビット整数 (VB では Long、C# では long) にも当てはまります。
たとえば、アンマネージ関数が型 double の 2 つのパラメータを受け取り、
C で定義されている double を返す場合は、次のようになります。
double DoSomeWork(double a, double b) { }
次に、.NET Compact Framework から関数を呼び出すために、 初めに eMbedded Visual C を使用してアンマネージ関数を作成する必要があります。 eMbedded Visual C は、 参照による両方の引数を (ポインタとして) 受け取り、 元の関数を呼び出すことで出力パラメータとして結果を返しました。 これを以下に示します。
void DoSomeWorkShim(double *pa, double *pb, double *pret)
{
*pret = DoSomeWork(pa, pb);
}
次に、
以下のように DllImportAttribute ステートメントを使用して、
Compact Framework の DoSomeWorkShim 関数を宣言します。
<DllImport("ShimFunction.dll")> _
Private Shared Sub DoSomeWorkShim(ByRef a As Double, _
ByRef b As Double, ByRef ret As Double)
End Sub
最後に、DoSomeWork
の元の呼び出しシグニチャを保持する新たなマネージ ラッパー関数で、
DoSomeWorkShim への呼び出しをラップする場合があります。
Public Shared Function DoSomeWork(ByVal a As Double, _ ByVal b As Double) As Double Dim ret As Double DoSomeWorkShim(a, b, ret) Return ret End Function
その他の問題点
開発者が P/Invoke について考えるときに懸念する問題の 1 つは、 共通言語ランタイムのガベージ コレクタ (GC) との相互運用性です。 .NET Compact Framework と完全な .NET Framework の両方のガベージ コレクタは、 ガベージ コレクションが発生したときにマネージ ヒープにオブジェクトを再配置できるので、 アンマネージ関数に渡されるポインタを使用してメモリを操作しようとするアンマネージ関数にとって、 このことが問題になる場合があります。 幸いにも、.NET Compact Framework マーシャラは、 呼び出しを行っている間、 アンマネージ関数に渡される任意の参照型を自動的に固定します (現在のメモリの場所にオブジェクトをロックします)。
開発者が直面する可能性が高い 2 つ目の問題点は、
P/Invoke に関連して、C# の unsafe キーワードと
fixed キーワードを使用することです。
このトピックについては、次回の資料で十分に説明しますが、
基本的には、C の場合と同様に、unsafe キーワードにより、
C# コードがポインタを使用して、メモリを直接操作できるようになります。
これにより、柔軟性が非常に高まりますが、
共通言語ランタイムのコード検証機能が無効にもなります。
この機能は、JIT 処理中に、共通言語ランタイムが実行するコードが、
割り当てられたメモリの読み取りだけを行うこと、
および適切な数の型と引数ですべてのメソッドが呼び出されることを、
共通言語ランタイムが確認できるようにします。
注意 完全な .NET Framework の場合、 確認不可能なコードの作成のほか、 信頼済みの環境だけで unsafe コードを実行できますが、 .NET Compact Framework Version 1.0 の場合、 コード アクセス セキュリティ (CAS) が含まれていないので、 現時点ではこれは問題ではありません。
unsafe と共に fixed キーワードを使用して、
通常、値型や System.Char の配列などの値型の配列であるマネージ オブジェクトを固定するので、
アンマネージ関数がマネージ オブジェクトを使用しているとき、GC はマネージ オブジェクトを移動できません。
まとめ
このように、.NET Compact Framework の Platform Invoke 機能は、 完全な .NET Framework で利用できる機能の優れたサブセットを提供します。 そのサブセットを使用して、アプリケーションが必要とするアンマネージ DLL への呼び出しの大部分を、 非常に容易に作成できるようになるでしょう。
構造体に埋め込まれた文字列の引き渡しなどのより複雑な状況では、 ポインタとメモリの割り当てを行うことが必要な場合があります。