はじめに
Delphi でアウトプロセスな COM サーバーを作ってみたいと思います。
COM サーバーというのは、端的に言えば「他のアプリから呼び出せる機能を提供するためのサーバー (公開する仕組み)」です。普通のアプリケーションに COM サーバー機能を追加し、外部アプリケーションに対して機能を公開する事もできます。
See also:
COM サーバーを作る
COM サーバーには 3 種類あります。
| サーバーの種類 | サーバーの拡張子 | 通信方法 |
|---|---|---|
| インプロセスサーバー | *.dll |
COM インターフェースの直接呼び出し |
| アウト(オブ)プロセスサーバー | *.exe |
COM RPC |
| リモートサーバー | *.exe |
DCOM RPC |
今回作るのは EXE 形式のアウトプロセス COM サーバーです。
インプロセスサーバーで有名なのは ActiveX コントロール (OCX) です。
Delphi で完結するアプリケーションにおいて、もはやインプロセスサーバーを使うメリットは殆どないように思います。DLL なら普通のを作って直接呼び出した方が高速に動作します。
今から新規に Visual Basic 6.0 や VBA、Internet Explorer 等と連携するアプリケーションを作るかと言われたら...?
リモートサーバーは DCOM で通信します...が。
- DCOM は Windows 10 / 11 ではレガシー扱い
- Microsoft は DCOM を新規開発で使うことを推奨していない
- DCOM の設定 (
dcomcnfg.exe) が昔より難しい - DCOM ハードニングの影響
- DCOM は Microsoft アカウントでは認証が通らない (ローカルアカウントが必要)
- GUI を持つ COM サーバーはセッション 0 の分離の関係で落ちる (COM サーバーとして機能しない)
- フォームのない VCL アプリケーションだとすぐに終了してしまう (メインフォームが閉じられる or 存在しないと終了)
- Delphi では VCL アプリケーションだと GUI を持ってしまうので、 (恐らく) サービスで作るか、自前でメッセージループを実装しないとリモートサーバーを作れない
- Delphi で作ったリモートサーバーを登録する際、レジストリに登録される情報が足りない (多分)
- リモートサーバーは Delphi においては (恐らく) オートメーションオブジェクトとして作る必要がある (後述)
- オートメーションオブジェクトは safecall 呼び出しでなければならない (パラメータの型が Automation Compatible Types に制限される)
- 通信環境があまりよろしくない所での運用が難しい (耐障害性が...)
こちらも現在では設定が非常に難しく、今から新規で作るのは現実的でないように思います。
See also:
- 単純な COM サーバーの作成 (DocWiki)
- 公式・準公式の Delphi 関連書籍を読んでみる (Qiita)
- ActiveX (Wikipedia)
- Distributed Component Object Model (Wikipedia)
- 【Delphi】フォームのない常駐アプリケーションを作る (Qiita)
LHA を操作するアプリケーションを作る
とりあえず最初に普通のアプリケーションを作ります。題材として *.LZH 形式のアーカイブを操作するものを作ってみます。このプログラムを実行するには UNLHA32.DLL が必要です。
プロジェクト名は LHACOMSVR にしておきました。
program LHACOMSVR;
uses
Vcl.Forms,
frmuMain in 'frmuMain.pas' {frmMain};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TfrmMain, frmMain);
Application.Run;
end.
object frmMain: TfrmMain
Left = 0
Top = 0
BorderIcons = [biSystemMenu]
BorderStyle = bsSingle
Caption = 'LHA Wrapper'
ClientHeight = 57
ClientWidth = 555
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
Position = poScreenCenter
TextHeight = 15
object Edit1: TEdit
Left = 8
Top = 16
Width = 457
Height = 23
TabOrder = 0
end
object Button1: TButton
Left = 472
Top = 16
Width = 75
Height = 23
Caption = 'Execute'
TabOrder = 1
OnClick = Button1Click
end
end
エディットボックスに入力された文字列を UNLHA32.DLL がエクスポートしている Unlha() 関数に渡すだけのプログラムです。
unit frmuMain;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function Unlha(hwnd: HWND; lpszCmdLine: PAnsiChar; lpszOutput: PAnsiChar;
wSize: DWORD): Integer; stdcall; external 'UNLHA32.DLL';
function UnlhaGetRunning: Boolean; stdcall; external 'UNLHA32.DLL';
function LHAExec(Param: string): Integer;
const
BUF_SIZE = 8192;
var
buf: array [0..BUF_SIZE-1] of AnsiChar;
dCmd: AnsiString;
begin
if UnlhaGetRunning then
Exit(1);
dCmd := AnsiString(Param);
Unlha(0, PAnsiChar(dCmd), buf, SizeOf(buf));
Exit(0);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if LHAExec(Edit1.Text) = 1 then
ShowMessage('"UNLHA32.DLL" is already runnning...');
end;
end.
UNLHA32.DLL は 32bit DLL なので、Windows 32bit アプリケーションとしてビルドする必要があります。
〔Shift〕+〔F9〕でビルドすると Win32\Debug フォルダに LHACOMSVR.exe が出来ていますので、同じフォルダに UNLHA32.DLL を配置します。
LHACOMSVR.exe を実行し、
エディットボックスに
a UNLHA32.LZH UNLHA32.DLL
と入力してから [Execute] ボタンを押すと UNLHA32.LZH ができます。
UNLHA32.DLL を使うアプリケーションとしては一応完成しました。
COM サーバー化
では、このアプリケーションを COM サーバー化してみます。Delphi には COM サーバーを作るためのテンプレートやウィザードが用意されています。
[ファイル | 新規作成 | その他] から [新規作成] ダイアログを開き、左側のツリーで [Delphi プロジェクト > ActiveX] と辿り [COM オブジェクト] を選択します。
| ウィザード | 説明 |
|---|---|
| ActiveX ライブラリ | インプロセス COM サーバー (*.dll) を作るためのテンプレート。プロジェクトごと生成する。タイプライブラリは自動生成される。 |
| オートメーションオブジェクト | 既存の COM サーバー (*.dll / *.exe) プロジェクトに対して IDispatch ベース (Automation) の COM クラス (CoClass) を追加するウィザード。タイプライブラリは自動生成される。 |
| COM オブジェクト | 既存の COM サーバー (*.dll / *.exe) プロジェクトに対して IUnknown ベースの COM クラス (CoClass) を追加するウィザード。タイプライブラリは自動生成される (*.tlb の生成とリソースへの埋め込みは任意)。 |
| タイプライブラリ | 既存の COM サーバー (*.dll / *.exe) プロジェクトに対してタイプライブラリだけを作成するウィザード。このウィザードを単独で使う事はあまりない。 |
[COM オブジェクトウィザード] は次のように設定します。
| 項目 | 値 |
|---|---|
| CoClass 名 | LHAWrapper |
| スレッドモデル | シングルスレッドモデル |
| インスタンス生成 | 多重インスタンス |
スレッドモデル の意味は次の通り:
| スレッドモデル | 値 | 意味 |
|---|---|---|
| シングルスレッドモデル | tmSingle | COM はすべてのクライアントリクエストをシリアル化します。オブジェクトはスレッドサポートを提供する必要がありません。 |
| アパートメントスレッドモデル | tmApartment | COM は、COM オブジェクトのすべてのインスタンスが一度に 1 つのリクエストを提供することを保証します。同一のサーバーからの異なるオブジェクトは異なるスレッドで呼び出すことができるが、各オブジェクトは 1 つのスレッドからのみ呼び出されます。インスタンスデータは保証され、グローバルデータはクリティカルセクションまたはほかの形式のシリアル化を使って保護されなければなりません。スレッドのローカル変数は、複数の呼び出し間で信頼されます。 |
| フリースレッドモデル | tmFree | マルチスレッドアパートメントとも呼ばれます。COM オブジェクトはいつでも、どのスレッドからの呼び出しも受け取ることができます。オブジェクトはクリティカルセクションまたはほかの形式のシリアル化を使用してすべてのインスタンスおよびグローバルデータを保護する必要があります。スレッドのローカル変数は複数の呼び出し間で信頼されません。 |
| フリー/アパートメント両用 | tmBoth | オブジェクトはアパートメントまたはフリースレッドモデルを使用するクライアントをサポートできます。クライアントが単一スレッドまたはフリースレッドモデルを使用している可能性がある場合は両方のスレッドモデルをサポートします。 |
| ニュートラル | tmNeutral | 複数のクライアントが、別々のスレッドで同時にオブジェクトを呼び出すことができるが、COM によって 2 つの呼び出しが競合しないことが保証されます。複数のメソッドでアクセスされるグローバルデータやインスタンスデータを巻き込んだスレッドの競合が起きないようにしなければなりません。このモデルはユーザーインターフェースを持つオブジェクトでは使ってはならなりません。このモデルは COM+ でのみ利用できます。COM の場合は、アパートメントモデルにマップされます。 |
インスタンス生成 の意味は次の通り:
| インスタンス生成 | 値 | 意味 |
|---|---|---|
| 内部 | ciInternal | COM オブジェクトは COM サーバーと同じプロセスによって作成される。つまり、外部アプリケーションはこのオブジェクトのインスタンスを直接作成できない。かわりに、外部プロセスはドキュメントオブジェクトを作成するアプリケーションのメソッドを呼び出す必要がある。 |
| 単一インスタンス | ciSingleInstance | 実行ファイル(アプリケーション)ごとに COM オブジェクトのインスタンスを 1 つだけ許可する。この 1 つのインスタンスが複数のクライアント間で共有されない場合、各クライアントはそれぞれの実行ファイルのインスタンスを起動する必要がある。 |
| 多重インスタンス | ciMultiInstance | 同一の実行ファイル内の複数インスタンスの 1 つとして COM オブジェクトが作成される。クライアントがサービスを要求するたびに、オブジェクトの独立したインスタンスが呼び出される。 |
ウィザードを実行すると Unit1.pas が出来ていると思うので、uLHAWrapper.pas として名前を付けて保存します ([ファイル | 名前を付けて保存])。
コードエディタで LHACOMSVR.ridl を開き、インターフェイス ILHAWrapper にメソッドを追加します。
[属性] タブでメソッド名を Execute にします。
[パラメータ] タブで戻り値とパラメータを設定します。
戻り値:
| 項目 | 値 |
|---|---|
| 戻り値の型 | long |
パラメータ:
| 項目 | 値 |
|---|---|
| 名前 | cmd |
| 型 | LPWSTR |
| 修飾子 | [in] |
入力が終わったら [実装の更新] ボタンを押します。
問題がなければ入力内容が LHACOMSVR_TLB.pas に反映されているハズです。
...
// *********************************************************************//
// インターフェイス: ILHAWrapper
// フラグ: (256) OleAutomation
// GUID: {F5C3D854-2813-4363-B315-3FE6FE5F9DB5}
// *********************************************************************//
ILHAWrapper = interface(IUnknown)
['{F5C3D854-2813-4363-B315-3FE6FE5F9DB5}']
function Execute(cmd: PWideChar): Integer; stdcall;
end;
...
unit uLHAWrapper;
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
Winapi.Windows, Winapi.ActiveX, System.Classes, System.Win.ComObj, LHACOMSVR_TLB, StdVcl;
type
TLHAWrapper = class(TTypedComObject, ILHAWrapper)
protected
function Execute(cmd: PWideChar): Integer; winapi;
end;
implementation
uses System.Win.ComServ;
function TLHAWrapper.Execute(cmd: PWideChar): Integer;
begin
end;
initialization
TTypedComObjectFactory.Create(ComServer, TLHAWrapper, Class_LHAWrapper,
ciMultiInstance, tmSingle);
end.
ここで 〔Ctrl〕+〔Shift〕+〔S〕 を押して、一旦すべて保存しましょう。
次に、frmuMain.pas にあった LHA のルーチンを uLHAWrapper.pas に移動します。
unit uLHAWrapper;
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
Winapi.Windows, Winapi.ActiveX, System.Classes, System.Win.ComObj, LHACOMSVR_TLB, StdVcl;
type
TLHAWrapper = class(TTypedComObject, ILHAWrapper)
protected
function Execute(cmd: PWideChar): Integer; winapi;
end;
function LHAExec(Param: string): Integer; // 追加
implementation
uses System.Win.ComServ;
// ここから
// --------------------------------------------------------------------------------
function Unlha(hwnd: HWND; lpszCmdLine: PAnsiChar; lpszOutput: PAnsiChar;
wSize: DWORD): Integer; stdcall; external 'UNLHA32.DLL';
function UnlhaGetRunning: Boolean; stdcall; external 'UNLHA32.DLL';
function LHAExec(Param: string): Integer;
const
BUF_SIZE = 8192;
var
buf: array [0..BUF_SIZE-1] of AnsiChar;
dCmd: AnsiString;
begin
if UnlhaGetRunning then
Exit(1);
dCmd := AnsiString(Param);
Unlha(0, PAnsiChar(dCmd), buf, SizeOf(buf));
Exit(0);
end;
// --------------------------------------------------------------------------------
// ここまで
function TLHAWrapper.Execute(cmd: PWideChar): Integer;
begin
result := LHAExec(cmd); // 記述
end;
initialization
TTypedComObjectFactory.Create(ComServer, TLHAWrapper, Class_LHAWrapper,
ciMultiInstance, tmSingle);
end.
frmuMain.pas の uses には uLHAWrapper を追加しておきます。
unit frmuMain;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, uLHAWrapper; // 追加
type
TForm1 = class(TForm)
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
if LHAExec(Edit1.Text) = 1 then
ShowMessage('"UNLHA32.DLL" is already runnning...');
end;
end.
この状態でビルドし、LHACOMSVR.exe を実行して以前と同じように動作するかテストします。
正しく動作したのであれば、これで COM サーバーは完成です。
COM サーバーとして公開する機能を記述する必要はありますが、COM サーバー化するための処理 というのは特に必要ありません。プロジェクトファイルに uLHAWrapper.pas が追加されていれば、その initialization 節により COM サーバーとして機能します。
See also:
- COM ウィザードを使用する (DocWiki)
- System.Win.ComObj.TThreadingModel (DocWiki)
- System.Win.ComObj.TClassInstancing (DocWiki)
- タイプライブラリエディタ (DocWiki)
- RIDL ファイル (DocWiki)
- タイプライブラリの更新 (DocWiki)
COM サーバーの登録 / 解除
COM サーバーを使うには事前に登録が必要です。
COM サーバーを登録するには、COM サーバーアプリケーションを任意の場所に配置し、/regserver スイッチを付けて管理者権限で実行します。
例えば今回の LHACOMSVR.exe は、
LHACOMSVR /regserver
のようにして管理者権限で実行します。EXE は実行後に自動で閉じられます。
LHACOMSVR /unregserver
登録解除は /unregserver で行います。
ユーザー毎に登録/解除を行う事もできます。こちらは管理者権限が不要です。
-
/RegServerPerUser(登録) -
/UnregServerPerUser(登録解除)
IDE のメニュー [実行 | ActiveX サーバー] で登録 / 登録解除を行う事もできますが、[登録] (全ユーザーへの登録) は IDE を管理者権限で起動していなければ失敗すると思います。
基本的に開発時は [現在のユーザーとして登録] と [登録解除] を使う事になると思います。
インターフェイスに変更があった場合には一旦登録解除してから再度登録する必要があります。登録を適切に行わないと、追加したはずの機能が利用できない等のトラブルが発生します。
テストプログラムを作る
COM サーバーが完成したので、今度は COM サーバーをテストするプログラムを作ります。
プロジェクト名は LHATEST にしておきました。
program LHATEST;
uses
Vcl.Forms,
frmuMain in 'frmuMain.pas' {frmMain};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TfrmMain, frmMain);
Application.Run;
end.
object frmTest: TfrmTest
Left = 0
Top = 0
BorderIcons = [biSystemMenu]
BorderStyle = bsSingle
Caption = 'LHA TEST'
ClientHeight = 57
ClientWidth = 555
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
Position = poScreenCenter
TextHeight = 15
object Edit1: TEdit
Left = 8
Top = 16
Width = 457
Height = 23
TabOrder = 0
end
object Button1: TButton
Left = 472
Top = 16
Width = 75
Height = 23
Caption = 'Execute'
TabOrder = 1
OnClick = Button1Click
end
end
COM サーバー LHACOMSVR の Execute() メソッドを呼び出すプログラムです。エディットボックスに入力された文字列をパラメータとして渡します。
COM サーバーを作った時に生成された LHACOMSVR_TLB を uses に追加する必要があります。
unit frmuTest;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, LHACOMSVR_TLB;
type
TfrmTest = class(TForm)
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
frmTest: TfrmTest;
implementation
{$R *.dfm}
procedure TfrmTest.Button1Click(Sender: TObject);
var
LHAWrapper: ILHAWrapper;
begin
LHAWrapper := CoLHAWrapper.Create;
LHAWrapper.Execute(PWideChar(Edit1.Text));
end;
end.
リモートサーバーを呼び出すには、CoLHAWrapper.Create() メソッドの代わりに CoLHAWrapper.CreateRemote() メソッドを使います。
〔Shift〕+〔F9〕でビルドしたら LHATEST.exe を実行し、
エディットボックスに
a C:\WORK\UNLHA32.LZH C:\WORK\UNLHA32.DLL
のように入力してから [Execute] ボタンを押すと UNLHA32.LZH ができます。今回はファイル名にフルパスを指定する必要があります。
LHATEST.exe を実行する前に COM サーバー LHACOMSVR.exe を起動しておく必要はありません。COM サーバーが必要になった時、Windows が自動で起動してくれます。同じ場所にある必要もありません。
64bit アプリケーション
LHATEST.exe のターゲットプラットフォームを 64bit Windows アプリケーションに変更してみましょう。
〔Shift〕+〔F9〕でビルドしたら LHATEST.exe を実行してみましょう。32bit 版と同じように動作したと思います。当たり前ですね。
...本当に当たり前でしょうか?
これは何を意味しているかと言えば、64bit アプリケーションから間接的に 32bit DLL を呼び出したという事です。
つまり、アウトプロセス COM サーバーは「アプリケーションを 64bit 化したいけれど、使っている DLL が 32bit のものしか用意されていない」時に使える手段となります。
32bit COM サーバーは 32bit / 64bit アプリケーションから呼び出せますし、その逆で 64bit COM サーバーも 32bit / 64bit アプリケーションから呼び出せます。
おわりに
今回の COM サーバーは、単独のアプリケーションとしても使え、登録すれば COM サーバーとしても使えるアプリケーションでした。プロジェクトファイルの uses に System.Win.ComServ を加えると、何として起動されたのか? を判定して動作を変更する事も可能です。
if ComServer.StartMode = smStandalone then
...
例えば完全に機能を提供するだけのサーバー (GUI がない) は、単独起動されてもすぐに終了させる、なんてことが可能です。
| 開始モード | スイッチ | 意味 |
|---|---|---|
| smAutomation | オートメーションコントローラからの要求への応答として、アプリケーションは Windows によって起動された。 | |
| smRegServer | regserver | サーバーをシステムレジストリに追加するためだけに、アプリケーションが起動された。 |
| smStandalone | ユーザーがアプリケーションをスタンドアロンの対話型アプリケーションとして起動した。 | |
| smUnregServer | unregserver | システムレジストリからサーバーを削除するためだけに、アプリケーションが起動された。 |
そういえば、今はアプリケーションに COM サーバー機能ではなく、REST サーバー機能を持たせる事がありますよね。
適材適所だとは思いますが、REST サーバーだと事前に起動しておく必要があるので、COM サーバーにも利点がありますね。
本記事の内容を、エンバカさんのデベロッパーキャンプでやった気がしたので、イロイロ調べてみたのですが見つかりませんでした。没ネタか、あるいは時間が無くて披露できなかったか?...🤔
自サイトのものも不完全だったため (SWF 動画は再生できなくなってるし)、今回あらためて記事にしてみました。
https://ht-deko.com/tech070.html
See also:















