I'm researching about the configuration of a private registration free WinSxS with the plain provision of assembly manifest files, to stitch Delphi executables (COM clients) and .NET (C#) COM visible DLLs together at deployment and runtime.
I already studied the documentation available at MSDN "Interoperating with Unmanaged Code", the sections about "COM Callable Wrapper" and "How to: Configure .NET Framework-Based COM Components for Registration-Free Activation" in particular.
After even more than one week of research and being (re-)directed in cycles of insufficient documentation, I decided to place my 1st question ever here.
The planned deployment structure looks as follows:
./install-root
├───ProgramSuite1
│ ├───bin
│ │ DelphiNativeCOMClient1.exe
│ │ DelphiNativeCOMClient1.exe.config
│ │ DelphiNativeCOMClient2.exe
│ │ DelphiNativeCOMClient2.exe.config
│ | ...
│ │
│ └───data
│ ...
├───ProgramSuite2
│ ├───bin
│ │ DelphiNativeCOMClient3.exe
│ │ DelphiNativeCOMClient3.exe.config
│ │ DelphiNativeCOMClient4.exe
│ │ DelphiNativeCOMClient4.exe.config
│ | ...
│ │
│ └───data
│ ...
└───SharedLibs
├───MyCompany.Libs.Set1
│ MyCompany.Libs.Set1.manifest
│ SomeManagedCOMServerA.dll
│ SomeNativeCOMServerB.dll
│ SomeNativeCOMServerC.dll
│
└───MyCompany.Libs.Set2
MyCompany.Libs.Set2.manifest
SomeManagedCOMServerB.dll
SomeNativeCOMServerX.dll
SomeManagedCOMServerA.dll
Here's a short sketch about the implementation of the implementation of the Delphi native executables and the C# .NET COM server DLLs (I left out the examples for the native COM Servers, since this stuff already works well and is out of question).
I mainly followed what was provided at "Registration-Free Activation of COM Components: A Walkthrough". The main difference is that I'm utilizing Delphi rather than C, C++ or old VB as a native client.
TestDllConsoleApp.exe
TestDllConsoleApp.dpr
program TestDllConsoleApp;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
DllTests.Common,
WinApi.ActiveX,
WinApi.Windows,
// These were generated using the tlbimplib tool
CSharpCOMDll_TLB in 'CSharpCOMDll_TLB.pas',
mscorlib_TLB in 'mscorlib_TLB.pas';
var
comInterface1 : ICOMInterface1;
comInterface2 : ICOMInterface2;
intf1CoClass : _COMImplClass1;
intf2CoClass : _COMImplClass2;
res : HRESULT;
coInitializeRes : integer;
begin
//Initialize COM
coInitializeRes := CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
if (coInitializeRes <> S_OK) and (coInitializeRes <> S_FALSE) then begin
System.ExitCode := 1;
Exit(); // GUARD
end;
try
try
intf1CoClass := CoCOMImplClass1.Create();
res := intf1CoClass.QueryInterface(IID_ICOMInterface1,comInterface1);
System.WriteLn(comInterface1.GetModuleName());
intf2CoClass := CoCOMImplClass2.Create();
res := intf2CoClass.QueryInterface(IID_ICOMInterface2,comInterface2);
System.WriteLn(comInterface2.GetModuleName());
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
finally
//Uninitialize COM
CoUninitialize();
end;
end.
TestDllConsoleApp.manifest
(embedded with resource ID 1)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity name="MyCompany.Software.Application" processorArchitecture="x86" version="1.0.0.0" type="win32" />
<description>A native COM client application.</description>
<asmv3:trustInfo>
<asmv3:security>
<asmv3:requestedPrivileges>
<asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
</asmv3:requestedPrivileges>
</asmv3:security>
</asmv3:trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows Server 2016 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 8.1 and Windows Server 2012 R2 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 8 and Windows Server 2012 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 and Windows Server 2008 R2 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows Vista and Windows Server 2008 -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
</application>
</compatibility>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="MyCompany.Libs.Set1" version="1.0.0.0" processorArchitecture="x86" />
</dependentAssembly>
</dependency>
</assembly>
TestDllConsoleApp.exe.config
(deployed at the same file location as the executable)
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="..\..\SharedLibs"/>
</assemblyBinding>
</runtime>
</configuration>
CSharpCOMDll.dll
(will be deployed at the SharedLibs\MyCompany.Libs.Set1
directory)
Assemblyinfo.cs
#region Using directives
using System;
using System.Reflection;
using System.Runtime.InteropServices;
#endregion
[assembly: AssemblyTitle ("CSharpCOMDll")]
[assembly: AssemblyProduct ("CSharpCOMDll")]
[assembly: AssemblyCopyright ("Copyright 2018")]
[assembly: ComVisible (true)]
[assembly: AssemblyVersion ("1.0.0.0")]
[assembly: Guid ("045d53ab-a9e4-4036-a21b-4fe0cf433065")]
// Using namespaces ...
namespace CSharpCOMDll
{
[Guid("6BDAF8DD-B0CF-4CBE-90F5-EA208D5A2BB0")]
public interface ICOMInterface1
{
string GetModuleName();
}
[Guid("4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805")]
public class COMImplClass1 : ICOMInterface1
{
public string GetModuleName()
{
return typeof(COMImplClass1).Module.FullyQualifiedName;
}
}
}
// Using namespaces ...
namespace CSharpCOMDll
{
[Guid("BE69E9C7-1B37-4CA8-A3C1-10BFA9230940")]
public interface ICOMInterface2
{
string GetModuleName();
}
[Guid("067E5980-0C46-49C7-A8F0-E830877FB29C")]
public class COMImplClass2 : ICOMInterface2
{
public string GetModuleName()
{
return typeof(COMImplClass1).Module.FullyQualifiedName;
}
}
}
CSharpCOMDll.manifest
(Embedded into the DLL with resource ID 2)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<assemblyIdentity
type="win32"
processorArchitecture="x86"
name="CSharpCOMDll"
version="1.0.0.0" />
<clrClass
clsid="{4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}"
progid="CSharpCOMDll.COMImplClass1"
threadingModel="Both"
name="CSharpCOMDll.COMImplClass1"
runtimeVersion="v4.0.30319">
</clrClass>
<clrClass
clsid="{067E5980-0C46-49C7-A8F0-E830877FB29C}"
progid="CSharpCOMDll.COMImplClass2"
threadingModel="Both"
name="CSharpCOMDll.COMImplClass2"
runtimeVersion="v4.0.30319">
</clrClass>
</assembly>
And finally the assembly manifest as resolved from the TestDllConsoleApp.manifest
dependency
entries:
MyCompany.Libs.Set1.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="MyCompany.Libs.Set1" version="1.0.0.0" processorArchitecture="x86" />
<file name="CSharpCOMDll.dll">
<comClass
clsid="{4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}"
threadingModel="Both"
/>
<comClass
clsid="{067E5980-0C46-49C7-A8F0-E830877FB29C}"
threadingModel="Both"
/>
<comInterfaceProxyStub
name="ICOMInterface1"
iid="{6BDAF8DD-B0CF-4CBE-90F5-EA208D5A2BB0}"
proxyStubClsid32="????"
/>
<comInterfaceProxyStub
name="ICOMInterface2"
iid="{BE69E9C7-1B37-4CA8-A3C1-10BFA9230940}"
proxyStubClsid32="????"
/>
</file>
</assembly>
It seems I'm halfway there, but still can't diagnose the actual problem.
There are two variants of failure right now (Please note, that deploying the managed COM server DLLs beside the executable instead of referring to the resolved manifests directory just works fine and as intended):
I completely remove the proxyStubClsid32
attribute in the global manifest:
Starting the executable ends up with an exception
EOleSysError: Error in dll, clsid = {4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}
Debugging the exception leads to a HRESULT
value
Error in the DLL (Exception from HRESULT: 0x800401F9 (CO_E_ERRORINDLL))
I provide a proxyStubClsid32
attribute in the global manifest:
CLSID
) as mentioned in the comClass
elements clsid
attribute.,pas
file there.Both variants leave me with a pretty useless error traceable with the sxstrace
tool1:
...
INFORMATION: Manifestdatei ".\install-root\SharedLibs\MyCompany.Libs.Set1\MyCompany.Libs.Set1.MANIFEST" wird analysiert.
INFORMATION: Die Manifestsdefinitionsidentität ist ",processorArchitecture="x86",type="win32",version="1.0.0.0"".
FEHLER: Bei der Generierung des Aktivierungskontextes ist ein Fehler aufgetreten.
Beendet die Generierung des Aktivierungskontextes.
Note that there wasn't any concise error/info message like
... cannot resolve assembly XY ...
before the Activation Context Generation screwed up. There's plenty of references indicating this particular error situation.
Also the ubiquitous mentions of missing Visual C++ redistributable framework doesn't help here. I'm calling from Delphi, and that's something different.
Another attempt to reference the CSharpCOMDll.dll
explicitly (another dependency in the executable manifest), and just place it into the SharedLibs
got a successfully created Activation Context, but fails with a slightly different exception than before
EOleSysError: Cannot find file, clsid = {4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}
Does anyone here know how to do what I want straightforward, or what can be done additionally (besides the sxstrace
) to diagnose the problem in more depth.
I'm almost sure it must be possible to provide a deployment like this.
Researching further today, I realized that (despite the very similar terminology), resolving the ActivationContext with a private SxS and resolving the location of .NET DLLs which serve for a COM callable wrapper instantiation are two completely distinct and separated mechanisms. I mostly got that from these 2 and some more of Jufeng Zhang's brilliant and in depth explaining blog articles:
The problem with the locating of the unregistered .NET assemblies (managed COM server DLLs) is, that this will only happen inside the applications deployment directory and below.
Using any method like specifying a <codebase>
or <probing>
element inside the configuration <runtime>
section pointing outside the the directory where the .config
file is deployed, simply doesn't work.
I verified that using the Sysinternals Process Monitor and the Fusion log viewer tool2.
I'm not posting that as a final answer, because I'll try next somehow to trick that .NET mechanism to locate the managed COM server DLLs, using an assembly manifest, or native DLL specifying the dependencies and <probing>
/ <codebase>
element to redirect the locating mechanism.
As a last resort (sic!) it seems to be even possible to provide your own customized appDomainManagerAssembly
and appDomainManagerType
in the application configuration under the <runtime>
element.
I'm afraid we have to go for managing the AppDomain
ourselves using the CLR API from a native CLR Host.
Needs further investigation. One promising resource how to do that I found here:
"Customizing the Microsoft .NET Framework Common Language Runtime"
1) Excuse the German error messages please. I don't have an English version compiler at hand. But the translation given at google should work well.
2) So the question about better tools for diagnosing the problems, can be considered as solved.
- Is it even possible to provide a deployment structure like mentioned above, and maintain certain .NET COM server DLLs outside the referring executables locations?
It's definitely not possible(!) to resolve any assemblies provided for the intrinsic CLR hosting mechanism outside the AppDomain
's executable directory.
You can use the
<probing privatePath="<some directory below your executable's location>" />`
But the <probing>
tag works differently for SxS resolving (appearing under the manifest <windows>
tag), and the CLR's mechanism to instantiate COM Callable Wrappers appearing under the <runtime>
tag.
It's even undocumented, but specifying
<windows>
<probing privatePath="../<xxx>" />
</windows>
for resolving the SxS dependencies supports relative paths for <xxx>
up to 3 ../
parent directory levels from your executable's location works for any native COM server, while
<runtime>
<probing privatePath="../<xxx>" />
<!-- ^^^ -->
</runtime>
or
<runtime>
<codebase href="../<xxx>/xyz.dll" version="1.0.0.0"/>
<!-- ^^^ -->
</runtime>
won't allow you to specify assembly locations pointing to locations upwards outside your AppDomain's hosting directory using the standard windows .NET mechanisms to resolve candidates to be instantiated as COM Callable Wrappers (hosted by the mscoreee.dll
).
Descending deeper from your executable's deployment directory works well and as intended.
One way (probably the easiest) to intercept the CLR probing mechanism, is to provide a custom AppDomainManager
implementation and specify it in the <appDomainManagerAssembly>
and <appDomainManagerType>
elements of the application configuration file:
<configuration>
<runtime>
<appDomainManagerAssembly value="MyAppDomainMgr" />
<appDomainManagerType value="MyAppDomainMgr.MyCustomAppDomainMgr, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</runtime>
<configuration>
The implementation of the MyAppDomainMgr.MyCustomAppDomainMgr
class should be in a .NET assembly, e.g. written in C#:
namespace MyAppDomainMgr
{
[ComVisible(true)]
public class MyCustomAppDomainMgr : AppDomainManager
{
public MyCustomAppDomainMgr()
{
}
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
{
Console.Write("Initialize new domain called: ");
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
InitializationFlags =
AppDomainManagerInitializationOptions.RegisterWithHost;
// Several ways to control settings of the AppDomainSetup class,
// or add a delegate for the AppDomain.CurrentDomain.AssemblyResolve
// event.
}
}
}
As soon your unmanaged application tries to access some COM interface (COM Callable Wrapper) through the CLR (i.e. a call to CoCreateInstance()
), the MyCustomAppDomainMgr
class will be instantiated and the InitializeNewDomain()
function is called first.
The least intrusive way seems to be to add that delegate function:
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
{
// ...
AppDomain.CurrentDomain.AssemblyResolve +=
new ResolveEventHandler(MyCustomAssemblyResolver);
}
static Assembly MyCustomAssemblyResolver(object sender, ResolveEventArgs args)
{
// Resolve how to find the requested Assembly using args.Name
// Assembly.LoadFrom() would be a good way, as soon you found
// some matching Assembly manifest or DLL whereever you like to look up for it
}
The resulting assembly (MyAppDomainMgr.dll
), must be placed beneath the unmanaged executable application.
User contributions licensed under CC BY-SA 3.0