Search code examples
c#c++visual-studiocom

Why do I have to go from x86 to msil back to x86 when I change signature of C# COM dll


I am trying to have a C# library expose it's classes/methods to a native C++ application via COM. I am using registration free com (i.e. with manifest files) so as to not have to register the COM library with Windows. I am running in to a problem where if I modify the C# library and do things like add a new class or change the name of a class then when that class is instantiated in the C++ application it throws a EETypeLoadException which my understanding means that the COM library doesn't match what the C++ app thinks it should look like. My projects are set to automatically generate the type library and manifest file for the C# library every time you build so that the C++ app will get the most recent version. To fix this error I have to modify the manifest file for the C++ app to say that the C# dll targets msil, build, then flip it back to x86 and build again. At that point the C++ program and C# program are in sync and throw no errors. I tried wiping out the output directory and obj directory to get rid of any possible cached files but this doesn't work, only toggling the manifest file back and forth.

Here is how I have my solution setup:

Create a new solution and add a C# Class Library (.Net Framework) called ExampleLib to it.

Add an interface that contains an arbitrary method:

namespace ExampleLib
{
    public interface ITestClass
    {
        int Add(int a, int b);
    }
}

Add a class with a guid attribute that uses that interface and implement the required method.

using System.Runtime.InteropServices;

namespace ExampleLib
{
    [Guid("5054A38A-946A-4BB2-854B-E1A31633DD77")]
    public class TestClass : ITestClass
    {
        public int Add(int a, int b)
        {
            return a + b;
        }
    }
}

Go in to the project properties. In the Application section click Assembly Information and check the Make assembly COM-Visible box. In the Build section set the platform target to x86 and change the Output Path to:

..\Debug\

In the Build Events section add the following to the Post-build event:

"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.2 Tools\tlbexp.exe" "$(TargetPath)" /out:"$(TargetDir)$(TargetName).tlb"
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x86\mt.exe" -managedassemblyname:"$(TargetPath)" -out:"$(TargetName).manifest" -nodependency

If you have a different version of the Windows SDK installed or to a different location you will need to alter the above commands to match the locations for tblexp.exe and mt.exe.

Add a C++ Console app to the project called ExampleClient.

In the Source Files section add a stdafx.cpp file with the following code (this is boilerplate):

#include "stdafx.h"

In the ExampleClient.cpp file replace to default code from the template with the following:

#include "stdafx.h"
#include "atlbase.h"
#include <conio.h>

#ifdef DEBUG
#import "..\Debug\ExampleLib.tlb" raw_interfaces_only
#else
#import "..\Release\ExampleLib.tlb" raw_interfaces_only
#endif

using namespace ExampleLib;

int main()
{
    HRESULT coInitResult = CoInitialize(0);
    try
    {
        ITestClassPtr pTest(__uuidof(TestClass));
    }
    catch (_com_error _com_err)
    {
        wprintf(L"\n %s", _com_err.ErrorMessage());
        auto _ = _getch();
    }
    CoUninitialize();
    return 0;
}

Also in the Source Files section add ExampleClient.exe.manifest file with the following:

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

  <dependency>
    <dependentAssembly>
      <assemblyIdentity name="ExampleLib" version="1.0.0.0" processorArchitecture="x86"/>
    </dependentAssembly>
  </dependency>
</assembly>

In the Header Files section add a stdafx.h file (boilerplate):

#pragma once

#include "targetver.h"

#include <stdio.h>
#include <tchar.h>

Also in the Header Files section add a targetver.h file (boilerplate):

#pragma once
#include <SDKDDKVer.h>

Right click on the C++ project and unload the project then edit the ExampleClient.vcxproj file. There are many changes to this file compared to what it currently is out of the box including precompiled header changes, not embedding the manifest, additional include directories, and copying the manifest to the output directory. To make reproducing simplier I will just include the entire file which should work fine

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Debug|x64">
      <Configuration>Debug</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|x64">
      <Configuration>Release</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <VCProjectVersion>16.0</VCProjectVersion>
    <ProjectGuid>{32A23FFD-3FD3-4191-8799-3A8DD34DCB94}</ProjectGuid>
    <Keyword>Win32Proj</Keyword>
    <RootNamespace>ExampleClient</RootNamespace>
    <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
  <ImportGroup Label="ExtensionSettings">
  </ImportGroup>
  <ImportGroup Label="Shared">
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <PropertyGroup Label="UserMacros" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <LinkIncremental>true</LinkIncremental>
    <EmbedManifest>false</EmbedManifest>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <LinkIncremental>true</LinkIncremental>
    <GenerateManifest>false</GenerateManifest>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <LinkIncremental>false</LinkIncremental>
    <EmbedManifest>false</EmbedManifest>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <LinkIncremental>false</LinkIncremental>
  </PropertyGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>Disabled</Optimization>
      <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <AdditionalIncludeDirectories>..\$(Configuration)</AdditionalIncludeDirectories>
      <AdditionalUsingDirectories>
      </AdditionalUsingDirectories>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
    </Link>
    <PostBuildEvent>
      <Command>xcopy $(ProjectName).exe.manifest ..\$(Configuration)\ /Y</Command>
    </PostBuildEvent>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>Disabled</Optimization>
      <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <AdditionalIncludeDirectories>..\$(Configuration)</AdditionalIncludeDirectories>
      <AdditionalUsingDirectories>
      </AdditionalUsingDirectories>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
    </Link>
    <PostBuildEvent>
      <Command>
      </Command>
    </PostBuildEvent>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <Optimization>MaxSpeed</Optimization>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
      <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
      <EnableCOMDATFolding>true</EnableCOMDATFolding>
      <OptimizeReferences>true</OptimizeReferences>
    </Link>
    <PostBuildEvent>
      <Command>xcopy $(ProjectName).exe.manifest ..\$(Configuration)\ /Y</Command>
    </PostBuildEvent>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <Optimization>MaxSpeed</Optimization>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
      <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    </ClCompile>
    <Link>
      <SubSystem>Console</SubSystem>
      <EnableCOMDATFolding>true</EnableCOMDATFolding>
      <OptimizeReferences>true</OptimizeReferences>
    </Link>
  </ItemDefinitionGroup>
  <ItemGroup>
    <ClInclude Include="stdafx.h" />
    <ClInclude Include="targetver.h" />
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="ExampleClient.cpp" />
    <ClCompile Include="stdafx.cpp">
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
      <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
    </ClCompile>
  </ItemGroup>
  <ItemGroup>
    <Manifest Include="ExampleClient.exe.manifest">
      <SubType>Designer</SubType>
    </Manifest>
  </ItemGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <ImportGroup Label="ExtensionTargets">
  </ImportGroup>
</Project>

Now set the debugger to target x86, set your startup project to ExampleClient, and run the app. You can step through it and you will notice it runs without errors. Working as intended.

Now go in to TestClass.cs and change the class name to something like TestClass2 (doesnt matter what). And also go in to the ExampleClient.cpp and change the reference to TestClass to TestClass2. Rebuild the solution. When you step through the code you will get an error when trying to instantiate TestClass2. To fix it go to the ExampleClient.exe.manifest file and change the processorArchitecture to msil. Rebuild. Then change processorArchitecture back to x86. Rebuild. Now the app will work again.

You should be able to make changes to the C# library and as long as you change the C++ app to reflect the changes it should work. You shouldn't have to toggle processorArchitecture back and forth. There must be something getting cached somewhere but I can't figure out where.


Solution

  • On Windows 10, the OS seems to cache manifest loading. For an experiment I would try an experiment of "touch"-ing your exe and exe manifest files. If you don't have some kind of touch, this command line will work:

    powershell (ls $1).LastWriteTime = Get-Date
    

    where $1 is the name of the file. I have a doskey macro defined as

    touch=powershell (ls $1).LastWriteTime = Get-Date
    

    You might try just touching the exe or the manifest. On Windows 7, I used to be able to log off or restart the computer to clear the manifest cache, but Windows 10 seems to be harder to clear. So I use touch so that Windows ignores the manifest cache and loads from the new files.

    (When I say "manifest cache", that's just a description of my deductions of what I think Windows is doing under the hood)