From 91c16faf35485282a16f5eed9d7095888eb2c85a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 12 Apr 2021 17:10:48 -0700 Subject: [PATCH] Add support in comhost tooling to embed type libraries. (#50986) * Add support in comhost tooling to embed type libraries. Sdk work will still be needed to enable developers to embed tlbs in their comhosts. * Cleanup. * PR feedback. Update validation to throw specific exception types for specific errors so the SDK can accurately report errors to the user. --- .../ComHost/ComHost.cs | 26 ++++++++- .../ComHost/InvalidTypeLibraryIdException.cs | 23 ++++++++ .../TypeLibraryDoesNotExistException.cs | 21 +++++++ .../ResourceUpdater.cs | 38 ++++++++++++- .../TestProjects/ComLibrary/ComLibrary.cs | 18 +++++- .../NativeHosting/Comhost.cs | 46 ++++++++++++++- src/native/corehost/test/CMakeLists.txt | 3 + .../corehost/test/nativehost/comhost_test.cpp | 56 ++++++++++++++++++- .../corehost/test/nativehost/comhost_test.h | 2 + .../corehost/test/nativehost/nativehost.cpp | 4 ++ .../corehost/test/typelibs/CMakeLists.txt | 30 ++++++++++ src/native/corehost/test/typelibs/Nested.idl | 28 ++++++++++ src/native/corehost/test/typelibs/Server.idl | 28 ++++++++++ 13 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 src/installer/managed/Microsoft.NET.HostModel/ComHost/InvalidTypeLibraryIdException.cs create mode 100644 src/installer/managed/Microsoft.NET.HostModel/ComHost/TypeLibraryDoesNotExistException.cs create mode 100644 src/native/corehost/test/typelibs/CMakeLists.txt create mode 100644 src/native/corehost/test/typelibs/Nested.idl create mode 100644 src/native/corehost/test/typelibs/Server.idl diff --git a/src/installer/managed/Microsoft.NET.HostModel/ComHost/ComHost.cs b/src/installer/managed/Microsoft.NET.HostModel/ComHost/ComHost.cs index 2e1d8a949ef..3cf9cbf4e10 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 class ComHost 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 00000000000..4bb1a85d4d4 --- /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 00000000000..28d20b547d4 --- /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 e18eedf9804..bf1c0c489a9 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs @@ -44,6 +44,16 @@ private sealed class Kernel32 [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 d5eb1adadae..cf4418acd27 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 505b0c00c6c..4b693e371f1 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 72ba29f8169..e5d93fd2c03 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 68d0c93b62b..1d132f17573 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 767071f2917..189a0a1ebe6 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 db90ded3559..8987cb821ca 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 00000000000..0b21548ef96 --- /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 00000000000..9fca7ec74d7 --- /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 00000000000..29abcd84267 --- /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; + } +} -- GitLab