Quantcast
Channel: イグトランスの頭の中
Viewing all 123 articles
Browse latest View live

STAのメソッド呼び出しを見てみる

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの13日目の記事です。


STAはメッセージループの元で動作します。

  • 他のアパートメントからのメソッド呼び出しはウィンドウメッセージで受信します。
  • 他のアパートメントのメソッド呼び出しを行っている間、モーダルなメッセージループが回ります。

これが組み合わさると、1スレッドのみでありながら、あるメソッドの呼び出し中に、他のメソッド呼び出しを受け付けること(再入)があります。

そのことを見てみましょう。

STAからSTAへのメソッド呼び出し

こんなコードを書きました。適当なISequentialStream実装クラスHogeを作り、1つ目のSTA上にオブジェクトを作成します。2つ目のSTAスレッドからその中のメソッドReadを呼び出しています。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <thread>
#include <tchar.h>
#include <windows.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
ATL::CComGITPtr<ISequentialStream> g1;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
  , public ISequentialStream
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(ISequentialStream)
  END_COM_MAP()
 
  IFACEMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbRead) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(Write)(
    _In_reads_bytes_(cb) const void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbWritten) override
  {
    return E_NOTIMPL;
  }
};
 
void worker(DWORD mainThreadId)
{
  // 2つ目のSTAを作る。
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return;
 
  try
  {
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g1.CopyTo(&s));
 
    // 1つ目のSTAのオブジェクトのメソッドを呼び出す。
    char buf;
    s->Read(&buf, sizeof buf, nullptr);
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
 
  PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
}
 
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
  // 1つ目のSTAを作る。
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return 1;
 
  {
    ATL::CComObjectStackEx<Hoge> obj;
    g1 = &obj;
    std::thread t(worker, GetCurrentThreadId());
 
    MSG msg;
    for (;;)
    {
      int ret = GetMessage(&msg, nullptr, 0, 0);
      if (ret == 0 || ret == -1)
        break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
 
    t.join();
    g1.Revoke();
  }
  CoUninitialize();
  return 0;
}

Hoge::ReadにVisual Studioでブレークポイントを貼って待ち構えます。

その状態で、まずはメインスレッド側の呼び出し履歴を表示します。DispatchMessageから最終的にHoge::Readに到達しています。

メインスレッドの呼び出し履歴(その1)

次に関数workerのスレッドも見てみましょう。こちらはcombase.dll!CCliModalLoop::BlockFnを通りMsgWaitForMultipleObjectsExで待機していることが分かります。

ワーカースレッドの呼び出し履歴(その1)

メソッド呼び出し中にメソッド呼び出し

その状態からさらに2つ目のSTAに対してメソッドを呼び出してみましょう。お手軽に、先のコードのRead関数を改造します。

ATL::CComGITPtr<ISequentialStream> g1; // 1つ目のSTAのオブジェクト
ATL::CComGITPtr<ISequentialStream> g2; // 2つ目のSTAのオブジェクト
 
// ……
 
// Hoge内
  IFACEMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbRead) override
  {
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g2.CopyTo(&s));
 
    // 2つ目のSTAのオブジェクトのメソッドを呼び出す。
    char buf;
    s->Write(&buf, sizeof buf, nullptr);
 
    return E_NOTIMPL;
  }
 
// ……
 
void worker(DWORD mainThreadId)
{
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return;
 
  try
  {
    // 2つ目のSTA上でオブジェクトを作成
    ATL::CComObjectStackEx<Hoge> obj;
    g2 = &obj;
 
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g1.CopyTo(&s));
 
    char buf;
    s->Read(&buf, sizeof buf, nullptr);
 
    g2.Revoke();
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
 
  PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
}

今度はHoge::Writeにブレークポイントを貼って止めます。worker関数(2つ目のSTA)→Hoge::Read関数(1つ目のSTA)→Hoge::Write関数(2つ目のSTA)という流れです。

こちらもメインスレッド側の呼び出し履歴を見ます。Hoge::ReadからMsgWaitForMultipleObjectsExに伸びていますね。

main-thread-2

そして、次が関数workerのスレッドです。先ほどもあったcombase.dll!CCliModalLoop::BlockFnから最終的にHoge::Writeに辿り着いています。

worker-thread-2

STAでは、このように外へのメソッド呼び出しの最中に外からのメソッド呼び出しを受け付けることがあります。これが再入 (Re-entrancy)です。

余談:メッセージフィルタ

なお、STAではメソッド・ウィンドウメッセージの受け付けをIMessageFilterCoRegisterMessageFilterである程度制御できます。

終わりに

STAはメッセージループの元で動作するシングルスレッドのアパートメントです。今回は呼び出し履歴(コールスタック)で簡易的にそのことを見てみました。

再入が起こることを忘れていると、予期せぬところでメンバ変数の値が書き換わっているように見えるという一見不思議なバグに遭遇することがあります(ありました)。もちろん、再入で呼び出されたメンバ関数で書き換えられていたのが実態です。STA固有の問題ではありませんが、忘れているとハマることがあるので気をつけましょう。

STAのメソッド呼び出しを見てみる is a post from: イグトランスの頭の中(のかけら)


MTAのメソッド呼び出しを見てみる

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの14日目の記事です。


前回(STAのメソッド呼び出しを見てみる)はSTAだったので、今度はMTAです。

MTAは再入が無い代わりに、スレッドプール(MTA所属)で実行されることがあります。前回同様にコールスタックを見ても良いのですが、こちらはそこまで念入りに確認することもないと思いました。というわけで、今回は簡単にスレッドIDの出力で見てみることにします。

前回のプログラムを少し改造しただけです。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <thread>
#include <tchar.h>
#include <windows.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
ATL::CComGITPtr<ISequentialStream> g1;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
  , public ISequentialStream
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(ISequentialStream)
  END_COM_MAP()
 
  IFACEMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbRead) override
  {
    std::cout << "Read:\t" << std::this_thread::get_id() << std::endl;
 
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(Write)(
    _In_reads_bytes_(cb) const void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbWritten) override
  {
    return E_NOTIMPL;
  }
};
 
void worker()
{
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return;
 
  std::cout << "worker:\t" << std::this_thread::get_id() << std::endl;
  try
  {
 
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g1.CopyTo(&s));
 
    char buf;
    s->Read(&buf, sizeof buf, nullptr);
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
}
 
int main()
{
  if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE)))
    return 1;
 
  std::cout << "Main:\t" << std::this_thread::get_id() << std::endl;
  {
    ATL::CComObjectStackEx<Hoge> obj;
    g1 = &obj;
    std::thread t(worker);
 
    t.join();
    g1.Revoke();
  }
  CoUninitialize();
}

main関数をMTAにし、worker関数のスレッドをSTAにしています。関数workerからs->ReadでMTAへのメソッド呼び出しを行っています。main、worker、Hoge::Readの3ヶ所でスレッドIDを出力させており、実行結果はこんな感じです。

Main:   38252
worker: 37672
Read:   25552

このように、3つとも別々のスレッドです。このように、MTAに対する他アパートメントからの呼び出しはAPIが抱えているスレッドで実行されます。


MSDNライブラリ上では、このスレッドはWindows VistaまではRPC APIのスレッドプール、Windows 7以降はデフォルトのスレッドプールを使うと書かれています。アプリケーションマニフェストのSupportedOS要素での指定で動作が切り替わる項目です。アプリケーション マニフェストの「RPC の既定のスレッド プール」欄をご覧ください。

なお、アプリケーションマニフェストのほか、IGlobalOptionsのCOMGLB_RPC_THREADPOOL_SETTINGでも設定できるようです。

MTAのメソッド呼び出しを見てみる is a post from: イグトランスの頭の中(のかけら)

アパートメントが死ぬときオブジェクトも死ぬ

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの15日目の記事です。


マーシャリングされたオブジェクトの寿命は、アパートメントに縛られます。アパートメントが終わったら、そのアパートメントにいるオブジェクトにはアクセスできなくなります。

今度のプログラムは、STAのスレッドを(1) TerminateThread (2) CoUninitializeで終了させて、オブジェクトが使えなくなることを見てみます。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <future>
#include <thread>
#include <windows.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
  , public ATL::CComCoClass<Hoge>
  , public ISequentialStream
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(ISequentialStream)
  END_COM_MAP()
 
  IFACEMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbRead) override
  {
    if (pcbRead == nullptr)
      return E_POINTER;
    *pcbRead = 0;
    return S_OK;
  }
 
  IFACEMETHOD(Write)(
    _In_reads_bytes_(cb) const void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbWritten) override
  {
    return E_NOTIMPL;
  }
};
 
std::promise<ATL::CComGITPtr<ISequentialStream>> p;
 
void worker()
{
  if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
    return;
 
  try
  {
    ATL::CComPtr<ISequentialStream> u;
    Hoge::CreateInstance(&u);
    ATL::CComGITPtr<ISequentialStream> g = u;
    p.set_value(std::move(g));
 
    MSG msg;
    for (;;)
    {
      int ret = GetMessage(&msg, nullptr, 0, 0);
      if (ret == 0 || ret == -1)
        break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
}
 
int main()
{
  if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE)))
    return 1;
 
  try
  {
    auto f = p.get_future();
    std::thread t(worker);
    auto g = f.get();
    ATL::CComPtr<ISequentialStream> s;
    ATLENSURE_SUCCEEDED(g.CopyTo(&s));
 
#if 1
    // (1)
    TerminateThread(t.native_handle(), 1);
    t.join();
#else
    // (2)
    PostThreadMessage(GetThreadId(t.native_handle()), WM_QUIT, 0, 0);
    t.join();
#endif
 
    std::cout << std::hex;
    char c;
    ULONG read;
    HRESULT hr = s->Read(&c, sizeof c, &read);
    std::cout << "" << hr << ' ' << ATL::AtlGetErrorDescription(hr) << std::endl;
  }
  catch (const std::exception& e)
  {
    std::clog << e.what() << std::endl;
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << ATL::AtlGetErrorDescription(e) << std::endl;
  }
  CoUninitialize();
  return 0;
}

関数workerからISequentialStreamを受け取り、それに対するReadをスレッドの終了より後に実行されるようにしています。そのエラーコードを出力させているのが今回の目的です。

まず、TerminateThreadのほうで実行すると、こうなります。

80010100 システム コールに失敗しました。

#if 0に書き換えて、CoUninitializeで自発的に終了するようにすると次のようになります。

80010012 システム コールに失敗しました。

これはWindows 8.1での結果です。OSなどの違いによっては異なる結果になる可能性があります。

ちなみに、WinError.hを見ると定数名が分かります。0x80010100はRPC_E_SYS_CALL_FAILED、0x80010012はRPC_E_SERVER_DIED_DNEです。


同一アパートメントでは無理ですが、アパートメントをまたぐとこのようにオブジェクトが死んだことを検知できます。そういうとき、このように0x8001xxxx系(FACILITY_RPC)のエラーコードが返ってきます。

同一プロセス内のスレッドが死ぬことはあまり想定しないかもしれませんが、相手が別プロセスとなると十分起こりえる事態です(ユーザーがタスクマネージャーなどで無差別にプロセスを強制終了できるわけですから)。

アパートメントが死ぬときオブジェクトも死ぬ is a post from: イグトランスの頭の中(のかけら)

マーシャリングできない場合の動き

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの16日目の記事です。


インタフェースをマーシャリングできるようにするには、レジストリかマニフェスト、あるいはCoRegisterPSClsid関数でプロキシ・スタブのクラスを登録しなければなりません。それらの解説はほかに譲り、今日はそのような処理を行っていないインタフェースをマーシャリングしようとするとどうなるかという話です。

早速、そういうプログラムを出します。このコードでは、マーシャリング非対応のインタフェースIHogeを作っています。IUnknownとしてマーシャリングし、QueryInterfaceでIHogeを読み出そうとしています。

#include <iostream>
#include <thread>
#include <windows.h>
#include <shlwapi.h>
 
MIDL_INTERFACE("c4230d66-43bb-42a9-9f70-f580f5b96866") IHoge : IUnknown
{
  STDMETHOD(Run)();
};
 
class Hoge : public IHoge
{
public:
  IFACEMETHOD(QueryInterface)(
    _In_ REFIID riid, _COM_Outptr_ void** ppv) override
  {
    static const QITAB qit[] = {
      {&__uuidof(IHoge), OFFSETOFCLASS(IHoge, Hoge)},
      {},
    };
    return QISearch(this, qit, riid, ppv);
  }
  IFACEMETHOD_(ULONG, AddRef)() override { return 0; }
  IFACEMETHOD_(ULONG, Release)() override { return 0; }
 
  IFACEMETHOD(Run)() override { return S_OK; }
};
 
void worker(_In_ IStream* s)
{
  if (FAILED(CoInitializeEx(nullptr,
    COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
  {
    return;
  }
 
  {
#if 1
    IUnknown* u;
    std::cout << CoGetInterfaceAndReleaseStream(
      s, IID_PPV_ARGS(&u)) << std::endl;
    IHoge* hoge;
    std::cout << u->QueryInterface(IID_PPV_ARGS(&hoge)) << std::endl;
#else
    IHoge* hoge;
    std::cout << CoGetInterfaceAndReleaseStream(
      s, IID_PPV_ARGS(&hoge)) << std::endl;
#endif
  }
  CoUninitialize();
}
 
int main()
{
  std::cout << std::hex;
  if (FAILED(CoInitializeEx(nullptr,
    COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE)))
  {
    return 1;
  }
 
  {
    Hoge hoge;
 
    IStream* s;
    std::cout << CoMarshalInterThreadInterfaceInStream(
      IID_IUnknown, &hoge, &s) << std::endl;
    std::thread(worker, s).join();
  }
  CoUninitialize();
}

これの実行結果は次のとおりです。

0
0
80004002

0x80004002はE_NOINTERFACEです。つまり、困ったことにオブジェクトがインタフェースを実装していないのと区別がつきません。そのため、ヌルポインタのはずがないと想定しているところにヌルがやってきて訳が分からない、と調べてみたら、レジストリやマニフェストにミスがあったということが時たまあります。こんな経験、私だけでしょうか?

なお、以下のように、CoMarshalInterThreadInterfaceInStreamの時点で登録していないIIDを渡すと、0x80040155 (REGDB_E_IIDNOTREG)が返ってきます。

std::cout << CoMarshalInterThreadInterfaceInStream(
  __uuidof(IHoge), &hoge, &s) << std::endl;

もちろん、CoMarshalInterThreadInterfaceInStreamに限らず、他の手段でマーシャリングする場合も同じです。以下はGlobal Interface Table(ATL::CComGITPtr使用)による例です。これもE_NOINTERFACE (0x80004002)になります。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <thread>
#include <tchar.h>
#include <windows.h>
#include <atlbase.h>
#include <atlcom.h>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
MIDL_INTERFACE("c4230d66-43bb-42a9-9f70-f580f5b96866") IHoge : IUnknown
{
  STDMETHOD(Run)();
};
 
ATL::CComGITPtr<IUnknown> g1;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
  , public IHoge
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(IHoge)
  END_COM_MAP()
 
  IFACEMETHOD(Run)() override { return S_OK; }
};
 
void worker()
{
  if (FAILED(CoInitializeEx(
    nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)))
  {
    return;
  }
 
  {
    IUnknown* u;
    std::cout << g1.CopyTo(&u) << std::endl;
    IHoge* hoge;
    std::cout << u->QueryInterface(IID_PPV_ARGS(&hoge)) << std::endl;
  }
  CoUninitialize();
}
 
int main()
{
  std::cout << std::hex;
  if (FAILED(CoInitializeEx(nullptr,
    COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE)))
  {
    return 1;
  }
 
  try
  {
    ATL::CComObjectStackEx<Hoge> hoge;
    g1 = &hoge;
    std::thread t(worker);
 
    t.join();
    g1.Revoke();
  }
  catch (const ATL::CAtlException& e)
  {
    std::cout << e.m_hr << std::endl;
  }
  CoUninitialize();
}

というわけで、マーシャリング先でのQueryInterfaceでE_NOINTERFACEが返ってきたら、マーシャリングの失敗の可能性も疑いましょうという話でした。

マーシャリングできない場合の動き is a post from: イグトランスの頭の中(のかけら)

マーシャル禁止のオブジェクト

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの17日目の記事です。


前回のマーシャリングできないインタフェースの話に続いて、今回はオブジェクト自らがマーシャリングの禁止を宣言する話です。

マーシャリングできないことを表明するインタフェースINoMarshalがWindows 8で追加されました。早速サンプルコードを出します。

#include <iostream>
#include <thread>
#include <windows.h>
#include <shlwapi.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComSingleThreadModel>
  , public ATL::CComCoClass<Hoge>
  , public ISequentialStream
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(ISequentialStream)
    COM_INTERFACE_ENTRY_IID(__uuidof (INoMarshal), ISequentialStream)
  END_COM_MAP()
 
  IFACEMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbRead) override
  {
    if (pcbRead == nullptr)
      return E_POINTER;
    *pcbRead = 0;
    return S_OK;
  }
 
  IFACEMETHOD(Write)(
    _In_reads_bytes_(cb) const void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG *pcbWritten) override
  {
    return E_NOTIMPL;
  }
};
 
int main()
{
  std::cout << std::hex << std::showbase;
  if (FAILED(CoInitializeEx(nullptr,
    COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE)))
  {
    return 1;
  }
 
  {
    ATL::CComObjectStackEx<Hoge> hoge;
 
    IStream* s;
    std::cout << CoMarshalInterThreadInterfaceInStream(
      IID_IUnknown, &hoge, &s) << std::endl;
  }
  CoUninitialize();
}

実行結果はこうでした。

0x80004021

CoMarshalInterThreadInterfaceInStreamで指定しているのは本来マーシャリングできるIUnknownにもかかわらず、このようにエラーコード0x80004021 (CO_E_NOT_SUPPORTED)になりました。


余談です。INoMarshalはメソッドを全く持たないため、C++クラスHogeの基底クラスに入れずに実装しました。COM_INTERFACE_ENTRY_IIDを使っています。

COM_INTERFACE_ENTRY_IID(__uuidof (INoMarshal), ISequentialStream)

こうすると、QueryInterfaceで__uuidof (INoMarshal)が来たとき、static_cast<ISequentialStream*>(this)相当を返すという意味合いになります。INoMarshalはIUnknownと全く同じ構造のvtblになるため、ISequentialStreamに限らずIUnknownから派生する任意のインタフェースを指定して構いません。


なお、INoMarshalはおそらくWinRT API (Windows Runtime)用に追加されたものですが、このようにWinRT APIと無関係に使用できます。

マーシャル禁止のオブジェクト is a post from: イグトランスの頭の中(のかけら)

今いるアパートメントの種類を知りたい

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの18日目の記事です。


今日は、現在実行中のスレッドのアパートメントの種類を知る方法です。

まず、Windows 7からCoGetApartmentType関数があります。それとATLにATL::AtlGetApartmentType関数があります。

#include <iostream>
#include <windows.h>
#include <shlwapi.h>
#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <atlbase.h>
#include <atlcom.h>
 
int main()
{
  std::cout << std::boolalpha;
#if 1
  DWORD coInit = COINIT_MULTITHREADED;
#else
  DWORD coInit = COINIT_APARTMENTTHREADED;
#endif
  if (FAILED(CoInitializeEx(nullptr, coInit | COINIT_DISABLE_OLE1DDE)))
  {
    return 1;
  }
 
  APTTYPE aptType;
  APTTYPEQUALIFIER aptQualifier;
  if (SUCCEEDED(CoGetApartmentType(&aptType, &aptQualifier)))
  {
    std::cout << "CoGetApartmentType" << std::endl;
    std::cout << "Is STA: "
      << (aptType == APTTYPE_STA || aptType == APTTYPE_MAINSTA) << std::endl;
    std::cout << "Is MTA: " << (aptType == APTTYPE_MTA) << std::endl;
  }
  std::cout << "--------" << std::endl;
  DWORD apartmentType;
  if (ATL::AtlGetApartmentType(&apartmentType))
  {
    std::cout << "ATL::AtlGetApartmentType" << std::endl;
    std::cout << "Is STA: "
      << (apartmentType == COINIT_APARTMENTTHREADED) << std::endl;
    std::cout << "Is MTA: "
      << (apartmentType == COINIT_MULTITHREADED) << std::endl;
  }
 
  CoUninitialize();
}
<pre>
 
実行結果は次のとおりになります。
 
<pre>
CoGetApartmentType
Is STA: false
Is MTA: true
--------
ATL::AtlGetApartmentType
Is STA: false
Is MTA: true

本Advent Calendarで珍しく個別のライブラリであるATLの関数を取り上げた理由は、CoInitialize関数の挙動をうまく使っているので実装を紹介したいと思ったからです。以下に引用します。

// Returns the apartment type that the current thread is in. false is returned
// if the thread isn't in an apartment.
inline _Success_(return != false)
bool AtlGetApartmentType(_Out_ DWORD* pApartmentType)
{
    HRESULT hr = CoInitialize(NULL);
    if (SUCCEEDED(hr))
        CoUninitialize();
 
    if (hr == S_FALSE)
    {
        *pApartmentType = COINIT_APARTMENTTHREADED;
        return true;
    }
    else if (hr == RPC_E_CHANGED_MODE)
    {
        *pApartmentType = COINIT_MULTITHREADED;
        return true;
    }
 
    return false;
}

CoGetApartmentTypeと違ってNAなどの判定ができませんが、STAとMTA(と未初期化)の区別が付けば十分な場合はこれで満たせます。もちろん、Windows 7未満で動作するのも利点です。

今いるアパートメントの種類を知りたい is a post from: イグトランスの頭の中(のかけら)

Agile Objectまたはフリースレッドマーシャラーの集成

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの19日目の記事です。


COMインタフェースを実装するクラスを書いていると、どのアパートメントからでもマーシャリングせず直に呼び出しを受け付けられるものになることがあります。あるいは、マーシャリングが邪魔だと思うことがあります。COMには、そんなオブジェクトを作成する手段が用意されています。

このようなオブジェクトの概念はフリースレッドマーシャラー (FTM)として以前から存在していましたが、Windows Runtime APIでこの概念にAgile Objectと名前が与えられました。最近では、Accessing Interfaces Across Apartmentsなど、MSDNライブラリの従来のCOM関係の記事でもこの言葉が使われているようです。

サンプルコード

その方法は2つあり、従来用意されてきたフリースレッドマーシャラー (FTM)の集成 (Aggregation)と、Windows 8で新しく追加されたIAgileObjectです。両者ともに実装するのが良いでしょう。

#include <atomic>
#include <iostream>
#include <thread>
#include <windows.h>
#include <comdef.h>
 
MIDL_INTERFACE("c4230d66-43bb-42a9-9f70-f580f5b96866") IHoge : IUnknown
{
  STDMETHOD(Run)();
};
 
_COM_SMARTPTR_TYPEDEF(IHoge, __uuidof (IHoge));
 
class Hoge : public IHoge
{
  Hoge()
  {
    auto hr = CoCreateFreeThreadedMarshaler(this, &m_marshaler);
    if (FAILED(hr))
      throw _com_error(hr);
  }
 
public:
  static IHogePtr Create() { return new Hoge; }
 
  IFACEMETHOD(QueryInterface)(REFIID riid, void** ppv) override
  {
    if (ppv == nullptr)
    {
      return E_POINTER;
    }
    // IAgileObjectは追加のメソッドを持たないので
    // IUnknownと同じ値を返せば良い。
    if (riid == __uuidof (IHoge)
      || riid == __uuidof (IUnknown)
      || riid == __uuidof (IAgileObject))
    {
      *ppv = static_cast<IHoge*>(this);
      AddRef();
      return S_OK;
    }
    else if (riid == __uuidof (IMarshal))
    {
      // フリースレッドマーシャラに丸投げ。
      return m_marshaler->QueryInterface(riid, ppv);
    }
    *ppv = nullptr;
    return E_NOINTERFACE;
  }
 
  IFACEMETHOD_(ULONG, AddRef)() override
  {
    return ++m_count;
  }
 
  IFACEMETHOD_(ULONG, Release)() override
  {
    auto count = --m_count;
    if (count == 0)
    {
      delete this;
    }
    return count;
  }
 
  IFACEMETHOD(Run)()
  {
    std::cout << "Run: " << std::this_thread::get_id() << std::endl;
    return S_OK;
  }
 
private:
  IUnknownPtr m_marshaler;
  std::atomic<ULONG> m_count{};
};
 
void worker(_In_ IStream* s)
{
  try
  {
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));
 
    IHogePtr hoge;
    _com_util::CheckError(CoGetInterfaceAndReleaseStream(
      s, IID_PPV_ARGS(&hoge)));
 
    std::cout << "hoge in worker: " << hoge.GetInterfacePtr() << std::endl;
    std::cout << "worker thread: " << std::this_thread::get_id() << std::endl;
 
    hoge->Run();
  }
  catch(const _com_error& e)
  {
    std::cout << std::hex << std::showbase << e.Error() << std::endl;
  }
  CoUninitialize();
}
 
int main()
{
  try
  {
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
 
    auto hoge = Hoge::Create();
    std::cout << "hoge in main: " << hoge.GetInterfacePtr() << std::endl;
    IStream* s;
    _com_util::CheckError(
      CoMarshalInterThreadInterfaceInStream(__uuidof (IUnknown), hoge, &s));
    std::thread(worker, s).join();
  }
  catch(const _com_error& e)
  {
    std::cout << std::hex << std::showbase << e.Error() << std::endl;
  }
 
  CoUninitialize();
}

フリースレッドマーシャラの作成にはCoCreateFreeThreadedMarshaler関数を使います。この実引数には、CoCreateInstandeの2番目の実引数と同じく外側のオブジェクトへのポインタを渡します。

QueryInterfaceでは、この2つを行います。

  • IAgileObjectの要求に対して自身のオブジェクトを返します。
  • IMarshalの要求に対してフリースレッドマーシャラを返します。

IAgileObjectはメソッドを持たないインタフェースです。そのため、INoMarshalの場合(参考:前々回のマーシャル禁止のオブジェクト)と同じように、C++基底クラスに入れずに実装しています。

実行結果は以下のようになります。

hoge in main: 011D8720
hoge in worker: 011D8720
worker thread: 11092
Run: 11092

この結果では、以下のことが示されています。

  • マーシャリングに対応させていないインタフェースIHogeでも別アパートメントに持っていけている(マーシャリングできない場合の動きで失敗したのとは対照的に)
  • ポインタの値がアパートメントをまたいでも元と同じ
  • Runメソッドが呼び出し元のworkerスレッドで実行されている

なお、その性質から、今回のようにAgile Objectだと分かっているならわざわざCoGetInterfaceAndReleaseStreamなどでマーシャリングする必要はありません。直接ポインタをやりとりして平気です。

Agile Object実装時の注意

マーシャリングのことを考えなくて済むのが便利なAgile Objectですが、そのことに起因する注意点もあります。

メソッドが実行されるアパートメントが毎回異なる可能性があるため、C++などの言語でクラスとして実装する場合、メンバー変数にCOMインタフェースへのポインタを持つには注意が必要です。

  • 基本的には、適切にマーシャリングする必要があります。ATL::CComGITPtrPlatform::Agile (C++/CX)などのマーシャリング処理をラップしたクラステンプレートが便利です。
  • 例外として、ポインタの指すオブジェクトがAgile Objectであればそのままポインタを保持して問題ありません。

従来のCOMでは、マーシャリングが必要なオブジェクトの多いので前者の注意点が目立ちます。ところが、Windows Runtime APIでは後述するようにAgile Objectが大半であるため、ほとんど気にする必要がなくなってしまいました。

参考:

Windows Runtime APIにおけるAgile Object

今日の記事では、たびたびWindows Runtime API(WinRT API: Windowsストア・Phoneアプリ向けAPI)の言葉が登場しています。というのも、WinRT APIではAgile Objectが第一級に抜擢されたためです。

まず、従来のCOMにおいてFTMはやや特殊な位置づけでしたが、WinRT APIではクラスの実装形態の1つとして明確に位置づけられました。メタデータ (WinMD)上で、クラスに対する属性として表現されます。MarshalingBehaviorAttribute属性に以下のMarshalingType列挙体が使用されます。

  1. None
  2. Agile
  3. Standard

Noneがマーシャリング禁止 (INoMarshal)、Agileがただ今扱っているAgile Object、Standardが従来のCOM同様のマーシャリングとなります。

MSDNライブラリのThreading and Marshaling (C++/CX)によれば、Windows Runtime APIのクラスのうち90%はAgile Objectだそうです。ASTAのUIスレッドが1つのみ、それ以外はすべてMTAであることも併せて考えれば、Windowsストアアプリにおいては概ね以下のように理解すれば十分ということになります。

  • UI関係のオブジェクトがUIスレッドへマーシャリングされる
  • それ以外のオブジェクトの実行スレッドについてCOMおよびWinRT APIは関与しない

このように、Windowsストア・Phoneアプリ作成においては、アパートメントやマーシャリングなどCOMのことをあまり知らなくてもいいようにしようと工夫していることが窺えます。そのため、Agile Objectが第一級と表現したわけです。

終わりに

今回WinRT APIの連呼になりましたが、Agile Object自体は非WinRT APIアプリであるデスクトップアプリでも便利に使えます。

WinRT APIのように、UI関係以外は積極的にAgile Object化して、マーシャリングを考えずに済むようにするのが良いと思います。もちろん、ブロック処理が含まれていればちょっと考え物ですね。Agile Object化をやめてMTAオブジェクトにするか、非同期処理にしてAgile Object化するか、時と場合によります。

以上、利用頻度の低いわけがない(と私は思っている)機能にもかかわらず、インターネット上であまり話を見ないAgile Object(フリースレッドマーシャラー)の話でした。

Agile Objectまたはフリースレッドマーシャラーの集成 is a post from: イグトランスの頭の中(のかけら)

OBJREFモニカー

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの20日目の記事です。


OBJREFモニカーはオブジェクトへの参照を扱うモニカーです。文字列で表現できるため、プロセス間での受け渡しに使用できます。

サンプルプログラム: C++からVBScriptへ

今回のサンプルでは、C++で実装したオブジェクトをVBScriptから使うというプログラムにしています。system関数でVBScriptを起動し、コマンドライン引数でモニカーの文字列を渡しています。

まず、オブジェクトを作るC++側のコードです。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <sstream>
#include <stdlib.h>
#include <windows.h>
#include <comdef.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
#include <boost/scope_exit.hpp>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComMultiThreadModel>
  , public ATL::CComCoClass<Hoge>
  , public IDispatch
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
 
  IFACEMETHOD(GetTypeInfoCount)(
    __RPC__out UINT*) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(GetTypeInfo)(
    UINT,
    LCID,
    __RPC__deref_out_opt ITypeInfo**) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(GetIDsOfNames)(
    __RPC__in REFIID,
    /*__RPC__in_ecount_full(cNames)*/ LPOLESTR*,
    __RPC__in_range(0,16384) UINT,
    LCID,
    /*__RPC__out_ecount_full(cNames)*/ DISPID*) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(Invoke)(
    _In_ DISPID dispIdMember,
    _In_ REFIID riid,
    _In_ LCID,
    _In_ WORD flags,
    _In_ DISPPARAMS *pDispParams,
    _Out_opt_ VARIANT* pVarResult,
    _Out_opt_ EXCEPINFO*,
    _Out_opt_ UINT*) override
  {
    if (dispIdMember != DISPID_VALUE || flags != DISPATCH_METHOD)
      return DISP_E_MEMBERNOTFOUND;
    if (riid != IID_NULL)
      return DISP_E_UNKNOWNINTERFACE;
    if (pDispParams->cArgs != 0)
      return DISP_E_BADPARAMCOUNT;
    if (pDispParams->cNamedArgs != 0)
      return DISP_E_NONAMEDARGS;
    VariantInit(pVarResult);
    std::cout << "Hoge::Invoke" << std::endl;
    return S_OK;
  }
};
 
int main()
{
  try
  {
    ATLENSURE_SUCCEEDED(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
 
    // OBJREFモニカーを作る。
    IDispatchPtr hoge;
    ATLENSURE_SUCCEEDED(Hoge::CreateInstance(&hoge));
    IMonikerPtr moniker;
    ATLENSURE_SUCCEEDED(CreateObjrefMoniker(hoge, &moniker));
    hoge = nullptr;
 
    // 文字列を取り出す。
    IBindCtxPtr bc;
    ATLENSURE_SUCCEEDED(CreateBindCtx(0, &bc));
    LPOLESTR monikerStr;
    ATLENSURE_SUCCEEDED(moniker->GetDisplayName(bc, nullptr, &monikerStr));
    BOOST_SCOPE_EXIT_ALL(&monikerStr) { CoTaskMemFree(monikerStr); };
 
    // 子プロセスを作る。
    WCHAR sysDir[MAX_PATH] = {};
    GetSystemDirectoryW(sysDir, ARRAYSIZE(sysDir));
    std::wostringstream ss;
    ss << sysDir << L"\\cscript.exe //nologo objref.vbs /str:" << monikerStr;
    std::wcout << L"main: " << ss.str() << std::endl;
    _wsystem(ss.str().c_str());
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << std::hex << std::showbase;
    std::clog << e.m_hr << ' ' << ATL::AtlGetErrorDescription(e) << std::endl;
  }
 
  CoUninitialize();
}

CreateObjrefMoniker関数でIMonikerが得られ、それに対してIMoniker::GetDisplayName関数を呼び出すと文字列表現が得られます。

上のコード内から_wsystem関数で起動されるVBScriptのコードです。VBScriptでは、GetObjectでモニカーの文字列からオブジェクトを得られます。

objref = WScript.Arguments.Named("str")
WScript.Echo "--------"
WScript.Echo "objref.vbs: " & objref
Set hoge = GetObject(objref)
hoge()

実行すると以下のような感じになります。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke

main:とobjref.vbs:の行は、確認用にC++側とVBScript側で出力させているものです。最終的に、C++で定義したHogeクラスのInvokeメンバ関数が呼び出されています。

今回の例はVBScriptだったのでIDispatchにしましたが、もちろんマーシャリング可能なインタフェースなら何でも可能です。プロセス間だと、たとえ双方がMTAでもマーシャリングが必要です。

モニカーからオブジェクトを得る関数など

プラットフォームごとのモニカーからオブジェクトを取得する関数などです。モニカーからのオブジェクトの取得は、多くの環境で関数・メソッド1つでできるようになっています。

その他の手法

プロセス間でオブジェクトを受け渡す方法はほかにもあります。OBJREFモニカーとの比較として、いくつか紹介します。

CoMarshalInterface関数

文字列に拘らなければ、CoMarshalInterface関数でも同様のことが可能なはずです。たとえば、試していませんが標準入出力経由で受け渡す方法があるでしょう。

バイナリではない(テキストだと保証されている)点ではOBJREFモニカーに軍配が上がります。一方、引数でマーシャルする範囲(スレッド・プロセス・マシン間)を指定できるのはCoMarshalInterfaceのほうが優れています。

Running Object Table

VBScriptのGetObject関数などでオブジェクトを取得できるようにしたいのであれば、Running Object Table (IRunningObjectTable)で任意のモニカーを登録する方法もあります。

これは用途によって使い分けるところだと思います。上記サンプルコードの用途で使うならば、CreateObjrefMonikerとGetDisplayNameよりは手順が多いので面倒です。

ポインターモニカー

オブジェクトへの参照を表現するモニカーという点では、ポインターモニカー (Pointer Monikers, CreatePointerMoniker)もあります。文字列化できないのがOBJREFモニカーとの違いです。

DCOMによるリモートアクセス

ところで、MSDNライブラリのCreateObjrefMonikerの説明にあるように、実はコンピュータ越しでも使えるようです。もちろん、デフォルトではWindowsのセキュリティが効いているはずです。

これについては私がまだ分かっていないので、次回以降に取り上げるかもしれません。

おわりに

今回のサンプルコード、CoCreateInstance関数と対照的だと考えています。CoCreateInstanceでは、クライアントからサーバーに要求を出すことでプロセス間通信が始まります。一方、今回のサンプルコードは、オブジェクトを持っている側からクライアントとなる子プロセスを作り、そこにオブジェクトへの参照を引き渡しています。

実は、こういうことに使える手法がないかと探しているときにOBJREFモニカーを見つけました。

というわけで、今回はOBJREFモニカーとそれを使った子プロセスへのオブジェクト引き渡しがテーマでした。ここまで書いた今、ちょっと欲張りな内容だったと反省しています。

OBJREFモニカー is a post from: イグトランスの頭の中(のかけら)


スタブオブジェクトの寿命とそのロック

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの21日目の記事です。まだネタは尽きていないのに、日数の残りが少なくて、少し戸惑っています。


今回は、OBJREFモニカーの寿命について調べてみました。すると結局、タイトルにあるようにスタブオブジェクトの寿命であることに気付いたという話です。

サンプルコードは前回からの改変で話を進めますが、そんなわけで、OBJREFモニカーだけでなく、特に後半はアパートメントをまたぐ場合すべてに当てはまる話です。

OBJREFモニカーの寿命

OBJREFモニカーおよびその文字列が有効な期間は、OBJREFが参照するスタブオブジェクトの寿命と同じです。つまり、他アパートメントの側でReleaseして参照カウントが0になったら終わってしまう、というのがデフォルトの挙動です。

まずはその挙動を確かめてみます。前回のサンプルコードのVBScript側を以下のように2回GetObjectするように変えます。途中でNothingを代入して参照を消しているのがミソです。

objref = WScript.Arguments.Named("str")
WScript.Echo "--------"
WScript.Echo "objref.vbs: " & objref
 
WScript.Echo "--------"
Set hoge = GetObject(objref)
hoge()
Set hoge = Nothing
 
WScript.Echo "--------"
Set hoge = GetObject(objref)
hoge()

すると実行結果はこうなります。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
T:\objref.vbs(9, 1) (null): オブジェクトは登録されていません

真ん中のSet hoge = Nothingの行で、スタブオブジェクトが用済みとなったため捨てられてしまい、OBJREFも無効になります。そのため、2回目のGetObjectは失敗に終わります。大本のオブジェクト自体の参照カウントがたとえまだ0でなかったとしても、結果は同じです。

そのため、試しに真ん中のSet hgoe = Nothingの行を無くすと、以下のように2回目のGetObjectも成功することが分かります。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
Hoge::Invoke

スタブを生かし続ける

上記の挙動では困る、すなわち1度ならず何度でもOBJREFモニカーを使えるようにしたい場合もあります。

その方法は2つあります。1つはIExternalConnectionを実装する方法、もう1つはCoLockObjectExternal関数を利用する方法です。今回はCoLockObjectExternal関数を使ってみます。

前回のC++側コードを少し変更し、CreateObjrefMoniker関数呼び出しの付近にCoLockObjectExternal関数を追加します。CreateObjrefMonikerとの順番はどちらが先でも構いません。

IDispatchPtr hoge;
ATLENSURE_SUCCEEDED(Hoge::CreateInstance(&hoge));
IMonikerPtr moniker;
ATLENSURE_SUCCEEDED(CreateObjrefMoniker(hoge, &moniker));
ATLENSURE_SUCCEEDED(CoLockObjectExternal(hoge, TRUE, FALSE));

VBScript側のSet hoge = Nothingの行を元に戻して実行した結果です。以下のように2回目のGetObjectも成功します。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
Hoge::Invoke

スタブが捨てられて、また作られることの確認

先ほど「スタブオブジェクトが捨てられる」と書きました。それを確認してみます。もう1度OBJREFモニカーを作り、それが別の文字列になっていることでもって確かめられます。

前回のコードのmain関数部分を以下のコードに置き換えます。OBJREFモニカーの文字列を作る処理を別の関数に切り出しました。

std::wstring GetObjrefMonikerDisplayString(IUnknown* obj)
{
  IMonikerPtr moniker;
  ATLENSURE_SUCCEEDED(CreateObjrefMoniker(obj, &moniker));
  IBindCtxPtr bc;
  ATLENSURE_SUCCEEDED(CreateBindCtx(0, &bc));
  LPOLESTR monikerStr;
  ATLENSURE_SUCCEEDED(moniker->GetDisplayName(bc, nullptr, &monikerStr));
  BOOST_SCOPE_EXIT_ALL(&monikerStr) { CoTaskMemFree(monikerStr); };
  return monikerStr;
}
 
int main()
{
  try
  {
    ATLENSURE_SUCCEEDED(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
 
    IDispatchPtr hoge;
    ATLENSURE_SUCCEEDED(Hoge::CreateInstance(&hoge));
 
    std::wstring monikerStr1 = GetObjrefMonikerDisplayString(hoge);
    ATLENSURE_SUCCEEDED(CoLockObjectExternal(hoge, TRUE, FALSE));
 
    WCHAR sysDir[MAX_PATH] = {};
    GetSystemDirectoryW(sysDir, ARRAYSIZE(sysDir));
    std::wostringstream ss;
    ss << sysDir << L"\\cscript.exe //nologo objref.vbs /str:" << monikerStr1;
    std::wcout << L"main: " << ss.str() << std::endl;
    _wsystem(ss.str().c_str());
 
    std::wstring monikerStr2 = GetObjrefMonikerDisplayString(hoge);
    std::wcout << L"main 2: " << monikerStr2 << std::endl;
    std::wcout << L"--------" << std::endl;
    std::wcout << std::boolalpha << (monikerStr1 == monikerStr2) << std::endl;
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << std::hex << std::showbase;
    std::clog << e.m_hr << ' ' << ATL::AtlGetErrorDescription(e) << std::endl;
  }
 
  CoUninitialize();
}

OBJREFモニカーを作る→VBScript側でGetObjectしてスタブを破棄→OBJREFモニカーを作る、という順で実行されます。最後に2つのOBJREFモニカーの文字列を比較した結果を出力させています。

実行すると次のようになります。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
T:\objref.vbs(11, 1) (null): オブジェクトは登録されていません
main 2: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
false

最後の出力(monikerStr1 == monikerStr2)がfalseとなっています。

このプログラムも、最初のGetObjrefMonikerDisplayString (CreateObjrefMoniker)関数呼び出しの前後にCoLockObjectExternal関数呼び出しを追加した場合を試します。

main: C:\Windows\system32\cscript.exe //nologo objref.vbs /str:objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
objref.vbs: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
--------
Hoge::Invoke
--------
Hoge::Invoke
main 2: objref:TUVPVwEAAAA(中略)//AAAOAP//AAAAAA==:
true

見事、trueになります。同じオブジェクトに対するスタブが存在するため、2度目のCreateObjrefMonikerでも同じOBJREFが使用されます。そのため、GetDisplayNameでも同じ文字列が得られました。

おまけ:IExternalConnection

今回IExternalConnectionは取り上げませんでした。そのため、IExternalConnectionを扱っている日本語ウェブページのリンクを並べておきます。私も、幾度となく参照しました。

取り上げなかった理由を強いて挙げるとすれば、私のIExternalConnectionを使う理由が外部からの切断(最終Release)の検知であることが多いからかなと思います。

ロックしたオブジェクトの寿命を考える

CoLockObjectExternalやIExternalConnectionでロックすると、他アパートメントからいくらReleaseしてもスタブオブジェクトから大元のオブジェクトへの参照を保持し続けます。オブジェクトの参照カウントはいったいいつ0になって、いつ削除されるのでしょう。

答えは、「自ら決める」です。たとえば、GUIアプリケーションで主たるウィンドウが閉じられたり、Windowsサービスアプリケーションで終了が要求されたりしたらオブジェクトも利用不可能にする方式が考えられます。他アパートメントの利用状況とは一切関係なく、自らの都合で切断するのです

具体的には、CoUninitialize()でアパートメントごと終了させたり、CoDisconnectObject関数で個々のオブジェクトごとに切断を言い渡したりできます。また、CoLockObjectExternal関数の2番目の引数にFALSEを渡すことで、ロックを解除する(スタブは必要に応じて生き残るかもしれない)方法もあります。

まとめ

OBJREFモニカーはOBJREFに連動して、プロキシオブジェクト(他アパートメント)からの参照次第で寿命が決まります。それが困るならCoLockObjectExternalまたはIExternalConnectionでスタブオブジェクトをロックできます。自オブジェクトあるいは自アプリの都合で寿命を決めたい場合にロックは有用です。

マーシャリングのことなのでプロセス間・プロセス内関係なく当てはまる話ですが、プロセス間でないと遭遇しにくい問題という印象があります。やはり、同一プロセス内、CoMarshalInterThreadInterfaceInStreamやGlobal Interface Tableを使っている限り、スタブの寿命を気にする事態に直面することはなかなかありません。

スタブオブジェクトの寿命とそのロック is a post from: イグトランスの頭の中(のかけら)

OBJREFモニカーによるコンピュータ間の通信を試す

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの22日目の記事です。


OBJREFモニカーの回で、コンピュータ間のマーシャリングもできるらしいことを書きました。今回はそれを試してみることにしました。というわけで、結局DCOMに足を突っ込んでしまいました。

2台のWindowsマシンを用意します。接続を待ち受けるC++側と接続しに行くVBScript側です。以下の準備を行っておきます。

  • 双方を同じLANに接続する。
  • 双方で同じ名前・パスワードのユーザーを作り、そこでプログラムを実行する。ただし、デフォルトではパスワードが空だとうまくいきません。
  • C++プログラムを実行する側では、Windowsファイアウォールなどを切っておくか、除外設定を行う。
  • もしDCOMを無効にしていたら、有効に戻す。参考:Geekなぺーじ:DCOM(分散COM)を無効にする

C++側プログラムは以下です。main関数以外は前々回(OBJREFモニカー)とほぼ同じです。OBJREFモニカーの文字列を標準出力に表示して、Enterが入力されるまで待ち受ける、という内容です。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <sstream>
#include <stdlib.h>
#include <windows.h>
#include <comdef.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
#include <boost/scope_exit.hpp>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComMultiThreadModel>
  , public ATL::CComCoClass<Hoge>
  , public IDispatch
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
 
  IFACEMETHOD(GetTypeInfoCount)(
    __RPC__out UINT*) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(GetTypeInfo)(
    UINT,
    LCID,
    __RPC__deref_out_opt ITypeInfo**) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(GetIDsOfNames)(
    __RPC__in REFIID,
    /*__RPC__in_ecount_full(cNames)*/ LPOLESTR*,
    __RPC__in_range(0,16384) UINT,
    LCID,
    /*__RPC__out_ecount_full(cNames)*/ DISPID*) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(Invoke)(
    _In_ DISPID dispIdMember,
    _In_ REFIID riid,
    _In_ LCID,
    _In_ WORD flags,
    _In_ DISPPARAMS *pDispParams,
    _Out_opt_ VARIANT* pVarResult,
    _Out_opt_ EXCEPINFO*,
    _Out_opt_ UINT*) override
  {
    if (dispIdMember != DISPID_VALUE || flags != DISPATCH_METHOD)
      return DISP_E_MEMBERNOTFOUND;
    if (riid != IID_NULL)
      return DISP_E_UNKNOWNINTERFACE;
    if (pDispParams->cArgs != 0)
      return DISP_E_BADPARAMCOUNT;
    if (pDispParams->cNamedArgs != 0)
      return DISP_E_NONAMEDARGS;
    VariantInit(pVarResult);
    std::cout << "Hoge::Invoke" << std::endl;
    return S_OK;
  }
};
 
std::wstring GetObjrefMonikerDisplayString(IUnknown* obj)
{
  IMonikerPtr moniker;
  ATLENSURE_SUCCEEDED(CreateObjrefMoniker(obj, &moniker));
  IBindCtxPtr bc;
  ATLENSURE_SUCCEEDED(CreateBindCtx(0, &bc));
  LPOLESTR monikerStr;
  ATLENSURE_SUCCEEDED(moniker->GetDisplayName(bc, nullptr, &monikerStr));
  BOOST_SCOPE_EXIT_ALL(&monikerStr) { CoTaskMemFree(monikerStr); };
  return monikerStr;
}
 
int main()
{
  try
  {
    ATLENSURE_SUCCEEDED(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
 
    IDispatchPtr hoge;
    ATLENSURE_SUCCEEDED(Hoge::CreateInstance(&hoge));
 
    std::wstring monikerStr = GetObjrefMonikerDisplayString(hoge);
    ATLENSURE_SUCCEEDED(CoLockObjectExternal(hoge, TRUE, FALSE));
 
    std::wcout << monikerStr << std::endl;
 
    std::cin.get();
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << std::hex << std::showbase;
    std::clog << e.m_hr << ' ' << ATL::AtlGetErrorDescription(e) << std::endl;
  }
 
  CoUninitialize();
}

VBScript側は以下です。前々回と変わっていません。cscript objref.vbs /str:モニカーのように、コマンドライン引数でモニカーを指定します。

objref = WScript.Arguments.Named("str")
WScript.Echo "--------"
WScript.Echo "objref.vbs: " & objref
 
WScript.Echo "--------"
Set hoge = GetObject(objref)
hoge()

これで動かしてみました。まず、1台目のコンピュータでC++プログラムを起動しておきます。次に、画面に表示されたOBJREFモニカーを2台目のコンピュータで、objref.vbsのコマンドライン引数に書き写し、起動するという手順です。

すると、無事(?)C++側でHoge::Invokeが出力され、Hoge::Invoke関数が呼び出されたことが確認できました。CoLockObjectExternalしているため、VBScriptを実行するたび、何度でも動きます。


認証が掛かっているとは言え、これはちょっとやめてほしい場合もあると思います。というわけで、次回はCOMサーバープロセスにおいて他コンピュータからのアクセスを禁じる話の予定です。

OBJREFモニカーによるコンピュータ間の通信を試す is a post from: イグトランスの頭の中(のかけら)

アクセス許可の指定でリモートアクセスを禁じる

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの23日目の記事です。


前回の続きです。前回(OBJREFモニカーによるコンピュータ間の通信を試す)では、OBJREFモニカーでリモート(他コンピュータ)からのアクセスができることが分かりました。今回はそれをアクセス許可の設定で禁止する話です。

なお、無条件に以下のようなリモートアクセス禁止の処理を入れるべきとは考えていません。こういうことも可能だという紹介です。


あるプロセス内のオブジェクトへのアクセス許可はDACLで制御できます。ファイルやレジストリ、各種カーネルオブジェクトと同じですね。アクセスマスクは次の5つですが、現在意味があるは下4つです。

  • COM_RIGHTS_EXECUTE
  • COM_RIGHTS_EXECUTE_LOCAL
  • COM_RIGHTS_EXECUTE_REMOTE
  • COM_RIGHTS_ACTIVATE_LOCAL
  • COM_RIGHTS_ACTIVATE_REMOTE

COM_RIGHTS_EXECUTEは過去のもので現在無意味、EXECUTE_*系はプロセスが起動していない場合に起動する許可、ACTIVATEは起動しているプロセスに接続する許可です。

この設定を行うにはCoInitializeSecurity関数を使います。アクセス許可の指定方法は3種類ありますが、今回ではセキュリティデスクリプターを使いました。以下、サンプルプログラムです。前回のコードにCoInitializeSecurity関数の呼び出しを追加しただけです。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <sstream>
#include <stdlib.h>
#include <windows.h>
#include <aclapi.h>
#include <comdef.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
#include <boost/scope_exit.hpp>
 
class Module : public ATL::CAtlExeModuleT<Module> {};
Module module;
 
class ATL_NO_VTABLE Hoge
  : public ATL::CComObjectRootEx<ATL::CComMultiThreadModel>
  , public ATL::CComCoClass<Hoge>
  , public IDispatch
{
public:
  BEGIN_COM_MAP(Hoge)
    COM_INTERFACE_ENTRY(IDispatch)
  END_COM_MAP()
 
  IFACEMETHOD(GetTypeInfoCount)(
    __RPC__out UINT*) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(GetTypeInfo)(
    UINT,
    LCID,
    __RPC__deref_out_opt ITypeInfo**) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(GetIDsOfNames)(
    __RPC__in REFIID,
    /*__RPC__in_ecount_full(cNames)*/ LPOLESTR*,
    __RPC__in_range(0,16384) UINT,
    LCID,
    /*__RPC__out_ecount_full(cNames)*/ DISPID*) override
  {
    return E_NOTIMPL;
  }
 
  IFACEMETHOD(Invoke)(
    _In_ DISPID dispIdMember,
    _In_ REFIID riid,
    _In_ LCID,
    _In_ WORD flags,
    _In_ DISPPARAMS *pDispParams,
    _Out_opt_ VARIANT* pVarResult,
    _Out_opt_ EXCEPINFO*,
    _Out_opt_ UINT*) override
  {
    if (dispIdMember != DISPID_VALUE || flags != DISPATCH_METHOD)
      return DISP_E_MEMBERNOTFOUND;
    if (riid != IID_NULL)
      return DISP_E_UNKNOWNINTERFACE;
    if (pDispParams->cArgs != 0)
      return DISP_E_BADPARAMCOUNT;
    if (pDispParams->cNamedArgs != 0)
      return DISP_E_NONAMEDARGS;
    VariantInit(pVarResult);
    std::cout << "Hoge::Invoke" << std::endl;
    return S_OK;
  }
};
 
std::wstring GetObjrefMonikerDisplayString(IUnknown* obj)
{
  IMonikerPtr moniker;
  ATLENSURE_SUCCEEDED(CreateObjrefMoniker(obj, &moniker));
  IBindCtxPtr bc;
  ATLENSURE_SUCCEEDED(CreateBindCtx(0, &bc));
  LPOLESTR monikerStr;
  ATLENSURE_SUCCEEDED(moniker->GetDisplayName(bc, nullptr, &monikerStr));
  BOOST_SCOPE_EXIT_ALL(&monikerStr) { CoTaskMemFree(monikerStr); };
  return monikerStr;
}
 
int main()
{
  try
  {
    ATLENSURE_SUCCEEDED(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
 
    // 前回からの追加分ここから
 
    union
    {
      char buffer[SECURITY_MAX_SID_SIZE];
      SID sid;
    } networkLogon = {};
    DWORD sidNetworkSize = sizeof networkLogon;
    if (!CreateWellKnownSid(
      WinNetworkSid, nullptr, &networkLogon.sid, &sidNetworkSize))
    {
      ATL::AtlThrowLastWin32();
    }
    // COM_RIGHTS_EXECUTEは常にORする必要がある。
    EXPLICIT_ACCESS ea[] =
    {
      {
        /*.grfAccessPermissions =*/ COM_RIGHTS_EXECUTE
          | COM_RIGHTS_ACTIVATE_LOCAL,
        /*.grfAccessMode =*/ SET_ACCESS,
        /*.grfInheritance =*/ NO_INHERITANCE,
        /*.Trustee =*/ {
          /*.pMultipleTrustee =*/ nullptr,
          /*.MultipleTrusteeOperation =*/ NO_MULTIPLE_TRUSTEE,
          /*.TrusteeForm =*/ TRUSTEE_IS_NAME,
          /*.TrusteeType =*/ TRUSTEE_IS_UNKNOWN,
          /*.ptstrName =*/ TEXT("EVERYONE"),
        },
      },
#if 1
      {
        /*.grfAccessPermissions =*/ COM_RIGHTS_EXECUTE
          | COM_RIGHTS_EXECUTE_LOCAL | COM_RIGHTS_EXECUTE_REMOTE
          | COM_RIGHTS_ACTIVATE_LOCAL | COM_RIGHTS_ACTIVATE_REMOTE,
        /*.grfAccessMode =*/ DENY_ACCESS,
        /*.grfInheritance =*/ NO_INHERITANCE,
        /*.Trustee =*/ {
          /*.pMultipleTrustee =*/ nullptr,
          /*.MultipleTrusteeOperation =*/ NO_MULTIPLE_TRUSTEE,
          /*.TrusteeForm =*/ TRUSTEE_IS_SID,
          /*.TrusteeType =*/ TRUSTEE_IS_UNKNOWN,
          /*.ptstrName =*/ reinterpret_cast<LPTSTR>(&networkLogon.sid),
        },
      },
#endif
    };
 
    PACL dacl = nullptr;
    DWORD result = SetEntriesInAcl(ARRAYSIZE(ea), ea, nullptr, &dacl);
    ATLENSURE_THROW(result == ERROR_SUCCESS, HRESULT_FROM_WIN32(result));
 
    SECURITY_DESCRIPTOR sd;
    if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
      ATL::AtlThrowLastWin32();
    if (!SetSecurityDescriptorDacl(&sd, TRUE, dacl, FALSE))
      ATL::AtlThrowLastWin32();
    union
    {
      char buffer[SECURITY_MAX_SID_SIZE];
      SID sid;
    } admin = {};
    DWORD sidSize = sizeof admin;
    if (!CreateWellKnownSid(
      WinBuiltinAdministratorsSid, nullptr, &admin.sid, &sidSize))
    {
      ATL::AtlThrowLastWin32();
    }
    if (!SetSecurityDescriptorOwner(&sd, &admin.sid, TRUE))
    {
      ATL::AtlThrowLastWin32();
    }
    if (!SetSecurityDescriptorGroup(&sd, &admin.sid, TRUE))
    {
      ATL::AtlThrowLastWin32();
    }
    ATLENSURE_SUCCEEDED(CoInitializeSecurity(&sd, -1, nullptr, nullptr,
      RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr,
      EOAC_NONE, nullptr));
 
    // 前回からの追加分ここまで
 
    IDispatchPtr hoge;
    ATLENSURE_SUCCEEDED(Hoge::CreateInstance(&hoge));
 
    std::wstring monikerStr = GetObjrefMonikerDisplayString(hoge);
    ATLENSURE_SUCCEEDED(CoLockObjectExternal(hoge, TRUE, FALSE));
 
    std::wcout << monikerStr << std::endl;
 
    std::cin.get();
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog << std::hex << std::showbase;
    std::clog << e.m_hr << ' ' << ATL::AtlGetErrorDescription(e) << std::endl;
  }
 
  CoUninitialize();
}

クライアントのVBScript側のコードは前回から変更ありません。C++側を上記に差し替えて実行すると、今度はエラーになります。また、#if 1を#if 0にすると、再び成功します。

上のプログラムでは、所有者: Administrators、所有グループ: Administatorsとし、DACLは以下のようにしています。画像はDCOM構成ユーティリティーででっち上げました。コード上にコメントしているように、COM_RIGHTS_EXECUTEは常にORしておかないといけないようです。

  • EVERYONE: COM_RIGHTS_ACTIVATE_LOCALを許可 (SET_ACCESS)
    Everyoneにローカルからのアクティブ化を許可のダイアログ表示
  • NETWORK: すべて拒否 (DENY_ACCESS)
    NETWORKに対してすべてを拒否

EVERYONEでリモート関係の2つを許可しないだけで大丈夫かと思いましたが、やってみたらOBJREFモニカーの場合、接続できてしまいました。上記プログラムの#if 1を0にするとその状態になります。そこでネットワークログオンで必ず付与されるNETWORKに対してすべて拒否を与えたところ、うまくいきました。


許可を与える対象がEVERYONEであることなど、改善点はあるとは思いますが、ひとまずネットワークからのアクセスの拒否は可能そうだということが分かりました。

今回の主な情報元はMSDNライブラリの以下のページです。

今回はこういう目的に使いましたが、もちろんほかの使い方も考えられます。たとえば、「Administratorsグループ(あるいは特定ユーザ)からしか使用できないようにする」、「保護モードのInternet Explorerなど低ILプロセスからの接続を許可する」などです。

私はオブジェクト単位でのアクセス許可が設定できると良いと思ったのですが、残念ながらそういう仕組みはないようです。プロセスを分けるか自前でやる(各メソッドにアクセス許可の確認処理を入れる)しかなさそうでした。

アクセス許可の指定でリモートアクセスを禁じる is a post from: イグトランスの頭の中(のかけら)

WinRT APIを使ってみる

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの24日目の記事です。


最後(CoUninitializeの前)に、Windowsストアアプリ用APIであるWindows Runtime API (WinRT API)を使うサンプルコードも取り上げようと思いました。「WinRT APIはCOMなんだよ」を示すべく、ネイティブのC++でWinRT APIを使うサンプルコードです。

なお、Windowsストアアプリではなく、デスクトップアプリ、しかもstd::coutで出力するコンソールアプリです(参考:デスクトップ アプリからのWinRT API利用 | ++C++; // 未確認飛行 C ブログ)。

#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <windows.h>
#include <roapi.h>
#include <wrl/wrappers/corewrappers.h>
#include <windows.data.json.h>
#include <atlbase.h>
#include <atlcom.h>
#include <atlutil.h>
 
#pragma comment(lib, "runtimeobject.lib")
 
int main()
{
  using namespace ABI::Windows::Data::Json;
  using Microsoft::WRL::Wrappers::HStringReference;
 
  try
  {
    ATLENSURE_SUCCEEDED(RoInitialize(RO_INIT_MULTITHREADED));
 
    HStringReference jsonObjectClass(
      RuntimeClass_Windows_Data_Json_JsonObject);
    ATL::CComPtr<IJsonObjectStatics> jsonObjectStatics;
    ATLENSURE_SUCCEEDED(RoGetActivationFactory(
      jsonObjectClass.Get(), IID_PPV_ARGS(&jsonObjectStatics)));
 
    HStringReference text(LR"json(
{
  "hoge": 103,
  "piyo": 201
}
)json");
 
    boolean succeeded;
    ATL::CComPtr<IJsonObject> jsonObject;
    ATLENSURE_SUCCEEDED(
      jsonObjectStatics->TryParse(text.Get(), &jsonObject, &succeeded));
    ATLENSURE(succeeded);
 
    HStringReference key(L"piyo");
    double x;
    ATLENSURE_SUCCEEDED(jsonObject->GetNamedNumber(key.Get(), &x));
    std::cout << "piyo: " << x << std::endl;
  }
  catch (const ATL::CAtlException& e)
  {
    std::clog
      << std::hex << std::showbase;
      << e.m_hr << ' ' << ATL::AtlGetErrorDescription(e) << std::endl;
    return 1;
  }
  RoUninitialize();
}

どれくらいCOMかというと、上記のI~型はみな元を辿るとIUnknownになるインタフェースなのでATL::CComPtrで扱えますし、関数の戻り値はHRESULTなのでATLENSURE_SUCCEEDEDマクロ(失敗値なら例外を投げる)が使えるというくらいCOMです。

上記に相当するC++/CXのコードを下に載せます。

#include <iostream>
 
template<typename Tr>
inline std::wostream& operator<<(
  std::basic_ostream<wchar_t, Tr>& os,
  Platform::String^ s)
{
  return os << s->Data();
}
 
[Platform::MTAThread]
int main()
{
  using namespace Windows::Data::Json;
 
  try
  {
    Platform::String^ text = R"json(
{
  "hoge": 103,
  "piyo": 201
}
)json";
 
    JsonObject^ jsonObject;
    bool succeeded = JsonObject::TryParse(text, &jsonObject);
    if (!succeeded)
      throw ref new Platform::FailureException();
 
    double x = jsonObject->GetNamedNumber("piyo");
    std::cout << "piyo: " << x << std::endl;
  }
  catch (Platform::Exception^ e)
  {
    std::wclog
      << std::hex << std::showbase;
      << e->HResult << L' ' << e->ToString() << std::endl;
    return 1;
  }
}

ここからは従来とのCOMとの差異、あるいはWinRT API特有の話です。

WinRT APIには、CoGetClasObjectに相当するRoGetActivationFactoryとCoCreateInstanceに相当するRoActivateInstanceがあります。今回使っているのは見てのとおりRoGetActivationFactoryです。

クラスの指定方法はGUID (CLSID)から文字列になりました。RuntimeClass_Windows_Data_Json_JsonObjectはヘッダファイルでL”Windows.Data.Json.JsonObject”と定義されています。

RoGetActivationFactoryから得られるオブジェクトは、IClassFactoryに相当するIActivationFactoryのほか、場合によってクラス固有のインタフェースを実装しています。

I(クラス名)RuntimeClassFactory
仮引数を持つコンストラクタがある場合(このインタフェースにコンストラクタを呼び出すメソッドが定義される)
I(クラス名)Statics
staticメンバーを持つ場合

これらのインタフェースの定義は、ABI名前空間の中に元々と同じ名前空間の階層が作られた中にあります。また、Windows SDKには<名前空間名.h>のヘッダでこれらが定義されています。

今回は、JsonObject.TryParse静的メソッドのため、<windows.data.json.h>のABI::Windows::Data::Json::IJsonObjectStaticsを使っています。

あとは、上のコードを眺めていれば気付くことも多いと思いますが、こんな感じです。

  • 文字列はHSTRINGで取り扱う(HStringReferenceは書き換え不可文字列のラッパークラスです)。
  • boolはtypedef unsigned char boolean;になっている。
  • クラスに対応するインタフェースI(クラス)が定義されている。
  • 戻り値がHRESULTになり、本来の戻り値は最後の引数に移動している。

なお、自作コンポーネントの場合は、MSDNライブラリの方法: winmdidl.exe と midlrt.exe を使用して、Windows メタデータから .h ファイルを作成するに書かれている手順で、同様のヘッダーファイルが得られるのだと思います。


もちろん、WinRTをC++から使うコードのほとんどがスマートポインタとしてMicrosoft::WRL::ComPtrを使っていることは認識しています。今回は従来のCOMっぽさの演出のため、意図してATL::CComPtrを使いました。

今回、一番苦労したのはデスクトップアプリでまともに使えるWinRT APIクラスを見つけることでした。Windows Runtime APIs for desktop appsを眺め、簡単に使えそうなものを探しました。Clipboard.SetContentは成功するけどなぜか機能しない、Clipboard.GetContentは非同期だから面倒、などという具合でちょうど良いものを見つけるまでに難航しました。

明日はいよいよ最後、予告どおりCoUninitializeに関する話の予定です。

WinRT APIを使ってみる is a post from: イグトランスの頭の中(のかけら)

CoInitializeとCoUninitializeが呼び出されたことを知る

$
0
0

本記事は、COM Advent Calendar 2014 – Qiitaの25日目の記事です。今日で最後です。


COMには、CoInitializeやCoUninitializeが呼び出されたことの通知を受け取る手段が用意されています。IInitializeSpyおよびCoRegisterInitializeSpyそしてCoRevokeInitializeSpyです。

#include <iostream>
#include <windows.h>
#include <shlwapi.h>
#include <roapi.h>
#include <comdef.h>
 
class InitializeSpy : public IInitializeSpy
{
public:
  InitializeSpy() = default;
  InitializeSpy(const InitializeSpy&) = delete;
  InitializeSpy& operator=(const InitializeSpy&) = delete;
  ~InitializeSpy() = default;
 
  IFACEMETHOD(QueryInterface)(
    _In_ REFIID riid, _COM_Outptr_ void** ppv) override
  {
    static const QITAB qit[] = {
      {&__uuidof(IInitializeSpy), OFFSETOFCLASS(IInitializeSpy, InitializeSpy)},
      {},
    };
    return QISearch(this, qit, riid, ppv);
  }
  IFACEMETHOD_(ULONG, AddRef)() override { return 0; }
  IFACEMETHOD_(ULONG, Release)() override { return 0; }
 
  IFACEMETHOD(PreInitialize)(
    _In_ DWORD dwCoInit,
    _In_ DWORD dwCurThreadAptRefs) override
  {
    std::cout << "PreInitialize:    "
      << std::dec << dwCurThreadAptRefs
      << ", dwCoInit: " << std::hex << dwCoInit << std::endl;
    return S_OK;
  }
 
  IFACEMETHOD(PostInitialize)(
    _In_ HRESULT hrCoInit,
    _In_ DWORD dwCoInit,
    _In_ DWORD dwNewThreadAptRefs) override
  {
    std::cout << "PostInitialize:   "
      << std::dec << dwNewThreadAptRefs
      << ", dwCoInit: " << dwCoInit
      << ", hrCoInit: " << std::hex << hrCoInit << std::endl;
    return S_OK;
  }
 
  IFACEMETHOD(PreUninitialize)(
    _In_ DWORD dwCurThreadAptRefs) override
  {
    std::cout << "PreUninitialize:  "
      << std::dec << dwCurThreadAptRefs << std::endl;
    return S_OK;
  }
 
  IFACEMETHOD(PostUninitialize)(
    _In_ DWORD dwNewThreadAptRefs) override
  {
    std::cout << "PostUninitialize: "
      << std::dec << dwNewThreadAptRefs << std::endl;
    return S_OK;
  }
};
 
int main()
{
  try
  {
    InitializeSpy spy;
    ULARGE_INTEGER cookie;
    _com_util::CheckError(CoRegisterInitializeSpy(&spy, &cookie));
 
    std::cout << std::showbase;
 
    std::cout << "---- CoInitialize" << std::endl;
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));
    CoUninitialize();
    CoUninitialize();
 
    std::cout << "---- CoInitialize (failed)" << std::endl;
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));
    _com_util::CheckError(CoInitializeEx(
      nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE));
    CoUninitialize();
 
    std::cout << "---- OleInitialize" << std::endl;
    OleInitialize(nullptr);
    OleUninitialize();
 
    std::cout << "---- RoInitialize (MTA)" << std::endl;
    RoInitialize(RO_INIT_MULTITHREADED);
    RoUninitialize();
 
    std::cout << "---- RoInitialize (STA)" << std::endl;
    RoInitialize(RO_INIT_SINGLETHREADED);
    RoUninitialize();
 
    _com_util::CheckError(CoRevokeInitializeSpy(cookie));
  }
  catch(const _com_error& e)
  {
    std::cout << std::hex << std::showbase << e.Error() << std::endl;
  }
}

実行結果は以下のようになりました。Windows 8.1です。

---- CoInitialize
PreInitialize:    0, dwCoInit: 0x6
PostInitialize:   1, dwCoInit: 6, hrCoInit: 0
PreInitialize:    1, dwCoInit: 0x6
PostInitialize:   2, dwCoInit: 6, hrCoInit: 0x1
PreUninitialize:  2
PostUninitialize: 1
PreUninitialize:  1
PostUninitialize: 0
---- CoInitialize (failed)
PreInitialize:    0, dwCoInit: 0x6
PostInitialize:   1, dwCoInit: 6, hrCoInit: 0
PreInitialize:    1, dwCoInit: 0x4
PostInitialize:   1, dwCoInit: 4, hrCoInit: 0x80010106
PreUninitialize:  1
PostUninitialize: 0
---- OleInitialize
PreInitialize:    0, dwCoInit: 0x2
PostInitialize:   1, dwCoInit: 2, hrCoInit: 0
PreUninitialize:  1
PostUninitialize: 0
---- RoInitialize (MTA)
PreInitialize:    0, dwCoInit: 0
PostInitialize:   1, dwCoInit: 0, hrCoInit: 0
PreUninitialize:  1
PostUninitialize: 0
---- RoInitialize (STA)
PreInitialize:    0, dwCoInit: 0x6
PostInitialize:   1, dwCoInit: 6, hrCoInit: 0
PreUninitialize:  1
PostUninitialize: 0

変更前または変更後の参照カウントや、初期化関数に渡されたCOINIT値、初期化関数の結果 (HRESULT)などが渡されます。

dwCoInitの0x2はCOINIT_APARTMENTTHREADED、0x6はCOINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDEになります。RoInitialize(RO_INIT_SINGLETHREADED)がCOINIT_DISABLE_OLE1DDEを組み合わせて指定していることが分かりますね。

また、hrCoInitの0x80010106はRPC_E_CHANGED_MODEで、MSDNライブラリの記載どおりの挙動です。

なお、CoRegisterInitializeSpyはMSDNライブラリに記載があるとおり、呼び出したスレッドのみに有効です。


なんとか25日すべてが終わりました。候補に挙げたものの、結局取り上げなかったものがまだまだあるのが心残りですが、いったんこれでおしまいです。26日目はありません。もし12月26日にブログを書いたとしても、おそらくCOMに関係ない話にすると思います。

ここまですべて読んでいただいた皆さま、お疲れ様でした。また、「読んでいます」と声を掛けてくださった方、大変励みになりました。これにてCOM Advent Calendar 2014終わります。

CoInitializeとCoUninitializeが呼び出されたことを知る is a post from: イグトランスの頭の中(のかけら)

VC++でstd::threadから数値のスレッドIDを取り出す

$
0
0

今回はVisual C++限定の話です。VC++のstd::thread::id型オブジェクトは、boost::lexical_castで整数型に変換できます。Windows APIなどに渡す場合に使えます。

#include <iostream>
#include <thread>
#include <windows.h>
#include <boost/lexical_cast.hpp>
 
int main()
{
  std::thread t([]{});
 
  auto id1 = boost::lexical_cast<DWORD>(t.get_id());
  auto id2 = GetThreadId(t.native_handle());
 
  std::cout << "thread::id " << id1 << std::endl;
  std::cout << "GetThreadId " << id2 << std::endl;
 
  t.join();
}

get_idメンバ関数はstd::thread::id型のオブジェクトを返します。ここからWindows APIで使えるDWORD型の値をなんとか取り出したかったのです。

そこで目を付けたのが<<演算子です。標準規格でstd::thread::idの出力結果は規定されていませんので、実際にVC++で試してみました。すると、WindowsのスレッドIDの十進法表現でした。これならboost::lexical_castで数値型に変換できるというわけです。

Boostを使わず標準ライブラリだけで済ますことも可能です。この場合、ostringstreamとstoul関数を使います。

#include <iostream>
#include <string>
#include <sstream>
#include <thread>
#include <windows.h>
 
int main()
{
  std::thread t([]{});
 
  std::ostringstream os;
  os << t.get_id();
  auto id1 = std::stoul(os.str());
  auto id2 = GetThreadId(t.native_handle());
 
  std::cout << "thread::id " << id1 << std::endl;
  std::cout << "GetThreadId " << id2 << std::endl;
 
  t.join();
}

なお、上記コードで比較対象に使用したGetThreadIdは、Windows Server 2003およびWindows Vista以上で使える関数です。Windows XPでは使えない惜しい関数です。と言っても、もはやWindows XPも引退しつつあるので、気にせず使える場合も多いことでしょう。

VC++でstd::threadから数値のスレッドIDを取り出す is a post from: イグトランスの頭の中(のかけら)

C++の正規表現は全角文字にマッチする(かもしれない)

$
0
0

C++の正規表現はロケールが考慮されます。初期状態はCロケールなのでASCIIの範囲にしかマッチしませんが、その他のロケールを使用すれば、たとえばワイド文字で全角文字にマッチすることもあります。

途中の過程を飛ばして述べてしまえば、メタ文字の\s, \d, \wのマッチはstd::localeのctypeで判定されます。言い換えれば、std::localeを引数に取るisspace, isdigit, isalnum関数と同じ判定処理です。

以下のプログラムは\sで全角空白” ”がマッチすることを試すプログラムです。私が試した範囲だとみなtrueを出力します。

#include <iostream>
#include <string>
#include <regex>
 
int main()
{
  std::wregex r;
  r.imbue(std::locale(""));
  r.assign(LR"(\s)");
 
  std::wstring s = L" "; // 全角空白
  std::cout << std::boolalpha << regex_match(s, r) << std::endl;
}

試した環境は以下です。Windowsは日本語版です。UbuntuとFreeBSDでは、LANG=ja_JP.UTF-8とLANG=en_US.UTF-8のそれぞれで試しましたので、実質的には5環境です。

  • Visual C++ (Windows 8.1)
  • GCC, libstdc++ (Ubuntu Linux 12.04.5)
  • Clang, libc++ (FreeBSD 10.1-RELEASE)

一方、\dは実装により結果が異なりました。

#include <iostream>
#include <string>
#include <regex>
 
int main()
{
  std::wregex r;
  r.imbue(std::locale(""));
  r.assign(LR"(\d)");
 
  std::wstring s = L"1"; // 全角数字
  std::cout << std::boolalpha << regex_match(s, r) << std::endl;
}

Visual C++だとtrue、それ以外だとfalseが出力されました。Cロケール以外の挙動は処理系によって異なるので、こういうこともありえてしまうのです。

上記のコードではimbueメンバ関数を使用しましたが、ほかにstd::locale::globalも効果があります。std::regexとstd::wregexオブジェクトの初期値はグローバルロケールであり、imbueで指定しなければそちらになります。main関数の先頭で安易にstd::locale::global(std::locale(“”));としているプログラムでは注意しましょう。

C++の正規表現は全角文字にマッチする(かもしれない) is a post from: イグトランスの頭の中(のかけら)


Windows 8.1再起動時の「オプションの選択」画面をAPIで呼び出す

$
0
0

windows-8.1-boot-optionWindows 8~8.1で、完全なシャットダウンやセーフモードで起動したいときなどに使う「オプションの選択」の画面(右側画像のあれ、チャーム→PC設定→……などの手順で再起動すると出てくる)をWindows APIで呼び出す方法の話です。

その方法は、「InitiateShutdown関数に、SHUTDOWN_RESTART_BOOTOPTIONSとSHUTDOWN_RESTARTのORを指定する」です。SHUTDOWN_RESTART_BOOTOPTIONSはMSDNライブラリには説明されていませんが、他の定数とともにwinreg.hで定義されています。

#define _ATL_NO_AUTOMATIC_NAMESPACE
#include <iostream>
#include <windows.h>
#include <atlsecurity.h>
 
int main()
{
  ATL::CAccessToken token;
  if (!token.GetProcessToken(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY))
  {
    DWORD e = GetLastError();
    std::clog << "CAccessToken::GetProcessToken failed: " << e << std::endl;
    return 1;
  }
  if (!token.EnablePrivilege(SE_SHUTDOWN_NAME))
  {
    DWORD e = GetLastError();
    std::clog << "CAccessToken::EnablePrivilege failed: " << e << std::endl;
    return 1;
  }
 
  InitiateShutdown(nullptr, nullptr, 0,
    SHUTDOWN_RESTART_BOOTOPTIONS | SHUTDOWN_RESTART, 0);
}

シャットダウン処理なので、もちろん特権SE_SHUTDOWN_NAMEが必要です。

SHUTDOWN_RESTART_BOOTOPTIONS単独ではただの再起動になってしまいました。SHUTDOWN_RESTARTを組み合わせないとダメなようです。なお、それに加え、SHUTDOWN_GRACE_OVERRIDEなど更に他の値を組み合わせるのは大丈夫でした。

shutdownコマンドの/oオプションを指定したときの動作を追いかけてみて、判明しました。

Windows 8.1再起動時の「オプションの選択」画面をAPIで呼び出す is a post from: イグトランスの頭の中(のかけら)

fstreamを返す関数

$
0
0

今更ですが、C++11からはfstreamやstringstreamを関数の戻り値にできるようになりました。

#include <iostream>
#include <fstream>
 
std::ifstream open_config_file()
{
  std::ifstream s("app.conf");
  s.exceptions(std::ios_base::badbit | std::ios_base::failbit);
  return std::move(s);
}
 
int main()
{
  auto f = open_config_file();
  // ここでfから読み込む処理。
}

例はifstreamですが、もちろんofstreamでも可能です。ムーブコンストラクタ・ムーブ代入演算子そしてswapメンバ関数が追加されています。

fstreamを返す関数 is a post from: イグトランスの頭の中(のかけら)

SetCoalescableTimerに置き換えるべきかと思った話

$
0
0

要約すると「SetTimerのすごい版、SetCoalescableTimerが出た。けど、わざわざSetTimerから移行する必要はないと分かった」というお話です。


Windows 8よりSetTimerの系統にSetCoalescableTimerが増えていることに気付きました。

Windows 7からのSetWaitableTimerExと同方向(精度と引き替えに省電力性が増すとされる)に強化されている関数のようです。ただし、SetWaitableTimerExと比べて便利になっている点があります。それは、タイマーのずれの許容範囲について、TIMERV_DEFAULT_COALESCINGを指定すればいい感じにやってくれる点です。SetWaitableTimerExではタイマー間隔に応じて手動で指定しなければなりませんでした。

「さて、SetTimer使っている箇所をSetCoalescableTimerに置き換える必要があるのかな」と思いました。しかし、よく見るとMSDNライブラリの当該関数のページのRemarksにこう書いてあります。

When uToleranceDelay is set to 0, the system default timer coalescing is used and SetCoalescableTimer behaves the same as SetTimer.

TIMERV_DEFAULT_COALESCINGの値は0です。つまり、これまでどおりSetTimerを使っているだけでTIMERV_DEFAULT_COALESCING相当の効果が得られるということですね。微調整を行いたい場合のみSetCoalescableTimerを使えば良いということでしょう。

そういえば、SetWaitableTimerExが出たとき「SetTimer使っているところ、勝手にいい感じにやってくれないかなあ。精度を重要視していないからSetTimer使っているわけで」と考えていたことを思い出しました。叶いました。

参照(SetWaitableTimerExの話はこちら):Designing Energy Efficient Applications (エネルギー効率の良いアプリケーションを設計する) – Microsoft

SetCoalescableTimerに置き換えるべきかと思った話 is a post from: イグトランスの頭の中(のかけら)

標準出力のパイプ経由でCOMインタフェースを受け渡す

$
0
0

プロセス越しにCOMインタフェースを受け渡す話、久しぶりにします。今度はCoMarshalInterfaceCoUnmarshalInterfaceです。この間でのデータの受け渡しにCreatePipeによるパイプを使いました。

今回のサンプルコード、OBJREFモニカーのときとは少し処理の流れが異なります。

  1. 親プロセスp.exe (p.cpp): 子プロセスc.exe (c.cpp)を起動。この際、標準出力を無名パイプでリダイレクトしておく。
  2. 子プロセスc.exe: オブジェクトを作り、CoMarshalInterfaceでの結果を標準出力に書き込む。
  3. 親プロセスp.exe: 無名パイプからCoUnmarshalInterfaceでインタフェースへのポインタを読み出す。
  4. 親プロセスp.exe: 上で得られたポインタに対してメソッド呼び出し。
  5. 子プロセスc.exe: メソッドが呼び出される。

まず、HANDLEをラップするIStream実装です。CoMarshalInterfaceとCoUnmarshalInterfaceはIStreamでマーシャル用データを受け渡すためです。親・子プロセス共通で使います。ReadとWriteだけ実装し、残りはE_NOTIMPLで構いません。

// HandleStream.h
#pragma once
 
#include <atlbase.h>
#include <atlcom.h>
 
class ATL_NO_VTABLE HandleStream
  : public ATL::CComObjectRootEx<ATL::CComMultiThreadModelNoCS>
  , public IStream
{
public:
  BEGIN_COM_MAP(HandleStream)
     COM_INTERFACE_ENTRY(ISequentialStream)
     COM_INTERFACE_ENTRY(IStream)
  END_COM_MAP()
 
  STDMETHOD(Read)(
    _Out_writes_bytes_to_(cb, *pcbRead) void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG* pcbRead) override
  {
    DWORD read;
    if (!ReadFile(m_h, pv, cb, &read, nullptr))
      return ATL::AtlHresultFromLastError();
    if (pcbRead != nullptr)
      *pcbRead = read;
    return read < cb ? S_FALSE : S_OK;
  }
  STDMETHOD(Write)(
    _In_reads_bytes_(cb) const void* pv,
    _In_ ULONG cb,
    _Out_opt_ ULONG* pcbWritten) override
  {
    DWORD written;
    if (!WriteFile(m_h, pv, cb, &written, nullptr))
      return ATL::AtlHresultFromLastError();
    if (pcbWritten != nullptr)
      *pcbWritten = written;
    return S_OK;
  }
  IFACEMETHOD(Seek)(
    LARGE_INTEGER dlibMove,
    DWORD dwOrigin,
    _Out_opt_ ULARGE_INTEGER* plibNewPosition) override
  {
    return E_NOTIMPL;
  }
  IFACEMETHOD(SetSize)(
    ULARGE_INTEGER libNewSize) override
  {
    return E_NOTIMPL;
  }
  IFACEMETHOD(CopyTo)(
    _In_ IStream* pstm,
    ULARGE_INTEGER cb,
    _Out_opt_ ULARGE_INTEGER* pcbRead,
    _Out_opt_ ULARGE_INTEGER* pcbWritten) override
  {
    return E_NOTIMPL;
  }
  IFACEMETHOD(Commit)(DWORD /*grfCommitFlags*/) override
  {
    return E_NOTIMPL;
  }
  IFACEMETHOD(Revert)() override
  {
    return E_NOTIMPL;
  }
  IFACEMETHOD(LockRegion)(
    ULARGE_INTEGER /*libOffset*/,
    ULARGE_INTEGER /*cb*/,
    DWORD /*dwLockType*/) override
  {
    return E_NOTIMPL;
  }
  IFACEMETHOD(UnlockRegion)(
    ULARGE_INTEGER /*libOffset*/,
    ULARGE_INTEGER /*cb*/,
    DWORD /*dwLockType*/) override
  {
    return E_NOTIMPL;
  }
  IFACEMETHOD(Stat)(
    _Out_ STATSTG* pstatstg,
    DWORD grfStatFlag) override
  {
    if (pstatstg != nullptr)
      *pstatstg = {};
    return E_NOTIMPL;
  }
  IFACEMETHOD(Clone)(_COM_Outptr_ IStream** ppstm) override
  {
    if (ppstm != nullptr)
      *ppstm = {};
    return E_NOTIMPL;
  }
 
  HANDLE m_h = nullptr;
};

次に、子プロセス側です。C++/CLIでIDispatch実装しています。IDispatch実装に.NETが便利なので使いました。

// cl /clr p.cpp
#define UNICODE
#define _UNICODE
#define WIN32_LEAN_AND_MEAN
#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <windows.h>
#include "HandleStream.h"
 
using namespace System;
using namespace System::Runtime::InteropServices;
 
public ref class Hoge
{
public:
  // これが親プロセスから呼び出される関数
  [DispId(DISPID_VALUE)]
  void f()
  {
    MessageBox(nullptr, L"Hoge::f", L"c.exe", 0);
  }
};
 
[MTAThread]
int WINAPI WinMain(HINSTANCE, HINSTANCE, PSTR, int)
{
  auto obj = gcnew Hoge;
  ATL::CComPtr<IDispatch> disp;
  disp.Attach(static_cast<IDispatch*>(
    Marshal::GetIDispatchForObject(obj).ToPointer()));
 
  ATL::CComObjectStack<HandleStream> s;
  s.m_h = GetStdHandle(STD_OUTPUT_HANDLE);
  Marshal::ThrowExceptionForHR(CoMarshalInterface(
    &s, __uuidof(disp.p), disp, MSHCTX_LOCAL, nullptr, MSHLFLAGS_NORMAL));
 
  MessageBox(nullptr, L"OKを押すと終了します", L"c.exe", 0);
}

最後に親プロセス側です。

// cl p.cpp
#define UNICODE
#define _UNICODE
#define WIN32_LEAN_AND_MEAN
#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <windows.h>
#include <atlbase.h>
#include <atlstr.h>
#include <atlutil.h>
#include "HandleStream.h"
 
void OutputErrorMessgae(_In_ PCWSTR functionName, HRESULT hr)
{
  std::wclog << functionName << '\n';
  std::wclog << ATL::AtlGetErrorDescription(hr) << std::endl;
}
 
void OutputLastError(_In_ PCWSTR functionName)
{
  OutputErrorMessgae(functionName, ATL::AtlHresultFromLastError());
}
 
int main()
{
  CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);
 
  ATL::CHandle hOutputRead, hOutputWrite;
  if (!CreatePipe(&hOutputRead.m_h, &hOutputWrite.m_h, nullptr, 0))
  {
    OutputLastError(L"CreatePipe");
    return 1;
  }
  if (!SetHandleInformation(
    hOutputWrite, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT))
  {
    OutputLastError(L"SetHandleInformation");
    return 1;
  }
 
  STARTUPINFO si{sizeof si};
  si.dwFlags = STARTF_USESTDHANDLES;
  si.hStdOutput = hOutputWrite;
  PROCESS_INFORMATION pi{};
  if (!CreateProcess(L"c.exe", nullptr,
    nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi))
  {
    OutputLastError(L"CreateProcess");
    return 1;
  }
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
 
  ATL::CComObjectStack<HandleStream> s;
  s.m_h = hOutputRead;
 
  ATL::CComPtr<IDispatch> disp;
  auto hrUnmarshal = CoUnmarshalInterface(&s, IID_PPV_ARGS(&disp));
  if (FAILED(hrUnmarshal))
  {
    OutputErrorMessgae(L"CoUnmarshalInterface", hrUnmarshal);
    return 1;
  }
 
  DISPID dispid = DISPID_VALUE;
  auto hr = disp.Invoke0(dispid); // Hoge::fの呼び出し
  if (FAILED(hr))
  {
    OutputErrorMessgae(L"Hoge::f", hr);
    return 1;
  }
  disp.Release();
  std::cout << "完了" << std::endl;
 
  CoUninitialize();
}

オブジェクト・ハンドルの寿命管理やエラー処理が大雑把な点はご容赦ください。パイプの読み書きにはタイムアウトなど強制切断する手段を用意したほうが良いのかもしれません。XPを忘れればCancelSynchronousIoがあるので難しくないでしょう。

IDispatchを使っていますが、もちろん任意のインタフェースが使用可能です。IDispatchはプロキシ・スタブを作らなくてもマーシャリングできるので、サンプルコードを書く際に便利なのです。

CoMarshalInterfaceは以前OBJREFモニカーの記事で関数名だけ登場させました。気になっていたので実際に試してみたというわけです。

標準出力のパイプ経由でCOMインタフェースを受け渡す is a post from: イグトランスの頭の中(のかけら)

保護モードよりもう少しだけ制限の強いプロセスを作る

$
0
0

ちょっと制限のあるプロセスを作ろうと思いました。

  • Integrity Levelを低にする
  • CreateRestrictedTokenのDISABLE_MAX_PRIVILEGE | LUA_TOKENで制限付きのアクセストークンを作る
  • 上記のアクセストークンでCreateProcessAsUserする

プロセスのアクセストークンについてこれより厳しくできないかとも試してみましたが、公開されているAPIではこれが限度のようでした。その話は別の機会にしたいと思います。そんなわけで、Internet Explorerの保護モード(低IL)よりほんの少しだけ制限が強まった程度になりました。

これがそのコードです。CreateRestrictedTokenとSetTokenInformationのTokenIntegrityLevel、そしてCreateProcessAsUserを順に呼び出しているだけです。

#define UNICODE
#define _UNICODE
#define WIN32_LEAN_AND_MEAN
#define _ATL_NO_AUTOMATIC_NAMESPACE
 
#include <iostream>
#include <memory>
#include <windows.h>
#include <userenv.h>
#include <atlbase.h>
#include <atlutil.h>
 
#pragma comment(lib, "userenv.lib")
 
struct env_deleter
{
  void operator()(void* env) const throw()
  {
    DestroyEnvironmentBlock(env);
  }
};
 
std::unique_ptr<void, env_deleter> create_environment_block(
  _In_ HANDLE hToken, BOOL inherit)
{
  void* tmp;
  if (CreateEnvironmentBlock(&tmp, hToken, inherit))
  {
    return std::unique_ptr<void, env_deleter>(tmp);
  }
  else
  {
    return nullptr;
  }
}
 
HRESULT OutputErrorMessgae(_In_ PCWSTR functionName, HRESULT hr)
{
  std::wclog << functionName << '\n';
  std::wclog << ATL::AtlGetErrorDescription(hr) << std::endl;
  return hr;
}
 
HRESULT OutputLastError(_In_ PCWSTR functionName)
{
  auto hr = ATL::AtlHresultFromLastError();
  OutputErrorMessgae(functionName, hr);
  return hr;
}
 
int main()
{
  ATL::CHandle hOriginalToken, hToken;
  if (!OpenProcessToken(GetCurrentProcess(),
    TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE
    | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT,
    &hOriginalToken.m_h))
  {
    return OutputLastError(L"CreatePipe");
  }
  if (!CreateRestrictedToken(
    hOriginalToken, DISABLE_MAX_PRIVILEGE | LUA_TOKEN,
    0, nullptr, 0, nullptr, 0, nullptr, &hToken.m_h))
  {
    return OutputLastError(L"CreateRestrictedToken");
  }
  SID lowIL{
    SID_REVISION,
    1,
    SECURITY_MANDATORY_LABEL_AUTHORITY,
    SECURITY_MANDATORY_LOW_RID,
  };
  TOKEN_MANDATORY_LABEL tml{
    {
      &lowIL,
      SE_GROUP_INTEGRITY,
    },
  };
  if (!SetTokenInformation(hToken, TokenIntegrityLevel, &tml, sizeof tml))
  {
    return OutputLastError(L"SetTokenInformation");
  }
  auto env = create_environment_block(hToken, FALSE);
  if (env == nullptr)
  {
    return OutputLastError(L"CreateEnvironmentBlock");
  }
  STARTUPINFO si{ sizeof si };
  PROCESS_INFORMATION pi{};
  if (!CreateProcessAsUser(
    hToken, L"hello.exe", nullptr, nullptr, nullptr, TRUE,
    CREATE_UNICODE_ENVIRONMENT, env.get(), nullptr, &si, &pi))
  {
    return OutputLastError(L"CreateProcessAsUser");
  }
  WaitForSingleObject(pi.hProcess, INFINITE);
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
}

CreateProcessAsUserで指定しているHello.exeは、Hello worldを出力するだけのものです。

#include <stdio.h>
 
int main()
{
  puts("hello world");
  getchar(); // 何かキーが押されるのを待ってから終了する
}

LUA_TOKENトークンの効果は、「管理者として実行」で起動すると分かります。具体的には、ビルトインのAdministratorsグループなどに対してSE_GROUP_USE_FOR_DENY_ONLYが付与されます。

なお、Chromium/Google Chromeのようにアクセストークン以外で制限する余地がまだあります。今回は見送りました。

今回はここまでです。次は前回(標準出力のパイプ経由でCOMインタフェースを受け渡す)の内容とこれを組み合わせたいと考えています。

参考:

保護モードよりもう少しだけ制限の強いプロセスを作る is a post from: イグトランスの頭の中(のかけら)

Viewing all 123 articles
Browse latest View live