Chapter.8

はまり道

2005/1/6
 ・インターフェースが継承可能であることが判明したので内容を修正。

1.インターフェースの継承

前章のサンプルでは、IOleInPlaceUIWindowIOleInPlaceFrame、および、IOleInPlaceSiteは、継承せずに定義しています。 しかし、本物のOLEIDL.IDLでは、IOleInPlaceUIWindowIOleInPlaceSiteIOleWindowを継承して定義されており、 IOleInPlaceFrameIOleInPlaceUIWindowを継承して定義されています。 下のコードはOLEIDL.IDLの抜粋です。

[
    object,
    uuid(00000115-0000-0000-C000-000000000046),
    pointer_default(unique)
]

interface IOleInPlaceUIWindow : IOleWindow
{

[
    object,
    uuid(00000116-0000-0000-C000-000000000046),
    pointer_default(unique)
]

interface IOleInPlaceFrame : IOleInPlaceUIWindow
{

[
    object,
    uuid(00000119-0000-0000-C000-000000000046),
    pointer_default(unique)
]

interface IOleInPlaceSite : IOleWindow
{

これらをVB.NETに移行する際、下に提示したソースのように継承を利用してインターフェースを定義する誘惑に駆られます。

' IOleInPlaceUIWindow
<ComImport, _
 Guid("00000115-0000-0000-C000-000000000046"), _
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IOleInPlaceUIWindow
    Inherits IOleWindow

' IOleInPlaceFrame
<ComImport, _
 Guid("00000116-0000-0000-C000-000000000046"), _
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IOleInPlaceFrame
    Inherits IOleInPlaceUIWindow

' IOleInPlaceSite
<ComImport, _
 Guid("00000119-0000-0000-C000-000000000046"), _
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IOleInPlaceSite
    Inherits IOleWindow
このように定義したインターフェースを使用し、IOleWindowを多重継承するクラスを作成しても、正常にコンパイルされます。 しかし、プラットフォーム呼び出しでは、このように定義されたインターフェースは正常に動作しません。 実際に動作させると分かるのですが、COMのV-Tableレイアウトに継承元のインターフェースのメソッドが含まれないのです。 IOleInPlaceSiteでは、IOleWindowの二つのメソッドが含まれないため、呼び出されるメソッドが2つずれます。 一見、正常に動作しているように見えるところも、この問題をより深刻にしています。 IOleInPlaceSiteで実際に呼び出されるメソッドの対応を図にしてみました。

たいていの場合、呼び出された側はアクセス違反を起こしてコードの実行を中断することが多いので、重大で潜在的なバグになる可能性は低いと言えます。 ただし、上図にあるCanInPlaceActivateの呼び出しで、OnUIActivateが呼び出されるような、メソッドのシグネチャが完全に一致している呼び出しの場合、何事も無かったように動作を継続するので、問題の発覚が遅れます。 今回は、OLEの仕様で最初にCanInPlaceActivateを呼び出すことを推奨しているため、この問題はすぐに発覚しました。


▼ 2005/1/6 修正 ▼

2.まとめ

上記をまとめると、COMのインターフェースを定義する場合、継承を用いてはならない。と言えます。 この問題は何らかの属性によって回避できると予想し、System.Runtime名前空間の下にある属性クラスを検索したのですが、結局、有効な解決策を見つけることは出来ませんでした。 これからも継承を用いずにコーディングを進めることにします。

なお、ILDASM.EXEで、System.Windows.Forms.UnsafeNativeMethods配下のインターフェースを検索すると私が定義したインターフェースと同じインターフェースを見つけることが出来ます。 ここでも、継承は用いていないようです。

今回のサンプルは無しです。

2.インターフェースの正しい継承方法

実際には、COMインターフェースを継承して実装することは可能でした。 これは言語仕様を正しく理解していなかったことが原因でした。 継承を使用してインターフェースを定義すると下のようになります。

' IOleInPlaceSite
<ComImport, _
 Guid("00000119-0000-0000-C000-000000000046"), _
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface IOleInPlaceSite
    Inherits IOleWindow

    <PreserveSig> _
    Shadows Function GetWindow( _
        <Out()> _
        ByRef phwnd As IntPtr) As HRESULT

    <PreserveSig> _
    Shadows Function ContextSensitiveHelp( _
        <MarshalAs(UnmanagedType.Bool)> _
        ByVal fEnterMode As Boolean) As HRESULT
上記のように、Shadowsキーワードを指定して、継承元と同じメソッドを定義します。 これによって、IOleInPlaceSiteIOleWindowの二つのメソッドが追加されます。 このインターフェースを実装するコードは、下のようになります。

    '########################################
    ' IOleWindowインプリメント

    Function IOleWindow_GetWindow( _
        ByRef phwnd As IntPtr) As HRESULT _
        Implements  IOleWindow.GetWindow, _
                    IOleInPlaceSite.GetWindow
Implementsキーワードによって、IOleWindow_GetWindowメソッドはIOleWindowIOleInPlaceSite両方のGetWindowメソッドを実装していることを示します。 こうすることによって、IOleWindowから派生したIOleInPlaceSiteを正しく実装できたことになります。


3.まとめ

言語仕様を理解していないと思わぬところで痛い目に遭うことが分かったと思います。

残った問題としては、System.Windows.Forms.UnsafeNativeMethods配下のインターフェースの定義に、継承が用いられていない理由が謎のままです。 恐らくですが、継承を使用して定義したインターフェースは、コードサイズが若干増加することに起因しているのではないかと推測しています。 この定義方法では、.NET Frameworkを使用して作成したActiveXコントロールが多少の影響を受けますが、大した問題ではない気がします。

本連載では、コードの論理的整合性を保つために、継承を用いてインターフェースを定義していきたいと思います。 最後になりましたが、継承を用いるように修正したサンプルソースです。
VB.NETサンプルソース:chap8.vb.lzh(38KB未満 2005/1/6)
これ以降のサンプルソースも徐々に継承を使用するように修正していきます。

▲ 2005/1/6 修正 ▲
前へ 次へ
OLE on .NET Frameworkへ
総合トップへ