このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。
さて、では次の手を考えてみよう。
InternetShortcutClass が実装しているインタフェースは
ISellLinkW 以外にもまだまだ数多くある。
その中に、IUniformResourceLocatorW というのがある。
URL そのものを表すらしい、変な名前のインタフェースだ。
MSDN を探る。みっけ。
そのものズバリの GetURL ってメソッドがある。
これは使えそうだ。
では、これも一気に定義を書いてみよう。
場所は Interop フォルダ内に入れる。
これも、使うメソッドのみ定義することにする。
========== IUniformResourceLocatorW.cs ==========
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace LoaferShellEx.Interop {
[ComImport]
[Guid("CABB0DA0-DA57-11CF-9974-0020AFD79762")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IUniformResourceLocatorW {
// 今回使うものだけちゃんと定義し、それ以外端折る
// 端折ったやつは絶対に呼び出さないように。
void _0(); // SetURL
void GetURL(
[MarshalAs(UnmanagedType.LPWStr)]
out string ppszURL);
void _2(); // InvokeCommand
}
}
========== end of IUniformResourceLocatorW.cs ==========
定義するのは GetURL メソッドだけなので簡単に見えるが、
これまた文字列の扱いを考えてないと罠にはまる。
GetURL の元のシグネチャを引用しよう。
ばお、このシグネチャは Unicode 版(W)のものだ。
HRESULT GetURL(
LPWSTR *ppszURL
);
非常に見落とし易いが、引数である ppszURL は、
「LPWSTR *ppszURL」であって「LPWSTR ppszURL」ではない。
C 言語で言うと、文字配列を確保して渡すのではなく、
メソッド側が文字列領域を確保して、
そのポインタを引数に返すのである。
IShellLinkW の時は、バッファを割り当てて、
メソッドがそこに書き込む処理であったが、
これは、バッファの割り当てはメソッド側が行うのだ。
これは少し厄介だ。
.NET の管理下でないメモリが割り当てられて返却されるので、
呼び出し側で解放しなければならないということになる。
ppszURL パラメータの説明を読むと、このように書いてある。
Address of an LPSTR that will be filled with a pointer to the object's URL. Because this method allocates memory for the string, you must create and instance of an IMalloc interface and free the memory using IMalloc::Free when it is no longer needed.
メソッドの確保したメモリは、
IMalloc::Free を呼び出して解放しろとある。
これは、シェルのメモリアロケータが持つ、
IMalloc インタフェースを、CoGetMalloc API で取得し、
その Free を呼び出して解放するという手順を踏むか、
一連の手順と同じ役割を持つ API である、
CoTaskMemFree を呼び出して解放しろということだ。
なお、.NET には、Marshal.FreeCoTaskMem という名前で、
CoTaskMemFree API が定義されているため、
このメソッドを使えば解放することは可能である。
以上に基づいて C# で定義する場合、
メソッド定義は、以下のようになると考えられる。
void GetURL(out IntPtr ppszURL);
IntPtr は、汎用ポインタ型である。
ポインタへの参照を渡すことで、
メソッド側が確保したメモリのアドレスが返却される。
これを呼び出す場合、以下のような手順となるはずだ。
IntPtr ptr;
~.GetURL(out ptr);
string url = Marshal.PtrToStringUni(ptr);
Marshal.FreeCoTaskMem(ptr);
GetPath が返すポインタは、ptr に格納される。
ptr が指すのは NULL 終端 Unicode 文字列なので、
Marshal.PtrToStringUni で String にコピーを取る。
そして、Marshal.FreeCoTaskMem で ptr を解放する。
これは、少し面倒ではないだろうか。
GetPath メソッドの機能を呼び出し側からの視点で見ると、
文字列(String)への参照を渡せば、
メソッド側が新しい String インスタンスを作成して、
その参照を引数に返却するということになる。
上記のコードは、IntPtr を使ってアドレスを扱っており、
IntPtr の解放忘れによるメモリリークの危険があるのだ。
そうなると、GetURL の後に例外処理コードが必要になり、
さらに見にくいコードになってしまう。
実は .NET は上のようなシナリオを考慮しており、
COM や API 側が文字列を内部で割り当てて返す場合、
自動的に Marshal.FreeCoTaskMem で解放する機能がある。
(BSTR の場合は Marshal.FreeBSTR)
この機能を利用すると、上に書いた自然な考え方で
GetPath を宣言することが可能となるのだ。
void GetURL(
[MarshalAs(UnmanagedType.LPWStr)]
out string ppszURL);
ppszURL は String を受け取るということなので、
C# では out string となる。
これによって、メソッドが文字列を返すという意味になる。
.NET は、String を out で受け取る場合、
COM や API 側で文字列が割り当てられていると判断し、
戻り値を .NET の String にコピーした後で
適切なメソッドを呼んで解放しようとする。
MarshalAs は、その解放の方法を指定する役割もある。
指定しないと String は BSTR としてマーシャリングされる。
GetPath は NULL 終端 Unicode 文字列を扱うので、
MarshalAs で LPWStr を指定することで、
解放する際に Marshal.FreeCoTaskMem が呼ばれるのだ。
上記のように宣言した場合、
GetPath を呼び出す手順は以下のようになる。
string url;
~.GetURL(out url);
どうだろう、非常にすっきりとして見やすくなった。
url を out として渡す際に、.NET によって
自動的に String の変換とメモリの解放が行われるのだ。
このように COM や API の相互運用機能をうまく利用すると、
それらを .NET のクラスの一部のように定義し、
低水準の型を意識せず、自然に使用することができる。
確かに宣言を書くのが少し面倒なのは否定できないが、
一度正しく宣言さえしておけば、
過去の資産を利用しつつ、効率が良い開発が可能となるのだ。