次の方法で共有


ネイティブ コードを .NET CLR に移行する
Don Box
D

管理、株主、または過度に野心的な開発者の希望を示しています。Microsoft .NET 共通言語ランタイム (CLR) で®マネージド コードとして記述されたすべてのソフトウェアを実行することは単に不可能です。 プログラミング能力が不足しているため、多くの組織が、新しいランタイムがどれほど魅力的であっても、既存の Win32® ベース、C、C++、Visual Basic® 6.0 ベース、または COM コードベースの 100% を書き換える可能性は低いです。 つまり、.NET 時代以前に記述されたネイティブ コードの世界は、.NET で記述された新しいマネージド コンポーネントと平和的に共存する必要があります。NET に対応した言語。 幸いなことに、CLR は、過度の痛みや苦しみなしに、2 つの世界を橋渡しすることをサポートしています。
      ネイティブ コードとマネージド コードを共存させるためには、CLR (MSCOREE.DLL) 内で実行されるマネージド コードを、MSCOREE.DLLの外部で実行されているコードと統合できるようにするためのメカニズムがいくつか必要です。 つまり、マネージド プログラムは、従来の COM または Win32 ベースのコードを実行するのに十分な長さである "アンマネージド モード" で実行する必要があります。 また、アンマネージ スレッドは、マネージド型でメソッドを呼び出すことができるのに十分な長さMSCOREE.DLLにアクセスする必要があります。

図 1 マネージド コードとアンマネージド コードの統合
図 1マネージド コードとアンマネージド コードの統合

      幸いにも、CLR は両方の種類の遷移をサポートしているため、マネージド DLL とネイティブ DLL の両方を 1 つのプロセスで共存させ、CLR 内でホストされているかどうかに関係なく、DLL 間で呼び出しを行うことができます。 一般的な 2001 年頃のプロセスの例を図 1示します。 一部のコードはMSCOREE.DLL内で実行され、一部は実行されないことに注意してください。 オペレーティング システム全体がマネージド コードで書き換えられる場合を除き、これはしばらくの間問題の状態である可能性があります。現在、基になる Windows® オペレーティング システムは、マネージド DLL ではなくネイティブ DLL を介して公開されます。

P/Invoke

      CLR から Win32 ベースのネイティブ DLL を呼び出すことは、P/Invoke を使用して非常に簡単です (P はプラットフォームの略です)。 P/Invoke は、LoadLibrary/GetProcAddress を介して解決できる PE/COFF エントリ ポイントに静的メソッド宣言をマップできるテクノロジです。 前の Java ネイティブ インターフェイス (JNI) や J/Direct® と同様に、P/Invoke はマネージド メソッド宣言を使用してスタック フレームを記述しますが、メソッド本体は外部のネイティブ DLL によって提供されることを前提としています。 ただし、JNI とは異なり、P/Invoke は CLR を念頭に置いて記述されていない "heritage" DLL をインポートする場合に便利です。
      メソッドが外部のネイティブ DLL で定義されていることを示すには、静的メソッドを extern としてマークし、System.Runtime.InteropServices.DllImport メソッド属性を使用します。 DllImport 属性は、メソッドを呼び出すときに LoadLibrary と GetProcAddress に渡す引数を CLR に指示します。 組み込みの C# DllImport 属性は、System.Runtime.InteropServices.DllImport のエイリアスにすぎません。
      DllImport 属性は、さまざまなパラメーターを受け取ります。 図 2示すように、DllImport 属性には少なくともファイル名を指定する必要があります。 このファイル名は、メソッド呼び出しをディスパッチする前に LoadLibrary を呼び出すためにランタイムによって使用されます。 EntryPoint パラメーターが DllImport に渡されない限り、GetProcAddress に使用する文字列はメソッドのシンボリック名になります。 図 3 、kernel32.dllで Sleep メソッドを呼び出す 2 つの方法を示します。 最初の例では、DLL 内のシンボルの名前と一致する C# 関数の名前に依存しています。 2 番目の例では、代わりに EntryPoint パラメーターに依存しています。
      文字列を受け取るメソッドを呼び出す場合は、メソッドまたは周囲の型に Unicode/ANSI ポリシーを設定する必要があります。 これは、アンマネージ コードで使用するために文字列型を変換する方法を制御するために必要です。 DllImport の CharSet パラメーターを使用すると、Unicode (CharSet.Unicode) または ANSI (CharSet.Ansi) を常に使用するか、基になるプラットフォームが Windows NT® または Windows 2000 と Windows 9x または Windows Millennium Edition (Me) (CharSet.Auto) に基づいて自動的に決定するかを指定できます。 CharSet.Auto の使用は、TCHAR データ型を使用して C で Win32 ベースのコードを記述するのと似ていますが、文字型と API はコンパイル時ではなく読み込み時に決定され、1 つのバイナリがすべてのバージョンの Windows で適切かつ効率的に動作することを可能にする点が異なります。
      Windows プラットフォームには、呼び出し規則と文字セットを示すさまざまな名前マングリング スキームがあります。 CharSet が CharSet.Auto に設定されている場合、ランタイムで Unicode または ANSI が使用されているかどうかに応じて、シンボリック名に W または A サフィックスが自動的に付けられます。 さらに、プレーン シンボルが見つからない場合、ランタイムは stdcall 規則を使用してシンボルを組み込むようになります (たとえば、Sleep が_Sleep@4場合があります)。 このシンボリック マングリングは、DllImport 属性に対する ExactSpelling パラメーターを使用して抑制できます。
      最後に、COM スタイルの HRESULT を使用する Win32 関数を呼び出す場合は、2 つのオプションがあります。 既定では、P/Invoke は HRESULT を関数から返される単なる 32 ビット整数として扱い、プログラマは手動でエラーをテストする必要があります。 このような関数を呼び出すより便利な方法は、TransformSig=true パラメーターを DllImport 属性に渡すことです。 これにより、P/Invoke レイヤーは、その 32 ビット整数を COM HRESULT として扱い、失敗した結果が発生した場合に COMException をスローするように指示します。 (ベータ 1 の TransformSig パラメーターはベータ 2 では PreserveSig に名前が変更され、その意味はベータ 1 から反転されることに注意してください)。図 4
示す 2 つの宣言を指定すると、OLE32Wrapper.CoImpersonateClient1 を呼び出すには、プログラマが手動で結果を確認し、失敗した HRESULT を明示的に処理する必要があります。 TransformSig パラメーターが使用されていたため、OLE32Wrapper.CoImpersonateClient2 を呼び出すと、失敗した HRESULT をプログラマの介入なしに暗黙的に COMException にマップするように CLR に指示します。 OLE32Wrapper.CoImpersonateClient2 の場合、基になる関数から返される値がないため、このメソッドは void を返します。 型指定された値 (double など) を返すようにメソッドが宣言されている場合、P/Invoke レイヤーは、基になるネイティブ関数が参照によって追加のパラメーター (la COM の [retval] 属性) を受け入れると想定していました。 このマッピングは、TransformSig パラメーターが true の場合にのみ行われます。

型指定された画面切り替え

      ランタイムから呼び出す (またはランタイムに) 呼び出すとき、パラメーターは、図 5に示すように、必ず呼び出し履歴に渡されます。 これらのパラメーターは、ランタイムと外部の両方の型のインスタンスです。 相互運用のしくみを理解する鍵は、指定された "値" にマネージド型とアンマネージ型の 2 つの型があることを理解することです。 さらに重要なのは、一部のマネージド型はアンマネージ型に等形です。つまり、その型のインスタンスをランタイムの外部に渡す必要がある場合、変換は必要ありません。 しかし、多くの型は同形ではなく、外部に適した表現を思い付くために何らかの変換が必要です。

図 5 呼び出し履歴 のパラメーター
図 5 呼び出し履歴 のパラメーター

      図 6
、基本的な同型と非同型の一覧を示します。 同型のみをパラメーターとして受け取る外部ルーチンを呼び出す場合、変換は必要なく、呼び出し元と呼び出し先は、一方がアンマネージド モードで実行されているにもかかわらず、実際にはスタック フレームを共有できます。 少なくとも 1 つのパラメーターが非同型の場合、スタック フレームを MSCOREE の外部の世界と互換性のある形式にマーシャリングする必要があります。 その変換は、関数から呼び出し元に戻されるパラメーター値に対して、他の方向に行う必要がある場合があります。 幸い、C# コンパイラは、ref キーワードと out キーワードに基づいて、どの方向パラメーター値がフローするかを認識します。 図 7 、この効果を示します。

図 7 非同型パラメーター
図 7非同型パラメーター

      MarshalAs 属性を使用して、指定されたパラメーター (または構造体内のフィールド) をマーシャリングする方法を自由に制御できます。 この属性は、MSCOREE 外の世界に表示するアンマネージ型を示します。 少なくとも、MarshalAs 属性を使用して、特定のパラメーターまたはフィールドに対応するネイティブ型を示すことができます。 多くの型では、CLR は適切な既定値を選択します。 ただし、MarshalAs 属性を使用して、これらの既定値をオーバーライドできます。 たとえば、図 8
のコードは、MarshalAs 属性を使用して CLR System.String 型を 4 つの一般的な Win32 形式のいずれかにマップする方法を示しています。 関心のある点として、UnmanagedType.LPWStr パラメーター以外のすべてが、基になるネイティブ形式に準拠するために作成される文字列のコピーになります。
      MarshalAs 属性を使用して、フィールド単位またはパラメーターごとの型マッピングを制御するだけでなく、構造体とクラスの基になる表現を制御することもできます。 特に、StructLayout 属性と FieldOffset 属性を使用すると、クラスと構造体のメモリ内レイアウトを正確に制御できます。これは、ランタイムの外部で渡される構造体にとって重要です。 注釈付き C# 構造体の例を次に示します。
  using System.Runtime.InteropServices;
[ StructLayout(LayoutKind.Sequential) ]
public struct PERSON_REP {
  [ MarshalAs(UnmanagedType.BStr) ] 
  public String name;
  public double age;
  [ MarshalAs(UnmanagedType.VariantBool) ]
  bool dead;
}

このコードは、COM IDL の同等の定義です。
  struct PERSON_REP {
  BSTR name;
    public double age;
    VARIANT_BOOL dead;
  };

RCW と CCW

      System.String または System.Object 以外のオブジェクト参照を渡す場合、既定のマーシャリング動作では、CLR オブジェクト参照と COM オブジェクト参照の間で変換が行われます。 図 9
に示すように、CLR オブジェクトへの参照が MSCOREE 境界を越えてマーシャリングされると、CLR オブジェクトへのプロキシとして機能する COM 呼び出し可能ラッパー (CCW) が作成されます。 同様に、COM オブジェクトへの参照が MSCOREE 境界を介してマーシャリングされると、ランタイム呼び出し可能ラッパー (RCW) が作成され、COM オブジェクトへのプロキシとして機能します。 どちらの場合も、"proxy" は基になるオブジェクトのすべてのインターフェイスを実装します。 さらに、"プロキシ" は、IDispatch、オブジェクトの永続化、イベントなどの COM および CLR イディオムを、他のテクノロジの対応するコンストラクトにマップしようとします。

図 9 RCW および CCW アーキテクチャ
図 9RCW および CCW アーキテクチャ

      RCW または CCW を介して MSCOREE 境界をまたぐインターフェイスの場合、CLR はマネージド インターフェイス定義への一連の注釈に依存して、型を変換する方法に関する基になるマーシャリング レイヤー ヒントを提供します。 これらのヒントは、P/Invoke で説明したヒントのスーパーセットです。 定義する必要があるその他の側面としては、UUID、vtable と dispatch、デュアル マッピング、IDispatch の処理方法、配列の変換方法などがあります。 これらの側面は、System.Runtime.InteropServices 名前空間の属性を使用して、マネージド インターフェイス定義に追加されます。 これらの属性がない場合、CLR は、特定のインターフェイスとメソッドの既定の設定を慎重に推測します。 ゼロから定義された新しいマネージド インターフェイスの場合、共通言語ランタイムの外部でインターフェイスを使用する場合は、属性を明示的に使用すると便利です。

TLBIMP と TLBEXP

      ネイティブ COM 型定義 (構造体、インターフェイスなど) を CLR に手動で変換できます。特に正確な TLB が使用できない場合は、これが必要な場合があります。 CLR でのリフレクションのユビキタスを考えると、型定義を逆の方向に変換する方が簡単ですが、やはり、手動翻訳に頼るよりもツールを使用することをお勧めします。 CLR には、COM TLB が十分に正確である限り、この翻訳を行う妥当な作業を行うコードが付属しています。 System.Runtime.InteropServices.TypeLibConverter は、TLB アセンブリと CLR アセンブリ間で変換できます。 ConvertAssemblyToTypeLib メソッドは CLR アセンブリを読み取り、対応する COM 型定義を含む TLB を出力します。 この変換プロセスのヒント (MarshalA など) は、ソース型のインターフェイス、メソッド、フィールド、およびパラメーターにカスタム属性として表示される必要があります。 ConvertTypeLibToAssembly メソッドは COM TLB を読み取り、対応する CLR 型定義を含む CLR アセンブリを出力します。 SDK には、NMAKE での使用に適したコマンド ライン インターフェイスの背後にこれら 2 つの呼び出しをラップする 2 つのツール (TLBIMP.EXEとTLBEXP.EXE) が付属しています。 これらのツール間の関係を図 10
示します。

図 10 TLBIMP と TLBEXP
図 10TLBIMP と TLBEXP

      一般に、最初に CLR ベースの言語で型を定義してから、TLB を出力する方が簡単です。 たとえば、図 11
に示されている C# コード 考えてみます。 このコードを擬似 IDL ファイルとして扱う場合は、csc.exe と tlbexp.exe を実行して、図 12に示されている実際の IDL ファイルによって生成されたものと機能的に同じ TLB 生成できます。 C# アプローチを使用する利点は、型定義が拡張可能で読みやすく、どちらも TLB または IDL ファイルでは言えないということです。

REGASM

      一部の開発者は、COM の CoCreateInstance を使用して CLR クラスにアクセスできるようにする必要があります。 これを行うには、COM CLSID をアセンブリと型にバインドする一部のレジストリ エントリを挿入する必要があります。 REGASM.EXEツールはこれを行います。 REGASM.EXEは、アセンブリを引数として受け取り、各パブリック クラスの適切なレジストリ エントリを書き込みます。 InprocServer32 エントリは、CLR ベースのオブジェクトの COM ファサードとして機能するMSCOREE.DLLを指します。 ネイティブ コードから CLR をかなり簡単にホストし、既定の AppDomain を使用して型を読み込み、任意のクラスの新しいインスタンスをインスタンス化できるため、REGASM.EXEは厳密には必要ではないことに注意してください。 図 13
、Visual Basic 6.0 の System.Collections.Stack クラスを使用した例を示します。 これはモニカーを使用してさらに自動化できます。 http://discuss.develop.com/archives/wa.exe?A2=ind0008&へのサーフィン;L=DOTNET&P=R63059 では、従来の COM の CoGetObject または Visual Basic 6.0 の GetObject メソッドを使用して CLR 型をインスタンス化できる ".NET モニカー" について説明します。

戦略

      ランタイムにネイティブ コードを移動するための 3 つの基本的な戦略があります。 図 14
、ソース コードが CLR 前言語 (Visual Basic 6.0 など) から CLR 対応言語 (Visual Basic.NET や C# など) に機械的に変換される部分ポート アプローチを示しています。 この方法は、相互運用サービスを使用して、下位のネイティブ ライブラリを CLR にインポートできるという事実に依存します。

図 14 部分ソース コードによる .NET の移植
図 14 部分ソース コード移植 を使用した .NET の

      図 15 は、ネイティブ サブコンポーネントに依存せずに CLR 対応言語でソース コードを完全に書き換える完全なポート アプローチを示しています。 この方法は、下位のネイティブ ライブラリのマネージド バージョンの存在に依存します。 また、マネージド ライブラリが要求するスタイルの違いにソース コードを適応させる意欲にも依存します。 このアプローチでは最も労力が必要ですが、相互運用の移行が少なくなるだけでなく、(通常は) より豊富で優れた設計のライブラリという利点があります。

図 15 完全なソース コードによる .NET の移植
図 15 完全なソース コード 移植
を使用した .NET の


      図 16 は、ソース コードを単独で残し、相互運用サービスを使用してコードを CLR にインポートする "punt" アプローチを示しています。 この方法は、下位のネイティブ ライブラリのマネージド バージョンの存在に依存せず、開発者がソース コードを重要な方法で適応させる必要もありません。 この方法では、必要な労力が最も少なく、メソッドごとの下位ライブラリ呼び出しの比率が高いため、相互運用の遷移が少なくなるという利点があります。

図 16 .NET via Binary Import (Punt Approach)
図 16 バイナリ インポートを使用した .NET の(Punt アプローチ)

      XML や SOAP などのテクノロジはいつでも使用できますが、CLR では、COM と Win32 の古い世界から C# と Web サービスの新しい世界に移行するためのさまざまな選択肢が開発者に提供されます。

Don to housecom@microsoft.comの質問とコメントを送信します。
Don Box は、ソフトウェア業界に教育とサポートを提供する DevelopMentor の共同設立者です。 書き込み Essential COM、およびcorote Effective COM Essential XML (すべて Addison-Wesley)。W3C SOAP 仕様を共同編集しました。Reach Don at http://www.develop.com/dbox.この列は、Don Box と Ted Pattison (2002 年第 1 © 章) が共同執筆した、C# を使用した .NET プログラミングに関する次の書籍に基づいて作成されています。使用はピアソン・エデュケーション社の許可による。すべての権限が予約されています。

MSDNマガジン 2001年5月号より