2024年の今、いかにしてVS2005を捨ててVS2015にする戦いは終わったか、そしてなぜCOMとの苦しい戦いが繰り広げられたか ~再入の悪魔~

概要

Optimal BizのWindows AgentはながらくVisual Studio 2005とVisual Studio 2015を併用してビルドする必要がありました。Visual Studio 2015化対応は2012年のVisual Studio 2012化対応からスタートしていましたが、対応範囲の大きさからモジュールごとにVisual Studio 2015化対応を行ってきました。そして2024年リリースのBiz 9.19.0にてVisual Studio 2015化対応は完了を迎えました。

しかし、埋め込まれたバグの修正にはCOMの理解が不可欠であったため、2020年代に突入した今になって私達はこれまで正面戦争を避けてきたCOMを0から学び直す必要がありました。そしてATLの不思議な挙動やSTAにおける再入との戦いを乗り越え、無事にリリースされました。

はじめに

Optimal Biz DS Windowsチームです。Optimal BizはMDMというカテゴリに位置づけられる製品です。WindowsのMDMを行うために、一般的なWindows Serviceアプリケーションとして開発されたWindows Agentが制御を行っています。

このWindows Agentの開発を担うチームの一つが私たちのチームです。また、純粋なWindows Agentの開発にとどまらず、サーバーサイドの改修とセットで必要になる開発、サーバーサイド単独の開発、サーバーOSのEoL対応など、低いレイヤーから高いレイヤーまでをC++を理解できる強みとメンバーの技術力を生かして幅広く対応しています。このようにOptimal Biz開発チームの中でもすこし特殊部隊感も醸し出しています。

2024年2月18日にリリースされた Optiaml Biz 9.19.0において、開発に用いているVisual Studio(および付随するC++コンパイラ)のバージョンアップに成功しました。これはWindows Agentの開発の歴史においては大きな変化となります。

この対応のことを、以下VS2015化対応と呼称することにします。

Visual Studioとは

Windows Agentはその大半がC++ で書かれています。WindowsでC++ 開発をするときによく利用されているのがVisual Studioとそれに含まれるVisual C++ コンパイラです。

処理系 - cpprefjp C++日本語リファレンス#Microsoft Visual C++

ここをみてもらうと分かる通り、長い歴史をもっています。最新は記事執筆段階ではVisual Studio 2022です。

以降簡単のためにVS2005とかVS2015のように略記します。

VS2010になったときに、プロジェクトの書き方が大幅に変わり互換性がなくなりました。プロジェクトファイルの拡張子も.vcprojから.vcxprojに変わりました。このことはVSのバージョンアップを困難にさせる一因となりました。

C++という言語の進化

C++ という言語は1998年にISOで標準化され、その後微修正が2003年になされました。これをC++03と呼称しています。

その次期バージョンと言われたC++0xは「コンセプト」の追加に関して議論が紛糾したことをはじめとする様々な要因から遅れに遅れ(ref: 本の虫: コンセプトの経緯)、xは実は16進数なんだというジョークが飛び交う中、2011年にC++11は発行されました。

C++11 - cpprefjp C++日本語リファレンス

2011年当時の他の言語が備えていた現代的な機能が追加され、C++を記述する時のパラダイムシフトが起こりました。こうしてC++11はC++における歴史的転換点を迎えたのです。

後述するVS2015はC++11に大体対応が終わっていたバージョンとなります。

その後のC++は、C++03からC++11までの期間が空きすぎたことに対する反省から3年おきにリリースするように方針を変えました。

COMとは

ja.wikipedia.org

Component Object Modelの略です。著しくgooglabilityが低くて大変苦労しました。

COMの説明の前にC++に立ち返って考えてみます。C++には仮想関数があります。

#include <iostream>
#include <memory>
class IDisposable {
public:
    virtual void dispose() = 0;
};
class IExecutable {
public:
    virtual void Execute() = 0;
};
class Foo : public IDisposable, public IExecutable {
public:
    virtual void dispose() override { std::cout << "disposed" << std::endl; }
    virtual void Execute() override { std::cout << "executed" << std::endl; }
    virtual ~Foo() = default;
};
int main()
{
    std::shared_ptr<IDisposable> d = std::make_shared<Foo>();
    // do something
    d->dispose(); // => disposed
    const auto e = std::dynamic_pointer_cast<IExecutable>(d);
    if (!e) return 1;
    e->Execute(); // => executed
    const auto d2 = std::dynamic_pointer_cast<IDisposable>(e);
    if (!d2) return 1;
    d2->dispose(); // => disposed
}

wandbox.org

さて、この仮想関数がどのようにして実装されるかというと、多くの処理系で、vtableを用います。

pabloariasal.github.io

qiita.com

上の例に示したように、基底クラス間の間でもdynamic_castを用いることでキャストできます。IDisposabledに問い合わせてIExecutableを要求しeを得て、さらにeに問い合わせてIDisposabled2を得られているのがわかると思います。

COMでは純粋仮想関数で表されるインターフェースをより明確にクラスから分離した上で、vtableを一般化します。COMでは各オブジェクトが最低でもIUnknownというインターフェースを実装しています。

interface IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) = 0;
    virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
    virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
};

dynamic_castに変わってQueryInterfaceを導入し、任意のインターフェスでのポインタをオブジェクトに要求するようになります。 また上の例ではオブジェクトの寿命をstd::shared_ptrに管理させましたが、COMではAddRef/Releaseを呼び出して対応します。またQueryInterfaceを呼び出すと新たなポインタを得るために参照カウントを1増やします。

こうした一般化によって、COMで作られたモジュールはC++に限定されず、幅広い言語から呼び出すことが可能になるわけです。

COMはプロセスの壁を超えることができます。自動的にある種のプロキシーオブジェクトを呼び出し元のプロセス内に生成して、プロセス間通信を行って外部のプロセスを生成したりすでにあるプロセスと通信して、COMのメソッドを別プロセス内で実行できます。実のところCOMはコンピュータの壁も乗り越えられるのですが(Distributed COM)それは今回の記事に関係ないので割愛します。

Visual Studio 2015対応前の状況

Visual Studio 2005とVisual Studio 2015を併用していた

さて、自分が2021年に新卒で入社してWindowsチームに配属されて最初に衝撃を受けたドキュメントがこちらです。

プロジェクトのビルド

  • 次の順番でVisual Studioプロジェクトをビルドすること。

    手順 ソリューション ビルドするプロジェクト 備考
    1 supportbiz_full.sln (VS2005) Build\AutoPreIncrediBuild VS2005とVS2015で共通して使うTLBやリソースのビルド
    2 biz-2015.sln (VS2015) Build\AutoIncrediBuild EXEとDLLのビルド
    3 supportbiz_full.sln (VS2005) Build\AutoIncrediBuild EXEとDLLのビルド
    4 biz-2015.sln (VS2015) Build\AutoBuildまたはInstaller\BizAgentMsi インストーラー(MSI形式)のビルド

    ビルドが完了すると、BizAgentMsi/bin/DebugまたはBizAgentMsi/bin/Release配下にファイルBizAgent.msiが作成される。

ソリューションファイルが2つあり、VS2005とVS2015の両方が登場します。

またこの手順を踏むと、依存ライブラリのビルドがVS2005とVS2015双方で走ります。例えばVS2005向けにboost 1.55、VS2015向けにboost 1.59のビルドが走っていました。同じ依存ライブラリを2回ビルドすることと等しいので、ビルド時間は伸びます。

またVS2005のコンパイラは古いので現代のPCが搭載するマルチスレッドな環境に適応していません。十分にPCの能力を使い切ってくれません。

これを緩和するために製品版をビルドするサーバー向けのビルドスクリプトでは手順2と3が並列で動くようにmsbuildを呼び出すGNU Makefileが記述されていました。

C++11が使いたい

VS2005はC++11ができるより前にできたコンパイラなのでC++11が使えません。

プロジェクトのある部分ではC++11が使えて、別の部分ではC++03で書かないといけない、というのは案外つらい開発体験です。

C++11で追加されてたコア言語機能に絞って、なくて特に辛かったものと回避策を挙げたのが以下です。

C++11で追加されたライブラリ機能はその大半をboostに頼っていました。とはいえboostでは不足なこともあります。

qiita.com

例えばこの記事で書いた問題の解決にはstd::refstd::reference_wrapper::operator()を使いたかったのですが、C++03にもboostにもないので自作しました。

今回のVS2015化対応の先駆け

遡ること2012年、VS2015対応の先駆けとなるVS2012対応が行われていました(図の黄色の線)。また2015年にはVS2015対応が行われていました(図の水色の線、ピンク色の線が分かれる前まで)。さらに、これらに関連して2014年には後述するUserModuleProvider廃止対応が行われていました(図の緑色の線)。

しかし、Windows Agentのコードベースは非常に大きいため、リスクの問題と工数の問題から、すべてを一度にバージョンアップさせるのではなく、分割してモジュールごとにバージョンアップさせることになりました。

2017年頃、このVS2015化対応を完結させてOptimal Biz 9.5としてリリースしようという動きが発生しました。ただ、やはりWindows Agentのコードベースは非常に大きいので残念ながら完結させられずに、引き続きモジュールごとにバージョンアップさせることになりました。

以降は手が空いているときにその時点での最新リリースをVS2015化対応ブランチに取り込むことが行われていました。

始まったVS2015対応

こんどこそVS2015対応を完結させるべく、2023年7月よりVS2015対応の要件が動き始めました。

あらためてその背景を整理するとつぎのようになります。

  • VS2005に関しては以下のような問題を抱えている
    • サポート終了している製品であり、マイクロソフトのサポートを受けられない
    • VS2005が存在する事で使用しているライブラリの多くが更新できない
    • 現代的なC++記法を使用することができない
    • ビルド時間が長い
    • IDEの支援が少ない
  • VS2005とVS2015を併用している事でビルドフローが複雑

そういうわけで、Windows AgentをVS2015のみでビルドできるようにすることをゴールに定めました。

最大でWindows Agent開発チーム6人が同時に稼働して開発を行いました。

また、過去にVS2012/2015化対応を進めていた別プロダクトに異動されている方に、技術的なアドバイスを定期的にいただく場を設けて、VS2015化対応の品質が高められるように努めました。

過去のVS2015化対応のコードを、現在の基準のコードと照らし合わせて、不足分を補う作業を行う

先に述べた通り、VS2015対応のbranchには10年以上の歴史があります。昔と今ではOptimal Biz全体として要求される品質が全く異なります。そのため10年分のすべてのcommitのすべての差分1行ずつについて、妥当性を確認していきました。

1commitずつ見ると全体を見通せなくなるので、一旦まずは読んでもよくわからないところを抜き出していって、その詳細を短時間で追いかけて、解決できないものは詳細に調査するチケットを切るようにしました。また詳細に調査したとてわかりそうもないものは、かつてVS2012/VS2015化対応を進めていた別プロダクトに異動されている方に問い合わせるように質問リストに足すことにしました。

最終的に詳細に調査するべきチケットは12チケットになり、そのうちの一つであり後述するUserModuleProvider廃止対応は別要件化して同じくOptimal Biz 9.19にてリリースしました。

Optimal Biz 9.9より後の取り込み

Optimal Biz 9.9まではVS2015化対応対応ブランチにとりこまれていたので、それより後の分についてVS2015化を行い取り込む作業をしました。

また機械的に置換しやすい範囲において、C++11以降の言語機能でのコード書き換えを行いました。これについては「VSを上げたからと言って動いているコードを新しい書き方にしなければならないってことはない、すべての変更にはリスクが伴う」という意見がチーム内であり、筆者の「これから生まれるコードは既存のコードの影響を受けるので、今書き換えずしていつ書き換えるのか」という持論と対立しましたが、個別に議論して折り合いをつけていきました。

「すべての変更にはリスクが伴う」の実例として、BOOST_FOREACHをC++11の範囲for文に書き換える作業で作り込んだバグを見てみましょう。

-#define FOREACH_HANDLER(handlerVariable) \
-       std::vector<GUID> keys = m_handlers.GetKeys(); \
-       for (const HandlerMap::Handler& handlerVariable : \
-               keys \
-               | boost::adaptors::transformed(boost::bind(&HandlerMap::TryGet, &m_handlers, _1)) \
-               | boost::adaptors::filtered(boost::lambda::_1 != HandlerMap::Handler()))
-
+auto HandlerRange(HandlerMap& handlers)^M
+{^M
+       const auto keys = handlers.GetKeys();^M
+       return keys^M
+               | boost::adaptors::transformed(boost::bind(&HandlerMap::TryGet, &handlers, _1))^M
+               | boost::adaptors::filtered(boost::lambda::_1 != HandlerMap::Handler());^M
+}^M

 std::vector<AgentProperty> AgentPropertyStorage::MakeQuery()
 {
        BIZ_LOG_SCOPE;

        std::vector<AgentProperty> query;
-       FOREACH_HANDLER(handler)
+       for (const HandlerMap::Handler& handler : HandlerRange(m_handlers))^M
        {

この書き換えのどこにバグが潜んでいるのか、見抜けたでしょうか?

マクロをやめてhandlers.GetKeys()の結果をboost::adaptorsoperator |で操作して返却する関数に書き換えています。 ところがhandlers.GetKeys()を受けている変数keysの寿命は関数を抜けると切れるのでdangling referenceとなるわけです。

こういうのを踏むたびAddressSanitizerが欲しくなりますが、残念ながらVS2015には非対応です

コードの現代化と同時並行で、下のようなワークアラウンドコードを削除する対応も行いました。

algorithm系関数に状態を持つ関数オブジェクトを生で渡すのはやめてください、しんでしまいます #C++ - Qiita

namespace inferior {
  /// @brief `std::reference_wrapper` のバックポート実装。boostのものはoperator()を実装しないので要求を満たさない
  ///
  /// VS2005を捨て去った暁には削除する
  class reference_wrapper
  {
  public:
      reference_wrapper(Pred& u) : p(u) {}
      reference_wrapper(const reference_wrapper& x) : p(x.p) {}
      // reference_wrapper& operator=(const reference_wrapper&); //実装を省略
      operator Pred& () const { return p; }
      Pred& get() const { return p; }
      bool operator()(Foo& arg) const { return p(arg); }
  private:
      // operator=を省略したのでポインタではなく参照で持つ
      Pred& p;
  };
  inline reference_wrapper ref(Pred& t) { return reference_wrapper(t); }
}

検証からバグを探り出す

大きく3種類の検証を行いました

  • OSバージョンアップ検証の検証項目実施
  • 毎リリースごとに実施している長期試験の実施
  • アドホックテスト

また、検証に用いるバイナリは通常リリースビルド版だけですが、今回対応においてはDebug Assertや_ATL_DEBUG_INTERFACESマクロによるATLのハンドル追跡機能を活用したかったので、アドホックテストと一部の検証についてはデバッグビルド版でも実施しました。

OSバージョンアップ検証の検証項目実施は、すこしでもはやく重大なバグを検出するために、今回の開発の早い段階から行われました。

アドホックテストは、ふだんよく検証を担当するメンバーだけではなく、VS2015対応のコード改修が終わって手が空いたメンバーも参加して、実施しました。また、かつてVS2012/VS2015化対応を進めていた別プロダクトに異動されている方も手元でアドホックテストに近しいことを実施してくださっていたようです。

アドホックテストで怪しい動作を発見し、それは仕様動作なのかバグなのかを切り分けていく作業は時間がかかりますが、VS2015化対応のような影響範囲が大きいリスクの高い開発要件には適した検証手法だったと思います。

UserModuleProvider廃止対応 ~それは私達とCOMとの戦いの幕開けだった~

UserModuleProviderとは

そもそもUserModuleProviderとは何なのかを説明する必要があります。

まずWindows Agentの実装において、サービスという概念があります。これはCOMクラスであり、例えば認証機能とか、リモートロック機能とかデバイス情報取得機能といった機能ごとに存在しています。

これらサービスのインスタンスはIServiceProviderを実装したクラスを経由してQueryServiceメソッドを呼ぶことである程度自由に取ってこれます。

learn.microsoft.com

WindowsAgentには大きく分けてBizServiceHost.exeBizModuleHost.exeという2種類のプロセスが存在します。このうちBizModuleHost.exeはsession id 0で動いているものを除けば、Windowsにログオンしているユーザーの数だけ存在しています。

このBizModuleHost.exeでうごくサービスを配下に持っているのがUserModuleProviderです。一方でBizServiceHost.exe側のサービスを配下に持っているのがManagerServiceProviderです。

ただ、UserModuleProviderがManagerServiceProviderと密であるために、各サービスには下図のような循環依存が発生していました。

注: 破線矢印は遅延された依存関係

IServiceProviderという機構を実装する利点は他のサービスのインスタンスを簡単に取ってこられるところにあるわけですが、UserModuleProvider配下のサービスにおいてはその利点はいかされていませんでした。UserModuleProviderを廃止するとManagerServiceProviderが提供するサービスを取ってこれなくなりますが、固定的な関係であれば、必要なインタフェース・その他データは個別にやり取りする仕組みを設ければよいので問題ないという判断がされました。

かくして、2014年にはUserModuleProviderを廃止する対応が行われていました。この成果の一部はその当時に製品版にマージされましたが、UIAgentActivationDialogという2つのサービスが残り続けていました。

VS2015化対応ブランチにすでにUserModuleProviderを完全に廃止する対応が存在していたので、これをそのまま採用しようとしたのが今回VS2015化対応に伴ってこの対応を行った理由です。

対応後のサービスの依存関係は次のようになりました。ActivationDialogはCOMクラスではなく通常のC++のクラスとし、UIAgentが管理するようになりました。

ではなぜこの対応が私達とCOMとの戦いの幕開けになったのでしょうか?

Debug Assertion Failed! Expression: m_dwRef == 0

DebugビルドしたWindows AgentをインストールしてWindows Serviceが起動するとDebug Assertion Failedが出るようになってしまいました。
ATL内部のassertに引っかかっているのは見ればわかりますが、なぜ引っかかったのかを調べるためにはそもそもCOMのオブジェクトとは何であるか、どのようにしてインスタンス化されるのかということやATLの内部仕様を詳細に理解する必要がありました。

その成果をまとめたのが以下のQiitaの記事です。
qiita.com

UserModuleProviderがあった頃のUIAgentクラスはIserviceProviderに対応させるために集約(Aggregation)を利用していました。このため、ATLのDECLARE_POLY_AGGREGATABLEマクロを利用していました。

ところがUserModuleProviderを廃止したことで、UIAgentクラスはIserviceProviderを実装する機構によってではなく、単にCoCreateInstanceされるようになり、outer objectは渡されなくなりました。

ここで、ATLのDECLARE_POLY_AGGREGATABLEマクロはouter objectを渡さずに使うことを想定していないらしく、UIAgentの基底クラスにDECLARE_PROTECT_FINAL_CONSTRUCTを指定したにもかかわらずこれを無視し、かわりにATL::CComPolyObjectの基底クラスであるATL::CComObjectRootBaseInternalFinalConstructReleaseが呼び出されていしまい、問題が起きていました。

解決策としては、DECLARE_POLY_AGGREGATABLEマクロを使うのをやめて、DECLARE_NOT_AGGREGATABLEマクロを使うようにしました。登場人物が減って一気にスッキリしたクラス図になりますね。

タスクトレイのOptimal Bizのアイコンが増殖しツールバーも増殖する

画像の通り増殖します。増殖したどちらもクリックしたりしてもちゃんと動作しているようにみえます。

この問題についてはC++ Advent Calender 2023に投稿した以下の記事で取り扱いました。
qiita.com

Windows AgentのBizModuleHost側の処理に大きく関わるUIAgentクラスをセッションごとに管理しているboost::container::flat_mapが存在するのですが、STAでうごいているためにCOMの再入によって、以下の3つの処理が並行して動くことがありました。

  1. 空の要素をboost::container::flat_mapから消すループ処理
  2. すべてのセッションについて何かしらの操作をするループ処理
  3. UIAgentを再生成する処理

このときにboost::container::flat_mapの管理から外れたUIAgentが生まれたり、ループが途中で不正な状態になり正しく回らなくなるのが問題でした。

そこはかとなくマルチスレッドプログラミングにおける競合状態(rase condition)を思い起こすような構図ですが、シングルスレッド上でコンテキストスイッチのようなことが発生するのがCOMの再入です。

dev.activebasic.com

そもそものSTAとか再入を理解するところから始まり、その対策をどうするかでも苦戦をした障害でした。

C++erですがCOMに翻弄されています: 再入との戦い #C++ - Qiita
では4つの敗北を取り扱いました。

  1. (一敗) P0145R3実装前は式の評価順序が不定だった
  2. (二敗)boost::container::flat_mapではイテレータループ中に再入が起きたときに問題となる
  3. (三敗)std::unordered_mapもrehashされるとバグる。
  4. (四敗)そもそもイテレータループ中に要素削除が起こるとstd::mapといえど死ぬ

(五敗)良かれと思ってキャストをこねくり回して一時オブジェクトを生成してしまいdanglingする

じつはもう一回敗北をしています。

void UIAgentManager::UpdateAgent(DWORD sessionId)
{
    TypeLib::IUIAgentPtr& agent = m_uiElement[sessionId];

    if (Com::PingRemoteObject(agent) != nullptr)
    {
        // do something
    }
    else
    {
        agent = Helper::CreateSessionObject<TypeLib::UIAgent>(sessionId);
        agent->Initialize(Something());
        // do something
    }
}
void UIAgentManager::EraseOldElement()
{
    for (UIMap::iterator it = m_uiElement.begin(); it != m_uiElement.end(); )
    {
        if (it->second == nullptr)
        {
            it = m_uiElement.erase(it);
        }
        else
        {
            ++it;
        }
    }
}

再入への対策として、上の記事で取り上げていたEraseOldElementUpdateAgentを見直しをおこないました。すなわちEraseOldElementで削除対象となっていた要素がnullptrになるケースについて、それそのものは排除できないために、mapからの除去とUIAgentの再生成を一ループにまとめる対応をしてました。

 // insertしてもイテレータが無効にならないいstd::mapを採用する
    // boost::container::flat_mapやstd::unordered_mapではイテレータループ中に再入が起きたときに問題となる
    using UIMap = std::map<DWORD, TypeLib::IUIAgentPtr>;
    class NonErasableUIMap : public UIMap
    {
    public:
        using UIMap::UIMap;
    private:
        using UIMap::erase;
        using UIMap::clear;
    };
    static_assert(std::is_same<NonErasableUIMap::iterator, UIMap::iterator>::value, "");
    class NonErasableAndNonIteratableUIMap : public NonErasableUIMap
    {
    public:
        using NonErasableUIMap::NonErasableUIMap;
        /**
        * @brief nullではないagentを取得する
        *
        * 再入対策としてイテレータを回している間に再入が発生して要素が削除されるとイテレータが無効になってしまう。
        * この対策として予めコピーをしておくために用いる
        * @return std::vector<TypeLib::IUIAgentPtr>
        */
        std::vector<TypeLib::IUIAgentPtr> get_valid_agents();
    private:
        using NonErasableUIMap::begin;
        using NonErasableUIMap::cbegin;
        using NonErasableUIMap::end;
        using NonErasableUIMap::cend;
    };

    NonErasableAndNonIteratableUIMap m_uiElement;
void UIAgentManager::VerifyAndReconnectUIProcess()
{
    BIZ_LOG_SCOPE;

    // このイテレータループは要素の削除をまさに行っているループであり、イテレータが無効になる心配はないので
    // NonErasableAndNonIteratableUIMapからNonErasableUIMapにキャストしてイテレータを回す
    for (auto it = static_cast<NonErasableUIMap>(m_uiElement).begin(); it != static_cast<NonErasableUIMap>(m_uiElement).end(); )
    {
        const auto sessionId = it->first;
        try {
            const auto sessionIdStr = std::to_wstring(sessionId);
            BIZ_LOG << sessionIdStr;
            if (it->second == nullptr)
            {
                // ここでは要素の削除をしても問題ないのでNonErasableAndNonIteratableUIMapからUIMapにキャストしてeraseを呼ぶ
                // ここ以外で要素を削除するとSTAなので再入が起きたときにイテレータが死んでしまう
                it = static_cast<UIMap>(m_uiElement).erase(it);
                BIZ_LOG_RESCUE(BIZ_LOG << L"forgot session" << sessionIdStr);
                continue;
            }
            if (Com::PingRemoteObject(it->second) == nullptr)
            {
                it->second = nullptr; // CreateAgentが失敗してもいいようにまず空にする
                it->second = CreateAgent(sessionId, GetCurrentUIStatus());
                BIZ_LOG << L"reconnected session" << sessionIdStr;
            }
        }
        catch(const std::exception& e) {
            BIZ_LOG.Catch(e);
        }
        ++it;
    }
}

キャストしないと危ない操作ができないというのは、むやみにキャストを書くべきではないというC++の設計思想とマッチしていてよさそうに一見思えます。しかしバグが紛れ込んでいます。どこにバグが潜んでいるのか、見抜けたでしょうか?

static_cast<NonErasableUIMap>(m_uiElement)は一時オブジェクトを生成します。
この一時オブジェクトに対してbegin()を呼び出していますが、
次の瞬間一時オブジェクトの寿命は切れており、itは不正な状態となるわけです。

対策としてはキャストするときにlvalue referenceとしてキャストするようにしました。

 auto& uiElement = static_cast<NonErasableUIMap&>(m_uiElement);
    for (auto it = uiElement.begin(); it != uiElement.end(); ) 

またそもそも各クラスをコピー禁止にしました。

GlobalInterfaceTableからインスタンスが取れない

COM のマーシャリングに対応していないクラスに対してマーシャリングが必要な実装をして例外になっていた。というお話。

GlobalInterfaceTable(GIT)は、異なるアパートメント内のオブジェクトに実装されているインターフェースにアクセスするときに、インターフェースのやり取りをするために使用します。

このGITでのやり取りには、マーシャリングが使用されるそうですが、問題となったクラスはマーシャリングに対応していませんでした。 マーシャリングでは、プロキシ・スタブクラスを介してアクセスするため、そのクラスが存在しないことで、COMランタイムシステムからしたら、インターフェースをマーシャリングする方法が分からなかった、ということらしいです。

プロキシ・スタブクラスはIDLファイルにインターフェースを定義すると自動的に生成してくれますが、このクラスはIDLには定義せずに.hに定義していたため、今回のようにマーシャリング非対応のクラスとなっていました。

コードのコメントにも COMのマーシャリングを使用させないため、IDLで定義せず、ここに定義している。 と書いてあり、意図的にマーシャリングに対応していなかったようです。

この件はGITを使用しない元のコードに戻すことで解決となりました。

今回は有識者からの助言によってこの問題を解決することができましたが、仮に助言が無かった場合、このコメントがなかったら意図を汲めずにマーシャリングに対応する形で修正していたかもしれません。 改めてコメントの大切さを感じました。

得られた成果

ビルドという側面からは、VS2005とVS2015を併用するビルド手順をやめることができたために、Windows Agentのフルビルドにかかる時間は21分から16分に短縮しました。

C++ 言語という側面からは、コードベース全面において、C++11 を利用できるようになりました。C++の歴史においてパラダイムシフトとなったC++11を全面的に採用できるのは、今後の開発の生産性に大きく寄与するでしょう。

開発環境的という側面からは、VS2005をWindows11で動かすには幾つもの修正KBを当てる必要があるので、これを避けられるようになったことから開発環境構築時間を短縮することができそうです。また開発中に2つのIDEを立ち上げておかなくて良くなるので、PCのメモリーに優しくなりました。

今回の対応を通じて、コードベース全体、とくにWindows Agentの基幹となる部分についてのコード読解が進み、詳細設計書を補充することができました。このことは今後の開発における技術選択の幅を広げることに繋げられそうです。

おわりに

今回の対応でVisual Studio 2005を廃止してVisual Studio 2015に統一することができましたが、そのVisual Studio 2015もまた古いため、さらなるバージョンアップが必要です。しかしそのためには利用している依存ライブラリの見直しに着手する必要がありそうです。俺達の戦いはこれからだ!

OPTiMではC++のコードを読み書きするのが楽しいと思えるエンジニアや2024年の今になってCOMと戦うことになっても心が折れないエンジニアも募集していると思います。・・・いや、やっぱりCOMと戦うときは心が折れてもいいかもしれません。

www.optim.co.jp