【IE限定】JavaScriptからCOMを呼び出す

IEからCOMを呼び出す方法です。
最近やったので、ノウハウをまとめます。
IE離れが進む中で今更こんなことする機会は滅多にないと思いますが、やるとなるとレガシー過ぎて情報が少なく、苦労すると思います。

# 開発環境
開発環境は以下の通りです。

* Windows10
* Visual Studio 2019
* IE11
* C++

# インストール
Visual Studio 2019をインストールします。
今回は個人の勉強用として、Visual Studio Community 2019をインストールしました。
基本、インストーラに従って、デフォルトのまま、インストールすればよいですが、「ワークロード」で「C++ によるデスクトップ開発」を選んでください。

|![install_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/8563383d-c4b3-9355-413d-7bbd0dd72dba.png)|
|:--|

# プロジェクト作成
Visual Studio Community 2019のインストールが終わったら、プロジェクトを作成します。
Windowsの検索窓で「visual」と入力すると、Visual Studio 2019が候補に表示されます。
右クリックでメニュー表示し、「管理者として実行」を選択してください。

|![make_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/645624c1-0f16-9d74-7b68-98aed261d26c.png)|
|:--|

Visual Studioが起動したら、「新しいプロジェクトの作成」からプロジェクトを作成します。
「新しいプロジェクトの作成」画面が表示されるので、検索欄に「atl」と入力して、「ATL プロジェクト」を検索します。
「ATL プロジェクト」が表示されたら、選択して、次に進みます。

|![make_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/24c01f81-f78f-e0d9-4789-7d4a0cecfade.png)|
|:--|
あとは、デフォルトのままプロジェクト作成を進めてよいですが、「アプリケーションの種類」が「ダイナミック リンク ライブラリ (.dll)」であることは、一応、確認してください。

|![make_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/7cc28fa9-ebbb-a36b-61a3-711dc972627a.png)|
|:--|

# 実装
プロジェクトが作成できたら、実装していきます。

## COM側の実装
### シンプルオブジェクトの作成
まずは、シンプルオブジェクトを追加します。
シンプルオブジェクトを追加すると、実装に必要なファイルが生成されます。
Visual Studioの「ソリューション エクスプローラー」から、プロジェクトを選択します。
右クリックでメニューを表示し、「追加」→「新しい項目」を選択します。

|![dev1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/94444a67-082d-ba40-69c3-7dc2acefb101.png)|
|:--|

「新しい項目の追加」画面が表示されます。
「インストール済み」から「ATL」を選択し、「ATL シンプル オブジェクト」を選択します。
「追加」ボタンをクリックして次に進みます。

|![dev2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/820a462d-2dd3-6691-91f8-901f3e8a994d.png)|
|:--|

「ATL シンプル オブジェクト」画面が表示されます。
自動生成されるファイルの名前が表示されます。
今回は名前を変更せずに進めたいと思います。「完了」ボタンをクリックします。

|![dev3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/ed7fdbe6-0207-4f86-9d9e-24dbb20d3a96.png)|
|:--|

これで実装に必要なファイルが作成されます。

### ファイルに実装を記述
作成されたファイルに実装を記述していきます。
記述が必要なファイルは、以下3つです。

|ファイル名 |例 |記述内容 |
|---|---|---|
|IDLファイル|ATLProject1.idl|公開するインターフェイス定義|
|ヘッダーファイル|ATLSimpleObject.h|C++ヘッダーファイル|
|実装ファイル|ATLSimpleObject.cpp|C++実装ファイル|

#### IDLファイルを記述
まず、IDLファイルを記述していきます。
プロジェクト名.idl(例:ATLProject1.idl)を開きます。
<code>interface IATLXxxxxXxxxx</code>(例:<code>interface IATLSimpleObject</code>)のブロックにプロパティとメソッドを記述します。

```c++:IDLファイル
// ATLProject1.idl : ATLProject1 の IDL ソース
//

// このファイルは、タイプ ライブラリ ([!output SAFE_IDL_NAME].tlb) およびマーシャリング コードを
// タイプ ライブラリ (ATLProject1.tlb) とマーシャリング コードを生成します。

import "oaidl.idl";
import "ocidl.idl";

[
	object,
	uuid(d0b8ec50-7953-4607-86c6-6f4b2499db6f),
	dual,
	nonextensible,
	pointer_default(unique)
]
interface IATLSimpleObject : IDispatch
{
	// ★★★★★★★★★★ ここから ★★★★★★★★★★
	[propget, id(1)] HRESULT Prop1([out, retval] LONG* pVal);
	[propput, id(1)] HRESULT Prop1([in] LONG newVal);
	[propget, id(2)] HRESULT Prop2([out, retval] LONG* pVal);
	[propput, id(2)] HRESULT Prop2([in] LONG newVal);
	[propget, id(3)] HRESULT Result([out, retval] LONG* pVal);
	[propput, id(3)] HRESULT Result([in] LONG newVal);

	[id(4)] HRESULT CalcByArgs([in] LONG arg1, [in] LONG arg2, [out, retval] LONG* result);
	[id(5)] HRESULT CalcByProp();
	// ★★★★★★★★★★ ここまで ★★★★★★★★★★
};
[
	uuid(8cb35385-6c0f-4d8c-aef3-864ff7ec2143),
	version(1.0),
]
library ATLProject1Lib
{
	importlib("stdole2.tlb");
	[
		uuid(da1a207a-5427-49b2-b2fb-08b9f5fef902)
	]
	coclass ATLSimpleObject
	{
		[default] interface IATLSimpleObject;
	};
};

import "shobjidl.idl";
```

##### プロパティについて
メンバ変数に外部から値を取得・設定するためのアクセサです。
<code>propget</code>・<code>propput</code>が、値取得・設定のセットで、<code>id</code>はそのセットで同じ<code>id</code>を振ってください。
値取得のシンタックスは以下で、コピって、<code>ID</code>、<code>プロパティ名</code>、<code>型名</code>を変更すれば、量産できます。

```c++:値取得
[propget, id(ID)] HRESULT プロパティ名([out, retval] 型名* pVal);
```

値設定のシンタックスは以下で、コピって、<code>ID</code>、<code>プロパティ名</code>、<code>型名</code>を変更すれば、量産できます。

```c++:値設定
[propput, id(ID)] HRESULT プロパティ名([in] 型名 newVal);
```

##### メソッドについて
何かしらの処理をする関数です。
メソッドのシンタックスは以下です。
<code>[in]</code>が引数、<code>[out, retval]</code>が戻り値になります。

```c++:メソッド
[id(ID)] HRESULT メソッド名([in] 型名 引数名, [in] 型名 引数名, ・・・, [out, retval] 型名* 戻り値名);
```

##### idについて

<code>id</code>は重複しない任意の値を振ります。

##### 型について

基本的な型として、例えば、以下が使えます。

* BSTR
* BYTE
* CHAR
* DATE
* DOUBLE
* FLOAT
* LONG
* SHORT
* VARIANT

他にもいろいろ使えるかもしれません。
構造体のような独自定義型も使えるかもしれませんが、試してません。

#### ヘッダーファイルを記述
IDLファイルを記述したら、ヘッダーファイルを記述します。
拡張子.hのファイル(例:ATLSimpleObject.h)を開きます。
<code>public:</code>にプロパティとメソッド、<code>private:</code>にプロパティのメンバ変数を記述します。

```c++:ヘッダーファイル
// ATLSimpleObject.h : CATLSimpleObject の宣言

#pragma once
#include "resource.h"       // メイン シンボル



#include "ATLProject1_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "DCOM の完全サポートを含んでいない Windows Mobile プラットフォームのような Windows CE プラットフォームでは、単一スレッド COM オブジェクトは正しくサポートされていません。ATL が単一スレッド COM オブジェクトの作成をサポートすること、およびその単一スレッド COM オブジェクトの実装の使用を許可することを強制するには、_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA を定義してください。ご使用の rgs ファイルのスレッド モデルは 'Free' に設定されており、DCOM Windows CE 以外のプラットフォームでサポートされる唯一のスレッド モデルと設定されていました。"
#endif

using namespace ATL;


// CATLSimpleObject

class ATL_NO_VTABLE CATLSimpleObject :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CATLSimpleObject, &CLSID_ATLSimpleObject>,
	public IDispatchImpl<IATLSimpleObject, &IID_IATLSimpleObject, &LIBID_ATLProject1Lib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CATLSimpleObject()
	{
	}

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CATLSimpleObject)
	COM_INTERFACE_ENTRY(IATLSimpleObject)
	COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()



	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

// ★★★★★★★★★★ ここから ★★★★★★★★★★
private:
	LONG mProp1;
	LONG mProp2;
	LONG mResult;

public:
	STDMETHOD(get_Prop1)(LONG* pVal);
	STDMETHOD(put_Prop1)(LONG newVal);
	STDMETHOD(get_Prop2)(LONG* pVal);
	STDMETHOD(put_Prop2)(LONG newVal);
	STDMETHOD(get_Result)(LONG* pVal);
	STDMETHOD(put_Result)(LONG newVal);

	STDMETHOD(CalcByArgs)(LONG arg1, LONG arg2, LONG* result);
	STDMETHOD(CalcByProp)();
};
// ★★★★★★★★★★ ここまで ★★★★★★★★★★

OBJECT_ENTRY_AUTO(__uuidof(ATLSimpleObject), CATLSimpleObject)
```

#### 実装ファイルを記述

ヘッダーファイルを記述したら、実装ファイルを記述します。
拡張子.cpp(例:ATLSimpleObject.cpp)のファイルを開きます。
以下のように記述していきます。

```c++:実装ファイル
// ATLSimpleObject.cpp : CATLSimpleObject の実装

#include "pch.h"
#include "ATLSimpleObject.h"


// CATLSimpleObject


// ★★★★★★★★★★ ここから ★★★★★★★★★★
STDMETHODIMP CATLSimpleObject::CalcByArgs(LONG arg1, LONG arg2, LONG* result)
{
    *result = arg1 + arg2;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::CalcByProp()
{
    mResult = mProp1 + mProp2;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::get_Prop1(LONG* pVal)
{
    *pVal = mProp1;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::put_Prop1(LONG newVal)
{
    mProp1 = newVal;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::get_Prop2(LONG* pVal)
{
    *pVal = mProp2;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::put_Prop2(LONG newVal)
{
    mProp2 = newVal;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::get_Result(LONG* pVal)
{
    *pVal = mResult;
    return S_OK;
}

STDMETHODIMP CATLSimpleObject::put_Result(LONG newVal)
{
    mResult = newVal;
    return S_OK;
}
// ★★★★★★★★★★ ここまで ★★★★★★★★★★
```

### ビルド

COM側を実装したら、ビルドします。
ビルド前に以下を確認してください。

* 「x86」であること
<br>IEからCOMを呼び出すためか、x64では呼び出せないようです。
* Visual Studioを管理者モードで起動していること

|![build.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/2e513448-43ca-9219-160c-e85323c5982c.png)|
|:--|

「ソリューション エクスプローラー」でプロジェクトを選択し、右クリックでメニューを開き、「リビルド」を選択します。

|![dev4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/bebb1604-94f7-c01e-aeb7-2c30b1e96062.png)|
|:--|

ビルドが終了すると、DLLがCOMに登録されています。

## JavaScript側の実装

COMの実装ができたら、JavaScript側の実装を記述します。
以下の内容のHTMLファイルを作成します。

```html:HTMLファイル
<!doctype html>

<html>

<head>
  <meta charset="utf-8">
  <title>ActiveX</title>
</head>
<body>
  <object id="activeXObj" classid="clsid:Your UUID"></object>
  <script>
    var activeXObj = document.getElementById('activeXObj');

    var result = activeXObj.CalcByArgs(1, 2);
    alert('CalcByArgs result: ' + result);

    activeXObj.Prop1 = 100;
    activeXObj.Prop2 = 200;
    activeXObj.CalcByProp();
    alert('CalcByProp result: ' + activeXObj.Result);
  </script>
</body>
</html>
```

Your UUIDには、IDLファイルにあるUUIDを記述します。
IDLファイル内にUUIDが複数あるので、注意してください。
([IDLファイルを記述](#idlファイルを記述)を参照)
以下のUUIDを使います。

```c++:IDLファイル
library ATLProject1Lib
{
	importlib("stdole2.tlb");
	[
		// ★★★★★★ これ　★★★★★
		uuid(da1a207a-5427-49b2-b2fb-08b9f5fef902)
	]
	coclass ATLSimpleObject
	{
		[default] interface IATLSimpleObject;
	};
};
```

上記の例では、HTMLの指定は以下になります。

```html:例
<object id="activeXObj" classid="clsid:da1a207a-5427-49b2-b2fb-08b9f5fef902"></object>
```

# 動作確認

実装が終わったら、動作確認します。
作成したHTMLファイルをIEで開きます。
IEで開くと、以下のメッセージが表示されます。
「ブロックされているコンテンツを許可」をクリックします。

|![dev5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/0811b27c-2974-3d9f-fe53-baf422e91860.png)|
|:--|

次に以下のメッセージが表示されます。
「はい」をクリックします。

|![dev6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/ff3df659-3f6a-4004-4ee7-2996d9d77446.png)|
|:--|

JavaScriptからCOMにアクセスが行われ、結果がアラートダイアログで表示されます。

|![dev7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224267/5d1984ad-dfca-d753-994a-b590b29b8452.png)|
|:--|