Skip to content
Closed
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
15 changes: 15 additions & 0 deletions docs/tools/illink/data-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,21 @@ Entire method body is replaces with `throw` instruction when method is reference
</linker>
```

### Throw PlatformNotSupportedException


Entire method body is replaces with `throw new PlatformNotSupportedException()` instruction when method is referenced.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

Spelling error: "replaces" should be "replaced" to match the grammatical structure and be consistent with similar documentation in the file (see line 244 for the same error pattern).

Copilot uses AI. Check for mistakes.

```xml
<linker>
<assembly fullname="Assembly">
<type fullname="Assembly.A">
<method signature="System.String TestMethod()" body="pnse" />
</type>
</assembly>
</linker>
```

### Remove embedded resources

```xml
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Threading/src/System.Threading.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<UseCompilerGeneratedDocXmlFile>false</UseCompilerGeneratedDocXmlFile>
<FeatureWasmManagedThreads Condition="'$(WasmEnableThreads)' == 'true'">true</FeatureWasmManagedThreads>
<DefineConstants Condition="'$(FeatureWasmManagedThreads)' == 'true'">$(DefineConstants);FEATURE_WASM_MANAGED_THREADS</DefineConstants>
<_ExtraTrimmerArgs Condition="'$(TargetsBrowser)' == 'true'">$(_ExtraTrimmerArgs) --target-os browser</_ExtraTrimmerArgs>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/libraries/sfx-finish.proj
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
<!-- update debug symbols -->
<SharedFrameworkILLinkArgs>$(SharedFrameworkILLinkArgs) -b true</SharedFrameworkILLinkArgs>
<SharedFrameworkILLinkArgs Condition="'@(SharedFrameworkSuppressionsXml)' != ''" >$(SharedFrameworkILLinkArgs) --link-attributes "@(SharedFrameworkSuppressionsXml, '" --link-attributes "')"</SharedFrameworkILLinkArgs>
<SharedFrameworkILLinkArgs Condition="'$(TargetsBrowser)' == 'true'">$(SharedFrameworkILLinkArgs) --target-os browser</SharedFrameworkILLinkArgs>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum WellKnownType
System_Object,
System_Attribute,
System_NotSupportedException,
System_PlatformNotSupportedException,
System_Runtime_CompilerServices_DisablePrivateReflectionAttribute,
System_Void
}
Expand All @@ -37,6 +38,7 @@ public static (string Namespace, string Name) GetNamespaceAndName(this WellKnown
WellKnownType.System_Object => ("System", "Object"),
WellKnownType.System_Attribute => ("System", "Attribute"),
WellKnownType.System_NotSupportedException => ("System", "NotSupportedException"),
WellKnownType.System_PlatformNotSupportedException => ("System", "PlatformNotSupportedException"),
WellKnownType.System_Runtime_CompilerServices_DisablePrivateReflectionAttribute => ("System.Runtime.CompilerServices", "DisablePrivateReflectionAttribute"),
WellKnownType.System_Void => ("System", "Void"),
_ => throw new System.ArgumentException(type.ToString())
Expand All @@ -57,6 +59,7 @@ public static (string Namespace, string Name) GetNamespaceAndName(this WellKnown
"Attribute" => WellKnownType.System_Attribute,
"Object" => WellKnownType.System_Object,
"NotSupportedException" => WellKnownType.System_NotSupportedException,
"PlatformNotSupportedException" => WellKnownType.System_PlatformNotSupportedException,
"Void" => WellKnownType.System_Void,
_ => null
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ Copyright (c) .NET Foundation. All rights reserved.
<NoWarn>$(NoWarn);IL2121</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetsBrowser)' == 'true'">
<_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --target-os browser</_ExtraTrimmerArgs>
</PropertyGroup>

<!--
============================================================
ILLink
Expand Down
12 changes: 12 additions & 0 deletions src/tools/illink/src/linker/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,10 @@
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Mono.Linker.LinkContext.PInvokesListFile</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Mono.Linker.MethodAction.ConvertToPNSE</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:Mono.Linker.MessageCategory.WarningAsError</Target>
Expand Down Expand Up @@ -989,6 +993,10 @@
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Mono.Linker.LinkContext.get_IgnoreLinkAttributes</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Mono.Linker.LinkContext.get_TargetOS</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Mono.Linker.LinkContext.get_IgnoreSubstitutions</Target>
Expand Down Expand Up @@ -1301,6 +1309,10 @@
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Mono.Linker.LinkContext.set_IgnoreLinkAttributes(System.Boolean)</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Mono.Linker.LinkContext.set_TargetOS(System.String)</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Mono.Linker.LinkContext.set_IgnoreSubstitutions(System.Boolean)</Target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ protected override void ProcessMethod(TypeDefinition type, XPathNavigator method
case "remove":
_substitutionInfo.SetMethodAction(method, MethodAction.ConvertToThrow);
return;
case "pnse":
_substitutionInfo.SetMethodAction(method, MethodAction.ConvertToPNSE);
return;
case "stub":
string value = GetAttribute(methodNav, "value");
if (!string.IsNullOrEmpty(value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public override void ProcessField(FieldDefinition field)

public override void ProcessMethod(MethodDefinition method)
{
if (Context.Annotations.GetAction(method) != MethodAction.ConvertToThrow)
if (Context.Annotations.GetAction(method) != MethodAction.ConvertToThrow
&& Context.Annotations.GetAction(method) != MethodAction.ConvertToPNSE)
Context.Suppressions.GatherSuppressions(method);
}

Expand Down
38 changes: 38 additions & 0 deletions src/tools/illink/src/linker/Linker.Steps/CodeRewriterStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ void ProcessMethod(MethodDefinition method)
case MethodAction.ConvertToThrow:
RewriteBodyToLinkedAway(method);
break;
case MethodAction.ConvertToPNSE:
RewriteBodyToPNSE(method);
break;
}
}

Expand All @@ -67,6 +70,16 @@ protected virtual void RewriteBodyToLinkedAway(MethodDefinition method)
method.ClearDebugInformation();
}

protected virtual void RewriteBodyToPNSE(MethodDefinition method)
{
method.ImplAttributes &= ~(MethodImplAttributes.AggressiveInlining | MethodImplAttributes.Synchronized);
method.ImplAttributes |= MethodImplAttributes.NoInlining;

method.Body = CreateThrowPNSEBody(method);

method.ClearDebugInformation();
}

protected virtual void RewriteBodyToStub(MethodDefinition method)
{
if (!method.IsIL)
Expand Down Expand Up @@ -103,6 +116,31 @@ MethodBody CreateThrowLinkedAwayBody(MethodDefinition method)
return body;
}

MethodBody CreateThrowPNSEBody(MethodDefinition method)
{
var body = new MethodBody(method);
var il = body.GetLinkerILProcessor();
MethodReference? ctor;

// Makes the body verifiable
if (method.IsConstructor && !method.DeclaringType.IsValueType)
{
ctor = Assembly.MainModule.ImportReference(Context.MarkedKnownMembers.ObjectCtor);

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, ctor);
}

// import the method into the current assembly
ctor = Context.MarkedKnownMembers.PlatformNotSupportedExceptionCtor;
ctor = Assembly.MainModule.ImportReference(ctor);

il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Throw);

return body;
}

MethodBody CreateStubBody(MethodDefinition method)
{
var body = new MethodBody(method);
Expand Down
101 changes: 101 additions & 0 deletions src/tools/illink/src/linker/Linker.Steps/MarkStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3350,6 +3350,9 @@ protected virtual void ProcessMethod(MethodDefinition method, in DependencyInfo
#endif
var methodOrigin = new MessageOrigin(method);

// Check for [UnsupportedOSPlatform] early to potentially skip body scanning
TryMarkMethodForUnsupportedPlatform(method);

bool markedForCall =
reason.Kind == DependencyKind.DirectCall ||
reason.Kind == DependencyKind.VirtualCall ||
Expand Down Expand Up @@ -3582,7 +3585,71 @@ void MarkNewCodeDependencies(MethodDefinition method, MessageOrigin origin)
case MethodAction.ConvertToThrow:
MarkAndCacheConvertToThrowExceptionCtor(new DependencyInfo(DependencyKind.UnreachableBodyRequirement, method), origin);
break;

case MethodAction.ConvertToPNSE:
MarkAndCacheConvertToThrowPNSECtor(new DependencyInfo(DependencyKind.UnreachableBodyRequirement, method), origin);
break;
}
}

/// <summary>
/// Checks if the method has [UnsupportedOSPlatform] attribute matching the target OS
/// and marks it for PNSE conversion if so.
/// </summary>
/// <returns>True if the method was marked for PNSE conversion.</returns>
bool TryMarkMethodForUnsupportedPlatform(MethodDefinition method)
{
if (Context.TargetOS is null)
return false;

// Check method-level attributes
if (HasMatchingUnsupportedOSPlatformAttribute(method))
{
Annotations.SetAction(method, MethodAction.ConvertToPNSE);
return true;
}

return false;
}

/// <summary>
/// Checks if the property has [UnsupportedOSPlatform] attribute matching the target OS
/// and marks its getter/setter for PNSE conversion if so.
/// </summary>
void TryMarkPropertyForUnsupportedPlatform(PropertyDefinition property)
{
if (Context.TargetOS is null)
return;

if (!HasMatchingUnsupportedOSPlatformAttribute(property))
return;

if (property.GetMethod is not null)
Annotations.SetAction(property.GetMethod, MethodAction.ConvertToPNSE);

if (property.SetMethod is not null)
Annotations.SetAction(property.SetMethod, MethodAction.ConvertToPNSE);
}
Comment on lines +3600 to +3632
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The TryMarkMethodForUnsupportedPlatform and TryMarkPropertyForUnsupportedPlatform methods unconditionally set the method action to ConvertToPNSE without checking if an action has already been set (e.g., from XML substitutions). This could overwrite explicit XML substitution directives. Consider checking the current action and only setting ConvertToPNSE if the action is MethodAction.Nothing or MethodAction.Parse to respect explicit substitution configuration.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Checks if the provider has [UnsupportedOSPlatform] attribute matching the target OS.
/// </summary>
bool HasMatchingUnsupportedOSPlatformAttribute(ICustomAttributeProvider provider)
{
if (!provider.HasCustomAttributes)
return false;

foreach (var attribute in Context.CustomAttributes.GetCustomAttributes(provider, "System.Runtime.Versioning", "UnsupportedOSPlatformAttribute"))
{
if (attribute.ConstructorArguments.Count > 0 &&
attribute.ConstructorArguments[0].Value is string platformName &&
string.Equals(platformName, Context.TargetOS, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

protected virtual void MarkAndCacheConvertToThrowExceptionCtor(DependencyInfo reason, MessageOrigin origin)
Expand Down Expand Up @@ -3611,6 +3678,32 @@ protected virtual void MarkAndCacheConvertToThrowExceptionCtor(DependencyInfo re
throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.CouldNotFindConstructor, objectType.GetDisplayName()));
}

protected virtual void MarkAndCacheConvertToThrowPNSECtor(DependencyInfo reason, MessageOrigin origin)
{
if (Context.MarkedKnownMembers.PlatformNotSupportedExceptionCtor != null)
return;

var pnse = BCL.FindPredefinedType(WellKnownType.System_PlatformNotSupportedException, Context);
if (pnse == null)
throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.CouldNotFindType, "System.PlatformNotSupportedException"));

MarkType(pnse, reason, origin);

var pnseCtor = MarkMethodIf(pnse.Methods, KnownMembers.IsPlatformNotSupportedExceptionCtor, reason, origin);
Context.MarkedKnownMembers.PlatformNotSupportedExceptionCtor = pnseCtor ??
throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.CouldNotFindConstructor, pnse.GetDisplayName()));

var objectType = BCL.FindPredefinedType(WellKnownType.System_Object, Context);
if (objectType == null)
throw new NotSupportedException("Missing predefined 'System.Object' type");

MarkType(objectType, reason, origin);

var objectCtor = MarkMethodIf(objectType.Methods, MethodDefinitionExtensions.IsDefaultConstructor, reason, origin);
Context.MarkedKnownMembers.ObjectCtor = objectCtor ??
throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.CouldNotFindConstructor, objectType.GetDisplayName()));
}

bool MarkDisablePrivateReflectionAttribute()
{
if (Context.MarkedKnownMembers.DisablePrivateReflectionAttributeCtor != null)
Expand Down Expand Up @@ -3743,6 +3836,11 @@ protected virtual bool ShouldParseMethodBody(MethodDefinition method)
default:
return false;
}
// Skip body scanning for methods that will be stubbed - their bodies will be replaced
case MethodAction.ConvertToStub:
case MethodAction.ConvertToThrow:
case MethodAction.ConvertToPNSE:
return false;
default:
return false;
}
Expand All @@ -3755,6 +3853,9 @@ protected internal void MarkProperty(PropertyDefinition prop, in DependencyInfo

var propertyOrigin = new MessageOrigin(prop);

// Check for [UnsupportedOSPlatform] on the property and mark getter/setter for PNSE
TryMarkPropertyForUnsupportedPlatform(prop);

// Consider making this more similar to MarkEvent method?
MarkCustomAttributes(prop, new DependencyInfo(DependencyKind.CustomAttribute, prop), propertyOrigin);
DoAdditionalPropertyProcessing(prop);
Expand Down
9 changes: 9 additions & 0 deletions src/tools/illink/src/linker/Linker/Driver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,13 @@ protected int SetupContext(ILogger? customLogger = null)
context.WarningSuppressionWriter = new WarningSuppressionWriter(context, fileOutputKind);
continue;

case "--target-os":
if (!GetStringParam(token, out string? targetOS))
return -1;

context.TargetOS = targetOS;
continue;

case "--notrimwarn":
context.NoTrimWarn = true;
continue;
Expand Down Expand Up @@ -1545,6 +1552,8 @@ static void Usage()
Console.WriteLine(" --link-attributes FILE Supplementary custom attribute definitions for attributes controlling the trimming behavior.");
Console.WriteLine(" --ignore-link-attributes Skips reading embedded attributes. Defaults to false");
Console.WriteLine(" --strip-link-attributes Remove XML link attributes resources for linked assemblies. Defaults to true");
Console.WriteLine(" --target-os NAME Target OS platform name (e.g., browser, ios, windows). Methods with matching");
Console.WriteLine(" [UnsupportedOSPlatform] attribute will be stubbed with PlatformNotSupportedException.");

Console.WriteLine();
Console.WriteLine("Analyzer");
Expand Down
12 changes: 12 additions & 0 deletions src/tools/illink/src/linker/Linker/KnownMembers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Mono.Linker
public class KnownMembers
{
public MethodDefinition? NotSupportedExceptionCtorString { get; set; }
public MethodDefinition? PlatformNotSupportedExceptionCtor { get; set; }
public MethodDefinition? DisablePrivateReflectionAttributeCtor { get; set; }
public MethodDefinition? ObjectCtor { get; set; }

Expand All @@ -25,6 +26,17 @@ public static bool IsNotSupportedExceptionCtorString(MethodDefinition method)
return true;
}

public static bool IsPlatformNotSupportedExceptionCtor(MethodDefinition method)
{
if (!method.IsConstructor || method.IsStatic)
return false;

if (method.GetMetadataParametersCount() != 0)
return false;

return true;
}

public static bool IsSatelliteAssemblyMarker(MethodDefinition method)
{
if (!method.IsConstructor || method.IsStatic)
Expand Down
2 changes: 2 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 bool IgnoreLinkAttributes { get; set; }

public string? TargetOS { get; set; }

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

public List<PInvokeInfo> PInvokes { get; private set; }
Expand Down
1 change: 1 addition & 0 deletions src/tools/illink/src/linker/Linker/MethodAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ public enum MethodAction
ForceParse,
ConvertToStub,
ConvertToThrow,
ConvertToPNSE,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,11 @@ public Task SubstitutionsErrorCases()
return RunTest(allowMissingWarnings: true);
}

[Fact]
public Task UnsupportedOSPlatformSubstitution()
{
return RunTest(allowMissingWarnings: true);
}

}
}
Loading
Loading