Reg-free COM 개체 사용을 위한 manifest 파일 생성 도구 - COMRegFreeManifest
COM 객체를 regsvr32.exe로 등록하지 않아도, manifest 파일을 통해 직접 생성해 사용할 수 있다고 설명했었습니다.
Registry 등록 없이 COM 개체 사용
; https://www.sysnet.pe.kr/2/1/262
일례로 다음의 글들이 모두 그런 식으로,
C# - VLC(ActiveX) 컨트롤을 레지스트리 등록없이 사용하는 방법
; https://www.sysnet.pe.kr/2/0/1640
eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지
; https://www.sysnet.pe.kr/2/0/12134
C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법
; https://www.sysnet.pe.kr/2/0/12129
COM 객체를 등록하지 않고 같은 폴더에 있는 dll로부터 로드하고 있습니다. 그런데... 가끔 해보는 거지만 그때마다 manifest 파일을 일일이 만들려니 꽤나... ^^; 귀찮습니다. 그래서 어차피, tlbimp.exe가 생성한 COM Interop DLL만 있으면 나머지는 Reflection으로 COM 객체의 정보를 열람할 수 있기 때문에 manifest 만드는 것을 자동화했습니다.
다음 코드는 간략하게 만들어 본 것이고,
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace COMRegFreeManifest
{
class Program
{
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("COMRegFreeManifest.exe [com_dll_path]");
return;
}
string path = args[0];
string currentPath = Environment.CurrentDirectory;
string dllManifestPath = Path.Combine(currentPath, Path.GetFileName(path));
string exeManifestPath = Path.Combine(currentPath, "Sample.exe");
string impLibPath = Path.Combine(Path.GetTempPath(), "test.dll");
try
{
if (RunTlbImp(path, impLibPath) == false)
{
Console.WriteLine("TLBIMP not work");
return;
}
byte[] buf = File.ReadAllBytes(impLibPath);
Assembly asm = Assembly.ReflectionOnlyLoad(buf);
ExportDllManifest(asm, impLibPath, dllManifestPath);
ExportExeManifest(asm, impLibPath, dllManifestPath, exeManifestPath);
}
finally
{
if (File.Exists(impLibPath) == true)
{
File.Delete(impLibPath);
}
}
}
private static void ExportExeManifest(Assembly asm, string impLibPath, string dllManifestPath, string exeManifestPath)
{
string manifestTemplate = @"<?xml version=""1.0"" encoding=""utf-8""?>
<asmv1:assembly manifestVersion=""1.0"" xmlns=""urn:schemas-microsoft-com:asm.v1"" xmlns:asmv1=""urn:schemas-microsoft-com:asm.v1"" xmlns:asmv2=""urn:schemas-microsoft-com:asm.v2"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"">
<assemblyIdentity version=""1.0.0.0"" name=""MyApplication.app""/>
<trustInfo xmlns=""urn:schemas-microsoft-com:asm.v2"">
<security>
<requestedPrivileges xmlns=""urn:schemas-microsoft-com:asm.v3"">
<requestedExecutionLevel level=""asInvoker"" uiAccess=""false"" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns=""urn:schemas-microsoft-com:compatibility.v1"">
<application>
</application>
</compatibility>
<dependency>
<dependentAssembly asmv2:dependencyType=""install"" asmv2:codebase=""{0}.manifest"">
<assemblyIdentity name=""{0}"" version=""{1}"" type=""win32"" />
</dependentAssembly>
</dependency>
</asmv1:assembly>
";
string dllName = Path.GetFileName(dllManifestPath);
string version = asm.GetName().Version.ToString();
string manifestText = string.Format(manifestTemplate, dllName, version);
File.WriteAllText(exeManifestPath + ".manifest", manifestText);
}
private static void ExportDllManifest(Assembly asm, string impLibPath, string dllManifestPath)
{
string txt = GetManifestContents(asm, dllManifestPath);
File.WriteAllText(dllManifestPath + ".manifest", txt);
}
static string GetManifestContents(Assembly asm, string dllManifestPath)
{
string manifestTemplate = @"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<assembly xmlns=""urn:schemas-microsoft-com:asm.v1"" manifestVersion=""1.0"">
<assemblyIdentity version=""{0}"" name=""{1}"" type=""win32"">
</assemblyIdentity>
<file name=""{1}"">
<comClass clsid=""{2}"" threadingModel=""Apartment"" tlbid=""{3}"" />
<typelib tlbid=""{3}"" version=""{4}"" helpdir="""" resourceid=""0"" />
</file>
{5}
</assembly>
";
string interfaceTemplate = @"
<comInterfaceExternalProxyStub name=""{0}""
iid=""{1}""
proxyStubClsid32=""{{00020424-0000-0000-C000-000000000046}}""
baseInterface=""{2}""
tlbid=""{3}"">
</comInterfaceExternalProxyStub>
";
string version = asm.GetName().Version.ToString();
string dllName = Path.GetFileName(dllManifestPath);
string clsid = $"{{{GetClsid(asm)}}}";
string tlbid = $"{{{GetAssemblyAttr(asm, typeof(GuidAttribute))}}}";
string tlbVersion = GetTypeLibVersion(asm);
StringBuilder sb = new StringBuilder();
foreach (Type type in asm.GetTypes())
{
if (type.IsInterface == false || HasCoClassAttribute(type))
{
continue;
}
string interfaceName = type.Name;
string interfaceIid = $"{{{GetGuid(type)}}}";
string baseInterfaceIid = GetBaseInterfaceIID(type);
string interfaceText = string.Format(interfaceTemplate, interfaceName, interfaceIid, baseInterfaceIid, tlbid);
sb.AppendLine(interfaceText);
}
string manifestText = string.Format(manifestTemplate, version, dllName, clsid, tlbid, tlbVersion, sb.ToString());
return manifestText;
}
private static string GetBaseInterfaceIID(Type type)
{
foreach (CustomAttributeData attr in type.GetCustomAttributesData())
{
if (attr.Constructor.DeclaringType == typeof(TypeLibTypeAttribute))
{
TypeLibTypeFlags flags = (TypeLibTypeFlags)attr.ConstructorArguments[0].Value;
if (flags.HasFlag(TypeLibTypeFlags.FDual))
{
return "{00020400-0000-0000-C000-000000000046}";
}
else
{
return "{00000000-0000-0000-C000-000000000046}";
}
}
}
return "";
}
private static string GetTypeLibVersion(Assembly asm)
{
foreach (CustomAttributeData attr in asm.GetCustomAttributesData())
{
if (attr.Constructor.DeclaringType == typeof(TypeLibVersionAttribute))
{
return $"{attr.ConstructorArguments[0].Value}.{attr.ConstructorArguments[1].Value}";
}
}
return "";
}
private static bool HasCoClassAttribute(Type type)
{
foreach (CustomAttributeData attr in type.GetCustomAttributesData())
{
if (attr.Constructor.DeclaringType == typeof(CoClassAttribute))
{
return true;
}
}
return false;
}
private static string GetClsid(Assembly asm)
{
foreach (Type type in asm.GetTypes())
{
foreach (CustomAttributeData attr in type.GetCustomAttributesData())
{
if (attr.Constructor.DeclaringType == typeof(CoClassAttribute))
{
return GetGuid(attr.ConstructorArguments[0].Value as Type);
}
}
}
return "";
}
private static string GetGuid(Type type)
{
foreach (CustomAttributeData attr in type.GetCustomAttributesData())
{
if (attr.Constructor.DeclaringType == typeof(GuidAttribute))
{
return attr.ConstructorArguments[0].Value as string;
}
}
return "";
}
private static string GetAssemblyAttr(Assembly asm, Type targetType)
{
foreach (CustomAttributeData attr in asm.GetCustomAttributesData())
{
if (attr.Constructor.DeclaringType == targetType)
{
return attr.ConstructorArguments[0].Value as string;
}
}
return "";
}
private static bool RunTlbImp(string path, string impLibPath)
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = GetTlbImpPath("tlbimp.exe");
psi.Arguments = $"\"{path}\" /out:\"{impLibPath}\"";
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
Process newProc = Process.Start(psi);
newProc.WaitForExit();
return File.Exists(impLibPath);
}
private static string GetTlbImpPath(string toolName)
{
using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\NETFXSDK", false))
{
foreach (string version in regKey.GetSubKeyNames())
{
using (RegistryKey versionKey = regKey.OpenSubKey(version, false))
{
foreach (string tool in versionKey.GetSubKeyNames())
{
using (RegistryKey toolKey = versionKey.OpenSubKey(tool))
{
string path = toolKey.GetValue("InstallationFolder") as string;
string tlbimpPath = Path.Combine(path, toolName);
if (File.Exists(tlbimpPath) == true)
{
return tlbimpPath;
}
}
}
}
}
}
return null;
}
}
}
실행은 단순하게, COM DLL 파일을 전달하면 됩니다.
c:\temp> COMRegFreeManifest.exe c:\test\my_atl.dll
그럼 my_atl.dll.manifest와 Sample.exe.manifest 파일이 각각 다음과 같은 식으로 생성됩니다.
[my_atl.dll.manifest]
(2024-07-21 업데이트) 참고로, DLL에 대한 manifest는 TLB 파일이 있는 경우 Windows SDK에 포함된 공식 도구인 mt.exe에 의해 생성하는 것도 가능합니다. 예를 들어, 이번 예제의 경우 다음과 같은 식으로 실행할 수 있습니다.
// 현재 디렉터리에 tlb 파일이 있어야 하지만, DLL 파일은 없어도 됨. (DLL 파일 이름을 주는 것은 manifest 내에 필요한 문자열이기 때문임)
c:\temp> mt -tlb:my_atl.tlb -dll:my_atl.dll -out:test.manifset
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" name="my_atl.dll" type="win32">
</assemblyIdentity>
<file name="my_atl.dll">
<comClass clsid="{2F71B57A-B6DF-4733-A29A-6ECA13FAE34F}" threadingModel="Apartment" tlbid="{0B2AAC68-8E4B-4BAA-85D7-4DF62A224D9F}" />
<typelib tlbid="{0B2AAC68-8E4B-4BAA-85D7-4DF62A224D9F}" version="1.0" helpdir="" resourceid="0" />
</file>
<comInterfaceExternalProxyStub name="IATLSimpleObject"
iid="{CB82A462-8F49-4434-987B-CB8FBC8A9115}"
proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"
baseInterface="{00020400-0000-0000-C000-000000000046}"
tlbid="{0B2AAC68-8E4B-4BAA-85D7-4DF62A224D9F}">
</comInterfaceExternalProxyStub>
</assembly>
[Sample.exe.manifest]
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
</application>
</compatibility>
<dependency>
<dependentAssembly asmv2:dependencyType="install" asmv2:codebase="my_atl.dll.manifest">
<assemblyIdentity name="my_atl.dll" version="1.0.0.0" type="win32" />
</dependentAssembly>
</dependency>
</asmv1:assembly>
오~~~ 제법 폼 나는군요. ^^ 이제 my_atl.dll.manifest 파일은 EXE 프로젝트에 포함해 "Copy to Output Directory" 옵션을 "Copy if newer"로 설정하고 Sample.exe.manifest 파일은 EXE 프로젝트 속성 창의 "Application" 범주의 "Resources" 영역에 "Manifest:" 콤보 박스에 등록해 주시면 됩니다.
(전체 프로젝트는 github에 올렸습니다. -
https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/COMRegFreeManifest)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]