如何结合RegisterDragDrop,RoInitialize在一个线程中一起工作?

问题描述 投票:0回答:3

我陷入两难境地。我的基于GUI的C ++应用程序需要实现拖放功能。与此同时,我正在将此Win32应用程序转换为UWP以提交到Windows应用商店。但是有一个问题:

要实现拖放,我需要调用这两个方法:

OleInitialize(NULL);
//...
HRESULT hr = RegisterDragDrop(hMainWnd, pDropTarget);

并初始化WinRT与Windows Store一起工作,我需要调用:

HRESULT hr = RoInitialize(RO_INIT_MULTITHREADED);

不幸的是,OleInitialize将COM初始化为single-thread apartment,而RoInitialize需要多线程模型,而RegisterDragDrop无法在不调用OleInitialize的情况下运行。

知道怎么解决吗? (除了将RoInitialize和所有WinRT代码移动到工作线程中,这会使事情变得复杂。)

c++ windows winapi com windows-runtime
3个回答
2
投票

Raymond Chen以他惯常的居高临下的方式擅长批评事情,但没有解决现有问题。我发布这个主要是为了以后的自我引用,以防其他人偶然发现同样的问题。我只花了几天时间试图解决这个bug,所以也许这会为别人节省时间。

Problem

首先,这是一个原生的Win32代码(没有.NETC++/CX。)它是C++与洒上WRL更容易处理WinRT / COM的东西。

在我的例子中,我有一个Win32 GUI应用程序,它实现了将文件拖放到主窗口中。因此,要初始化它,需要从主线程执行此操作,就在应用程序启动时:

OleInitialize(NULL);
//...
HRESULT hr = RegisterDragDrop(hMainWnd, pDropTarget);

上面的OleInitialize调用将为主线程初始化COM以使用single-thread apartment,这是RegisterDragDrop成功所必需的。没有它,拖放功能将无法工作。

然后,假设您决定使用Microsoft的Win32转换器将此UWP应用程序转换为Project Centennial,以包含在Windows 10商店中。

当应用程序在其试用许可证计划下转换并在商店中列出时,您将使用以下逻辑来检查用户是否具有试用版或已激活(即已购买)的应用程序副本。你会这样开始的:

//Init COM for WinRT
RoInitialize(RO_INIT_MULTITHREADED);

ComPtr<IStoreContextStatics> pStoreContextStatics;
if(SUCCEEDED(RoGetActivationFactory(
    HStringReference(L"Windows.Services.Store.StoreContext").Get(), 
    __uuidof(pStoreContextStatics), 
    &pStoreContextStatics)) &&
    pStoreContextStatics)
{
    //Get store context for the app
    ComPtr<IStoreContext> pStoreContext;
    if(SUCCEEDED(pStoreContextStatics->GetDefault(&pStoreContext)) &&
        pStoreContext)
    {
        //Got store context
        //....

    }
}

然后,如果你需要知道应用程序的试用与激活状态,使用this logic,你会打电话:

ComPtr<IAsyncOperation<StoreAppLicense*>> p_opAppLic;
if(SUCCEEDED(pStoreContext->GetAppLicenseAsync(p_opAppLic)) &&
    p_opAppLic)
{
    ComPtr<IAsyncOperationCompletedHandler<StoreAppLicense*>> p_onAppLicCallback =
        Callback<Implements<RuntimeClassFlags<ClassicCom>, IAsyncOperationCompletedHandler<StoreAppLicense*>, FtmBase>>(
        [](IAsyncOperation<StoreAppLicense*>* pOp, AsyncStatus status)
    {
        if (status == AsyncStatus::Completed)
        {
            ComPtr<IStoreAppLicense> pAppLicResult;
            if(SUCCEEDED(pOp->GetResults(&pAppLicResult)) &&
                pAppLicResult)
            {
                BYTE nActive = -1;
                BYTE nTrial = -1;
                pAppLicResult->get_IsActive(&nActive);
                pAppLicResult->get_IsTrial(&nTrial);

                //Get app's store ID with SKU
                HString strStoreId;
                pAppLicResult->get_SkuStoreId(strStoreId.GetAddressOf());

                if(nActive == 1 &&
                    nTrial == 0)
                {
                    //Activated, or purchased copy
                }
                else if(nActive == 1 &&
                    nTrial == 1)
                {
                    //Trial copy
                }
                else
                {
                    //Error -- store returned some gibberish
                }
            }
        }

        return S_OK;
    });

    if(SUCCEEDED(p_opAppLic->put_Completed(p_onAppLicCallback.Get())))
    {
        //Success initiating async call
    }
}

所以,如果你做了这一切,你的UWP转换的应用程序将以一种非常奇怪的方式运行。这是一个例子。假设用户通过Windows应用商店购买应用的许可证。反过来你的app逻辑调用上面的代码来查看应用程序是否被激活,但你得到的是nActive=0nTrial=1。然后,如果你检查strStoreId它将是你的应用程序商店ID但没有SKU。 WTF!?

我知道,这真的令人困惑。顺便说一下,让我解释一下。当您首次在Windows应用商店中列出您的应用时,将为其分配一个商店ID。像:ABCDEFG12345。然后,如果您向同一个应用程序的第一个版本提交任何后续更新,他们将向其添加一个SKU编号,这将使整个应用程序ID更改为ABCDEFG12345/0010,然后ABCDEFG12345/0011进行下一次更新,等等上。

好吧,上面的WinRT代码会将我的应用程序商店ID返回为ABCDEFG12345而不附加任何SKU。这是错误的,因为它是应用程序的第一个版本的第三次更新。因此,该应用商店ID的任何其他属性也是错误的。

这就是我面临的问题......

Cause

我上面描述的所有头痛都是由于我省略了检查第一次RoInitialize调用返回的结果代码。如果我这样做,我将能够更快地解决问题:

//Init COM for WinRT
if(FAILED(RoInitialize(RO_INIT_MULTITHREADED)))
{
    //WinRT COM initialization failed
    //Go scratch your head why....
}

在这种情况下,RoInitialize将失败,错误代码为RPC_E_CHANGED_MODEdocumentation for it与Windows帮助(F1)选项一样有用:

之前对RoInitialize的调用将此线程的并发模型指定为多线程单元(MTA)。这也可能表明从中性线程公寓到单线程公寓的变化已经发生。

之前的电话?任何人都可以称之为的唯一参数是RO_INIT_MULTITHREADED

所以我开始深入挖掘并且通过消除过程发现OleInitialize之前调用的原因是RoInitialize失败并导致我上面描述的级联事件。

因此,我在这里提出这个问题。

请注意,错误的WinRT库(ref1ref2ref3ref4ref5)没有迹象表明在RoInitialize之后的所有调用中都存在问题,并且内部默默无法检索应用程序的SKU因为single-thread apartment COM初始化。

Hack/Workaround

正如RbMm在上面的评论中所建议的那样,执行以下操作会有效,但这是完全没有记录的行为:

if(SUCCEEDED(OleInitialize(0))
{
    CoUninitialize();
}

CoInitializeEx(NULL, COINIT_MULTITHREADED);

因此,如果您不希望您的应用程序因为没有明显原因而开始崩溃,我就不会使用它。

Solution

我使用的解决方案是将所有WinRT COM内容(上面列出的代码:第2和第3代码段)移动到单独的工作线程中。它会在那里工作得很好。问题是在主线程和此工作线程之间编组调用。它是可行的,但需要一些工作,即使用mutexesevents进行同步访问等。

因此,如果有人发现更容易解决此问题,请发布您的解决方案。我会把它标记为答案。


1
投票

我之前评论中提到的DatePicker崩溃的解决方案,我刚才写的快速代码。

使用以下代码:

  TDsObjPicker lv_PickInfo;

  memset(&lv_PickInfo, 0, sizeof(TDsObjPicker));

  Sec_InitDsObjPicker(&lv_PickInfo, &lv_InitInfo);

  Sec_InvokeDsObjPicker(&lv_PickInfo, 0, &lv_oData);

解决方案是在另一个线程中运行对话框并在没有Ole + Com组合的情况下启动线程:

// command codes
#define DSOPCMD_EXITTHREAD   1
#define DSOPCMD_INITIALIZE   2
#define DSOPCMD_INVOKE       3



// parameters of object picker via thread
typedef struct tagDsObjPicker
{
  // thread handle
  HANDLE          hThread;

  // events
  HANDLE          hCmdEvt;
  HANDLE          hRdyEvt;

  // commands
  UINT            CmdCode;
  HRESULT         hResult;

  // command parameters - DSOPCMD_INITIALIZE
  DSOP_INIT_INFO *InitInfo;

  // command parameters - DSOPCMD_INVOKE
  HWND            hWnd;
  IDataObject   **oData;

  //
} TDsObjPicker;




DWORD CALLBACK _Sec_DsObjPickerThread(VOID *in_Param)
{
    /* locals */
    HRESULT          lv_hCreateResult;
    HRESULT          lv_hResult;
    TDsObjPicker    *lv_PickInfo;
    IDsObjectPicker *lv_oPicker;


  // get info structure
  lv_PickInfo = (TDsObjPicker*)in_Param;

  // init COM
  CoInitializeEx(NULL, COINIT_MULTITHREADED);

  // preclear object pointer
  lv_oPicker = NULL;

  // create instance of picker
  lv_hCreateResult = CoCreateInstance(
    CLSID_DsObjectPicker, NULL, CLSCTX_INPROC_SERVER,
    IID_IDsObjectPicker, (VOID**)&lv_oPicker);

  // while thread is not aborted
  while (lv_PickInfo->CmdCode != DSOPCMD_EXITTHREAD)
  {
    // wait for command event
    if (WaitForSingleObject(lv_PickInfo->hCmdEvt, INFINITE) == 0)
    {
      // what command?
      switch (lv_PickInfo->CmdCode)
      {
        // call init
        case DSOPCMD_INITIALIZE:
        {
          // call object
          if (lv_hCreateResult)
            lv_hResult = lv_hCreateResult;
          else
            lv_hResult = lv_oPicker->Initialize(lv_PickInfo->InitInfo);

          // done
          break;
        }

        // call invoke
        case DSOPCMD_INVOKE:
        {
          // call object
          if (lv_hCreateResult)
            lv_hResult = lv_hCreateResult;
          else
            lv_hResult = lv_oPicker->InvokeDialog(lv_PickInfo->hWnd, lv_PickInfo->oData);

          // done
          break;
        }

        // other command codes
        default:
          lv_hResult = E_FAIL;
          break;
      }

      // store result
      lv_PickInfo->hResult = lv_hResult;

      // notify caller
      SetEvent(lv_PickInfo->hRdyEvt);
    }
  }

  // destroy the picker object
  if (lv_oPicker)
    lv_oPicker->Release();

  // cleanup COM
  CoUninitialize();

  // leave the thread
  return 0;
}



VOID Sec_DoneDsObjPicker(TDsObjPicker *in_PickInfo)
{
  // is thread created?
  if (in_PickInfo->hThread)
  {
    // set command code
    in_PickInfo->CmdCode = DSOPCMD_EXITTHREAD;

    // trigger the thread to process the code
    SetEvent(in_PickInfo->hCmdEvt);

    // wait for thread to finish
    WaitForSingleObject(in_PickInfo->hThread, INFINITE);

    // close thread handle
    CloseHandle(in_PickInfo->hThread);
  }

  // close event handles
  if (in_PickInfo->hCmdEvt) CloseHandle(in_PickInfo->hCmdEvt);
  if (in_PickInfo->hRdyEvt) CloseHandle(in_PickInfo->hRdyEvt);

  // clear
  memset(in_PickInfo, 0, sizeof(TDsObjPicker));
}




HRESULT Sec_InitDsObjPicker(TDsObjPicker *in_PickInfo, DSOP_INIT_INFO *in_InitInfo)
{
    /* locals */
    DWORD lv_TID;


  // thread not yet created?
  if (!in_PickInfo->hThread)
  {
    // create events
    in_PickInfo->hCmdEvt = CreateEvent(0,0,0,0);
    in_PickInfo->hRdyEvt = CreateEvent(0,0,0,0);

    // if ok
    if (in_PickInfo->hCmdEvt && in_PickInfo->hRdyEvt)
    {
      // create the thread
      in_PickInfo->hThread = CreateThread(
        0, 0, _Sec_DsObjPickerThread, in_PickInfo, 0, &lv_TID);
    }

    // failed?
    if (!in_PickInfo->hThread)
    {
      // cleanup
      Sec_DoneDsObjPicker(in_PickInfo);

      // return with error
       return E_OUTOFMEMORY;
    }
  }

  // store parameter
  in_PickInfo->InitInfo = in_InitInfo;

  // set command code
  in_PickInfo->CmdCode = DSOPCMD_INITIALIZE;

  // trigger the thread to process the code
  SetEvent(in_PickInfo->hCmdEvt);

  // wait for result
  WaitForSingleObject(in_PickInfo->hRdyEvt, INFINITE);

  // return the result
  return in_PickInfo->hResult;
}

HRESULT Sec_InvokeDsObjPicker(TDsObjPicker *in_PickInfo, HWND in_hWnd, IDataObject **out_oData)
{
    /* locals */
    MSG lv_Msg;


  // thread not yet created?
  if (!in_PickInfo->hThread)
    return E_FAIL;

  // store parameters
  in_PickInfo->hWnd  = in_hWnd;
  in_PickInfo->oData = out_oData;

  // set command
  in_PickInfo->CmdCode = DSOPCMD_INVOKE;

  // trigger the thread
  SetEvent(in_PickInfo->hCmdEvt);

  // process messages of this thread while picker runs in other thread until event
  while (MsgWaitForMultipleObjects(1, &in_PickInfo->hRdyEvt, 0, INFINITE, QS_ALLINPUT) != 0)
  {
    // get next message
    while (PeekMessage(&lv_Msg, 0,0,0, PM_REMOVE))
    {
      // translate/dispatch the message
      TranslateMessage(&lv_Msg);
      DispatchMessage(&lv_Msg);
    }
  }

  // return the result
  return in_PickInfo->hResult;
}

0
投票

你问为什么首先调用OleInitialize(),然后调用CoUnintialize然后通过CoInitializeEx重新启动COM工作并且是安全的,看看WINE中重写的OLE服务器的代码,https://github.com/wine-mirror/wine/blob/master/dlls/ole32/ole2.c它非常接近“真实的东西”。 OleInitialize使用COINIT_APARTMENTTHREADED调用CoInitializeEx本身,并在CoInitializeEx失败时执行OLE特定的初始化之前失败。没有理由失败,因为OLE代码也可以在MULTITHREADED模式下运行。记住MULTITHREADED意味着调用者必须处理同步/锁定,而APARTMENTTHREADED COM库将为代码处理它。因此,如果您确保不从多个线程同时调用像拖放和剪贴板这样的OLE代码,那么就没有问题。保持主线程中的所有UI都可以做到这一点。因为您应该已经使用请求的MULTITHREADED模式自己编写多线程感知代码。

我有一个directshow过滤器/驱动程序的问题,即使在主要UI线程在APARTMENTTHREADED中运行时从具有THREADED的线程调用directshow时,也会在使用APARTMENTTHREADED初始化COM时锁定进程。

在初始化OLE之后取消初始化COM,然后在主UI线程中启动期间使用MULTITHREAED重新初始化COM会使您绕过OleInitialize中的失败。这是确保所有运行良好的最佳解决方案。

© www.soinside.com 2019 - 2024. All rights reserved.