日記というよりは過去を振り返るときのための単なる備忘録この広告は、90日以上更新していないブログに表示しています。レジストリは一切使わない。DLLを作成するにはATLでウィザードを使ってプロジェクトを作成すると、とても簡単にできる。DesktopにはATLは付属していない。レジストリフリーのCOMを作成するのであれば、ATLの力を借りずともVC++の言語サポートと、Expressの機能だけで、比較的容易に作成できる。DLLを作成する手順である。ビルド可能なプロジェクト一式は、GitHub上においてあります。C#で実装しない(できない)のかは、この記事の末尾で少し触れています。"MyRegFreeComSrv"というプロジェクト名にする。ソリューションも同時に作成して、ソリューション名は「MyRegFreeCom」にしておく。(あとでCOMを利用するクライアントのプロジェクトも、このソリューションに作るため)Gitリポジトリの生成も行うと、ウィザードが完了したときに、はじめからローカルリポジトリが作成済みになる。(VS用のgitignoreの設定もされている。)動くことを確認したら、ときどきコミットして、いろいろ試行錯誤して間違えても一発で戻れるようにしておくと気が楽である。DLLのエントリポイントの「DllMain」関数だけが定義されている。関数は、DLLがプロセスにロードされたとき、スレッドにアタッチされたとき等々のイベントで、この関数を呼び出す。ここで重要なのはDLLのモジュールハンドルが取得されることである。これは後で使うのでグローバル変数として保存しておく。COMサーバーとして機能するDLLを作成するには、最低でも以下の2つの関数をエクスポートする必要がある。(CLSIDを指定してCOMオブジェクトを生成して返すもの)(このほかにレジストリへの登録、登録解除のためのエントリもあるが、これは今回は不要である)この2つの関数をエクスポートするために、昔ながらのDEFファイルによるエクスポートを行う。PRIVATELIBRARYには、作成されるDLL名を指定する。(これはプロジェクトプロパティで指定するDLL名と一致していなければならない。ウィザードにより生成されたあとは既定では"プロジェクト名.DLL"として設定されている。)この2つの関数は、(どこでもいいが、今回は)"MyRegFreeComSrv.cpp"で実装するものとする。endregionまだ実装の中身はないので、とりあえず、いまは常に定数値を返しておく。regionでソースコードの折りたたみ範囲を指定できる。(この時点で、まずコンパイルできるか確認する。次に、IDLファイルを作成する。Language)であり、COMインターフェイスの定義と、COMオブジェクトが、どのCOMインターフェイスを実装しているか、などの「型情報」を記述するものである。これはコンパイルすることで、C++のソースとヘッダを生成し、タイプライブラリという型情報を記録したバイナリ形式のファイルも出力する。「タイプライブラリ」はCOMをスクリプトから利用する場合には型情報を知るためにきわめて重要なものである。生成されたC++のソースとヘッダはCOMの実装時に使う他、クライアントがCOMインターフェイスを参照する場合にも使える。idlに、以下のIMyRegFreeCOMSrvというインターフェイスを定義する。(一般的にインターフェイス名は「I」で始まることになっている。)これが、今回作成するCOMのインターフェイス定義となる。いうのは、これがスクリプトから呼び出すことのできるデュアルインターフェイスであることを示している。このインターフェイスを識別するユニークなIDを割り当てる。Studioのツールメニューから「GUIDの作成」を選んで、そこからランダムなGUIDをもらってくる。このGUIDによって識別するようになっている。異なるものに同じ値は使ってはならない。プロパティのgetter/setterの定義を表す。COMではプロパティもメソッドとして実装されるので、このような形で定義される。(C#などでプロパティを扱っていれば分かると思う。DISPIDの定義である。スクリプトからCOMを呼び出す場合の番号となる。このDISPIDは1から始まる任意の番号をつけてよいが、いくつか特殊な番号がある。Eachで列挙子を返すためのメソッドを表す、DISPID_NEWENUM(-4)などの定義がある。今回は、とくに使わないので、1以上の適当な番号をふっておけばよい。COMでは全て結果コードをHRESULTとして返すので、すべてのプロパティ、メソッドはHRESULTを返す。retval]のように指定する。指定した引数には、返却用のアドレスをクライアントから渡されるので、そこに書き込めば良い。(返却用のアドレスを用意するのはクライアントの責務である。タイプライブラリとイベントソースの定義つぎに、このインターフェイスを実装するCOMクラス(CoClass)とイベントソース、タイプライブラリの定義を行う。COMクラスとイベントソースはタイプライブラリの中に包含するように記述する。タイプライブラリの指定であり、タイプライブラリのIDをuuidで指定している。バージョンも指定できるが、今回はレジストリに登録しないCOMであるし、とりあえず1.0にしている。システム標準のIUnknownやIDispatchなどの既定のタイプライブラリをインポートするもので常に必要である。この下に、まずイベントソースを指定する。ようにしてオブジェクトからのイベント通知を受けるためのもので、そのイベントについて定義するものである。COMにおいてはイベントは、(通常は)IDispatchインターフェイスで実装するので、dispinterfaceで定義する。イベントソースもインターフェイスの一種であるから、識別子としてuuidを必要とする。今回は以下の2つのイベントをもつものとする。COMクラスの定義つぎに、インターフェイスを実装するCOMクラス(CoClass)の定義を行う。COMクラスは、libraryの中のcoclassで定義する。こちらはクラスを識別するCLSIDとなるuuidを指定する必要がある。中身には、このクラスが実装するインターフェイスと、このクラスが送信するイベントのインターフェイスの指定となる。指定されているインターフェイスがイベントソースとなるインターフェイスである。IDLのコンパイルここまでIDLを定義すれば、MIDLによってIDLをビルドすることができる。ソリューションエクスプローラからidlファイルを右クリックして「コンパイル」を選択すると、コンパイルが行われる。すると、以下のファイルが生成される。idlからの生成物であるのでgitignoreには既定で(だいたい)除外設定されている。(*_h.hは入ってないので自分で追加する)生成された「MyRegFreeCOMSrv_h.h」と「MyRegFreeCOMSrv_i.c」はプロジェクトに加えてビルドされるようにする。追加する際には、プロジェクトに「生成されたファイル」というフィルタを追加して、そこに登録しておくと良い。「MyRegFreeCOMSrv_i.c」はstdafx.hを使わないので、プリコンパイルヘッダの使用をOffにしておく。タイプライブラリのリソースへの埋め込みつぎに生成されたタイプライブラリをwin32リソースに含めるようにする。タイプライブラリ(*.tlb)は、スクリプトからCOMを利用する際に型情報を取得するために、よく使われている。自分のDLL内にリソースとして格納し、レジストリを介さず、自分自身のリソースからタイプラブラリをロードする方法もある。今回はレジストリフリーでやりたいので、リソース内におく必要がある。Desktopでは、リソースエディタは存在しないが、リソース自身はプロジェクトに追加できるし、コンパイルもできる。リソースを追加したら、リソースエディタは開かないが、テキストエディタとして開くことは可能である。APSTUDIO_INVOKED」のブロックの内に、以下のようにタイプライブラリをリソースとして取り込むように指定する。Studioのリソースエディタが勝手に書き換えない場所となる。リソースから生成されました。ない場合なお、タイプライブラリのリソースIDは1としておく。このDLLからタイプライブラリをロードするとき、デフォルトでは、最初に見つかったタイプライブラリを使用するためである。Release、あるいはx64フォルダの下にいる。(タイプライブラリはx64/x86で生成される内容が異なるので。ビルド時にリソースの参照パスを設定する必要がある。プロジェクトのプロパティページからリソースを選択して「追加のインクルードディレクトリ」に$(IntDir)を追加する。これで、中間ディレクトリ内の"MyRegFreeCOMSrv.tlb"ファイルが参照できるようになる。(ここまででビルドが通ることを確認しておくと良い。MyRegFreeCOMSrvクラスの実装つぎに、いよいよ、MyRegFreeCOMSrvクラスの実装を行う。今回は、先ほどDLLのエクスポート関数を定義した、MyRegFreeCOMSrv.cppファイル内に実装することにする。以下のようにクラスを書き始める。coclassの名前と一致している必要がある。これは、ヘッダファイル"MyRegFreeCOMSrv_h.h"を見れば分かるが、ここで以下のように先行宣言されている。DECLSPEC_UUID("4475E395-FA5A-42A3-901C-F3062802B9B2")MyRegFreeCOMSrv;クラス名だけ先行定義をしているが、そこに属性として__declspec(uuid(...))としてIDLで指定したuuidが付与されている。このクラス名に対してVC++の拡張である__uuidof(MyRegFreeCOMSrv)という関数を呼び出すことで、クラス名から、それに割り当てられているUUID(4475E395-FA5A-42A3-901C-F3062802B9B2)を得られる、という便利な仕組みになっている。今回作成するCOMには_IMyRegFreeCOMSrvEventsというイベントソースをもつことになっている。C++のCOMでイベントソースを定義するのは、ちょっと面倒くさくて、IConnectionPointというインターフェイスを経由して、クライアント側が提供するイベント受け取り側(イベントシンク)と接続する、という方法を採らなければならない。このIConnectionPointは、それを保持するIConnectionPointContainerインターフェイスから取得されなければならないので、結局、最低でも、MyRegFreeCOMSrvクラスは、それ自身が実装すべきIMyRegFreeCOMSrvインターフェイスの他に、イベント接続のためのIConnectionPointContainerとIConnectionPointも実装する必要がある。WSHなどのスクリプト言語からイベントを接続可能にするためには、型情報をスクリプト側に教えてやるためのIProvideClassInfo2インターフェイスも実装しなければならない。(IProvideClassInfo2が実装されていないと、WSHのWScript.ConnectObject()を実行すると、接続できませんというようなエラーになる。"prefix_")だと接続できる。)なので、結局、以下のような形になる。IUnknownの実装まずは、もっとも基本となる、IUnknownで参照カウンタとインターフェイスの問い合わせ部について実装する。thisで自分を破棄するようにしている。そのため、デストラクタはprivateにおく。(これにより、ヒープ上のみ作成可能なオブジェクトとなる。要求されたインターフェイスに対応するポインタをキャストして返すだけである。(複数のIUnknown派生インターフェイスを継承しているので、それ単位でポインタが異なる。IDispatchの実装つぎに、IDispatchの実装を行う。IDispatchはスクリプト言語用のインターフェイスであり、文字列で関数名やプロパティ名を指定して、その番号を取得したり、その番号に対してパラメータを渡すことで、さまざまな処理を行ったり、というような動的な処理が行われることになる。APIが用意されている。タイプライブラリをロードして代行するAPIに丸投げするだけでよい。クラスの実行に事前に必要な初期化処理を行う。ヘッダをインクルードする。APIによってロードすることができる。DLLのパスを得るには、DLLのモジュールハンドルが必要になるので、最初にグローバル変数として保存したg_hModuleを使うことになる。(別ソースファイルで定義しているので、externで参照している。)また、タイプライブラリは変更されないものなので、最初に一度読み込めば十分なので、Staticとしている。標準のDispatch代行APIである。タイプライブラリからDISPIDに対応するメソッドの呼び出すを行う。これは単にイベントソースのIIDであるか判定して、自身のコネクションポイントを返せばよいだけである。クライアントはコンパイル時に既知のDIIDを知っているはずなので省略可。IConnectionPointは、自身がサポートしているコネクションインターフェイスのUUIDの返却、自身を含むIConnectionPointContainerの返却、および、クライアントとの接続と接続解除の処理を実装する。クライアントはIDispatchを実装したイベントシンクを渡すので、それに対してDWORDのCookie値を(適当に)割り当てて返却する。オブジェクトは保持しているイベントシンクに対して必要に応じてIDispatch#Invokeを用いてイベントを通知する。クライアントから接続解除のためにCookie値を渡されたら、これでイベントシンクを解放する。cookieと保持しているイベントシンクのIDispatchを保存するためのメンバフィールドと、cookie値を管理するカウンタが必要である。STLのmapをインクルードしておく。イベントシンクに対してイベントを送信するためのヘルパ。デストラクタでイベントシンクの解放漏れがあれば、ここで解放しておく。これは単にタイプライブラリの情報をクライアントに引き渡すだけのものである。最後に、ようやくオブジェクト固有のインターフェイスの実装ができる。Nameプロパティを更新する際に、イベントシンクに対してイベントを送信している。IClassFactoryの実装これで、MyRegFreeCOMSrvオブジェクトは完成したが、これをDLL外から利用するには、生成するためのファクトリが必要である。インターフェイスを実装したCOMオブジェクトであるが、DLLと生存期間が一致するものなので、参照カウンタのような寿命管理のメカニズムは必要ない。thisすることはなく、staticでインスタンス化して使う。)しかし、DLLをアンロードして良いか?というDllCanUnloadNow関数の問い合わせに必要なロックカウントは保持する必要がある。関数ではCLSIDを指定してファクトリを取得する必要があるので、そのあたりも踏まえたコードは、以下のような感じになる。MyRegFreeCOMSrvのクラスファクトリ。参照カウンタでは寿命を管理しない。DLLで一度だけ構築される、staticな生存期間をもつ。CLSIDで区別されることもないのでUUID属性の定義は不要である。以上まで実装すれば、ビルドは可能になっている。manifestの設定あとはCLSIDやPROGIDのレジストリを登録すればCOMオブジェクトとして起動できるようになっているはずである。Assemblyのマニフェストを作成する。ソリューションエクスプローラから追加するが、マニフェストファイルのテンプレートは存在しないので、テキストファイルを選択して名前を「MyRegFreeCOMSrv.manifest」として作成する。追加したファイルのプロパティを確認して、項目の種類が「マニフェストツール」になっていればOKである。(マニフェストツールになっていれば、マニフェストは暗黙でリソースファイルに埋め込まれる。)ここにレジストリのかわりにCOMの情報を書き込む。(テキストは初期状態でSJISになっていると思われるので、保存するときはUTF-8であることを確認すること。DLLがProxy/Stubをマージしている場合は、このDLLを示す<file>要素の子として、comInterfaceProxyStubの定義が必要。(Proxy/Stubが別DLLの場合は、*PS.DLLの<file>要素の子として作成する。name="*.dll">...</file>で自分のDLL名を指定し、このDLLが提供するCOMの情報を列挙する。代表ProgIDの宣言と、使用するタイプライブラリの制限。子要素として<progid>を追加で、必要なだけつけられる。(バージョン別ProgIdなどを表現するため。)comInterfaceExternalProxyStubはアパートメントが異なる場合にシステムがマーシャリングを行う際に利用されるようで、とりあえずつけておくことが推奨されている。(が、今回はIn-ProcのSTAのみを想定しており、今回の場合には使われることはなく厳密には不要であろう。)(とりあえずインターフェイスのIID、イベントソースのDIIDをつけておく?)また、DLLにマニフェスト中にUAC有効化の設定があるのはおかしいので、必ず削除しておく。Assemblyとして利用可能なDLLとして生成される。テスト用のコンソールアプリの作成テスト用のコンソールアプリのプロジェクトを追加する。最終的にはWSHから使いたいが、デバッグしたり動作確認するには同じC++で作られた、同一ソリューションのプロジェクトがあったほうが便利である。ビルド順序を指定して、MyRegFreeCOMSrvが先にビルドされるようにしておく。SideBySideでCOMが使えることを確認するため、以下のような単純なクライアントコードを記述する。(イベントまわりは、とりあえず、いまは省略。0;}COMの定義のヘッダファイルや定数類はサーバ側のプロジェクトから直接参照している。"*.tlb"したほうが、いろいろ便利である。)このまま実行すれば、当然、レジストリに登録されていないので実行は失敗する。クライアント用のマニフェストの作成これに対して、アプリケーションマニフェストを追加して、依存アセンブリを明示する。等の適当な名前で良い。追加方法はサーバ側と同様である。マニフェストツールとして認識されていればexeのリソースとして自動的に取り込まれる。CoCreateInstanceが成功してCOM呼び出しができていることが確認できる。Manifestの設定に不備がある場合は、そもそもEXEが起動できない。バージョンも含めてアセンブリ名が一致しているか?DLLのマニフェストにUACの設定が入っているとSxS起動は失敗する。Sideまわりの起動失敗は、SxsTraceというツールを使うとトレースログがとれる。トレースを有効にします。ログはファイル名に保存されます。指定すると、トレースを停止するかどうかを確認しません。オプションを使用します。停止していない場合は、トレースを停止します。-outfile:SxsTrace.txt上記のクライアントで動作することを確認する。WSHからレジストリフリーで呼び出す今度はWSHから試してみる。以下のようなVBScriptを定義する。Subスクリプトと同じフォルダに以下のclient.manifestファイルを保存する。</dependency></assembly>これは先に実験したclient用のマニフェストファイルから、依存アセンブリの部分だけにしたものである。ここに、コンパイルされたMyRegFreeCOMSrv.dllファイルも一緒に置いておく。(もし、使用するWSHが32ビット版であれば、このMyRegFreeCOMSrv.dllも32ビットでビルドしておく。WSHが64ビット版であれば、MyRegFreeCOMSrv.dllも64ビットでビルドしなければならない。)これで実行すると、プロパティの変更通知に対してメッセージが表示され、最終的にメッセージボックスが表示されるはずである。Assemblyの効果を与えるActiveXである。(これは、Windows2003以降に搭載されているようである。Windows10での動作を確認した。)これにより、VBSの中でclient.manifestを後からロードして、レジストリフリーでCOMオブジェクトを起動することができている。取得したオブジェクトはWScript.ConnectObjectによって接続することで、このCOMからのイベント通知も受け取ることができている。(ただし、VBSのイベントハンドラではByRef引数を受け取っても、それを変更して返却することができないようである。このサンプルでは、本来はNamePropertyChangingで引数cancelに対してTrueを返すと変更をキャンセルできるはずだが、VBSではうまくゆかない。2017でATLを使わずレジストリフリーなCOMを作成することができた。Excel/Word等のVBAから利用する場合WSHから利用可能であるように、VBAからも同様にレジストリフリーで利用可能である。こちらは、WSHの場合と異なり、ByRefのイベントでcancel値を返却できる。Subただし、これが利用可能になるためには、VBAの参照設定が必要である。生成されたDLLにはタイプライブラリがリソースとして含まれているので、このDLLを参照設定で直接指定すれば良い。(Officeが32ビット版であれば、DLLも32ビットでビルドしていなければならない。通常、Officeは32ビット版が推奨されているが、もし64ビット版のOfficeを使っている場合は、64ビットでビルドしたDLLを使用すること。MyRegFreeCOMSrvのように、WithEventsと同時に型の指定も必須であるため、コンパイル時点で型情報が必要である。このように、レジストリフリーではあるが、DLLを参照設定しなければならない、という手間はある。イベントを使わない場合は参照設定は不要である。明示的に解放していないと2回目以降で、よくわからんエラーになる。ProgID等々のレジストリ参照の仕組みであって、COMオブジェクトが自分のソースの中でレジストリを見ていたりしたら、当然、レジストリフリーにはできない。-1にしておかないと、自分のDLLのタイプライブラリではなく、レジストリ経由のタイプライブラリを見に行く処理が入るため、レジストリフリーにならないので注意が必要である。assemblyとして実行させようとすると、EXEと同じフォルダにDLLを入れておかないと動作しない。(同じフォルダに入れておけばレジストリフリーで動作する。Found」となっている。)なので、今回のようにWSHで使うには、ちょっと難がある。cscript.exeを自分のフォルダにコピーすれば動作はするが...。)C++/CLIの対応C++ではなく、C++/CLIにすることで、C++のコードからDotNETのクラスライブラリを自由に呼び出せるようになる。それには、以下の変更を行う。(/clr)」にする。(/DEBUG:FULL)」こうすれば、たとえば、以下のようなコードを書けるようになる。#...2006-10-30ActiveScriptSiteに独自のオブジェクトを公開する。ActiveScriptHostでスクリプトにオブジェクトを公開...2006-10-30ActiveScriptHostのスクリプトに渡すIDispatchの作り方(レジストリなし)ActiveScriptHostのスクリプトに渡すIDispatchの作り方のメモ。(1)はてなブログをはじめよう!seraphyさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。