How to create isolated/reg-free COM between Visual

2020-03-28 02:57发布

问题:

I have to use a VB (COM) DLL in a C++ DLL. I figured out how to access the VB (COM) DLL from the C++ DLL and it works.

Now I've got the problem that I have to use isolated COM/reg-free COM because I can't register the DLL on every PC it has to be used on.

I figured out to use manifest-files to achieve this but I can't get it to work and I don't know what is wrong.

I have a VB DLL called AccConnVB.dll with the following AccConnVB.manifest file:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" 
 manifestVersion="1.0">
<assemblyIdentity
        type="win32"
        name="AccConnVB"
        version="1.0.0.0" />
<clrClass
        clsid="{70da7ef0-1549-4b27-9b00-ade5f37aecbe}"
        progid="AccConnVB.AccConnVB"
        threadingModel="Both"
        name="AccConnVB.tables" >
</clrClass>
</assembly>

And a C++ DLL called AccConn.dll with the following AccConn.manifest file:

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

My C++ DLL #defines _WIN32_DCOM in its stdafx.h and #imports the AccConnVB.tlb with no_namespace.

The following is a method from the C++ DLL:

JNIEXPORT jint JNICALL Java_natives_AccessConnection_refreshImportZwei
(JNIEnv *env, jclass jobj, jstring jDatabase){
  jint result;
  CComBSTR database;

  const char* nativeDatabase = env->GetStringUTFChars(jDatabase,0);
  database.Append((LPCSTR) nativeDatabase);

  CoInitializeEx(NULL,COINIT_MULTITHREADED);
  {
      ITablesPtr ptr;
      HRESULT hr = ptr.CreateInstance(__uuidof(tables));
      if (SUCCEEDED(hr))
      {
          result = (jint) ptr->refreshImportZwei(BSTR(database));
      }
  }
  CoUninitialize();
}

I made sure that everything works with a registered AccConnVB.dll, but using it on a computer where it is not registered fails.

The manifest files are embedded through executing mt.exe in cmd.exe with the following line: mt -manifest H:\AccConnVB.manifest -outputresource:H:\AccConnVB.dll;#1, for AccConn.dll and AccConn.manifest respectively.

Nothing else is set, when accessing AccConn.dll the AccConnVB.dll, the AccConn.manifest and the AccConnVB.manifest are in the same folder.

I followed the walkthrough here and tried some variations of it but nothing worked.

Thank you all very much in advance!

Attachment 1:

AccConn.manifest:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" 
  manifestVersion="1.0">
<assemblyIdentity
     type = "win32"
    name = "AccConn"
    version = "1.0.0.0" />
<file name="AccConnVB.dll">
<comClass
        clsid="{70da7ef0-1549-4b27-9b00-ade5f37aecbe}" 
    tlbid="{1CA12FB4-4A5C-41FF-A508-DCC6CE0D26CD}"
    progid="AccConnVB.tables" />
<typelib
    tlbid="{1CA12FB4-4A5C-41FF-A508-DCC6CE0D26CD}"
    version="1.0" helpdir="" />
</file>
<dependency>
    <dependentAssembly>
                <assemblyIdentity
                            type="win32"
                            name="AccConnVB"
                            version="1.0.0.0" />
    </dependentAssembly>
</dependency>
</assembly>

AccConnVB.manifest:

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

Attachment 2:

OfficeConn.manifest - C++-DLL - (changed the name):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<file name="OfficeConn.dll" hashalg="SHA1">
    <comClass clsid="{2C0D73B5-7AA4-4D17-970D-042804E206B2}" tlbid="{DB27F83B-DD8E-4AD8-A6A3-9232A9C1692C}">
    </comClass>
    <typelib tlbid="{DB27F83B-DD8E-4AD8-A6A3-9232A9C1692C}" version="1.0" helpdir="" flags="HASDISKIMAGE">
    </typelib>
</file>
<comInterfaceExternalProxyStub name="IOffice" iid="{19485BDA-0BAE-3527-8F9B-C90B43746B03}" tlbid="{DB27F83B-DD8E-4AD8-A6A3-9232A9C1692C}" proxyStubClsid32="{00020424-0000-0000-C000-000000000046}">
</comInterfaceExternalProxyStub>
<comInterfaceExternalProxyStub name="_offClass" iid="{1FA5D7FC-1CAE-49E0-A99E-B97E8FE3466E}" tlbid="{DB27F83B-DD8E-4AD8-A6A3-9232A9C1692C}" proxyStubClsid32="{00020424-0000-0000-C000-000000000046}">
</comInterfaceExternalProxyStub>
</assembly>

OfficeConnVB.manifest - VB-DLL - (changed the name):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="OfficeConnVB" version="1.0.0.0" publicKeyToken="38d072ba2818144d" processorArchitecture="msil">
</assemblyIdentity>
<clrClass clsid="{2c0d73b5-7aa4-4d17-970d-042804e206b2}" progid="OfficeConnVB.offClass" threadingModel="Both" name="OfficeConnVB.offClass" runtimeVersion="">
</clrClass>
<clrSurrogate clsid="{453B8C28-201B-3705-8CF1-C492C7B259EA}" name="Microsoft.Office.Interop.Outlook.OlDefaultFolders">
</clrSurrogate>
<clrSurrogate clsid="{B5181856-6837-3E65-AF7B-5020DD408339}" name="Microsoft.Office.Interop.Outlook.OlItemType">
</clrSurrogate>
<clrSurrogate clsid="{ECE70AEA-B928-3392-AE59-01373B29D3DA}" name="Microsoft.Office.Interop.Outlook.OlImportance">
</clrSurrogate>
<clrSurrogate clsid="{D74B5B88-8D75-3D21-A9BA-F6DBDC905F75}" name="Microsoft.Office.Interop.Word.WdSaveOptions">
</clrSurrogate>
<file name="OfficeConnVB.dll" hashalg="SHA1">
</file>
</assembly>

回答1:

You are making an oddly common mistake, expecting Windows to solve the chicken-and-egg problem. A brief word about the way manifest works might help.

Windows loads the content of a manifest when it load an executable file, the entries are added to an internal lookup table. Whenever an application first asks to create a COM object, underlying call is CoCreateInstance() which supplies the CLSID guid, it first consults that lookup table. If the CLSID is a match then the entry tells it what DLL must be loaded. If there is no match then it falls back to the traditional registry lookup.

The chicken-and-egg is that your DLL didn't get loaded yet. So its manifest entries are not yet active.

The egg must come first, the <clrClass> entry needs to go into the manifest you embed in the C++ executable. Like this:

  <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
    <assemblyIdentity type="win32" name="AccConn" version="1.0.0.0" />
    <file name="foobar.dll"/>
    <clrClass ...etc>
    </clrClass>
  </assembly>

MSDN article is here.



回答2:

You should use resource id #2 for DLLs, so change your execution of mt.exe to use #2 instead of #1. At least it is so for native DLLs, I'm not sure it has to be so for managed DLLs, so try it out.

You should move everything about the VB.NET DLL, except the dependency, to the VB.NET manifest, as that's the right place to keep that information.

You should use clrClass nodes, instead of comClass nodes, for .NET COM classes.

You may need to generate a type library (tlbexp.exe), and declare it in the manifest (optionally, you may also embed it in the DLL, but I advise against this if the DLL itself is big), if you want your classes and interfaces to be inspectable and marshalable by type library consumers, e.g. VBA, VC++ #import directive and code generators in general. On top of this, you may need to declare exported interfaces in the manifest, if you create objects outside VB.NET that implement its exported interfaces and which must me marshaled.

If your library is the sole generator of objects that implement the exported interfaces used in the same apartment, you don't need to generate nor declare a type library. I think .NET implements IMarshal, so this particular scenario works without a type library.


AccConn.manifest:

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

AccConnVB.manifest:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity type="win32"
                    name="AccConnVB"
                    version="1.0.0.0" />
  <file name="AccConnVB.dll" />
  <clrClass clsid="{70da7ef0-1549-4b27-9b00-ade5f37aecbe}"
            name="AccConnVB.tables"
            progid="AccConnVB.AccConnVB"
            threadingModel="Both" />
</assembly>

EDIT: According to your question's update.

You don't have a dependency element on the C++ manifest, so it doesn't know about the VB.NET assembly COM-wise without registration.

It seems you're declaring the VB.NET type library in the C++ manifest.

It seems you're declaring a comClass in the C++ manifest that's declared as a clrClass in the VB.NET manifest. Don't do this!