Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
<!-- Sources -->
<ItemGroup>
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\ComActivationContextInternal.cs" />
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\InMemoryAssemblyLoader.cs" />
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\ComponentActivator.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\__Canon.cs" />
<Compile Include="$(BclSourcesRoot)\System\ArgIterator.cs" />
<Compile Include="$(BclSourcesRoot)\System\Array.CoreCLR.cs" />
Expand Down Expand Up @@ -285,10 +285,12 @@
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\ComActivator.PlatformNotSupported.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\InMemoryAssemblyLoader.PlatformNotSupported.cs" />
<Compile Include="$(BclSourcesRoot)\Interop\Unix\Interop.Libraries.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\LowLevelLifoSemaphore.Unix.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
<Compile Include="$(BclSourcesRoot)\Internal\Runtime\InteropServices\InMemoryAssemblyLoader.cs" />
<Compile Include="$(CommonPath)Interop\Windows\OleAut32\Interop.VariantClear.cs">
<Link>Common\Interop\Windows\OleAut32\Interop.VariantClear.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@
<type fullname="System.SZArrayHelper">
<method name=".ctor" />
</type>

<!-- Enables the .NET IJW host to load an in-memory module as a .NET assembly.
These are always rooted to ensure native calls get trimmer related errors
but will be trimmed away by the related feature switch -->
<type fullname="Internal.Runtime.InteropServices.InMemoryAssemblyLoader">
<method name="LoadInMemoryAssembly" />
</type>
</assembly>

<!-- The private Event methods are accessed by private reflection in the base EventSource class. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,19 @@
<method name="RegisterClassForTypeInternal" />
<method name="UnregisterClassForTypeInternal" />
</type>

<!-- Enables the .NET IJW host (before .NET 7.0) to load an in-memory module as a .NET assembly.
These are always rooted to ensure native calls get trimmer related errors
but their bodies will be mostly trimmed away by the related feature switch -->
<type fullname="Internal.Runtime.InteropServices.InMemoryAssemblyLoader">
<method name="LoadInMemoryAssembly" />
</type>
</assembly>

<assembly fullname="System.Private.CoreLib" feature="System.Runtime.InteropServices.EnableCppCLIHostActivation" featurevalue="true">
<!-- Enables the .NET IJW host (.NET 7.0+) to load an in-memory module as a .NET assembly. -->
<type fullname="Internal.Runtime.InteropServices.InMemoryAssemblyLoader">
<method name="LoadInMemoryAssemblyInContext" />
</type>
</assembly>
</linker>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;

namespace Internal.Runtime.InteropServices
{
public static partial class ComponentActivator
{
// This hook for when GetFunctionPointer is called when the feature is disabled allows us to
// provide error messages for known hosting scenarios such as C++/CLI.
private static void OnDisabledGetFunctionPointerCall(IntPtr typeNameNative, IntPtr methodNameNative)
{
if (!OperatingSystem.IsWindows())
return;

// Check for the exact type and method name used by ijwhost - see src/native/corehost/ijwhost/ijwhost.cpp
if (Marshal.PtrToStringUni(methodNameNative) == "LoadInMemoryAssemblyInContext"
Comment thread
jkotas marked this conversation as resolved.
&& Marshal.PtrToStringUni(typeNameNative) == $"Internal.Runtime.InteropServices.{nameof(InMemoryAssemblyLoader)}, {CoreLib.Name}")
{
throw new NotSupportedException(SR.NotSupported_CppCli);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
using System.Runtime.InteropServices;
using System.Runtime.Loader;

namespace Internal.Runtime.InteropServices
{
/// <summary>
/// This class enables the .NET IJW host to load an in-memory module as a .NET assembly
/// </summary>
public static class InMemoryAssemblyLoader
{
/// <summary>
/// Loads into an isolated AssemblyLoadContext an assembly that has already been loaded into memory by the OS loader as a native module.
/// </summary>
/// <param name="moduleHandle">The native module handle for the assembly.</param>
/// <param name="assemblyPath">The path to the assembly (as a pointer to a UTF-16 C string).</param>
public static unsafe void LoadInMemoryAssembly(IntPtr moduleHandle, IntPtr assemblyPath)
=> throw new PlatformNotSupportedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,87 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Runtime.Versioning;

namespace Internal.Runtime.InteropServices
{
/// <summary>
/// This class enables the .NET IJW host to load an in-memory module as a .NET assembly
/// </summary>
[SupportedOSPlatform("windows")]
public static class InMemoryAssemblyLoader
{
#if TARGET_WINDOWS
private static bool IsSupported { get; } = InitializeIsSupported();

private static bool InitializeIsSupported() => AppContext.TryGetSwitch("System.Runtime.InteropServices.EnableCppCLIHostActivation", out bool isSupported) ? isSupported : true;
#endif

/// <summary>
/// Loads into an isolated AssemblyLoadContext an assembly that has already been loaded into memory by the OS loader as a native module.
/// Loads an assembly that has already been loaded into memory by the OS loader as a native module
/// into an isolated AssemblyLoadContext.
/// </summary>
/// <param name="moduleHandle">The native module handle for the assembly.</param>
/// <param name="assemblyPath">The path to the assembly (as a pointer to a UTF-16 C string).</param>
public static unsafe void LoadInMemoryAssembly(IntPtr moduleHandle, IntPtr assemblyPath)
{
#if TARGET_WINDOWS
if (!IsSupported)
throw new NotSupportedException("This API is not enabled in trimmed scenarios. see https://aka.ms/dotnet-illink/nativehost for more details");
throw new NotSupportedException(SR.NotSupported_CppCli);

#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
LoadInMemoryAssemblyInContextImpl(moduleHandle, assemblyPath);
#pragma warning restore IL2026
}

/// <summary>
/// Loads into an assembly that has already been loaded into memory by the OS loader as a native module
/// into the specified load context.
/// </summary>
/// <param name="moduleHandle">The native module handle for the assembly.</param>
/// <param name="assemblyPath">The path to the assembly (as a pointer to a UTF-16 C string).</param>
/// <param name="loadContext">Load context (currently must be IntPtr.Zero)</param>
[UnmanagedCallersOnly]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "The same C++/CLI feature switch applies to LoadInMemoryAssembly and this function. We rely on the warning from LoadInMemoryAssembly.")]
public static unsafe void LoadInMemoryAssemblyInContext(IntPtr moduleHandle, IntPtr assemblyPath, IntPtr loadContext)
{
if (!IsSupported)
throw new NotSupportedException(SR.NotSupported_CppCli);

if (loadContext != IntPtr.Zero)
throw new ArgumentOutOfRangeException(nameof(loadContext));

LoadInMemoryAssemblyInContextImpl(moduleHandle, assemblyPath, AssemblyLoadContext.Default);
}

[RequiresUnreferencedCode("C++/CLI is not trim-compatible", Url = "https://aka.ms/dotnet-illink/nativehost")]
private static void LoadInMemoryAssemblyInContextImpl(IntPtr moduleHandle, IntPtr assemblyPath, AssemblyLoadContext? alc = null)
{
string? assemblyPathString = Marshal.PtrToStringUni(assemblyPath);
if (assemblyPathString == null)
{
throw new ArgumentOutOfRangeException(nameof(assemblyPath));
}

// We don't cache the ALCs here since each IJW assembly will call this method at most once
// We don't cache the ALCs or resolvers here since each IJW assembly will call this method at most once
// (the load process rewrites the stubs that call here to call the actual methods they're supposed to)
#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
AssemblyLoadContext context = new IsolatedComponentLoadContext(assemblyPathString);
#pragma warning restore IL2026
context.LoadFromInMemoryModule(moduleHandle);
#else
throw new PlatformNotSupportedException();
#endif
if (alc is null)
{
alc = new IsolatedComponentLoadContext(assemblyPathString);
}
else if (alc == AssemblyLoadContext.Default)
{
var resolver = new AssemblyDependencyResolver(assemblyPathString);
AssemblyLoadContext.Default.Resolving +=
[RequiresUnreferencedCode("C++/CLI is not trim-compatible", Url = "https://aka.ms/dotnet-illink/nativehost")]
Comment thread
elinor-fung marked this conversation as resolved.
(context, assemblyName) =>
{
string? assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath != null
? context.LoadFromAssemblyPath(assemblyPath)
: null;
};
}

alc.LoadFromInMemoryModule(moduleHandle);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void LoadLibrary()

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@
-->
<type fullname="Internal.Runtime.InteropServices.ComponentActivator">
<method name="LoadAssemblyAndGetFunctionPointer" />
</type>
</assembly>

<assembly fullname="System.Private.CoreLib">
<!-- Rooting GetFunctionPointer allows for a reasonable error experience when using any form of native hosting on a trimmed app. -->
<type fullname="Internal.Runtime.InteropServices.ComponentActivator">
<method name="GetFunctionPointer" />
</type>
</assembly>

<!-- Properties and methods used by a debugger. -->
<assembly fullname="System.Private.CoreLib" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="true" featuredefault="true">
<type fullname="System.Threading.Tasks.Task">
Expand Down Expand Up @@ -69,7 +75,7 @@
<method name="GetActiveTaskFromId" />
</type>

<!-- methods used by hot reload -->
<!-- methods used by hot reload -->
<type fullname="System.Reflection.Metadata.MetadataUpdater">
<method name="GetCapabilities" />
</type>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Internal.Runtime.InteropServices
{
public static class ComponentActivator
public static partial class ComponentActivator
{
private const string TrimIncompatibleWarningMessage = "Native hosting is not trim compatible and this warning will be seen if trimming is enabled.";
private const string NativeAOTIncompatibleWarningMessage = "The native code for the method requested might not be available at runtime.";
Expand Down Expand Up @@ -115,7 +115,24 @@ public static unsafe int GetFunctionPointer(IntPtr typeNameNative,
IntPtr functionHandle)
{
if (!IsSupported)
{
#if CORECLR
try
{
OnDisabledGetFunctionPointerCall(typeNameNative, methodNameNative);
}
catch (Exception e)
{
// The callback can intentionally throw NotSupportedException to provide errors to consumers,
// so we let that one through. Any other exceptions must not be leaked out.
if (e is NotSupportedException)
throw;

return e.HResult;
}
#endif
return HostFeatureDisabled;
}

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3727,6 +3727,9 @@
<data name="NotSupported_COM" xml:space="preserve">
<value>Built-in COM has been disabled via a feature switch. See https://aka.ms/dotnet-illink/com for more information.</value>
</data>
<data name="NotSupported_CppCli" xml:space="preserve">
<value>C++/CLI activation has been disabled via a feature switch. See https://aka.ms/dotnet-illink/nativehost for more information.</value>
</data>
<data name="InvalidOperation_EmptyQueue" xml:space="preserve">
<value>Queue empty.</value>
</data>
Expand Down
31 changes: 21 additions & 10 deletions src/native/corehost/ijwhost/ijwhost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.

#include "ijwhost.h"
#include "hostfxr.h"
#include "fxr_resolver.h"
#include "pal.h"
#include "trace.h"
#include "error_codes.h"
#include "utils.h"
#include <coreclr_delegates.h>
#include <hostfxr.h>
#include <fxr_resolver.h>
#include <pal.h>
#include <trace.h>
#include <error_codes.h>
#include <utils.h>
#include "bootstrap_thunk.h"


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

pal::hresult_t get_load_in_memory_assembly_delegate(pal::dll_t handle, load_in_memory_assembly_fn* delegate)
{
return load_fxr_and_get_delegate(
hostfxr_delegate_type::hdt_load_in_memory_assembly,
get_function_pointer_fn get_function_pointer;
int status = load_fxr_and_get_delegate(
hostfxr_delegate_type::hdt_get_function_pointer,
[handle](const pal::string_t& host_path, pal::string_t* config_path_out)
{
pal::string_t mod_path;
Expand All @@ -39,8 +40,18 @@ pal::hresult_t get_load_in_memory_assembly_delegate(pal::dll_t handle, load_in_m

return StatusCode::Success;
},
delegate
&get_function_pointer
);
if (status != StatusCode::Success)
return status;

return get_function_pointer(
_X("Internal.Runtime.InteropServices.InMemoryAssemblyLoader, System.Private.CoreLib"),
_X("LoadInMemoryAssemblyInContext"),
UNMANAGEDCALLERSONLY_METHOD,
nullptr, // load context
nullptr, // reserved
(void**)delegate);
}

IJW_API BOOL STDMETHODCALLTYPE _CorDllMain(HINSTANCE hInst,
Expand Down
2 changes: 1 addition & 1 deletion src/native/corehost/ijwhost/ijwhost.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ bool patch_vtable_entries(PEDecoder& decoder);
void release_bootstrap_thunks(PEDecoder& decoder);
bool are_thunks_installed_for_module(HMODULE instance);

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

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

Expand Down
Loading