8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Delphi】アウトプロセスな COM サーバーを作る

8
Last updated at Posted at 2026-04-10

はじめに

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:

LHA を操作するアプリケーションを作る

とりあえず最初に普通のアプリケーションを作ります。題材として *.LZH 形式のアーカイブを操作するものを作ってみます。このプログラムを実行するには UNLHA32.DLL が必要です。

プロジェクト名は LHACOMSVR にしておきました。

LHACOMSVR.dpr
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.

image.png

frmuMain.dfm
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() 関数に渡すだけのプログラムです。

frmuMain.pas
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 アプリケーションとしてビルドする必要があります。

image.png

〔Shift〕+〔F9〕でビルドすると Win32\Debug フォルダに LHACOMSVR.exe が出来ていますので、同じフォルダに UNLHA32.DLL を配置します。

image.png

LHACOMSVR.exe を実行し、

image.png

エディットボックスに

a UNLHA32.LZH UNLHA32.DLL

と入力してから [Execute] ボタンを押すと UNLHA32.LZH ができます。

image.png

UNLHA32.DLL を使うアプリケーションとしては一応完成しました。

COM サーバー化

では、このアプリケーションを COM サーバー化してみます。Delphi には COM サーバーを作るためのテンプレートやウィザードが用意されています。

[ファイル | 新規作成 | その他] から [新規作成] ダイアログを開き、左側のツリーで [Delphi プロジェクト > ActiveX] と辿り [COM オブジェクト] を選択します。

image.png

ウィザード 説明
ActiveX ライブラリ インプロセス COM サーバー (*.dll) を作るためのテンプレート。プロジェクトごと生成する。タイプライブラリは自動生成される。
オートメーションオブジェクト 既存の COM サーバー (*.dll / *.exe) プロジェクトに対して IDispatch ベース (Automation) の COM クラス (CoClass) を追加するウィザード。タイプライブラリは自動生成される。
COM オブジェクト 既存の COM サーバー (*.dll / *.exe) プロジェクトに対して IUnknown ベースの COM クラス (CoClass) を追加するウィザード。タイプライブラリは自動生成される (*.tlb の生成とリソースへの埋め込みは任意)。
タイプライブラリ 既存の COM サーバー (*.dll / *.exe) プロジェクトに対してタイプライブラリだけを作成するウィザード。このウィザードを単独で使う事はあまりない。

[COM オブジェクトウィザード] は次のように設定します。

image.png

項目
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 として名前を付けて保存します ([ファイル | 名前を付けて保存])。

image.png

コードエディタで LHACOMSVR.ridl を開き、インターフェイス ILHAWrapper にメソッドを追加します。

image.png

[属性] タブでメソッド名を Execute にします。

image.png

[パラメータ] タブで戻り値とパラメータを設定します。

image.png

戻り値:

項目
戻り値の型 long

パラメータ:

項目
名前 cmd
LPWSTR
修飾子 [in]

入力が終わったら [実装の更新] ボタンを押します。

image.png

問題がなければ入力内容が LHACOMSVR_TLB.pas に反映されているハズです。

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;

...
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;

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.pasuses には uLHAWrapper を追加しておきます。

frmuMain.pas
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 を実行して以前と同じように動作するかテストします。

image.png

正しく動作したのであれば、これで COM サーバーは完成です。

COM サーバーとして公開する機能を記述する必要はありますが、COM サーバー化するための処理 というのは特に必要ありません。プロジェクトファイルに uLHAWrapper.pas が追加されていれば、その initialization 節により COM サーバーとして機能します。

See also:

COM サーバーの登録 / 解除

COM サーバーを使うには事前に登録が必要です。

COM サーバーを登録するには、COM サーバーアプリケーションを任意の場所に配置し、/regserver スイッチを付けて管理者権限で実行します。

例えば今回の LHACOMSVR.exe は、

LHACOMSVR /regserver

のようにして管理者権限で実行します。EXE は実行後に自動で閉じられます。

LHACOMSVR /unregserver

登録解除は /unregserver で行います。

ユーザー毎に登録/解除を行う事もできます。こちらは管理者権限が不要です。

  • /RegServerPerUser (登録)
  • /UnregServerPerUser (登録解除)

IDE のメニュー [実行 | ActiveX サーバー] で登録 / 登録解除を行う事もできますが、[登録] (全ユーザーへの登録) は IDE を管理者権限で起動していなければ失敗すると思います。

image.png

基本的に開発時は [現在のユーザーとして登録][登録解除] を使う事になると思います。

インターフェイスに変更があった場合には一旦登録解除してから再度登録する必要があります。登録を適切に行わないと、追加したはずの機能が利用できない等のトラブルが発生します。

テストプログラムを作る

COM サーバーが完成したので、今度は COM サーバーをテストするプログラムを作ります。

プロジェクト名は LHATEST にしておきました。

LHATEST.dpr
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.

image.png

frmuTest.dpr
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 サーバー LHACOMSVRExecute() メソッドを呼び出すプログラムです。エディットボックスに入力された文字列をパラメータとして渡します。

COM サーバーを作った時に生成された LHACOMSVR_TLBuses に追加する必要があります。

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 を実行し、

image.png

エディットボックスに

a C:\WORK\UNLHA32.LZH C:\WORK\UNLHA32.DLL

のように入力してから [Execute] ボタンを押すと UNLHA32.LZH ができます。今回はファイル名にフルパスを指定する必要があります。

LHATEST.exe を実行する前に COM サーバー LHACOMSVR.exe を起動しておく必要はありません。COM サーバーが必要になった時、Windows が自動で起動してくれます。同じ場所にある必要もありません。

64bit アプリケーション

LHATEST.exe のターゲットプラットフォームを 64bit Windows アプリケーションに変更してみましょう。

image.png

〔Shift〕+〔F9〕でビルドしたら LHATEST.exe を実行してみましょう。32bit 版と同じように動作したと思います。当たり前ですね。

image.png

...本当に当たり前でしょうか?

これは何を意味しているかと言えば、64bit アプリケーションから間接的に 32bit DLL を呼び出したという事です。

つまり、アウトプロセス COM サーバーは「アプリケーションを 64bit 化したいけれど、使っている DLL が 32bit のものしか用意されていない」時に使える手段となります。

32bit COM サーバーは 32bit / 64bit アプリケーションから呼び出せますし、その逆で 64bit COM サーバーも 32bit / 64bit アプリケーションから呼び出せます。

おわりに

今回の COM サーバーは、単独のアプリケーションとしても使え、登録すれば COM サーバーとしても使えるアプリケーションでした。プロジェクトファイルの usesSystem.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:

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?