Skip to content

Commit

Permalink
Implement TLB support in the RegFreeComManifest generation code. (#51413
Browse files Browse the repository at this point in the history
)
  • Loading branch information
jkoritzinsky committed Apr 28, 2021
1 parent fe8a6c3 commit cecc76a
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.NET.HostModel.ComHost
{
public class ComHost
{
private const int E_INVALIDARG = unchecked((int)0x80070057);
// These need to match RESOURCEID_CLSIDMAP and RESOURCETYPE_CLSIDMAP defined in comhost.h.
private const int ClsidmapResourceId = 64;
private const int ClsidmapResourceType = 1024;
Expand Down Expand Up @@ -66,6 +67,10 @@ public class ComHost
{
throw new TypeLibraryDoesNotExistException(typeLibrary.Value, ex);
}
catch (HResultException hr) when (hr.Win32HResult == E_INVALIDARG)
{
throw new InvalidTypeLibraryException(typeLibrary.Value, hr);
}
}
}
updater.Update();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// The provided type library file is an invalid format.
/// </summary>
public class InvalidTypeLibraryException : Exception
{
public InvalidTypeLibraryException(string path)
{
Path = path;
}

public InvalidTypeLibraryException(string path, Exception innerException)
:base($"Invalid type library at '{path}'.", innerException)
{
Path = path;
}

public string Path { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class RegFreeComManifest
/// <param name="assemblyVersion">The version of the assembly.</param>
/// <param name="clsidMapPath">The path to the clsidmap file.</param>
/// <param name="comManifestPath">The path to which to write the manifest.</param>
public static void CreateManifestFromClsidmap(string assemblyName, string comHostName, string assemblyVersion, string clsidMapPath, string comManifestPath)
/// <param name="typeLibraries">The type libraries to include in the manifest.</param>
public static void CreateManifestFromClsidmap(string assemblyName, string comHostName, string assemblyVersion, string clsidMapPath, string comManifestPath, IReadOnlyDictionary<int, string> typeLibraries = null)
{
XNamespace ns = "urn:schemas-microsoft-com:asm.v1";

Expand All @@ -32,8 +33,27 @@ public static void CreateManifestFromClsidmap(string assemblyName, string comHos
new XAttribute("name", $"{assemblyName}.X"),
new XAttribute("version", assemblyVersion)));

XElement fileElement = new XElement(ns + "file", new XAttribute("name", comHostName));
var fileElement = CreateComHostFileElement(clsidMapPath, comHostName, ns);
if (typeLibraries is not null)
{
AddTypeLibElementsToFileElement(typeLibraries, ns, fileElement);
}
manifest.Add(fileElement);

XDocument manifestDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), manifest);
XmlWriterSettings settings = new XmlWriterSettings()
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)
};
using (XmlWriter manifestWriter = XmlWriter.Create(comManifestPath, settings))
{
manifestDocument.WriteTo(manifestWriter);
}
}

private static XElement CreateComHostFileElement(string clsidMapPath, string comHostName, XNamespace ns)
{
XElement fileElement = new XElement(ns + "file", new XAttribute("name", comHostName));
JsonElement clsidMap;
using (FileStream clsidMapStream = File.OpenRead(clsidMapPath))
{
Expand All @@ -52,17 +72,32 @@ public static void CreateManifestFromClsidmap(string assemblyName, string comHos

fileElement.Add(comClassElement);
}
return fileElement;
}

manifest.Add(fileElement);

XDocument manifestDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), manifest);
XmlWriterSettings settings = new XmlWriterSettings()
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)
};
using (XmlWriter manifestWriter = XmlWriter.Create(comManifestPath, settings))
private static void AddTypeLibElementsToFileElement(IReadOnlyDictionary<int, string> typeLibraries, XNamespace ns, XElement fileElement)
{
foreach (var typeLibrary in typeLibraries)
{
manifestDocument.WriteTo(manifestWriter);
try
{
byte[] tlbFileBytes = File.ReadAllBytes(typeLibrary.Value);
TypeLibReader reader = new TypeLibReader(tlbFileBytes);
if (!reader.TryReadTypeLibGuidAndVersion(out Guid name, out Version version))
{
throw new InvalidTypeLibraryException(typeLibrary.Value);
}
XElement typeLibElement = new XElement(ns + "typelib",
new XAttribute("tlbid", name.ToString("B")),
new XAttribute("resourceid", typeLibrary.Key),
new XAttribute("version", version),
new XAttribute("helpdir", ""));
fileElement.Add(typeLibElement);
}
catch (FileNotFoundException ex)
{
throw new TypeLibraryDoesNotExistException(typeLibrary.Value, ex);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.


using System;
using System.Buffers.Binary;

namespace Microsoft.NET.HostModel.ComHost
{
/// <summary>
/// Reads data from a COM Type Library file based on the official implementation in the Win32 function LoadTypeLib.
/// We do the reading ourselves instead of calling into the OS so we don't have to worry about the type library's
/// dependencies being discoverable on disk.
/// </summary>
internal class TypeLibReader
{
private byte[] tlbBytes;

public TypeLibReader(byte[] tlbBytes)
{
this.tlbBytes = tlbBytes;
}

private const int OffsetOfGuidOffset = sizeof(int) * 2;
private const int SizeOfGuidOffset = sizeof(int);

private const int OffsetOfMajorVersion = OffsetOfGuidOffset + SizeOfGuidOffset + sizeof(int) * 2 + sizeof(ushort) * 2;
private const int SizeOfMajorVersion = sizeof(ushort);
private const int OffsetOfMinorVersion = OffsetOfMajorVersion + SizeOfMajorVersion;
private const int SizeOfMinorVersion = sizeof(ushort);

private const int OffsetOfTypeInfosCount = OffsetOfMinorVersion + SizeOfMinorVersion + sizeof(int);
private const int SizeOfTypeInfosCount = sizeof(int);

private const int OffsetOfTablesStart = OffsetOfTypeInfosCount + SizeOfTypeInfosCount + sizeof(int) * 12;
private const int NumTablesToSkip = 5;
private const int SizeOfTableHeader = sizeof(int) * 4;

private Guid FindGuid(ReadOnlySpan<byte> fileContents)
{
checked
{
int typelibGuidEntryOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(OffsetOfGuidOffset));
int infoRefsOffsetCount = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(OffsetOfTypeInfosCount));
int infoBytes = infoRefsOffsetCount * SizeOfTypeInfosCount;
int guidTableOffset = OffsetOfTablesStart + infoBytes + SizeOfTableHeader * NumTablesToSkip;
int fileOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(fileContents.Slice(guidTableOffset));
return new Guid(fileContents.Slice(fileOffset + typelibGuidEntryOffset, 16).ToArray());
}
}

public bool TryReadTypeLibGuidAndVersion(out Guid typelibId, out Version version)
{
typelibId = default;
version = default;
try
{
var span = new ReadOnlySpan<byte>(tlbBytes);
typelibId = FindGuid(span);
ushort majorVer = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(OffsetOfMajorVersion));
ushort minorVer = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(OffsetOfMinorVersion));
version = new Version(majorVer, minorVer);
return true;
}
catch (System.OverflowException)
{
return false;
}
catch (System.IndexOutOfRangeException)
{
return false;
}
}
}
}
17 changes: 11 additions & 6 deletions src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,13 @@ public class SharedTestState : SharedTestStateBase
{
public string ComHostPath { get; }

public string ClsidString = "{438968CE-5950-4FBC-90B0-E64691350DF5}";
public string ClsidString { get; } = "{438968CE-5950-4FBC-90B0-E64691350DF5}";
public TestProjectFixture ComLibraryFixture { get; }

public string ClsidMapPath { get; }

public IReadOnlyDictionary<int, string> TypeLibraries { get; }

public SharedTestState()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand All @@ -172,14 +176,14 @@ public SharedTestState()
.BuildProject();

// Create a .clsidmap from the assembly
string clsidMapPath = Path.Combine(BaseDirectory, $"{ ComLibraryFixture.TestProject.AssemblyName }.clsidmap");
ClsidMapPath = Path.Combine(BaseDirectory, $"{ ComLibraryFixture.TestProject.AssemblyName }.clsidmap");
using (var assemblyStream = new FileStream(ComLibraryFixture.TestProject.AppDll, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.Read))
using (var peReader = new System.Reflection.PortableExecutable.PEReader(assemblyStream))
{
if (peReader.HasMetadata)
{
MetadataReader reader = peReader.GetMetadataReader();
ClsidMap.Create(reader, clsidMapPath);
ClsidMap.Create(reader, ClsidMapPath);
}
}

Expand All @@ -189,7 +193,7 @@ public SharedTestState()
$"{ ComLibraryFixture.TestProject.AssemblyName }.comhost.dll");

// Include the test type libraries in the ComHost tests.
var typeLibraries = new Dictionary<int, string>
TypeLibraries = new Dictionary<int, string>
{
{ 1, Path.Combine(RepoDirectories.Artifacts, "corehost_test", "Server.tlb") },
{ 2, Path.Combine(RepoDirectories.Artifacts, "corehost_test", "Nested.tlb") }
Expand All @@ -198,8 +202,9 @@ public SharedTestState()
ComHost.Create(
Path.Combine(RepoDirectories.HostArtifacts, "comhost.dll"),
ComHostPath,
clsidMapPath,
typeLibraries);
ClsidMapPath,
TypeLibraries);

}

protected override void Dispose(bool disposing)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;

using Microsoft.DotNet.Cli.Build.Framework;
using Microsoft.NET.HostModel.ComHost;
using Xunit;

namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting
{
public class ComhostSideBySide : IClassFixture<ComhostSideBySide.SharedTestState>
{
private readonly SharedTestState sharedState;

public ComhostSideBySide(SharedTestState sharedTestState)
{
sharedState = sharedTestState;
}

[Fact]
public void ActivateClass()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// COM activation is only supported on Windows
return;
}

string [] args = {
"activation",
sharedState.ClsidString
};

CommandResult result = Command.Create(sharedState.ComSxsPath, args)
.EnableTracingAndCaptureOutputs()
.DotNetRoot(sharedState.ComLibraryFixture.BuiltDotnet.BinPath)
.MultilevelLookup(false)
.Execute();

result.Should().Pass()
.And.HaveStdOutContaining("New instance of Server created");
}

[Fact]
public void LocateEmbeddedTlb()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// COM activation is only supported on Windows
return;
}

string [] args = {
"typelib_lookup",
sharedState.TypeLibId
};

CommandResult result = Command.Create(sharedState.ComSxsPath, args)
.EnableTracingAndCaptureOutputs()
.DotNetRoot(sharedState.ComLibraryFixture.BuiltDotnet.BinPath)
.MultilevelLookup(false)
.Execute();

result.Should().Pass()
.And.HaveStdOutContaining("Located type library by typeid.");
}

public class SharedTestState : Comhost.SharedTestState
{
public string TypeLibId { get; } = "{20151109-a0e8-46ae-b28e-8ff2c0e72166}";

public string ComSxsPath { get; }

public SharedTestState()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// COM activation is only supported on Windows
return;
}

using (var assemblyStream = new FileStream(ComLibraryFixture.TestProject.AppDll, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.Read))
using (var peReader = new System.Reflection.PortableExecutable.PEReader(assemblyStream))
{
if (peReader.HasMetadata)
{
string regFreeManifestPath = Path.Combine(BaseDirectory, $"{ ComLibraryFixture.TestProject.AssemblyName }.X.manifest");

MetadataReader reader = peReader.GetMetadataReader();
RegFreeComManifest.CreateManifestFromClsidmap(
ComLibraryFixture.TestProject.AssemblyName,
Path.GetFileName(ComHostPath),
reader.GetAssemblyDefinition().Version.ToString(),
ClsidMapPath,
regFreeManifestPath,
TypeLibraries
);
}
}

string testDirectoryPath = Path.GetDirectoryName(NativeHostPath);
string comsxsName = RuntimeInformationExtensions.GetExeFileNameForCurrentPlatform("comsxs");
ComSxsPath = Path.Combine(testDirectoryPath, comsxsName);
File.Copy(
Path.Combine(RepoDirectories.Artifacts, "corehost_test", comsxsName),
ComSxsPath);
File.Copy(
ComHostPath,
Path.Combine(testDirectoryPath, Path.GetFileName(ComHostPath)));
File.Copy(
ComLibraryFixture.TestProject.AppDll,
Path.Combine(testDirectoryPath, Path.GetFileName(ComLibraryFixture.TestProject.AppDll)));
File.Copy(
ComLibraryFixture.TestProject.DepsJson,
Path.Combine(testDirectoryPath, Path.GetFileName(ComLibraryFixture.TestProject.DepsJson)));
File.Copy(
ComLibraryFixture.TestProject.RuntimeConfigJson,
Path.Combine(testDirectoryPath, Path.GetFileName(ComLibraryFixture.TestProject.RuntimeConfigJson)));
}
}
}
}
1 change: 1 addition & 0 deletions src/native/corehost/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ add_subdirectory(mockhostpolicy)
add_subdirectory(nativehost)
if (CLR_CMAKE_TARGET_WIN32)
add_subdirectory(typelibs)
add_subdirectory(comsxs)
endif()

0 comments on commit cecc76a

Please sign in to comment.