2024/01/19 23時頃追記: ComObject.FinalRelease
の諸問題は.NET 9で解決されます: Make multiple calls to FinalRelease safe by jkoritzinsky · Pull Request #97059 · dotnet/runtime
2024/01/13 11時頃追記: レビュー時のコメントが守られていないのは流石に変に思ったのでissueを立てました。
2024/01/11 23時頃追記: ComObject.FinalRelease
後のメソッド呼び出しも未定義動作となる話と、APIレビュー時のコメントが守られていないらしい話を追記しました。その他細かい表現を修正しました。
.NET 8になって、LibraryImportAttribute
やGeneratedComInterfaceAttribute
を使って、COMオブジェクトをマーシャリングできるようになりました。より具体的には、ネイティブのCOMオブジェクトを保持する、マネージドなComObject
型へマーシャリングされます。さて、COMオブジェクトを扱う場合、参照カウンタを管理すること、すなわち適切にReleaseすることが必須です。しかしドキュメントを探しても、ComObject
が保持するネイティブCOMオブジェクトをどのようにReleaseすればいいのか分かりませんでした。実装確認や実験をすると、ComObject
型が持つネイティブCOMオブジェクトは、基本的にGCタイミングでのみReleaseされるということが分かりました。本記事は結果や実験方法について記述します。
- まとめ
- 検証バージョン
- 関連する型などの紹介
- 問題の現象の説明
- 動作検証コード
- 感想
まとめ
- System.Runtime.InteropServices.LibraryImportAttribute ClassやSystem.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute ClassでCOMオブジェクトをマーシャリングすると、System.Runtime.InteropServices.Marshalling.ComObject ClassがネイティブのCOMオブジェクトを持つ形式でマーシャリングされます。
- デフォルトのマーシャラーであるSystem.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<T> Classでマーシャリングすると、
internal
なComObject.UniqueInstanceプロパティがfalse
に設定されます。その結果、public
なComObject.FinalRelease Methodを呼び出しても無意味です。そのため、ネイティブのCOMオブジェクトのReleaseはGC時のComObject.Finalize待ちになります。 - マーシャラーにSystem.Runtime.InteropServices.Marshalling.UniqueComInterfaceMarshaller<T> Classを指定してマーシャリングすると、
internal
なComObject.UniqueInstance
プロパティはtrue
に設定されます。その結果、public
なComObject.FinalRelease
メソッドを呼び出すとネイティブのCOMオブジェクトをReleaseして解放できます。しかしそのままではComObject.Finalize
時でもReleaseしようとするため未定義動作、例えばAccess Violationを起こします。GC.SuppressFinalize
との併用が必須に思います。また、ComObject.FinalRelease
呼び出し後にさらなるReleaseやCOMインターフェースのメソッド呼び出しを行うとした場合でも、未定義動作を起こします。 - 正直なところ、従来の
ComImportAttribute
を使うマーシャリング方法の方が、安全であり、かつ確定的な解放が可能であるため、好ましい方法に思います。事前ソース生成が必須である場合に限り、GeneratedComInterfaceAttribute
を使うのが良いと思います。
検証バージョン
- Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.8.3
- .NET 8.0.0 (
System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription
で確認) - Microsoft Windows [Version 10.0.19045.3803] (
ver
コマンドで確認) - Windows 11 SDK 10.0.22621.0 (関係あるかは不明ですが念のため記載)
- 動作検証用ソリューションTan90909090/ComInteropTestの64-bitビルド版
- 詳細は動作検証コード節をご参照ください
関連する型などの紹介
本節は本題とはあまり関係しません。次の節までスキップしても問題ありません。
runtime/docs/design/libraries/LibraryImportGenerator/Compatibility.md at v8.0.0 · dotnet/runtimeに記載がある通り、LibraryImportAttribute
を使用したP/InvokeでCOMオブジェクトをマーシャリングできるようになりました。対応するPull RequestはAdd marshallers for GeneratedComInterface-based types by jkoritzinsky · Pull Request #86177 · dotnet/runtimeだと思います。
その際、C#側で再定義するCOMインターフェース型にはSystem.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute Classの属性を付与する必要があります。System.Runtime.InteropServices.ComImportAttribute Classは非対応です(SYSLIB1099 Casting between a 'ComImport' type and a source-generated COM type is not supported
警告で教えてくれます)。
ネイティブ側のCOMオブジェクトを保持する型として、System.Runtime.InteropServices.Marshalling.ComObject Classが使われます。ComObject
型へマーシャリングする方法は、System.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<T> Classを使う方法と、System.Runtime.InteropServices.Marshalling.UniqueComInterfaceMarshaller<T> Classを使う方法の2つがあります。このマーシャリングの方法も本記事の主題の1つです。詳細は後述します。
内部的には、ComObject
型インスタンスはSystem.Runtime.InteropServices.Marshalling.StrategyBasedComWrappers Classが生成します。StrategyBasedComWrappers
型は.NET 8で追加されました。一方でStrategyBasedComWrappers
型の抽象基底クラスであるSystem.Runtime.InteropServices.ComWrappers Classは.NET 5からあります。.NET 7までのComWrappers
型は、Using the ComWrappers API - .NET | Microsoft Learnに記述があるように、自分で頑張って派生型を作って運用する方式だったようです。
ComObject
型から各種COMインターフェースへのキャストは、System.Runtime.InteropServices.IDynamicInterfaceCastable Interfaceを使って実現しているようです。IDynamicInterfaceCastable
インターフェースはAdd IDynamicInterfaceCastable interface by elinor-fung · Pull Request #37042 · dotnet/runtimeのPRで追加されました。変更内容を見ると、コンパイル時ではなく、実行時のランタイムレベルでキャスト時の動作を特別扱いしているようです。
なお、DllImportAtribute
やComImportAttribute
でCOMオブジェクトを取得すると、マネージド側ではRuntime Callable Wrapper(略称RCW)にラップされます。LibraryImportAttribute
やGeneratedComInterfaceAttribute
経由で取得したComObject
のことは、おそらくRCW
とは呼ばないと思います(自信なし)。
問題の現象の説明
LibraryImportAttribute
適用結果のソースからComObject
が生成されるまでの流れ
LibraryImportAttribute
を適用したCoCreateInstance
等のソース生成結果は、マーシャリングにComInterfaceMarshaller<T>
を使用する場合と、UniqueComInterfaceMarshaller<T>
を使用する場合とで異なります。ComInterfaceMarshaller<T>
を使用する場合は、ppv = global::System.Runtime.InteropServices.Marshalling.ComInterfaceMarshaller<object>.ConvertToManaged(__ppv_native)
を含むソースが生成されます。UniqueComInterfaceMarshaller<T>
を使用する場合は、ppv = global::System.Runtime.InteropServices.Marshalling.UniqueComInterfaceMarshaller<object>.ConvertToManaged(__ppv_native)
等を含むソースが生成されます
- ComInterfaceMarshaller<T>.ConvertToManagedの実装では
StrategyBasedComWrappers.DefaultMarshallingInstance.GetOrCreateObjectForComInstance((nint)unmanaged, CreateObjectFlags.Unwrap)
の結果を、UniqueComInterfaceMarshaller<T>.ConvertToManagedの実装ではStrategyBasedComWrappers.DefaultMarshallingInstance.GetOrCreateObjectForComInstance((nint)unmanaged, CreateObjectFlags.Unwrap | CreateObjectFlags.UniqueInstance)
の結果を返します。この2つの呼び出しは、第2引数のCreateObjectFlags
内容のみが異なります。- その違いはドキュメント化されています。ComInterfaceMarshaller<T>のドキュメントには
This marshaller always passes the Unwrap flag
と、UniqueComInterfaceMarshaller<T>のドキュメントにはThis marshaller always passes the Unwrap and UniqueInstance flags
とあります。
- その違いはドキュメント化されています。ComInterfaceMarshaller<T>のドキュメントには
StrategyBasedComWrappers.DefaultMarshallingInstance
プロパティは単なるinternal
なシングルトンです。StrategyBasedComWrappers
自身はGetOrCreateObjectForComInstance
をオーバーライドしていないため、基底クラス側のComWrappers.GetOrCreateObjectForComInstanceを呼び出すことになります。ComWrappers.GetOrCreateObjectForComInstance
の実装は環境によって異なります。- 今回の検証環境ではruntime/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.cs側の実装を使用しました。この実装は最終的に
ComWrappers_TryGetOrCreateObjectForComInstance
をP/Invokeしています。デバッガーで動作確認すると、P/Invoke先からComWrapper.CallCreateObjectをComWrappersScenario.Instance
引数で呼び出しており、その結果CreateObject
を呼び出していました。 - 別の環境ではruntime/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.NativeAot.cs側の実装を使用することがあるようです。その途中でCreateObjectを呼び出すようです。
this
はStrategyBasedComWrappers
であるため、派生先のStrategyBasedComWrappers.CreateObject
を呼び出します。
- 今回の検証環境ではruntime/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.cs側の実装を使用しました。この実装は最終的に
- StrategyBasedComWrappers.CreateObjectの実装でようやく
ComObject
インスタンスを生成しています。その際にオブジェクト初期化子でUniqueInstance = flags.HasFlag(CreateObjectFlags.UniqueInstance)
を設定しています。すなわちComObject.UniqueInstance
プロパティは、ComInterfaceMarshaller<T>
でマーシャリングした場合はfalse
、UniqueComInterfaceMarshaller<T>
でマーシャリングした場合はtrue
になります。
なお、「COMオブジェクトをマーシャリングするコードをSource Generatorが生成する際、指定がない場合のデフォルトとしてComInterfaceMarshaller<T>
を使用する」の判定がどこにあるのかは探し当てられませんでした……。
ComObject
の解放処理
System.Runtime.InteropServices.Marshalling.ComObject Classのドキュメントを見ると、保持するネイティブCOMオブジェクトを以下のメソッドでReleaseできそうです:
- ComObject.Finalize Method、
Releases all references to the underlying COM object.
との説明があります。 - ComObject.FinalRelease Method、
Releases all references owned by this ComObject if it is a unique instance.
との説明があります。
ComObject.Finalizeの実装を見ると、以下の解放処理を行っています:
~ComObject() { CacheStrategy.Clear(IUnknownStrategy); IUnknownStrategy.Release(_instancePointer); }
一方で、FinalRelease
のドキュメントにはif it is a unique instance.
との条件が書かれていますが詳細が不明です。そこでComObject.FinalReleaseの実装の実装を見ると、internal
なUniqueInstance
プロパティがtrue
である場合にのみ、Finalize
同様の解放処理を行っています:
public void FinalRelease() { if (UniqueInstance) { CacheStrategy.Clear(IUnknownStrategy); IUnknownStrategy.Release(_instancePointer); } }
前述した通り、UniqueInstance
プロパティの値は、ComInterfaceMarshaller<T>
でマーシャリングした場合はfalse
なのでFinalRelease
メソッドを呼び出しても無意味です。UniqueComInterfaceMarshaller<T>
でマーシャリングした場合はtrue
になるため、FinalRelease
メソッドを呼び出すと解放できそうです。
ComObject
解放処理中の2つの内容
解放処理中の2つの処理はインターフェース経由で呼び出しています。実際に使用される具象型は、StrategyBasedComWrappers.CreateObjectでComObjectを生成する際に一緒に生成しています。
解放処理1つ目: CacheStrategy.Clear(IUnknownStrategy)
- ComObject.CacheStrategy用の実引数は、
public
なStrategyBasedComWrappers.DefaultIUnknownStrategy Propertyで生成されます。 - StrategyBasedComWrappers.DefaultIUnknownStrategyの初期値は
internal
なDefaultCaching
型インスタンスです。 - DefaultCaching.Clearの実装は以下のものです:
void IIUnknownCacheStrategy.Clear(IIUnknownStrategy unknownStrategy) { foreach (var info in _cache.Values) { _ = unknownStrategy.Release(info.ThisPtr); } _cache.Clear(); }
保持している各種キャッシュをReleaseし、キャッシュのコレクションであるConcurrentDictionary _cache
フィールドそのものもClear
しています。複数回呼び出しても安全です。
解放処理2つ目: IUnknownStrategy.Release(_instancePointer)
- ComObject.IUnknownStrategy用の実引数は、
protected
なStrategyBasedComWrappers.GetOrCreateIUnknownStrategy Methodで生成されます。 - StrategyBasedComWrappers.GetOrCreateIUnknownStrategyのデフォルト実装は
public
なStrategyBasedComWrappers.DefaultIUnknownInterfaceDetailsStrategy Propertyを返します。 - StrategyBasedComWrappers.DefaultIUnknownInterfaceDetailsStrategyの初期値は
internal
なFreeThreadedStrategy
型インスタンスです。 - FreeThreadedStrategy.Releaseの実装は以下のものです:
unsafe int IIUnknownStrategy.Release(void* thisPtr) => Marshal.Release((nint)thisPtr);
こちらはMarshal.Release(IntPtr) Methodへ全てを任せています。すなわち問答無用でネイティブCOMオブジェクトのReleaseメソッドを呼び出します。複数回呼び出すと、呼び出した回数だけReleaseされます。
UniqueComInterfaceMarshaller<T>
使用時にComObject.FinalRelease()
する際の重大な注意点
上述したように、UniqueComInterfaceMarshaller<T>
でマーシャリングしたComObject
の場合はComObject.FinalRelease()
で明示的に解放できます。しかし、絶対に考えるべき点があります:
ComObject.ReleaseObject
呼び出し後でも、当該ComObject
や、各種COMインターフェースを経由したメソッド呼び出しを行えてしまいます。すなわちUse-After-Freeであり、未定義動作です。- Use-After-Freeの影響は、もしかしたら環境や場合によって偶然問題がないかもしれませんし、System.AccessViolationException Class型の例外などが送出されるかもしれません。
ComImportAttribute
使用時のRCWでは、Marshal.ReleaseComObject
等で解放した後にアクセスすると、System.Runtime.InteropServices.InvalidComObjectException class型の例外が送出されるという、「安全な」挙動です。もちろん、未定義動作など起こしません。
- のちのGCタイミングで
ComObject.Finalize
が呼ばれ、その際に改めてRelease
しようとします。またもやUse-After-Freeを引き起こすものであり、未定義動作です。これを避けるためにはGC.SupressFinalize
によるGC抑制が必須に思います。- GCタイミングのUser-After-Free時に例外が発生する場合、どのオブジェクトが原因であるのかを探すことは困難でしょう。
GC.SupressFinalize
を使う箇所でCA1816 ClassName.MethodName() calls GC.SuppressFinalize(object) on something other than itself. Change the call site to pass 'this' ('Me' in Visual Basic) instead.
やCA1816 ClassName.MethodName() calls GC.SuppressFinalize(object), a method that is typically only called within an implementation of 'IDisposable.Dispose'. Refer to the IDisposable pattern for more information.
の警告が発生すると思います。しかしおそらく他に回避策はありません。それらの警告を抑制して良いと思います。ComImportAttribute
使用時のRCWでは、Marshal.ReleaseComObject
等で解放したら後は何も気にする必要はありません。もちろん、GC発生時に未定義動作など起こしません。
そういうわけで、ComObject.FinalRelease
呼び出しはGC.SupressFinalize
呼び出しと併用するか、どちらも呼び出さないか、が必要なります。当然ながら、どちらも呼び出さない場合は、ComInterfaceMarshaller<T>
と同じくReleaseタイミングはGC任せになります。
ComObject
がUniqueであるか判断できない以上、ComObject.FinalRelease
は実質的に使えないのでは?
ここまででComObject.FinalRelease
を扱う際の注意点を述べてきましたが、改めて全体を振り返ると、まともに扱うことは極めて困難であるように思います:
UniqueComInterfaceMarshaller<T>
でマーシャリングしたComObject
でのみ、ComObject.FinalRelease
する意味があります。かつGC.SupressFinalize
との併用が必須です。- 一方で、
ComInterfaceMarshaller<T>
でマーシャリングしたComObject
では、ComObject.FinalRelease
しても無意味であるため、GCによるComObject.Finalize
待ちになります。もし誤ってGC.SupressFinalize
してしまうとRelease不足となり、メモリリークします。 - そうなると、対象とする
ComObject
がどちらの方法でマーシャリングされたのかを知りたくなりますが、残念ながらComObject.UniqueInstance
プロパティはinternal
であるため、判別不能です(リフレクションを使えば取得できますが、将来的にプロパティ名等が変更される可能性があります)。 - つまり、
ComObject
をマーシャリングする2種類の方法が混ざってしまうと、ComObject.FinalRelease
およびGC.SupressFinalize
を呼び出すべきかどうかが判断不能になります。結局は安全策としてComObject.FinalRelease
を全く使わずに、GCでそのうち回収されるのを待つことになりそうです。
もしも、「すべてのマーシャリングをUniqueComInterfaceMarshaller<T>
で行わせるように、COMオブジェクトを取得するすべてのP/Invoke箇所でMarshalUsingAttribute
を指定する」ことを徹底できれば、ComObject.FinalRelease
を使えるでしょう。しかしうっかりミスなどで、デフォルトのマーシャラーであるComInterfaceMarshaller<T>
が混入してしまうことは簡単に起こり得ると思います。
動作検証コード
本節では、動作検証に使用したコードを抜粋して解説します。コード全体はGitHubのTan90909090/ComInteropTestへアップロードしています。64-bit設定でビルドして確認しました。
動作検証用のために、以下の性質を持つ組み込みのCOMインターフェースを探しました:
- コールバック等で別のCOMインターフェースを引数に取るCOMインターフェースであること
- インターフェースの継承を行っているCOMインターフェースであること
MSDNを適当に探していると、INamespaceWalk, INamespaceWalkCB, INamespaceWalkCB2が見つかったので、それらを独自実装して使いました。それらの本来の用途は全く知りません。
また、WindowsTerminalでは制御文字による文字色の変更に対応しているため、せっかくなので使ってみました。制御文字の非対応のターミナルで実行したい場合は、ComInteropTest/ServerDll/ImplementedClasses.hppのUSE_ESCAPE_CHARACTER_TO_COLOR_OUTPUT
マクロ定義内容を変更してください。
ログ出力やレジストリ登録を行うC++製COMサーバー用DLL
ComInteropTest/ServerDllプロジェクトです。ビルドしてできたDLLを、管理者権限のコマンドプロンプトでregsvr32.exe ServerDll.dll
を実行してレジストリへ登録してください。ここで管理者権限が必須である理由は、DllInstallエクスポート関数でのコマンドライン引数解釈をサボって省略したからですすみません。
実験が終わったら、管理者権限のコマンドプロンプトでregsvr32.exe /u ServerDll.dll
を実行して、レジストリ登録を解除してください。
動作確認用クラスでは、各種COMインターフェースの呼び出し時に標準出力へログを出力します。特にAddRef
, Release
時では参照カウンタをログ出力します。また、ComInteropTest/ServerDll/ClsIds.hppで動作確認用クラスのCLSIDを定義しています。
C++製COMクライアントから通常通り使う
ComInteropTest/ClientCpp/ClientCpp.cppです。動作確認用クラスのCLSID経由でCoCreateInstance
を呼び出してCOMオブジェクトを生成します。適当にCOMインターフェース経由で呼び出して、最後にRelease
します。問題なくCOMオブジェクトを解放できることが分かります。
C#製COMクライアントからComImportAttribute
経由で使う
ComInteropTest/ClientCsComImport/ClientCsComImport.csです。DllImportAttribute
でCoCreateInstance
をP/Invokeして、動作確認用クラスのCLSID経由でCOMオブジェクトを生成してRCWを取得します。その後、ComImportAttibute
属性を適用しつつ各種COMインターフェースを再定義して使用します。
最後に、標準入力の値に従って、以下のどちらかを行います。
- RCWに対して
Marshal.ReleaseComObject
を呼び出さない。GCによるRCWの解放を待つ。 - RCWに対して
Marshal.ReleaseComObject
を呼び出す。
どちらの場合でも、COMオブジェクトがReleaseされて解放されることが分かります。
C#製COMクライアントからGeneratedComInterfaceAttribtute
かつComInterfaceMarshaller<T>
経由で使う
ComInteropTest/ClientCsGeneratedComInterface/ClientCsGeneratedComInterface.csです。LibraryImportAttribute
でCoCreateInstance
をP/Invokeして、動作確認用のクラスのCLSID経由でCOMオブジェクトを生成します。その際、マーシャラーを指定していないため、デフォルトのマーシャラーであるComInterfaceMarshaller<T>
を使用してComObject
インスタンスを取得します。その後、GeneratedComInterfaceAttribute
属性を使用しつつ各種COMオブジェクトを再定義して使用します。
最後にComObject.FinalRelease
を呼び出していますが、無意味な呼び出しであるため、COMオブジェクトの参照カウンタが変化しないことが分かります。最後のGC.Collect
とGC.WaitForPendingFinalizers
を使用したGC強制実行時に、COMオブジェクトがReleaseされて解放されることが分かります。
C#製COMクライアントからGeneratedComInterfaceAttribtute
かつUniqueComInterfaceMarshaller<T>
経由で使う
ComInteropTest/ClientCsGeneratedComInterfaceUnique/ClientCsGeneratedComInterfaceUnique.csです。LibraryImportAttribute
でCoCreateInstance
をP/Invokeして、動作確認用のクラスのCLSID経由でCOMオブジェクトを生成します。その際、マーシャラーにUniqueComInterfaceMarshaller<T>を指定してiるため、当該マーシャラーを使用して
ComObjectインスタンスを取得します。その後、
GeneratedComInterfaceAttribute`属性を使用しつつ各種COMオブジェクトを再定義して使用します。
最後に、標準入力の値に従って、以下のいずれかを行います。
ComObject.FinalRelease
もGC.SupressFinalize
も呼び出さない。GCによるComObject.Finalize
呼び出しを待つ。ComObject.FinalRelease
を呼び出しますが、GC.SupressFinalize
は呼び出さない。ComObject.FinalRelease
もGC.SupressFinalize
も呼び出す。
1つ目か3つ目の場合は問題ありません。COMオブジェクトがReleaseされて解放されることや、プロセスが正常に終了コード0で終了することが分かります。
問題の2つ目の場合は、ComObject.FinalRelease
でCOMオブジェクトがReleaseされて解放された後に、改めてComObject.Finalize
で解放しようとします。動作確認した結果、System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.'
な例外が発生しました。
スタックトレースを見ても「どれかのComObject
が原因らしい」が分かるぐらいです。どのComObject
が、どこでFinalRelease
した結果のAccess Violationなのかが全く分かりません。もし複雑なプロジェクトでこの現象に遭遇してしまったら、非常に辛い状況になると思います。
また別の確認として、ComObject.FinalRelease
呼び出し後に当該ComObject
経由でメソッド呼び出しを行おうとしました。その場合でも、AccessViolationException
が発生しました。
感想
ComImportAttribute
をそのまま使うほうが、未定動作など起こりませんし、Marshal.ReleaseComObject
で明示的に解放できます。GeneratedComInterfaceAttribute
へ移行しなくてもいいのでは、と思ってしまいます。- 本記事の本題ではないで省略していますが、移行作業そのものが大変だと思います。
- そもそも、
GeneratedComInterfaceAttribute
関係のドキュメントが少なすぎます。ComWrappers source generation - .NET | Microsoft Learnくらいだけは見つかりました。 ComObject
に対してはMarshal.ReleaseComObject
やMarshal.FinalReleaseComObject
は使えなくなりますし、Type.IsCOMObject
で何かを判断している箇所はis ComObject
かどうかの判断へ変更する必要があるでしょう。GeneratedComInterfaceAttribute
が対応しているのはIUnknown
系統のみであり、IDispatch
系統は非対応です。もしIDispatch
派生インターフェースを使用している場合は、自分でIDispatch
を再定義する必要があるでしょう。- 関連するCOMインターフェースの再定義をすべて
ComImportAttribute
からGeneratedComInterfaceAttribute
へ変更する必要があります。もし、System.Runtime.InteropServices.ComTypes Namespace以下のインターフェースを使ってしまっているなら、それらのインターフェースはGeneratedComInterfaceAttribute
属性がないため、自分でGeneratedComInterfaceAttribute
版を再定義する必要があります。
- そもそも、
- 擁護すると、
GeneratedComInterfaceAttribute
は悪いことばかりではないです。COMインターフェースの継承時に基底インターフェースのメソッドのコピペが不要になる利点は大きいと思います。
- 本記事の本題ではないで省略していますが、移行作業そのものが大変だと思います。
- 正直なところ、なぜ
ComObject
がこのような設計になってしまったのか分かりません。そもそも、なぜUniqueComInterfaceMarshaller<T>
にUnique
という単語が使われているのかも分かりません。Unique
でない場合はどのような挙動なのか(1つのマネージドComObject
が複数のネイティブCOMオブジェクトを持つのか、1つのネイティブCOMオブジェクトを複数のマネージドComObject
が共有するのか)も分かりません。そして、開発者はどのような場合にUniqueComInterfaceMarshaller<T>
を指定するべきなのかも分かりません。分かる方がおられたらぜひコメントください。- API提案時のissueを探すと、「COM source generator APIs」という提案が原案らしく、その中のレビューを見るに、本来は安全に設計したい様子が伺えます。設計通りに実装できてない状況かもしれません。
ComObject.FinalRelease
へのコメントの抜粋です:
- API提案時のissueを探すと、「COM source generator APIs」という提案が原案らしく、その中のレビューを見るに、本来は安全に設計したい様子が伺えます。設計通りに実装できてない状況かもしれません。
Make the second call a no-op Throw InvalidOperationException rather than ObjectDisposedException when the COM object is used after calling FinalRelease
- 考えれば考えるほど「現状の実装はなにかバグっているのでは?」という思いに駆られたのでissueを立てました。コメントは付いていないもののマイルスートンが.NET 9へ設定されました。本記事の現象が将来的に改善されることを祈っています。 → .NET 9では修正されていそうです!
- .NET 8時点でも
ComImportAttribute
を使っている箇所で、SYSLIB1096 Mark the type 'IShellItem' with 'GeneratedComInterfaceAttribute' instead of 'ComImportAttribute' to generate COM marshalling code at compile time
などのMessageがエラー一覧に表示されます。ただ本記事に記載している理由や、QuickAction一発とはいかない移行作業の大変さから、#pragma warning disable SYSLIB1096
で抑制しまくってもいいと思います。