Skip to content

Commit 110cb9f

Browse files
authored
Load C++/CLI DLLs built against .NET 7.0+ in default ALC (#66486)
1 parent 731d936 commit 110cb9f

File tree

13 files changed

+179
-45
lines changed

13 files changed

+179
-45
lines changed

src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
<!-- Sources -->
118118
<ItemGroup>
119119
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\ComActivationContextInternal.cs" />
120-
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\InMemoryAssemblyLoader.cs" />
120+
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\ComponentActivator.CoreCLR.cs" />
121121
<Compile Include="$(BclSourcesRoot)\System\__Canon.cs" />
122122
<Compile Include="$(BclSourcesRoot)\System\ArgIterator.cs" />
123123
<Compile Include="$(BclSourcesRoot)\System\Array.CoreCLR.cs" />
@@ -285,10 +285,12 @@
285285
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\ComActivator.PlatformNotSupported.cs" />
286286
</ItemGroup>
287287
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
288+
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\InMemoryAssemblyLoader.PlatformNotSupported.cs" />
288289
<Compile Include="$(BclSourcesRoot)\Interop\Unix\Interop.Libraries.cs" />
289290
<Compile Include="$(BclSourcesRoot)\System\Threading\LowLevelLifoSemaphore.Unix.cs" />
290291
</ItemGroup>
291292
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
293+
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\InMemoryAssemblyLoader.cs" />
292294
<Compile Include="$(CommonPath)Interop\Windows\OleAut32\Interop.VariantClear.cs">
293295
<Link>Common\Interop\Windows\OleAut32\Interop.VariantClear.cs</Link>
294296
</Compile>

src/coreclr/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.Shared.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,6 @@
2121
<type fullname="System.SZArrayHelper">
2222
<method name=".ctor" />
2323
</type>
24-
25-
<!-- Enables the .NET IJW host to load an in-memory module as a .NET assembly.
26-
These are always rooted to ensure native calls get trimmer related errors
27-
but will be trimmed away by the related feature switch -->
28-
<type fullname="Internal.Runtime.InteropServices.InMemoryAssemblyLoader">
29-
<method name="LoadInMemoryAssembly" />
30-
</type>
3124
</assembly>
3225

3326
<!-- The private Event methods are accessed by private reflection in the base EventSource class. -->

src/coreclr/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.Windows.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,19 @@
1010
<method name="RegisterClassForTypeInternal" />
1111
<method name="UnregisterClassForTypeInternal" />
1212
</type>
13+
14+
<!-- Enables the .NET IJW host (before .NET 7.0) to load an in-memory module as a .NET assembly.
15+
These are always rooted to ensure native calls get trimmer related errors
16+
but their bodies will be mostly trimmed away by the related feature switch -->
17+
<type fullname="Internal.Runtime.InteropServices.InMemoryAssemblyLoader">
18+
<method name="LoadInMemoryAssembly" />
19+
</type>
20+
</assembly>
21+
22+
<assembly fullname="System.Private.CoreLib" feature="System.Runtime.InteropServices.EnableCppCLIHostActivation" featurevalue="true">
23+
<!-- Enables the .NET IJW host (.NET 7.0+) to load an in-memory module as a .NET assembly. -->
24+
<type fullname="Internal.Runtime.InteropServices.InMemoryAssemblyLoader">
25+
<method name="LoadInMemoryAssemblyInContext" />
26+
</type>
1327
</assembly>
1428
</linker>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
7+
namespace Internal.Runtime.InteropServices
8+
{
9+
public static partial class ComponentActivator
10+
{
11+
// This hook for when GetFunctionPointer is called when the feature is disabled allows us to
12+
// provide error messages for known hosting scenarios such as C++/CLI.
13+
private static void OnDisabledGetFunctionPointerCall(IntPtr typeNameNative, IntPtr methodNameNative)
14+
{
15+
if (!OperatingSystem.IsWindows())
16+
return;
17+
18+
// Check for the exact type and method name used by ijwhost - see src/native/corehost/ijwhost/ijwhost.cpp
19+
if (Marshal.PtrToStringUni(methodNameNative) == "LoadInMemoryAssemblyInContext"
20+
&& Marshal.PtrToStringUni(typeNameNative) == $"Internal.Runtime.InteropServices.{nameof(InMemoryAssemblyLoader)}, {CoreLib.Name}")
21+
{
22+
throw new NotSupportedException(SR.NotSupported_CppCli);
23+
}
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
using System.Runtime.Loader;
7+
8+
namespace Internal.Runtime.InteropServices
9+
{
10+
/// <summary>
11+
/// This class enables the .NET IJW host to load an in-memory module as a .NET assembly
12+
/// </summary>
13+
public static class InMemoryAssemblyLoader
14+
{
15+
/// <summary>
16+
/// Loads into an isolated AssemblyLoadContext an assembly that has already been loaded into memory by the OS loader as a native module.
17+
/// </summary>
18+
/// <param name="moduleHandle">The native module handle for the assembly.</param>
19+
/// <param name="assemblyPath">The path to the assembly (as a pointer to a UTF-16 C string).</param>
20+
public static unsafe void LoadInMemoryAssembly(IntPtr moduleHandle, IntPtr assemblyPath)
21+
=> throw new PlatformNotSupportedException();
22+
}
23+
}

src/coreclr/System.Private.CoreLib/src/Internal/Runtime/InteropServices/InMemoryAssemblyLoader.cs

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,87 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Runtime.InteropServices;
67
using System.Runtime.Loader;
8+
using System.Runtime.Versioning;
79

810
namespace Internal.Runtime.InteropServices
911
{
1012
/// <summary>
1113
/// This class enables the .NET IJW host to load an in-memory module as a .NET assembly
1214
/// </summary>
15+
[SupportedOSPlatform("windows")]
1316
public static class InMemoryAssemblyLoader
1417
{
15-
#if TARGET_WINDOWS
1618
private static bool IsSupported { get; } = InitializeIsSupported();
17-
1819
private static bool InitializeIsSupported() => AppContext.TryGetSwitch("System.Runtime.InteropServices.EnableCppCLIHostActivation", out bool isSupported) ? isSupported : true;
19-
#endif
2020

2121
/// <summary>
22-
/// Loads into an isolated AssemblyLoadContext an assembly that has already been loaded into memory by the OS loader as a native module.
22+
/// Loads an assembly that has already been loaded into memory by the OS loader as a native module
23+
/// into an isolated AssemblyLoadContext.
2324
/// </summary>
2425
/// <param name="moduleHandle">The native module handle for the assembly.</param>
2526
/// <param name="assemblyPath">The path to the assembly (as a pointer to a UTF-16 C string).</param>
2627
public static unsafe void LoadInMemoryAssembly(IntPtr moduleHandle, IntPtr assemblyPath)
2728
{
28-
#if TARGET_WINDOWS
2929
if (!IsSupported)
30-
throw new NotSupportedException("This API is not enabled in trimmed scenarios. see https://aka.ms/dotnet-illink/nativehost for more details");
30+
throw new NotSupportedException(SR.NotSupported_CppCli);
31+
32+
#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
33+
LoadInMemoryAssemblyInContextImpl(moduleHandle, assemblyPath);
34+
#pragma warning restore IL2026
35+
}
36+
37+
/// <summary>
38+
/// Loads into an assembly that has already been loaded into memory by the OS loader as a native module
39+
/// into the specified load context.
40+
/// </summary>
41+
/// <param name="moduleHandle">The native module handle for the assembly.</param>
42+
/// <param name="assemblyPath">The path to the assembly (as a pointer to a UTF-16 C string).</param>
43+
/// <param name="loadContext">Load context (currently must be IntPtr.Zero)</param>
44+
[UnmanagedCallersOnly]
45+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
46+
Justification = "The same C++/CLI feature switch applies to LoadInMemoryAssembly and this function. We rely on the warning from LoadInMemoryAssembly.")]
47+
public static unsafe void LoadInMemoryAssemblyInContext(IntPtr moduleHandle, IntPtr assemblyPath, IntPtr loadContext)
48+
{
49+
if (!IsSupported)
50+
throw new NotSupportedException(SR.NotSupported_CppCli);
51+
52+
if (loadContext != IntPtr.Zero)
53+
throw new ArgumentOutOfRangeException(nameof(loadContext));
3154

55+
LoadInMemoryAssemblyInContextImpl(moduleHandle, assemblyPath, AssemblyLoadContext.Default);
56+
}
57+
58+
[RequiresUnreferencedCode("C++/CLI is not trim-compatible", Url = "https://aka.ms/dotnet-illink/nativehost")]
59+
private static void LoadInMemoryAssemblyInContextImpl(IntPtr moduleHandle, IntPtr assemblyPath, AssemblyLoadContext? alc = null)
60+
{
3261
string? assemblyPathString = Marshal.PtrToStringUni(assemblyPath);
3362
if (assemblyPathString == null)
34-
{
3563
throw new ArgumentOutOfRangeException(nameof(assemblyPath));
36-
}
3764

38-
// We don't cache the ALCs here since each IJW assembly will call this method at most once
65+
// We don't cache the ALCs or resolvers here since each IJW assembly will call this method at most once
3966
// (the load process rewrites the stubs that call here to call the actual methods they're supposed to)
40-
#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
41-
AssemblyLoadContext context = new IsolatedComponentLoadContext(assemblyPathString);
42-
#pragma warning restore IL2026
43-
context.LoadFromInMemoryModule(moduleHandle);
44-
#else
45-
throw new PlatformNotSupportedException();
46-
#endif
67+
if (alc is null)
68+
{
69+
alc = new IsolatedComponentLoadContext(assemblyPathString);
70+
}
71+
else if (alc == AssemblyLoadContext.Default)
72+
{
73+
var resolver = new AssemblyDependencyResolver(assemblyPathString);
74+
AssemblyLoadContext.Default.Resolving +=
75+
[RequiresUnreferencedCode("C++/CLI is not trim-compatible", Url = "https://aka.ms/dotnet-illink/nativehost")]
76+
(context, assemblyName) =>
77+
{
78+
string? assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
79+
return assemblyPath != null
80+
? context.LoadFromAssemblyPath(assemblyPath)
81+
: null;
82+
};
83+
}
84+
85+
alc.LoadFromInMemoryModule(moduleHandle);
4786
}
4887
}
4988
}

src/installer/tests/HostActivation.Tests/NativeHosting/Ijwhost.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void LoadLibrary()
3535

3636
result.Should().Pass()
3737
.And.HaveStdOutContaining("[C++/CLI] NativeEntryPoint: calling managed class")
38-
.And.HaveStdOutContaining("[C++/CLI] ManagedClass: AssemblyLoadContext = \"IsolatedComponentLoadContext");
38+
.And.HaveStdOutContaining("[C++/CLI] ManagedClass: AssemblyLoadContext = \"Default\" System.Runtime.Loader.DefaultAssemblyLoadContext");
3939
}
4040

4141
[Theory]
@@ -57,7 +57,7 @@ public void ManagedHost(bool selfContained)
5757

5858
result.Should().Pass()
5959
.And.HaveStdOutContaining("[C++/CLI] NativeEntryPoint: calling managed class")
60-
.And.HaveStdOutContaining("[C++/CLI] ManagedClass: AssemblyLoadContext = \"IsolatedComponentLoadContext")
60+
.And.HaveStdOutContaining("[C++/CLI] ManagedClass: AssemblyLoadContext = \"Default\" System.Runtime.Loader.DefaultAssemblyLoadContext")
6161
.And.HaveStdErrContaining($"Executing as a {(selfContained ? "self-contained" : "framework-dependent")} app");
6262
}
6363

src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.Shared.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616
-->
1717
<type fullname="Internal.Runtime.InteropServices.ComponentActivator">
1818
<method name="LoadAssemblyAndGetFunctionPointer" />
19+
</type>
20+
</assembly>
21+
22+
<assembly fullname="System.Private.CoreLib">
23+
<!-- Rooting GetFunctionPointer allows for a reasonable error experience when using any form of native hosting on a trimmed app. -->
24+
<type fullname="Internal.Runtime.InteropServices.ComponentActivator">
1925
<method name="GetFunctionPointer" />
2026
</type>
2127
</assembly>
22-
28+
2329
<!-- Properties and methods used by a debugger. -->
2430
<assembly fullname="System.Private.CoreLib" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="true" featuredefault="true">
2531
<type fullname="System.Threading.Tasks.Task">
@@ -69,7 +75,7 @@
6975
<method name="GetActiveTaskFromId" />
7076
</type>
7177

72-
<!-- methods used by hot reload -->
78+
<!-- methods used by hot reload -->
7379
<type fullname="System.Reflection.Metadata.MetadataUpdater">
7480
<method name="GetCapabilities" />
7581
</type>

src/libraries/System.Private.CoreLib/src/Internal/Runtime/InteropServices/ComponentActivator.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Internal.Runtime.InteropServices
1313
{
14-
public static class ComponentActivator
14+
public static partial class ComponentActivator
1515
{
1616
private const string TrimIncompatibleWarningMessage = "Native hosting is not trim compatible and this warning will be seen if trimming is enabled.";
1717
private const string NativeAOTIncompatibleWarningMessage = "The native code for the method requested might not be available at runtime.";
@@ -115,7 +115,24 @@ public static unsafe int GetFunctionPointer(IntPtr typeNameNative,
115115
IntPtr functionHandle)
116116
{
117117
if (!IsSupported)
118+
{
119+
#if CORECLR
120+
try
121+
{
122+
OnDisabledGetFunctionPointerCall(typeNameNative, methodNameNative);
123+
}
124+
catch (Exception e)
125+
{
126+
// The callback can intentionally throw NotSupportedException to provide errors to consumers,
127+
// so we let that one through. Any other exceptions must not be leaked out.
128+
if (e is NotSupportedException)
129+
throw;
130+
131+
return e.HResult;
132+
}
133+
#endif
118134
return HostFeatureDisabled;
135+
}
119136

120137
try
121138
{

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3727,6 +3727,9 @@
37273727
<data name="NotSupported_COM" xml:space="preserve">
37283728
<value>Built-in COM has been disabled via a feature switch. See https://aka.ms/dotnet-illink/com for more information.</value>
37293729
</data>
3730+
<data name="NotSupported_CppCli" xml:space="preserve">
3731+
<value>C++/CLI activation has been disabled via a feature switch. See https://aka.ms/dotnet-illink/nativehost for more information.</value>
3732+
</data>
37303733
<data name="InvalidOperation_EmptyQueue" xml:space="preserve">
37313734
<value>Queue empty.</value>
37323735
</data>

src/native/corehost/ijwhost/ijwhost.cpp

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
#include "ijwhost.h"
5-
#include "hostfxr.h"
6-
#include "fxr_resolver.h"
7-
#include "pal.h"
8-
#include "trace.h"
9-
#include "error_codes.h"
10-
#include "utils.h"
5+
#include <coreclr_delegates.h>
6+
#include <hostfxr.h>
7+
#include <fxr_resolver.h>
8+
#include <pal.h>
9+
#include <trace.h>
10+
#include <error_codes.h>
11+
#include <utils.h>
1112
#include "bootstrap_thunk.h"
1213

13-
1414
#if defined(_WIN32)
1515
// IJW entry points are defined without the __declspec(dllexport) attribute.
1616
// The issue here is that the MSVC compiler links to the exact name _CorDllMain instead of their stdcall-managled names.
@@ -22,8 +22,9 @@
2222

2323
pal::hresult_t get_load_in_memory_assembly_delegate(pal::dll_t handle, load_in_memory_assembly_fn* delegate)
2424
{
25-
return load_fxr_and_get_delegate(
26-
hostfxr_delegate_type::hdt_load_in_memory_assembly,
25+
get_function_pointer_fn get_function_pointer;
26+
int status = load_fxr_and_get_delegate(
27+
hostfxr_delegate_type::hdt_get_function_pointer,
2728
[handle](const pal::string_t& host_path, pal::string_t* config_path_out)
2829
{
2930
pal::string_t mod_path;
@@ -39,8 +40,18 @@ pal::hresult_t get_load_in_memory_assembly_delegate(pal::dll_t handle, load_in_m
3940

4041
return StatusCode::Success;
4142
},
42-
delegate
43+
&get_function_pointer
4344
);
45+
if (status != StatusCode::Success)
46+
return status;
47+
48+
return get_function_pointer(
49+
_X("Internal.Runtime.InteropServices.InMemoryAssemblyLoader, System.Private.CoreLib"),
50+
_X("LoadInMemoryAssemblyInContext"),
51+
UNMANAGEDCALLERSONLY_METHOD,
52+
nullptr, // load context
53+
nullptr, // reserved
54+
(void**)delegate);
4455
}
4556

4657
IJW_API BOOL STDMETHODCALLTYPE _CorDllMain(HINSTANCE hInst,

src/native/corehost/ijwhost/ijwhost.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ bool patch_vtable_entries(PEDecoder& decoder);
1313
void release_bootstrap_thunks(PEDecoder& decoder);
1414
bool are_thunks_installed_for_module(HMODULE instance);
1515

16-
using load_in_memory_assembly_fn = void(STDMETHODCALLTYPE*)(pal::dll_t handle, const pal::char_t* path);
16+
using load_in_memory_assembly_fn = void(STDMETHODCALLTYPE*)(pal::dll_t handle, const pal::char_t* path, void* load_context);
1717

1818
pal::hresult_t get_load_in_memory_assembly_delegate(pal::dll_t handle, load_in_memory_assembly_fn* delegate);
1919

0 commit comments

Comments
 (0)