From afb05510e0aca0d4a45926e67344ac3c2e2403be Mon Sep 17 00:00:00 2001 From: Elinor Fung <47805090+elinor-fung@users.noreply.github.com> Date: Mon, 15 Apr 2019 12:22:05 -0700 Subject: [PATCH] Add basic native hosting test using comhost (dotnet/core-setup#5833) * Test loads comhost and calls DllGetClassObject to activate * Specify calling convention for COM activation delegate Commit migrated from https://github.com/dotnet/core-setup/commit/48437b978361b7af9b37cdbecc290d4e058ee3fd --- src/installer/corehost/cli/comhost/comhost.cpp | 2 +- .../corehost/cli/test/nativehost/CMakeLists.txt | 14 ++- .../corehost/cli/test/nativehost/comhost_test.cpp | 130 +++++++++++++++++++++ .../corehost/cli/test/nativehost/comhost_test.h | 12 ++ .../corehost/cli/test/nativehost/nativehost.cpp | 33 +++++- .../Assets/TestProjects/ComLibrary/ComLibrary.cs | 15 +++ .../TestProjects/ComLibrary/ComLibrary.csproj | 8 ++ .../HostActivationTests/NativeHosting/Comhost.cs | 104 +++++++++++++++++ .../HostActivationTests/NativeHosting/Nethost.cs | 30 +---- .../NativeHosting/SharedTestStateBase.cs | 42 +++++++ 10 files changed, 361 insertions(+), 29 deletions(-) create mode 100644 src/installer/corehost/cli/test/nativehost/comhost_test.cpp create mode 100644 src/installer/corehost/cli/test/nativehost/comhost_test.h create mode 100644 src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.cs create mode 100644 src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.csproj create mode 100644 src/installer/test/HostActivationTests/NativeHosting/Comhost.cs create mode 100644 src/installer/test/HostActivationTests/NativeHosting/SharedTestStateBase.cs diff --git a/src/installer/corehost/cli/comhost/comhost.cpp b/src/installer/corehost/cli/comhost/comhost.cpp index 6ec2342..171a4c8 100644 --- a/src/installer/corehost/cli/comhost/comhost.cpp +++ b/src/installer/corehost/cli/comhost/comhost.cpp @@ -42,7 +42,7 @@ struct com_activation_context void **class_factory_dest; }; -using com_activation_fn = int(*)(com_activation_context*); +using com_activation_fn = int(STDMETHODCALLTYPE*)(com_activation_context*); namespace { diff --git a/src/installer/corehost/cli/test/nativehost/CMakeLists.txt b/src/installer/corehost/cli/test/nativehost/CMakeLists.txt index e1eeec1..ab6abb6 100644 --- a/src/installer/corehost/cli/test/nativehost/CMakeLists.txt +++ b/src/installer/corehost/cli/test/nativehost/CMakeLists.txt @@ -21,6 +21,18 @@ set(SOURCES ./nativehost.cpp ) +if(WIN32) + list(APPEND SOURCES + ./comhost_test.cpp) + + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DELAYLOAD:nethost.dll") +endif() + include(../testexe.cmake) -target_link_libraries(${DOTNET_PROJECT_NAME} nethost) \ No newline at end of file +target_link_libraries(${DOTNET_PROJECT_NAME} nethost) + +# Specify non-default Windows libs to be used for Arm/Arm64 builds +if (WIN32 AND (CLI_CMAKE_PLATFORM_ARCH_ARM OR CLI_CMAKE_PLATFORM_ARCH_ARM64)) + target_link_libraries(${DOTNET_PROJECT_NAME} Advapi32.lib Ole32.lib OleAut32.lib) +endif() \ No newline at end of file diff --git a/src/installer/corehost/cli/test/nativehost/comhost_test.cpp b/src/installer/corehost/cli/test/nativehost/comhost_test.cpp new file mode 100644 index 0000000..8c44f2a --- /dev/null +++ b/src/installer/corehost/cli/test/nativehost/comhost_test.cpp @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "comhost_test.h" +#include +#include +#include +#include + +namespace +{ + class comhost_exports + { + public: + comhost_exports(const pal::string_t &comhost_path) + { + if (!pal::load_library(&comhost_path, &_dll)) + { + std::cout << "Load library of comhost failed" << std::endl; + throw StatusCode::CoreHostLibLoadFailure; + } + + get_class_obj_fn = (decltype(get_class_obj_fn))pal::get_symbol(_dll, "DllGetClassObject"); + if (get_class_obj_fn == nullptr) + { + std::cout << "Failed to get DllGetClassObject export from comhost" << std::endl; + throw StatusCode::CoreHostEntryPointFailure; + } + } + + ~comhost_exports() + { + pal::unload_library(_dll); + } + + decltype(&DllGetClassObject) get_class_obj_fn; + + private: + pal::dll_t _dll; + }; + + HRESULT activate_class(comhost_exports &comhost, REFCLSID clsid) + { + IClassFactory *classFactory; + HRESULT hr = comhost.get_class_obj_fn(clsid, __uuidof(IClassFactory), (void**)&classFactory); + if (FAILED(hr)) + return hr; + + IUnknown *instance; + hr = classFactory->CreateInstance(nullptr, __uuidof(instance), (void**)&instance); + classFactory->Release(); + if (FAILED(hr)) + return hr; + + instance->Release(); + return S_OK; + } + + bool get_clsid(const pal::string_t &clsid_str, CLSID *clsid, std::vector &clsidVect) + { + if (FAILED(::CLSIDFromString(clsid_str.c_str(), clsid))) + { + std::cout << "Invalid CLSID: " << clsid_str.c_str() << std::endl; + return false; + } + + return pal::pal_utf8string(clsid_str, &clsidVect); + } +} + +bool comhost_test::synchronous(const pal::string_t &comhost_path, const pal::string_t &clsid_str, int count) +{ + CLSID clsid; + std::vector clsidVect; + if (!get_clsid(clsid_str, &clsid, clsidVect)) + return false; + + comhost_exports comhost(comhost_path); + + for (int i = 0; i < count; ++i) + { + HRESULT hr = activate_class(comhost, clsid); + if (FAILED(hr)) + { + std::cout << "Activation of " << clsidVect.data() << " failed. " + << i + 1 << " of " << count << "(" << std::hex << std::showbase << hr << ")" << std::endl; + return false; + } + + std::cout << "Activation of " << clsidVect.data() << " succeeded. " + << i + 1 << " of " << count << std::endl; + } + + return true; +} + +bool comhost_test::concurrent(const pal::string_t &comhost_path, const pal::string_t &clsid_str, int count) +{ + CLSID clsid; + std::vector clsidVect; + if (!get_clsid(clsid_str, &clsid, clsidVect)) + return false; + + comhost_exports comhost(comhost_path); + + std::vector> activations; + activations.reserve(count); + for (int i = 0; i < count; ++i) + activations.push_back(std::async(std::launch::async, activate_class, comhost, clsid)); + + bool succeeded = true; + for (int i = 0; i < count; ++i) + { + HRESULT hr = activations[i].get(); + if (FAILED(hr)) + { + std::cout << "Activation of " << clsidVect.data() << " failed. " + << i + 1 << " of " << count << "(" << std::hex << std::showbase << hr << ")" << std::endl; + succeeded = false; + } + else + { + std::cout << "Activation of " << clsidVect.data() << " succeeded. " + << i + 1 << " of " << count << std::endl; + } + } + + return true; +} \ No newline at end of file diff --git a/src/installer/corehost/cli/test/nativehost/comhost_test.h b/src/installer/corehost/cli/test/nativehost/comhost_test.h new file mode 100644 index 0000000..5bb3a97 --- /dev/null +++ b/src/installer/corehost/cli/test/nativehost/comhost_test.h @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include + +namespace comhost_test +{ + bool synchronous(const pal::string_t &comhost_path, const pal::string_t &clsid_str, int count); + + bool concurrent(const pal::string_t &comhost_path, const pal::string_t &clsid_str, int count); +} \ No newline at end of file diff --git a/src/installer/corehost/cli/test/nativehost/nativehost.cpp b/src/installer/corehost/cli/test/nativehost/nativehost.cpp index 8e4b149..eff581a 100644 --- a/src/installer/corehost/cli/test/nativehost/nativehost.cpp +++ b/src/installer/corehost/cli/test/nativehost/nativehost.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "comhost_test.h" namespace { @@ -59,14 +60,42 @@ int main(const int argc, const pal::char_t *argv[]) { std::cout << "get_hostfxr_path succeeded" << std::endl; std::cout << "hostfxr_path: " << tostr(pal::to_lower(fxr_path)).data() << std::endl; - return 0; + return EXIT_SUCCESS; } else { std::cout << "get_hostfxr_path failed: " << std::hex << std::showbase << res << std::endl; - return 1; + return EXIT_FAILURE; } } +#if defined(_WIN32) + else if (pal::strcmp(command, _X("comhost")) == 0) + { + // args: ... + if (argc < 6) + { + std::cerr << "Invalid arguments" << std::endl; + return -1; + } + + const pal::char_t *scenario = argv[2]; + int count = pal::xtoi(argv[3]); + const pal::string_t comhost_path = argv[4]; + const pal::string_t clsid_str = argv[5]; + + bool success = false; + if (pal::strcmp(scenario, _X("synchronous")) == 0) + { + success = comhost_test::synchronous(comhost_path, clsid_str, count); + } + else if (pal::strcmp(scenario, _X("concurrent")) == 0) + { + success = comhost_test::concurrent(comhost_path, clsid_str, count); + } + + return success ? EXIT_SUCCESS : EXIT_FAILURE; + } +#endif else { std::cerr << "Invalid arguments" << std::endl; diff --git a/src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.cs b/src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.cs new file mode 100644 index 0000000..47a578b --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace ComLibrary +{ + [ComVisible(true)] + [Guid("438968CE-5950-4FBC-90B0-E64691350DF5")] + public class Server + { + public Server() + { + Console.WriteLine($"New instance of {nameof(Server)} created"); + } + } +} \ No newline at end of file diff --git a/src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.csproj b/src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.csproj new file mode 100644 index 0000000..07612ef --- /dev/null +++ b/src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.csproj @@ -0,0 +1,8 @@ + + + + $(NETCoreAppFramework) + $(MNAVersion) + + + diff --git a/src/installer/test/HostActivationTests/NativeHosting/Comhost.cs b/src/installer/test/HostActivationTests/NativeHosting/Comhost.cs new file mode 100644 index 0000000..8cf55da --- /dev/null +++ b/src/installer/test/HostActivationTests/NativeHosting/Comhost.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.DotNet.Cli.Build.Framework; +using Newtonsoft.Json.Linq; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; + +namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting +{ + public class Comhost : IClassFixture + { + private readonly SharedTestState sharedState; + + public Comhost(SharedTestState sharedTestState) + { + sharedState = sharedTestState; + } + + [Theory] + [InlineData(1, true)] + [InlineData(10, true)] + [InlineData(10, false)] + public void ActivateClass(int count, bool synchronous) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // COM activation is only supported on Windows + return; + } + + var fixture = sharedState.ComLibraryFixture + .Copy(); + + string scenario = synchronous ? "synchronous" : "concurrent"; + string args = $"comhost {scenario} {count} {sharedState.ComHostPath} {sharedState.ClsidString}"; + CommandResult result = Command.Create(sharedState.NativeHostPath, args) + .CaptureStdErr() + .CaptureStdOut() + .EnvironmentVariable("COREHOST_TRACE", "1") + .EnvironmentVariable("DOTNET_ROOT", fixture.BuiltDotnet.BinPath) + .EnvironmentVariable("DOTNET_ROOT(x86)", fixture.BuiltDotnet.BinPath) + .Execute(); + + result.Should().Pass() + .And.HaveStdOutContaining("New instance of Server created"); + + for (var i = 1; i <= count; ++i) + { + result.Should().HaveStdOutContaining($"Activation of {sharedState.ClsidString} succeeded. {i} of {count}"); + } + } + + public class SharedTestState : SharedTestStateBase + { + public string ComHostPath { get; } + + public string ClsidString = "{438968CE-5950-4FBC-90B0-E64691350DF5}"; + public TestProjectFixture ComLibraryFixture { get; } + + public SharedTestState() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // COM activation is only supported on Windows + return; + } + + ComLibraryFixture = new TestProjectFixture("ComLibrary", RepoDirectories) + .EnsureRestored(RepoDirectories.CorehostPackages) + .BuildProject(); + + ComHostPath = Path.Combine( + ComLibraryFixture.TestProject.BuiltApp.Location, + $"{ ComLibraryFixture.TestProject.AssemblyName }.comhost.dll"); + + File.Copy(Path.Combine(RepoDirectories.CorehostPackages, "comhost.dll"), ComHostPath); + + RuntimeConfig.FromFile(ComLibraryFixture.TestProject.RuntimeConfigJson) + .WithFramework(new RuntimeConfig.Framework("Microsoft.NETCore.App", RepoDirectories.MicrosoftNETCoreAppVersion)) + .Save(); + + JObject clsidMap = new JObject() + { + { + ClsidString, + new JObject() { {"assembly", "ComLibrary" }, {"type", "ComLibrary.Server" } } + } + }; + File.WriteAllText($"{ ComHostPath }.clsidmap", clsidMap.ToString()); + } + + protected override void Dispose(bool disposing) + { + if (ComLibraryFixture != null) + ComLibraryFixture.Dispose(); + + base.Dispose(disposing); + } + } + } +} diff --git a/src/installer/test/HostActivationTests/NativeHosting/Nethost.cs b/src/installer/test/HostActivationTests/NativeHosting/Nethost.cs index e946476..7942755 100644 --- a/src/installer/test/HostActivationTests/NativeHosting/Nethost.cs +++ b/src/installer/test/HostActivationTests/NativeHosting/Nethost.cs @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.DotNet.Cli.Build.Framework; -using Microsoft.Win32; -using System; using System.IO; using System.Runtime.InteropServices; using Xunit; @@ -131,12 +129,8 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting .And.HaveStdOutContaining($"hostfxr_path: {hostFxrPath}".ToLower()); } - public class SharedTestState : IDisposable + public class SharedTestState : SharedTestStateBase { - public string BaseDirectory { get; } - public string NativeHostPath { get; } - public RepoDirectoriesProvider RepoDirectories { get; } - public string HostFxrPath { get; } public string InvalidInstallRoot { get; } public string ValidInstallRoot { get; } @@ -145,17 +139,11 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting public SharedTestState() { - BaseDirectory = SharedFramework.CalculateUniqueTestDirectory(Path.Combine(TestArtifact.TestArtifactsPath, "nativeHosting")); - Directory.CreateDirectory(BaseDirectory); - - string nativeHostName = RuntimeInformationExtensions.GetExeFileNameForCurrentPlatform("nativehost"); - NativeHostPath = Path.Combine(BaseDirectory, nativeHostName); - - // Copy over native host and nethost - RepoDirectories = new RepoDirectoriesProvider(); + // Copy nethost next to native host string nethostName = RuntimeInformationExtensions.GetSharedLibraryFileNameForCurrentPlatform("nethost"); - File.Copy(Path.Combine(RepoDirectories.CorehostPackages, nethostName), Path.Combine(BaseDirectory, nethostName)); - File.Copy(Path.Combine(RepoDirectories.Artifacts, "corehost_test", nativeHostName), NativeHostPath); + File.Copy( + Path.Combine(RepoDirectories.CorehostPackages, nethostName), + Path.Combine(Path.GetDirectoryName(NativeHostPath), nethostName)); InvalidInstallRoot = Path.Combine(BaseDirectory, "invalid"); Directory.CreateDirectory(InvalidInstallRoot); @@ -185,14 +173,6 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting return Path.Combine(fxrRoot, "2.3.0", HostFxrName); } - - public void Dispose() - { - if (!TestArtifact.PreserveTestRuns() && Directory.Exists(BaseDirectory)) - { - Directory.Delete(BaseDirectory, true); - } - } } } } diff --git a/src/installer/test/HostActivationTests/NativeHosting/SharedTestStateBase.cs b/src/installer/test/HostActivationTests/NativeHosting/SharedTestStateBase.cs new file mode 100644 index 0000000..4c55b89 --- /dev/null +++ b/src/installer/test/HostActivationTests/NativeHosting/SharedTestStateBase.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.NativeHosting +{ + public class SharedTestStateBase : IDisposable + { + public string BaseDirectory { get; } + public string NativeHostPath { get; } + public RepoDirectoriesProvider RepoDirectories { get; } + + public SharedTestStateBase() + { + BaseDirectory = SharedFramework.CalculateUniqueTestDirectory(Path.Combine(TestArtifact.TestArtifactsPath, "nativeHosting")); + Directory.CreateDirectory(BaseDirectory); + + string nativeHostName = RuntimeInformationExtensions.GetExeFileNameForCurrentPlatform("nativehost"); + NativeHostPath = Path.Combine(BaseDirectory, nativeHostName); + + // Copy over native host + RepoDirectories = new RepoDirectoriesProvider(); + File.Copy(Path.Combine(RepoDirectories.Artifacts, "corehost_test", nativeHostName), NativeHostPath); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!TestArtifact.PreserveTestRuns() && Directory.Exists(BaseDirectory)) + { + Directory.Delete(BaseDirectory, true); + } + } + } +} -- 2.7.4