Add basic native hosting test using comhost (dotnet/core-setup#5833)
authorElinor Fung <47805090+elinor-fung@users.noreply.github.com>
Mon, 15 Apr 2019 19:22:05 +0000 (12:22 -0700)
committerGitHub <noreply@github.com>
Mon, 15 Apr 2019 19:22:05 +0000 (12:22 -0700)
* 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
src/installer/corehost/cli/test/nativehost/CMakeLists.txt
src/installer/corehost/cli/test/nativehost/comhost_test.cpp [new file with mode: 0644]
src/installer/corehost/cli/test/nativehost/comhost_test.h [new file with mode: 0644]
src/installer/corehost/cli/test/nativehost/nativehost.cpp
src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.cs [new file with mode: 0644]
src/installer/test/Assets/TestProjects/ComLibrary/ComLibrary.csproj [new file with mode: 0644]
src/installer/test/HostActivationTests/NativeHosting/Comhost.cs [new file with mode: 0644]
src/installer/test/HostActivationTests/NativeHosting/Nethost.cs
src/installer/test/HostActivationTests/NativeHosting/SharedTestStateBase.cs [new file with mode: 0644]

index 6ec2342..171a4c8 100644 (file)
@@ -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
 {
index e1eeec1..ab6abb6 100644 (file)
@@ -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 (file)
index 0000000..8c44f2a
--- /dev/null
@@ -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 <error_codes.h>
+#include <iostream>
+#include <future>
+#include <pal.h>
+
+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<char> &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<char> 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<char> clsidVect;
+    if (!get_clsid(clsid_str, &clsid, clsidVect))
+        return false;
+
+    comhost_exports comhost(comhost_path);
+
+    std::vector<std::future<HRESULT>> 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 (file)
index 0000000..5bb3a97
--- /dev/null
@@ -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 <pal.h>
+
+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
index 8e4b149..eff581a 100644 (file)
@@ -6,6 +6,7 @@
 #include <pal.h>
 #include <error_codes.h>
 #include <nethost.h>
+#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: ... <scenario> <activation_count> <comhost_path> <clsid>
+        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 (file)
index 0000000..47a578b
--- /dev/null
@@ -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 (file)
index 0000000..07612ef
--- /dev/null
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(NETCoreAppFramework)</TargetFramework>
+    <RuntimeFrameworkVersion>$(MNAVersion)</RuntimeFrameworkVersion>
+  </PropertyGroup>
+
+</Project>
diff --git a/src/installer/test/HostActivationTests/NativeHosting/Comhost.cs b/src/installer/test/HostActivationTests/NativeHosting/Comhost.cs
new file mode 100644 (file)
index 0000000..8cf55da
--- /dev/null
@@ -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<Comhost.SharedTestState>
+    {
+        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);
+            }
+        }
+    }
+}
index e946476..7942755 100644 (file)
@@ -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 (file)
index 0000000..4c55b89
--- /dev/null
@@ -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);
+            }
+        }
+    }
+}