Skip to content
Open
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 @@ -14,6 +14,7 @@ public class ILCompilerOptions
public List<string> AdditionalRootAssemblies = new List<string>();
public List<string> RootEntireAssemblies = new List<string>();
public Dictionary<string, bool> FeatureSwitches = new Dictionary<string, bool>();
public Dictionary<string, string> RuntimeKnobs = new Dictionary<string, string>();
public List<string> Descriptors = new List<string>();
public bool FrameworkCompilation;
public bool SingleWarn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ public virtual void AddAdditionalArgument(string flag, string[] values)
{
Options.FeatureSwitches.Add(values[0], bool.Parse(values[1]));
}
if (flag == "--runtimeknob")
{
Options.RuntimeKnobs.Add(values[0], values[1]);
}
else if (flag == "--singlewarn")
{
Options.SingleWarn = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,14 @@ public ILScanResults Trim(ILCompilerOptions options, TrimmingCustomizations? cus
InteropStubManager interopStubManager = new UsageBasedInteropStubManager(interopStateManager, pinvokePolicy, logger);

TypeMapManager typeMapManager = new UsageBasedTypeMapManager(TypeMapMetadata.Empty);
if (entrypointModule is { Assembly: EcmaAssembly entryAssembly })
if (options.RuntimeKnobs.TryGetValue("System.Runtime.InteropServices.TypeMappingEntryAssembly", out string? typeMappingEntryAssemblyName)
&& typeSystemContext.ResolveAssembly(AssemblyNameInfo.Parse(typeMappingEntryAssemblyName), throwIfNotFound: true) is EcmaAssembly typeMapEntryAssembly)
{
typeMapManager = new UsageBasedTypeMapManager(TypeMapMetadata.CreateFromAssembly(typeMapEntryAssembly, typeSystemContext));
}
else if (entrypointModule is { Assembly: EcmaAssembly entryAssembly })
{
// Pass null for typeMappingEntryAssembly to use default entry assembly behavior in tests
typeMapManager = new UsageBasedTypeMapManager(TypeMapMetadata.CreateFromAssembly(entryAssembly, typeSystemContext));
}

Expand Down
19 changes: 18 additions & 1 deletion src/coreclr/tools/aot/ILCompiler/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,25 @@ public int Run()
compilationRoots.Add(new ILCompiler.DependencyAnalysis.TrimmingDescriptorNode(linkTrimFilePath));
}

if (entrypointModule is { Assembly: EcmaAssembly entryAssembly })
// Get TypeMappingEntryAssembly from runtime knobs if specified
string typeMappingEntryAssembly = null;
foreach (var runtimeKnob in runtimeKnobs)
{
var knobAndValue = runtimeKnob.Split('=', 2);
if (knobAndValue.Length == 2 && knobAndValue[0] == "System.Runtime.InteropServices.TypeMappingEntryAssembly")
{
typeMappingEntryAssembly = knobAndValue[1];
break;
}
}
if (typeMappingEntryAssembly is not null)
{
var typeMapEntryAssembly = (EcmaAssembly)typeSystemContext.ResolveAssembly(AssemblyNameInfo.Parse(typeMappingEntryAssembly), throwIfNotFound: true);
typeMapManager = new UsageBasedTypeMapManager(TypeMapMetadata.CreateFromAssembly(typeMapEntryAssembly, typeSystemContext));
}
else if (entrypointModule is { Assembly: EcmaAssembly entryAssembly })
{
// Fall back to entryassembly if not specified
typeMapManager = new UsageBasedTypeMapManager(TypeMapMetadata.CreateFromAssembly(entryAssembly, typeSystemContext));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,16 @@ private static unsafe CallbackContext CreateMaps(
delegate* unmanaged<CallbackContext*, ProcessAttributesCallbackArg*, Interop.BOOL> newExternalTypeEntry,
delegate* unmanaged<CallbackContext*, ProcessAttributesCallbackArg*, Interop.BOOL> newProxyTypeEntry)
{
RuntimeAssembly? startingAssembly = (RuntimeAssembly?)Assembly.GetEntryAssembly();
RuntimeAssembly? startingAssembly;
if (AppContext.GetData("System.Runtime.InteropServices.TypeMappingEntryAssembly") is string entryAssemblyName)
{
startingAssembly = Assembly.Load(entryAssemblyName) as RuntimeAssembly;
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assembly.Load can throw exceptions (e.g., FileNotFoundException, BadImageFormatException) if the assembly name is invalid or not found. Consider wrapping this in a try-catch block and providing a clear error message that includes the assembly name that failed to load, or let the exception propagate with additional context.

Suggested change
startingAssembly = Assembly.Load(entryAssemblyName) as RuntimeAssembly;
try
{
startingAssembly = Assembly.Load(entryAssemblyName) as RuntimeAssembly;
}
catch (Exception ex) when (
ex is FileNotFoundException ||
ex is FileLoadException ||
ex is BadImageFormatException)
{
throw new InvalidOperationException(
$"Failed to load assembly '{entryAssemblyName}' specified by 'System.Runtime.InteropServices.TypeMappingEntryAssembly'.",
ex);
}

Copilot uses AI. Check for mistakes.
}
else
{
startingAssembly = Assembly.GetEntryAssembly() as RuntimeAssembly;
}

if (startingAssembly is null)
{
throw new InvalidOperationException(SR.InvalidOperation_TypeMapMissingEntryAssembly);
Expand Down
20 changes: 20 additions & 0 deletions src/tests/Interop/TypeMap/Lib5.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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;

// This library defines TypeMap entries that should be used when
// TypeMapLib5 is specified as the TypeMappingEntryAssembly
[assembly: TypeMap<AlternateEntryPoint>("lib5_type1", typeof(Lib5Type1))]
[assembly: TypeMap<AlternateEntryPoint>("lib5_type2", typeof(Lib5Type2))]

[assembly: TypeMapAssociation<AlternateEntryPoint>(typeof(Lib5Type1), typeof(Lib5Proxy1))]
[assembly: TypeMapAssociation<AlternateEntryPoint>(typeof(Lib5Type2), typeof(Lib5Proxy2))]

public class Lib5Type1 { }
public class Lib5Type2 { }
public class Lib5Proxy1 { }
public class Lib5Proxy2 { }

public class AlternateEntryPoint { }
54 changes: 54 additions & 0 deletions src/tests/Interop/TypeMap/TypeMapEntryAssemblyApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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.Collections.Generic;
using System.Runtime.InteropServices;
using Xunit;

// Note: This test does NOT define any TypeMap attributes.
// The TypeMappingEntryAssembly is set to TypeMapLib5 via RuntimeHostConfigurationOption,
// so the type maps should be resolved from TypeMapLib5 instead of this assembly.

public class TypeMapEntryAssemblyTest
{
[Fact]
public static void Validate_TypeMappingEntryAssembly_ExternalMap()
{
Console.WriteLine(nameof(Validate_TypeMappingEntryAssembly_ExternalMap));

// These types should be resolved from TypeMapLib5's type maps
IReadOnlyDictionary<string, Type> externalMap = TypeMapping.GetOrCreateExternalTypeMapping<AlternateEntryPoint>();

Assert.Equal(typeof(Lib5Type1), externalMap["lib5_type1"]);
Assert.Equal(typeof(Lib5Type2), externalMap["lib5_type2"]);

Assert.True(externalMap.TryGetValue("lib5_type1", out Type? type1));
Assert.Equal(typeof(Lib5Type1), type1);

Assert.True(externalMap.TryGetValue("lib5_type2", out Type? type2));
Assert.Equal(typeof(Lib5Type2), type2);

Assert.False(externalMap.TryGetValue("nonexistent", out Type? _));
}

[Fact]
public static void Validate_TypeMappingEntryAssembly_ProxyMap()
{
Console.WriteLine(nameof(Validate_TypeMappingEntryAssembly_ProxyMap));

// These proxy mappings should be resolved from TypeMapLib5's type maps
IReadOnlyDictionary<Type, Type> proxyMap = TypeMapping.GetOrCreateProxyTypeMapping<AlternateEntryPoint>();

Assert.Equal(typeof(Lib5Proxy1), proxyMap[typeof(Lib5Type1)]);
Assert.Equal(typeof(Lib5Proxy2), proxyMap[typeof(Lib5Type2)]);

Assert.True(proxyMap.TryGetValue(typeof(Lib5Type1), out Type? proxy1));
Assert.Equal(typeof(Lib5Proxy1), proxy1);

Assert.True(proxyMap.TryGetValue(typeof(Lib5Type2), out Type? proxy2));
Assert.Equal(typeof(Lib5Proxy2), proxy2);

Assert.False(proxyMap.TryGetValue(typeof(string), out Type? _));
}
}
29 changes: 29 additions & 0 deletions src/tests/Interop/TypeMap/TypeMapEntryAssemblyApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RequiresProcessIsolation>true</RequiresProcessIsolation>
<TypeMapEntryAssembly>TypeMapLib5</TypeMapEntryAssembly>
<MonoAotIncompatible>true</MonoAotIncompatible>
<DisableProjectBuild Condition="'$(RuntimeFlavor)' == 'mono'">true</DisableProjectBuild>
<IlcGenerateMstatFile>true</IlcGenerateMstatFile>
<IlcGenerateDgmlFile>true</IlcGenerateDgmlFile>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>

<ItemGroup>
<Compile Include="TypeMapEntryAssemblyApp.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="TypeMapLib5.csproj" />
<ProjectReference Include="TypeMapLib1.csproj" />
</ItemGroup>

<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Runtime.InteropServices.TypeMappingEntryAssembly"
Value="$(TypeMapEntryAssembly)"
Condition="'$(TypeMapEntryAssembly)' != ''"
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks if TypeMapEntryAssembly is not empty, but line 6 always sets it to 'TypeMapLib5', making this condition always true. Consider removing the condition or explaining why it's needed for cases where the property might not be set.

Suggested change
Condition="'$(TypeMapEntryAssembly)' != ''"

Copilot uses AI. Check for mistakes.
Trim="true" />
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions src/tests/Interop/TypeMap/TypeMapLib5.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<Compile Include="Lib5.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="TypeMapLib1.csproj" />
</ItemGroup>
</Project>
13 changes: 12 additions & 1 deletion src/tools/illink/src/linker/Linker.Steps/MarkStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,18 @@ protected virtual void Initialize()
{
InitializeCorelibAttributeXml();
Context.Pipeline.InitializeMarkHandlers(Context, MarkContext);
_typeMapHandler.Initialize(Context, this, Annotations.GetEntryPointAssembly());

// Check for TypeMappingEntryAssembly override from Features
AssemblyDefinition? startingAssembly = null;
if (Context.Features.TryGetValue("System.Runtime.InteropServices.TypeMappingEntryAssembly", out string? assemblyNameString))
{
var assemblyName = AssemblyNameReference.Parse(assemblyNameString);
startingAssembly = Context.TryResolve(assemblyName);
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Context.TryResolve returns null (assembly not found), the code falls back to the entry point assembly without logging or warning. Consider adding a warning message when the specified TypeMappingEntryAssembly cannot be resolved, as this could indicate a configuration error that should be surfaced to users.

Suggested change
startingAssembly = Context.TryResolve(assemblyName);
startingAssembly = Context.TryResolve(assemblyName);
if (startingAssembly is null)
{
Context.LogWarning(
$"Could not resolve assembly '{assemblyNameString}' specified by the 'System.Runtime.InteropServices.TypeMappingEntryAssembly' feature. Falling back to the entry point assembly.",
origin: null);
}

Copilot uses AI. Check for mistakes.
}
// If resolution fails, fall back to entry point assembly
startingAssembly ??= Annotations.GetEntryPointAssembly();

_typeMapHandler.Initialize(Context, this, startingAssembly);
ProcessMarkedPending();
}

Expand Down
17 changes: 12 additions & 5 deletions src/tools/illink/src/linker/Linker/Driver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -515,14 +515,21 @@ protected int SetupContext(ILogger? customLogger = null)
if (!GetStringParam(token, out string? featureName))
return -1;

if (!GetBoolParam(token, value =>
{
context.SetFeatureValue(featureName, value);
}))
if (!GetStringParam(token, out string? featureValue))
return -1;

// Store all features as strings
context.Features[featureName] = featureValue;

// Store boolean features also as booleans
if (bool.TryParse(featureValue, out bool boolValue))
{
context.SetFeatureValue(featureName, boolValue);
}

continue;
}

case "--new-mvid":
//
// This is not same as --deterministic which calculates MVID
Expand Down Expand Up @@ -1520,7 +1527,7 @@ static void Usage()
Console.WriteLine(" --enable-opt NAME [ASM] Enable one of the additional optimizations globaly or for a specific assembly name");
Console.WriteLine(" sealer: Any method or type which does not have override is marked as sealed");
Console.WriteLine(" --explicit-reflection Adds to members never used through reflection DisablePrivateReflection attribute. Defaults to false");
Console.WriteLine(" --feature FEATURE VALUE Apply any optimizations defined when this feature setting is a constant known at link time");
Console.WriteLine(" --feature FEATURE VALUE Set feature setting as a constant known at link time. The trimmer may be able to optimize code based on the value.");
Console.WriteLine(" --keep-com-interfaces Keep COM interfaces implemented by kept types. Defaults to true");
Console.WriteLine(" --keep-compilers-resources Keep assembly resources used for F# compilation resources. Defaults to false");
Console.WriteLine(" --keep-dep-attributes Keep attributes used for manual dependency tracking. Defaults to false");
Expand Down
3 changes: 3 additions & 0 deletions src/tools/illink/src/linker/Linker/LinkContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ public Pipeline Pipeline

public Dictionary<string, bool> FeatureSettings { get; init; }

public Dictionary<string, string> Features { get; init; }

public List<PInvokeInfo> PInvokes { get; private set; }

public string? PInvokesListFile;
Expand Down Expand Up @@ -219,6 +221,7 @@ protected LinkContext(Pipeline pipeline, ILogger logger, string outputDirectory,
_isTrimmable = new Dictionary<AssemblyDefinition, bool>();
OutputDirectory = outputDirectory;
FeatureSettings = new Dictionary<string, bool>(StringComparer.Ordinal);
Features = new Dictionary<string, string>(StringComparer.Ordinal);

SymbolReaderProvider = new DefaultSymbolReaderProvider(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ public Task RuntimeReflectionExtensionsCalls()
return RunTest();
}

[Fact]
public Task TypeMapEntryAssembly()
{
return RunTest();
}

[Fact]
public Task TypeBaseTypeUseViaReflection()
{
Expand Down
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;
using Mono.Linker.Tests.Cases.Reflection.Dependencies;

[assembly: TypeMap<TypeMapEntryAssemblyGroup>("entry_type1", typeof(EntryType1))]
[assembly: TypeMap<TypeMapEntryAssemblyGroup>("entry_type2", typeof(EntryType2))]

[assembly: TypeMapAssociation<TypeMapEntryAssemblyGroup>(typeof(EntrySource1), typeof(EntryProxy1))]
[assembly: TypeMapAssociation<TypeMapEntryAssemblyGroup>(typeof(EntrySource2), typeof(EntryProxy2))]

namespace Mono.Linker.Tests.Cases.Reflection.Dependencies
{
public class TypeMapEntryAssemblyGroup { }

public class EntryType1 { }
public class EntryType2 { }

public class EntrySource1 { }
public class EntrySource2 { }

public class EntryProxy1 { }
public class EntryProxy2 { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Mono.Linker.Tests.Cases.Reflection.Dependencies
{
public class TypeMapReferencedAssembly
{
public static void Main()
public static void Run()
{
// Mark expected trim targets
_ = new TrimTarget1();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ namespace Mono.Linker.Tests.Cases.Reflection
[KeptAssembly("library.dll")]
[KeptAssembly("library2.dll")]
[KeptTypeInAssembly("library.dll", typeof(TypeMapReferencedAssembly))]
[KeptMemberInAssembly("library.dll", typeof(TypeMapReferencedAssembly), "Main()")]
[KeptMemberInAssembly("library.dll", typeof(TypeMapReferencedAssembly), "Run()")]
[KeptTypeInAssembly("library.dll", typeof(TargetTypeUnconditional1), Tool = Tool.Trimmer)]
[KeptTypeInAssembly("library.dll", typeof(TrimTarget1))]
[KeptMemberInAssembly("library.dll", typeof(TrimTarget1), ".ctor()")]
Expand Down Expand Up @@ -179,7 +179,7 @@ static void ConstrainedStaticCall<T>(T t) where T : IStaticInterface

Console.WriteLine(new ConstructedNoTypeCheckNoBoxStruct(42).Value);

TypeMapReferencedAssembly.Main();
TypeMapReferencedAssembly.Run();

// TypeMapUniverses are independent between External and Proxy type maps.
// That is, if the External type map is used for a given universe, that doesn't keep the Proxy type map, and vice versa.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Runtime.InteropServices;
using Mono.Linker.Tests.Cases.Expectations.Assertions;
using Mono.Linker.Tests.Cases.Expectations.Metadata;
using Mono.Linker.Tests.Cases.Reflection.Dependencies;

namespace Mono.Linker.Tests.Cases.Reflection
{
[SetupCompileBefore("TypeMapEntryAssemblyLib.dll", new[] { "Dependencies/TypeMapEntryAssemblyLib.cs" })]
#if NATIVEAOT
[SetupLinkerArgument("--runtimeknob", "System.Runtime.InteropServices.TypeMappingEntryAssembly", "TypeMapEntryAssemblyLib")]
#else
[SetupLinkerArgument("--feature", "System.Runtime.InteropServices.TypeMappingEntryAssembly", "TypeMapEntryAssemblyLib")]
#endif

// The TypeMapEntryAssemblyGroup is defined in TypeMapEntryAssemblyLib, so its attributes should be kept
[KeptTypeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(TypeMapEntryAssemblyGroup))]
[KeptTypeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(EntryType1))]
[KeptTypeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(EntryType2))]
[KeptTypeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(EntrySource1))]
[KeptTypeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(EntrySource2))]
[KeptTypeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(EntryProxy1))]
[KeptTypeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(EntryProxy2))]

[KeptAttributeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(TypeMapAttribute<TypeMapEntryAssemblyGroup>))]
[KeptAttributeInAssembly("TypeMapEntryAssemblyLib.dll", typeof(TypeMapAssociationAttribute<TypeMapEntryAssemblyGroup>))]

public class TypeMapEntryAssembly
{
public static void Main()
{
// Access the type map to ensure it gets used
var externalMap = TypeMapping.GetOrCreateExternalTypeMapping<TypeMapEntryAssemblyGroup>();
var proxyMap = TypeMapping.GetOrCreateProxyTypeMapping<TypeMapEntryAssemblyGroup>();

Console.WriteLine(externalMap);
Console.WriteLine(proxyMap);
_ = new EntrySource1();
_ = new EntrySource2();
}
}
}
Loading