I have a .Net 4.72 DLL, exposed to COM using ComVisible(true) and the rest. I’ll put example in a sec. I have been modernizing this DLL to .Net 8. This DLL consumes some registered ocx files via Interops that are 32 bit, so I build the DLL always as x86. In .Net 4.72 you could use “Any CPU” for this. In .Net Core+ you have to explicitly target x86, which I think has to do with the comhost.dll that’s created now. This DLL is a bit of an abomination, consumed by an application that is entirely single threaded (PowerBuilder), so it is actually written as net8.0-windows with UseWindowsForms set to true to allow for a message pump so the UI doesn’t freeze entirely. Truly an old school Windows only application. And, it works. The PowerBuilder app is able to load the DLL just fine as I kept the GUID intact, as well as the ProgId.
However, because I’m never entirely satisfied when something doesn’t completely make sense to me, I modified (and also upgraded to net8.0-windows because it IS a Winforms app) the C# test app we install alongside the DLL to allow the user to load the DLL as a project reference, or as a COM object. It’s a quick way to verify things on the client where this package is installed. Here is where I hit problems two different ways.
What I found is that if I delete the deps.json file of the DLL, I can load it via it’s ProgId doing this:
var dynamic? _myDll = null;
var readerType = Type.GetTypeFromProgID("MyProgId");
_myDll = Activator.CreateInstance(readerType);
In the debugger this works every time. Installed, this throws an exception:
80040111 ClassFactory cannot supply requested class (Exception from HRESULT: 0x80040111 (CLASS_E_CLASSNOTAVAILABLE))
If I delete the generated deps.json file, however, it works via COM just fine from my C# app. I have exhaustedly gone through the generated file and verified every dependency is installed alongside the DLL, including dependencies of dependencies. I have verified the COM registration (though really that’s obvious because it does work.)
In frustration, I decided to go even older school and write a tiny console app in C++/Win32 to load the DLL via COM, CoInit the object, pull in the exposed interface, and call one method. I can’t get this to work either. I can load the COM object and I can instantiate it, but I cannot seem to get the interface to load properly. It’s been a long time since I’ve done C/C++ and longer since I’ve dealt with COM.
Here’s the gyst of what I have:
C#
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
[ProgId("MyProgId")]
[Guid("NewGuidAfterUpgrade")]
public class MyDll : IMyDll
{
//example
public int Connect()
{
//do stuff
return 0;
}
}
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[Guid("OldGuidBeforeUpgrade")]
public interface IMyDll
{
int Connect();
}
project file attributes:
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<Platforms>x86</Platforms>
<RuntimeIdentifiers>win-x86</RuntimeIdentifiers>
<UseWindowsForms>true</UseWindowsForms>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>MyDll.snk</AssemblyOriginatorKeyFile>
<EnableComHosting>true</EnableComHosting>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
Here’s one of the iterations of me trying to use in c++:
#include <iostream>
#include <comdef.h>
#include <atlbase.h>
#include <comutil.h>
#pragma comment(lib, "comsuppw.lib")
static const GUID IID_IMyDll =
{ broken out guid };
interface IMyDll : public IDispatch
{
virtual HRESULT STDMETHODCALLTYPE Connect(int* result) = 0;
};
int main()
{
// Initialize COM
HRESULT hr = CoInitialize(NULL);
if (FAILED(hr)) {
std::cerr << "Failed to initialize COM library." << std::endl;
return 1;
}
// Define the ProgID of the COM object
CLSID clsid;
hr = CLSIDFromProgID(L"MyProgId", &clsid);
if (FAILED(hr)) {
std::cerr << "Failed to get CLSID from ProgID." << std::endl;
CoUninitialize();
return 1;
}
// Create an instance of the COM object
CComPtr<IUnknown> pUnk;
hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnk);
if (FAILED(hr)) {
std::cerr << "Failed to create COM object." << std::endl;
CoUninitialize();
return 1;
}
// Query for the IMyDll interface: This is where things go south.
CComPtr<ICardReader> pMyDll;
hr = pUnk->QueryInterface(IID_ICardReader, (void**)&pMyDll);
if (FAILED(hr)) {
std::cerr << "Failed to query ICardReader interface." << std::endl;
CoUninitialize();
return 1;
}
// Prepare the method name
OLECHAR* methodName = (OLECHAR*)L"Connect"; // Cast to OLECHAR*
DISPID dispid;
hr = pMyDll->GetIDsOfNames(IID_NULL, &methodName, 1, LOCALE_USER_DEFAULT, &dispid);
if (FAILED(hr)) {
std::cerr << "Failed to get method ID." << std::endl;
CoUninitialize();
return 1;
}
// Clean up
CoUninitialize();
return 0;
}
I’ve tried several thing iterations around what happens after getting the pUnk loaded. This is truly successful. I went through quite a lot with the Debugger to verify it successfully initializes everything in the loaded COM object. Afterwards though, depending on the GUID, etc, I either flat out get an error, or I successfully load pMyDll but Connect returns OK without actually calling the method.
These days the latest versions of .Net have abandoned the TLB. It’s possible to roll your own, but that’s a bit outside my experience. Also, as mentioned earlier, this is all 32 bit code, and I am using SYSWOW regsvr32.
I’d love to better understand the C# deps.json issue, as well as what I may need to do above to get the C++ console app working.