4

Working on creating a .NET 8 library which will be called from Excel VBA via COM interop. I've had some success setting this up using regsvr32, but having gone down that path on earlier projects, I'm very attracted to the idea of "reg-free COM". It would make things a lot easier for me. Of course, it's giving me a bit of trouble.

First off, I've been following these two pieces of guidance:
Expose .NET Core components to COM
How to correctly create COM classes with .NET8?

Per that guidance, I've used the DSCOM tool to generate my .tlb file, which Excel can reference.

This is my project file:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net8.0-windows</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <EnableComHosting>true</EnableComHosting>
        <EnableRegFreeCom>true</EnableRegFreeCom>
    </PropertyGroup>

    <ItemGroup>
      <Using Include="System.Runtime.InteropServices" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="dSPACE.Runtime.InteropServices.BuildTasks" Version="1.15.2">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
    </ItemGroup>

    <PropertyGroup>
        <DsComTypeLibraryEmbedAfterBuild>true</DsComTypeLibraryEmbedAfterBuild>
        <DsComOverideLibraryName>MyTest</DsComOverideLibraryName>
    </PropertyGroup>
</Project>

As you can see, I have EnableRegFreeCom set. I am getting all the following files from my build:

MyTest.COM.dll
MyTest.COM.tlb
MyTest.COM.comhost.dll
MyTest.COM.X.manifest
[plus a few others I don't thing are relevant]

I have the following simple test object I'm attempting to use:

[assembly: ComVisible(true)]
[assembly: ClassInterface(ClassInterfaceType.None)]

namespace MyTest.COM
{
    [Guid("c99bc767-9def-4f24-8e6c-dc89496f90d8")]
    public class COMDebug : ICOMDebug
    {
        public int Add(int x, int y) => x + y;
    }

    [Guid("ac1bd331-2979-4ad3-ae29-d839d2d9d64c")]
    public interface ICOMDebug
    {
        int Add(int x, int y);
    }
}

I've found a few places that give examples of how to use regfree COM from VBA. This answer specifically says it is possible and gives a psudo-example. Here is my non-working VBA code, running from Excel:

Sub Test()
    Dim ctx
    Dim dbg
    Dim r As Integer
    
    Set ctx = CreateObject("Microsoft.Windows.ActCtx")
    ctx.Manifest = "C:\Path\To\Files\MyTest.COM.X.manifest"
    Set dbg = ctx.CreateObject("MyTest.COM.COMDebug")
    
    r = dbg.Add(2, 2)
    MsgBox b
End Sub

The expression ctx.CreateObject("MyTest.COM.COMDebug") throws an error:

Run-time error '-2147221231 (80040111)':

Automation error
ClassFactory cannot supply requested class

My research has not been able to turn up much helpful info on this problem. I've verified that the ProgId I'm using matches what's output into the manifest file. All my output files are in the same folder- I've even tried putting all of them into the same folder as Excel.exe just as a test. The above error message is also different than what I get if I try to use CreateObject on a ProgId that doesn't exist, or if I try to use CreateObject("MyTest.COM.COMDebug") without a customized ActCtx object, so I feel like I am on the right track.

Any idea how to make this work? Or why it isn't working?

EDIT:

This question was also pointed out to me: Exact steps for registration-free COM interop in .NET (invoke copied COM dll without regsvr32) It's one I hadn't seen, but it involves consuming regfree COM from a custom application being authored (like another .NET app). I don't see a way to apply this information to my problem in consuming the COM dll from Excel VBA.

11
  • This question is similar to: Exact steps for registration-free COM interop in .NET (invoke copied COM dll without regsvr32). If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. Commented Jun 1 at 22:43
  • The following may be of interest: Registration-Free COM Interop Commented Jun 1 at 22:44
  • Did you try placing both the DLL and manifest file in the same folder as Excel.exe (ex:: %ProgramFiles%\Microsoft Office\root\Office16)? Commented Jun 2 at 2:56
  • I did try that, yes. Commented Jun 2 at 3:25
  • 1
    The thing is Microsoft broke lots of COM built-in support with the .NET Core move. TLB generation was lost during this journey, although it's a quite important part of standard COM. They should provide it as an SDK tool or something but they don't seem to care. That's why some folks created the DsCom tool, that has its own bad temper sometimes. Another way is to keep a .NET Framework project around and use it only to build the TLB just like in the old times (you can share the interface & class .cs files between .NET Framework and .NET Core for example and defer the real meat to Core code) Commented Jun 16 at 16:10

1 Answer 1

1

It took a lot of work, but I was actually able to get my COM setup working.

It was entirely thanks to @SimonMourier who, through comments, pointed me to their working reg-free COM example repo on GitHub:
https://github.com/smourier/RefreeNetCom

The above example worked when I downloaded it, and I was able to make modifications to the example code and successfully use the modified classes. This is a huge step up in that it actually works for me, but it also comes with significant cost. Rather than using some tool like DSCOM (which I was NOT able to make work for me in the end), the example relies on the developer manually authoring an assembly manifest for both the COM server and COM client, as well as an .idl file which can then be transformed into a .tlb on build.

This means for each class I want to expose to COM reg-free, I need to:

  1. Declare an interface for the class, duplicating every single COM-visible member.
  2. Mark the interface with ComImport, InterfaceType(ComInterfaceType.InterfaceIsDual) and Guid("[interface-guid]").
  3. Mark the class with ComVisible, ClassInterface(ClassInterfaceType.None),Guid("[class-guid]") and ProgId("[MyApp.MyClass]").
  4. Have the class implement both this new interface, and the internal IDispatch interface. (see example repo for details)
  5. Add a comClass element to the COM server's assembly manifest, where I need to specify the name, guid and progid of the class.
  6. Add a comInterfaceExternalProxyStub element to the COM server's assembly manifest, where I need to specify the name and guid of the interface for the class.
  7. Add an interface to the .idl file including the name and guid of the C# interface, as well as re-writing the signature of every member in the MIDL language using MIDL type names and syntax.
  8. Add a coclass to the .idl file, including the guid of the class, the name of the class and the name of the interface.

If you include the interface name and progid, the name of the class needs to be repeated in at least 8 places. And every member of the class needs to have its signature written in 3 places. This doesn't even mention having to keep track of the guids, also repeated in several places. And none of this includes the boilerplate code required in each of these places just to define the output library/assembly itself, generate the .tlb from the .idl, etc.

I can't imagine the un-debug-able errors you might get if you accidentally typo the name in one spot, or use the wrong guid somewhere, or forget to change your method signature after an update.

BUT IT DOES WORK

So, seeing as this was the only way for me, I decided to get smart. I was NOT about to start a project with a framework requiring this much fragile, duplicate code. What I ended up actually doing, is bodge together a source generator.

With the use of partial classes, this let me auto-generate the COM interface and most of the attributes on the C# side. I also broke the rules about not using file IO in a generator/analyzer, and made the generator output the COM server .manifest XML file, as well as the .idl file.

It took tens of hours and many system reboots thanks to strange COM errors during development and testing, but I now have a system where (as long as I stick to simple data types in my class members) I write a normal C# class, build normally, and then just use it in VBA.

My final VBA test code looked something like this:

Option Explicit

Sub Test()
    Dim ctx
    Set ctx = CreateObject("Microsoft.Windows.ActCtx")
    
    Dim path
    path = "full path to COM client manifest file"
    ctx.Manifest = path
    
    Dim a As RegFreeComTest.COMTest
    
    Set a = ctx.CreateObject("RegFreeComTest.COMTest")
    
    Dim msg As String
    
    msg = "Sum:" & a.Add(2, 3)
    msg = msg & Chr(13) & "Product:" & a.Multiply(2, 3)
    msg = msg & Chr(13) & "Backwards:" & a.Reverse("Backwards")
    msg = msg & Chr(13) & "NotTrue:" & a.Not(True)
    msg = msg & Chr(13) & "NotFalse:" & a.Not(False)
    
    MsgBox msg
End Sub

The COM client manifest file looks like this:

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
    <assemblyIdentity version="1.0.0.0" name="ExcelVBAClient.app"/>
    <dependency>
        <dependentAssembly>
            <assemblyIdentity type="win32" name="RegFreeComTest" version="1.0.0.0" />
        </dependentAssembly>
    </dependency>
</assembly>

Crucially, this manifest file needs to be placed in the folder with the COM server output files, but does not need to be placed with the COM client application. This means I don't need to put any extra files in with excel.exe, and I can instead maintain my own folder for my COM server dll. I can even have multiple copies of of this dll (such as a separate development copy). I can switch between versions by only changing the path to the correct manifest file (plus changing the referenced .tlb if I also want early-binding and type names).

At the requset of a comment, I've posted what I have on GitHub. Hopefully it helps someone else out there. https://github.com/Origon/KHS.COMCodeGen

Sign up to request clarification or add additional context in comments.

2 Comments

Do you have the Roslyn code or a Github repo that demonstrates what you've described above? I'm interested in seeing it.
Added a GitHub link. Hopefully it works off my computer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.