diff --git a/src/installer/managed/Microsoft.NET.HostModel/ComHost/ComHost.cs b/src/installer/managed/Microsoft.NET.HostModel/ComHost/ComHost.cs
index 2e1d8a949efb6e..3cf9cbf4e1004d 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/ComHost/ComHost.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/ComHost/ComHost.cs
@@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.NET.HostModel.ComHost
@@ -19,10 +21,12 @@ public class ComHost
/// The path of Apphost template, which has the place holder
/// The destination path for desired location to place, including the file name
/// The path to the *.clsidmap file.
+ /// Resource ids for tlbs and paths to the tlb files to be embedded.
public static void Create(
string comHostSourceFilePath,
string comHostDestinationFilePath,
- string clsidmapFilePath)
+ string clsidmapFilePath,
+ IReadOnlyDictionary typeLibraries = null)
{
var destinationDirectory = new FileInfo(comHostDestinationFilePath).Directory.FullName;
if (!Directory.Exists(destinationDirectory))
@@ -44,6 +48,26 @@ public static void Create(
using (ResourceUpdater updater = new ResourceUpdater(comHostDestinationFilePath))
{
updater.AddResource(clsidMapBytes, (IntPtr)ClsidmapResourceType, (IntPtr)ClsidmapResourceId);
+ if (typeLibraries is not null)
+ {
+ foreach (var typeLibrary in typeLibraries)
+ {
+ if (!ResourceUpdater.IsIntResource((IntPtr)typeLibrary.Key))
+ {
+ throw new InvalidTypeLibraryIdException(typeLibrary.Value, typeLibrary.Key);
+ }
+
+ try
+ {
+ byte[] tlbFileBytes = File.ReadAllBytes(typeLibrary.Value);
+ updater.AddResource(tlbFileBytes, "typelib", (IntPtr)typeLibrary.Key);
+ }
+ catch (FileNotFoundException ex)
+ {
+ throw new TypeLibraryDoesNotExistException(typeLibrary.Value, ex);
+ }
+ }
+ }
updater.Update();
}
}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/ComHost/InvalidTypeLibraryIdException.cs b/src/installer/managed/Microsoft.NET.HostModel/ComHost/InvalidTypeLibraryIdException.cs
new file mode 100644
index 00000000000000..4bb1a85d4d42fc
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/ComHost/InvalidTypeLibraryIdException.cs
@@ -0,0 +1,23 @@
+// 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
+{
+ ///
+ /// The provided resource id for the type library is unsupported.
+ ///
+ public class InvalidTypeLibraryIdException : Exception
+ {
+ public InvalidTypeLibraryIdException(string path, int id)
+ {
+ Path = path;
+ Id = id;
+ }
+
+ public string Path { get; }
+
+ public int Id { get; }
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/ComHost/TypeLibraryDoesNotExistException.cs b/src/installer/managed/Microsoft.NET.HostModel/ComHost/TypeLibraryDoesNotExistException.cs
new file mode 100644
index 00000000000000..28d20b547d4dd0
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/ComHost/TypeLibraryDoesNotExistException.cs
@@ -0,0 +1,21 @@
+// 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
+{
+ ///
+ /// The specified type library path does not exist.
+ ///
+ public class TypeLibraryDoesNotExistException : Exception
+ {
+ public TypeLibraryDoesNotExistException(string path, Exception innerException)
+ :base($"Type library '{path}' does not exist.", innerException)
+ {
+ Path = path;
+ }
+
+ public string Path { get; }
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
index e18eedf9804d28..bf1c0c489a93aa 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
@@ -44,6 +44,16 @@ public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] byte[] lpData,
uint cbData);
+ // Update a resource with data from a managed byte[]
+ [DllImport(nameof(Kernel32), SetLastError=true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool UpdateResource(SafeUpdateHandle hUpdate,
+ string lpType,
+ IntPtr lpName,
+ ushort wLanguage,
+ [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] byte[] lpData,
+ uint cbData);
+
[DllImport(nameof(Kernel32), SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EndUpdateResource(SafeUpdateHandle hUpdate,
@@ -277,7 +287,7 @@ public ResourceUpdater AddResourcesFromPEImage(string peFile)
return this;
}
- private static bool IsIntResource(IntPtr lpType)
+ internal static bool IsIntResource(IntPtr lpType)
{
return ((uint)lpType >> 16) == 0;
}
@@ -308,6 +318,32 @@ public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
return this;
}
+ ///
+ /// Add a language-neutral integer resource from a byte[] with
+ /// a particular type and name. This will not modify the
+ /// target until Update() is called.
+ /// Throws an InvalidOperationException if Update() was already called.
+ ///
+ public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
+ {
+ if (hUpdate.IsInvalid)
+ {
+ ThrowExceptionForInvalidUpdate();
+ }
+
+ if (!IsIntResource(lpName))
+ {
+ throw new ArgumentException("AddResource can only be used with integer resource names");
+ }
+
+ if (!Kernel32.UpdateResource(hUpdate, lpType, lpName, Kernel32.LangID_LangNeutral_SublangNeutral, data, (uint)data.Length))
+ {
+ ThrowExceptionForLastWin32Error();
+ }
+
+ return this;
+ }
+
///
/// Write the pending resource updates to the target PE
/// file. After this, the ResourceUpdater no longer maintains
diff --git a/src/installer/tests/Assets/TestProjects/ComLibrary/ComLibrary.cs b/src/installer/tests/Assets/TestProjects/ComLibrary/ComLibrary.cs
index d5eb1adadae24a..cf4418acd27448 100644
--- a/src/installer/tests/Assets/TestProjects/ComLibrary/ComLibrary.cs
+++ b/src/installer/tests/Assets/TestProjects/ComLibrary/ComLibrary.cs
@@ -12,10 +12,16 @@ public class UserDefinedAttribute : Attribute
{
}
+ [ComVisible(true)]
+ [Guid("27293cc8-7933-4fdf-9fde-653cbf9b55df")]
+ public interface IServer
+ {
+ }
+
[UserDefined]
[ComVisible(true)]
[Guid("438968CE-5950-4FBC-90B0-E64691350DF5")]
- public class Server
+ public class Server : IServer
{
public Server()
{
@@ -28,6 +34,12 @@ public class NotComVisible
{
}
+ [ComVisible(true)]
+ [Guid("f7199267-9821-4f5b-924b-ab5246b455cd")]
+ public interface INested
+ {
+ }
+
[ComVisible(true)]
[Guid("36e75747-aecd-43bf-9082-1a605889c762")]
public class ComVisible
@@ -35,7 +47,7 @@ public class ComVisible
[UserDefined]
[ComVisible(true)]
[Guid("c82e4585-58bd-46e0-a76d-c0b6975e5984")]
- public class Nested
+ public class Nested : INested
{
}
}
@@ -46,7 +58,7 @@ internal class ComVisibleNonPublic
{
[ComVisible(true)]
[Guid("8a0a7085-aca4-4651-9878-ca42747e2206")]
- public class Nested
+ public class Nested : INested
{
}
}
diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs
index 505b0c00c6c59f..4b693e371f1079 100644
--- a/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs
+++ b/src/installer/tests/HostActivation.Tests/NativeHosting/Comhost.cs
@@ -1,6 +1,7 @@
// 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;
@@ -119,6 +120,38 @@ public void ActivateClass_ValidateIErrorInfoResult()
}
}
+ [Fact]
+ public void LoadTypeLibraries()
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // COM activation is only supported on Windows
+ return;
+ }
+
+ using (var fixture = sharedState.ComLibraryFixture.Copy())
+ {
+ var comHost = Path.Combine(
+ fixture.TestProject.BuiltApp.Location,
+ $"{ fixture.TestProject.AssemblyName }.comhost.dll");
+
+ string[] args = {
+ "comhost",
+ "typelib",
+ "2",
+ comHost,
+ sharedState.ClsidString
+ };
+ CommandResult result = sharedState.CreateNativeHostCommand(args, fixture.BuiltDotnet.BinPath)
+ .Execute();
+
+ result.Should().Pass()
+ .And.HaveStdOutContaining("Loading default type library succeeded.")
+ .And.HaveStdOutContaining("Loading type library 1 succeeded.")
+ .And.HaveStdOutContaining("Loading type library 2 succeeded.");
+ }
+ }
+
public class SharedTestState : SharedTestStateBase
{
public string ComHostPath { get; }
@@ -150,14 +183,23 @@ public SharedTestState()
}
}
- // Use the locally built comhost to create a comhost with the embedded .clsidmap
+ // Use the locally built comhost to create a comhost with the embedded .clsidmap
ComHostPath = Path.Combine(
ComLibraryFixture.TestProject.BuiltApp.Location,
$"{ ComLibraryFixture.TestProject.AssemblyName }.comhost.dll");
+
+ // Include the test type libraries in the ComHost tests.
+ var typeLibraries = new Dictionary
+ {
+ { 1, Path.Combine(RepoDirectories.Artifacts, "corehost_test", "Server.tlb") },
+ { 2, Path.Combine(RepoDirectories.Artifacts, "corehost_test", "Nested.tlb") }
+ };
+
ComHost.Create(
Path.Combine(RepoDirectories.HostArtifacts, "comhost.dll"),
ComHostPath,
- clsidMapPath);
+ clsidMapPath,
+ typeLibraries);
}
protected override void Dispose(bool disposing)
diff --git a/src/native/corehost/test/CMakeLists.txt b/src/native/corehost/test/CMakeLists.txt
index 72ba29f816925a..e5d93fd2c030f6 100644
--- a/src/native/corehost/test/CMakeLists.txt
+++ b/src/native/corehost/test/CMakeLists.txt
@@ -3,3 +3,6 @@ add_subdirectory(mockcoreclr)
add_subdirectory(mockhostfxr)
add_subdirectory(mockhostpolicy)
add_subdirectory(nativehost)
+if (CLR_CMAKE_TARGET_WIN32)
+ add_subdirectory(typelibs)
+endif()
diff --git a/src/native/corehost/test/nativehost/comhost_test.cpp b/src/native/corehost/test/nativehost/comhost_test.cpp
index 68d0c93b62baac..1d132f1757307e 100644
--- a/src/native/corehost/test/nativehost/comhost_test.cpp
+++ b/src/native/corehost/test/nativehost/comhost_test.cpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
namespace
{
@@ -67,12 +68,42 @@ namespace
return pal::pal_utf8string(clsid_str, &clsidVect);
}
+ void log_hresult(HRESULT hr, std::ostream &ss)
+ {
+ if (FAILED(hr))
+ ss << "(" << std::hex << std::showbase << hr << ")";
+ }
+
void log_activation(const char *clsid, int activationNumber, int total, HRESULT hr, std::ostream &ss)
{
ss << "Activation of " << clsid << (FAILED(hr) ? " failed. " : " succeeded. ") << activationNumber << " of " << total;
+ log_hresult(hr, ss);
+ ss << std::endl;
+ }
+
+ HRESULT load_typelib(const pal::string_t &typelib_path)
+ {
+ HRESULT hr;
+ ITypeLib* typelib = nullptr;
+ hr = LoadTypeLibEx(typelib_path.c_str(), REGKIND_NONE, &typelib);
if (FAILED(hr))
- ss << "(" << std::hex << std::showbase << hr << ")";
+ return hr;
+ typelib->Release();
+ return hr;
+ }
+
+ void log_typelib_load(int typelib_id, HRESULT hr, std::ostream &ss)
+ {
+ ss << "Loading type library " << typelib_id << (FAILED(hr) ? " failed. " : " succeeded. ");
+ log_hresult(hr, ss);
+ ss << std::endl;
+ }
+
+ void log_default_typelib_load(HRESULT hr, std::ostream &ss)
+ {
+ ss << "Loading default type library" << (FAILED(hr) ? " failed. " : " succeeded. ");
+ log_hresult(hr, ss);
ss << std::endl;
}
}
@@ -165,3 +196,26 @@ bool comhost_test::errorinfo(const pal::string_t &comhost_path, const pal::strin
return true;
}
+
+bool comhost_test::typelib(const pal::string_t &comhost_path, int count)
+{
+ HRESULT hr;
+
+ hr = load_typelib(comhost_path);
+ log_default_typelib_load(hr, std::cout);
+ if (FAILED(hr))
+ return false;
+
+ for (int i = 1; i < count + 1; i++)
+ {
+ // The path format for a non-default embedded TLB is 'C:\file\path\to.exe\\2' where '2' is the resource name of the tlb to load.
+ // See https://docs.microsoft.com/windows/win32/api/oleauto/nf-oleauto-loadtypelib#remarks for documentation on the path format.
+ pal::stringstream_t tlb_path;
+ tlb_path << comhost_path << '\\' << i;
+ hr = load_typelib(tlb_path.str());
+ log_typelib_load(i, hr, std::cout);
+ if (FAILED(hr))
+ return false;
+ }
+ return true;
+}
diff --git a/src/native/corehost/test/nativehost/comhost_test.h b/src/native/corehost/test/nativehost/comhost_test.h
index 767071f2917c8f..189a0a1ebe6138 100644
--- a/src/native/corehost/test/nativehost/comhost_test.h
+++ b/src/native/corehost/test/nativehost/comhost_test.h
@@ -10,4 +10,6 @@ namespace comhost_test
bool concurrent(const pal::string_t &comhost_path, const pal::string_t &clsid_str, int count);
bool errorinfo(const pal::string_t &comhost_path, const pal::string_t &clsid_str, int count);
+
+ bool typelib(const pal::string_t &comhost_path, int count);
}
diff --git a/src/native/corehost/test/nativehost/nativehost.cpp b/src/native/corehost/test/nativehost/nativehost.cpp
index db90ded35593df..8987cb821ca91d 100644
--- a/src/native/corehost/test/nativehost/nativehost.cpp
+++ b/src/native/corehost/test/nativehost/nativehost.cpp
@@ -403,6 +403,10 @@ int main(const int argc, const pal::char_t *argv[])
{
success = comhost_test::errorinfo(comhost_path, clsid_str, count);
}
+ else if (pal::strcmp(scenario, _X("typelib")) == 0)
+ {
+ success = comhost_test::typelib(comhost_path, count);
+ }
return success ? EXIT_SUCCESS : EXIT_FAILURE;
}
diff --git a/src/native/corehost/test/typelibs/CMakeLists.txt b/src/native/corehost/test/typelibs/CMakeLists.txt
new file mode 100644
index 00000000000000..0b21548ef96fd2
--- /dev/null
+++ b/src/native/corehost/test/typelibs/CMakeLists.txt
@@ -0,0 +1,30 @@
+
+# Get the current list of definitions to pass to midl
+get_compile_definitions(MIDL_DEFINITIONS)
+get_include_directories(MIDL_INCLUDE_DIRECTORIES)
+find_program(MIDL midl.exe)
+
+function(compile_idl idl_file tlb_out)
+ # Compile IDL file using MIDL
+ set(IDL_SOURCE ${idl_file})
+ get_filename_component(IDL_NAME ${IDL_SOURCE} NAME_WE)
+ set(tlb_out_local "${CMAKE_CURRENT_BINARY_DIR}/${IDL_NAME}.tlb")
+ set("${tlb_out}" "${tlb_out_local}" PARENT_SCOPE)
+
+ add_custom_command(
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${IDL_NAME}_i.c ${CMAKE_CURRENT_BINARY_DIR}/${IDL_NAME}.h ${tlb_out_local}
+ COMMAND ${MIDL} ${MIDL_INCLUDE_DIRECTORIES}
+ /h ${CMAKE_CURRENT_BINARY_DIR}/${IDL_NAME}.h ${MIDL_DEFINITIONS}
+ /out ${CMAKE_CURRENT_BINARY_DIR}
+ /tlb ${tlb_out_local}
+ ${IDL_SOURCE}
+ DEPENDS ${IDL_SOURCE}
+ COMMENT "Compiling ${IDL_SOURCE}")
+endfunction()
+
+compile_idl(${CMAKE_CURRENT_SOURCE_DIR}/Server.idl Server_tlb)
+compile_idl(${CMAKE_CURRENT_SOURCE_DIR}/Nested.idl Nested_tlb)
+
+add_custom_target(typelibs ALL DEPENDS "${Server_tlb}" "${Nested_tlb}")
+
+install(FILES "${Server_tlb}" "${Nested_tlb}" DESTINATION corehost_test)
diff --git a/src/native/corehost/test/typelibs/Nested.idl b/src/native/corehost/test/typelibs/Nested.idl
new file mode 100644
index 00000000000000..9fca7ec74d7760
--- /dev/null
+++ b/src/native/corehost/test/typelibs/Nested.idl
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import "oaidl.idl";
+import "ocidl.idl";
+
+[
+ object,
+ uuid(f7199267-9821-4f5b-924b-ab5246b455cd)
+]
+interface INested : IDispatch
+{
+};
+
+[
+ uuid(f7c46a13-a1fc-4bf1-a61d-4502215c24e9)
+]
+library ComLibrary
+{
+ importlib("stdole2.tlb");
+ [
+ uuid(c82e4585-58bd-46e0-a76d-c0b6975e5984)
+ ]
+ coclass ComVisible_Nested
+ {
+ [default] interface INested;
+ }
+}
diff --git a/src/native/corehost/test/typelibs/Server.idl b/src/native/corehost/test/typelibs/Server.idl
new file mode 100644
index 00000000000000..29abcd842670c0
--- /dev/null
+++ b/src/native/corehost/test/typelibs/Server.idl
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import "oaidl.idl";
+import "ocidl.idl";
+
+[
+ object,
+ uuid(27293cc8-7933-4fdf-9fde-653cbf9b55df)
+]
+interface IServer : IDispatch
+{
+};
+
+[
+ uuid(20151109-a0e8-46ae-b28e-8ff2c0e72166)
+]
+library ComLibrary
+{
+ importlib("stdole2.tlb");
+ [
+ uuid(438968CE-5950-4FBC-90B0-E64691350DF5)
+ ]
+ coclass Server
+ {
+ [default] interface IServer;
+ }
+}