0
votes

I'm in the process of creating a Heartrate Monitor plugin for OBS Studio. The idea is to access Bluetooth GATT services on C# side to get data from a Smart BT HRM, render it and then transfer that to C++ dll that acts as the OBS Studio plugin itself.

I've managed to get everything working, and I can call a function in the C# from C++ side that returns with an array for the image data, which I can then subsequently give to OBS. However, this requires the C# DLL to be registered. I'd rather not pollute the registry with useless DLL information, so I looked into Registration Free COM. (It also would make distribution/install a lot simpler)

After lots of fiddling, I've come to an impasse. With sxstrace, I know the proper library is being picked up by the side-by-side system:

=================
Begin Activation Context Generation.
Input Parameter:
    Flags = 0
    ProcessorArchitecture = AMD64
    CultureFallBacks = en-US;en
    ManifestPath = C:\Program Files\obs-studio\obs-plugins\64bit\HrmPlugin.dll
    AssemblyDirectory = C:\Program Files\obs-studio\obs-plugins\64bit\
    Application Config File = 
-----------------
INFO: Parsing Manifest File C:\Program Files\obs-studio\obs-plugins\64bit\HrmPlugin.dll.
    INFO: Manifest Definition Identity is HrmPlugin,processorArchitecture="AMD64",type="win32",version="1.0.0.0".
    INFO: Reference: OBSBluetoothHeartrate,processorArchitecture="*",type="win32",version="1.0.0.0"
INFO: Resolving reference OBSBluetoothHeartrate,processorArchitecture="*",type="win32",version="1.0.0.0".
    INFO: Resolving reference for ProcessorArchitecture AMD64.
        INFO: Resolving reference for culture Neutral.
            INFO: Applying Binding Policy.
                INFO: No binding policy redirect found.
            INFO: Begin assembly probing.
                INFO: Did not find the assembly in WinSxS.
                INFO: Attempt to probe manifest at C:\Program Files\obs-studio\obs-plugins\64bit\OBSBluetoothHeartrate.DLL.
                INFO: Manifest found at C:\Program Files\obs-studio\obs-plugins\64bit\OBSBluetoothHeartrate.DLL.
            INFO: End assembly probing.
INFO: Resolving reference OBSBluetoothHeartrate.mui,language="*",processorArchitecture="AMD64",type="win32",version="1.0.0.0".
    INFO: Resolving reference for ProcessorArchitecture AMD64.
        INFO: Resolving reference for culture en-US.
            INFO: Applying Binding Policy.
                INFO: No binding policy redirect found.
            INFO: Begin assembly probing.
                INFO: Did not find the assembly in WinSxS.
                INFO: Did not find manifest for culture en-US.
            INFO: End assembly probing.
        INFO: Resolving reference for culture en.
            INFO: Applying Binding Policy.
                INFO: No binding policy redirect found.
            INFO: Begin assembly probing.
                INFO: Did not find the assembly in WinSxS.
                INFO: Did not find manifest for culture en.
            INFO: End assembly probing.
INFO: Parsing Manifest File C:\Program Files\obs-studio\obs-plugins\64bit\OBSBluetoothHeartrate.DLL.
    INFO: Manifest Definition Identity is OBSBluetoothHeartrate,processorArchitecture="AMD64",type="win32",version="1.0.0.0".
INFO: Activation Context generation succeeded.
End Activation Context Generation.

However, when I try to create the class from the COM object itself, I get a class not found error. The code to create it is as follows:

static void Initialize(hrm_source *context)
{
    if (context->init_failed) return;

    try
    {
        HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
        IHrmPluginPtr pHRM(CLSID_HrmPlugin);
        context->pHRM = pHRM;
    }
    catch (_com_error _com_err)
    {
        MessageBox(NULL, _com_err.ErrorMessage(), L"Failed to initialize COM", MB_OK);
        context->pHRM = NULL;
        context->init_failed = (context->pHRM == NULL);
    }
}

The problem is, this whole thing works if either A) I register the assembly or B) I use from an exe. Actually, if I change the output type an .exe, add a main() to the plugin-dll-source and create it from there, I can run the exe and it works properly.

                 | C++ Exe | C++ DLL 
-------------------------------------
Registered       | Works   | Works
Register Free    | Works   | Doesn't Work

There is obviously some configuration flag (well, fingers crossed) or setting or attribute missing from the manifest files.

So, here's the question: how to make registration free COM work with a managed C# COM server and a native C++ DLL as the COM client?

Here are the manifest files as well:

COM Server (C# DLL)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity type="win32"
                    name="OBSBluetoothHeartrate" 
                    version="1.0.0.0" 
                    processorArchitecture="amd64"/>
  <clrClass clsid="{54667DC4-13CD-4D25-BB9E-C3BDDFAF019F}" 
            progid="OBSBluetoothHeartrate.HrmPlugin" 
            threadingModel="Both" 
            name="OBSBluetoothHeartrate.HrmPlugin" 
            runtimeVersion="v4.0.30319"/>
</assembly>

COM Client (C++ DLL)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity type="win32"
                    name="HrmPlugin"
                    version="1.0.0.0"
                    processorArchitecture="amd64"/>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32"
                        name="OBSBluetoothHeartrate"
                        version="1.0.0.0"
                        processorArchitecture="amd64"/>
    </dependentAssembly>
  </dependency>
</assembly>

Update 17th Jan 2020

I added an assemblyIdentity to the C++ DLL as well, in case it would help (as per a question in the comments). The only thing it changed is that now when it is loaded by side-by-system, it gets an identity instead of (null) - identity shouldn't be needed as I'm not using the C++ DLL as a COM server. However, I've updated the C++ DLL manifest and the sxstrace log - this is technically "more correct" now, so at least we know this is not the reason.

Update 29th Jan 2020

I had accidentally copied the C# DLL COM Server manifest as the C++ DLL COM Client manifest. This is now updated to the correct one. Made sure to double-check it, but the class is still not found, even though sxstrace says the proper dll is found and loaded. I'll try the manual activation context creation that was provided as an answer and report back (and of course mark it as the correct answer if I can get it working with that)

1
Why don't you have an assemblyIdentity item in your C++ manifest?Joseph Willcoxson
It's not needed (according to what I tested) but I can add it and try with that again. The reason mainly being that I'm not going to use the C++ DLL itself as a COM server, thus it's no value for me for the OS to know what it is.Jani Kärkkäinen
It really doesn't seem correct to have clrClass in your C++ DLL...especially without a reference to the C# DLL. The way I generally use C# reg free com is to generate a manifest for the C# DLL and then add a dependency to the C# DLL in my main manifest. There are ways to also manually load the manifest to the C# DLL from the C++ DLL using the activation context APIs.Joseph Willcoxson
What the heck, apparently I managed to copypaste the wrong file for the C++ DLL manifest. Of course it has only the assemblyIdentity and the dependentAssembly reference. I'll update that once I get home - sorry for the confusion. Also, what's this about context activation API? That could possibly help here.Jani Kärkkäinen
Alright, updated the C++ COM DLL manifest to reflect reality, sorry about that. I'll check the activation context API method if I could get it working that way and report back when I've gotten around to it. Thanks!Jani Kärkkäinen

1 Answers

0
votes

Here is the skeleton of an answer using manual activation contexts... the function ReportError() is a stub ... do what you want with it...log, trace, message box, or remove it...

   ACTCTX ctx = { sizeof(ctx),  ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, _T("NameOfYourDotNet.manifest")};
   ctx.lpAssemblyDirectory = szPath; // path is drive and directory where your COM DLL is


   HANDLE hCtx = CreateActCtx(&ctx);
   if (hCtx != INVALID_HANDLE_VALUE)
   {
      DWORD_PTR dwCookie = 0;
      BOOL bActivated = ActivateActCtx(hCtx, &dwCookie);
      if (bActivated)
      {
         wcout << L"After activating ActCtx..." << endl;


     // Do stuff with your .NET COM server here

         DeactivateActCtx(0, dwCookie);
      }
      else
      {
         ReportError(L"Could not activate context: ", GetLastError());
      }
      ReleaseActCtx(hCtx);
   }
   else
   {
      ReportError(_T("Problem creating ActCtx: "), GetLastError());

   }