diff --git a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs
index 3be221bc216195..00b3ac1277afbf 100644
--- a/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/PInvokeTableGeneratorTests.cs
@@ -422,6 +422,23 @@ public async Task EnsureWasmAbiRulesAreFollowedInAOT(Configuration config, bool
public async Task EnsureWasmAbiRulesAreFollowedInInterpreter(Configuration config, bool aot) =>
await EnsureWasmAbiRulesAreFollowed(config, aot);
+ [Theory]
+ [BuildAndRun(aot: false)]
+ [TestCategory("native-mono")]
+ public void UnsupportedOSPlatformPInvokeIsSkipped(Configuration config, bool aot)
+ {
+ // https://github.com/dotnet/runtime/issues/110870: a Windows-only pinvoke with
+ // non-blittable parameters must be skipped (not analyzed) when building for the
+ // browser, so it must not emit WASM0060/WASM0062/WASM0001.
+ ProjectInfo info = CopyTestAsset(config, aot, TestAsset.WasmBasicTestApp, "osplatform_pinvoke",
+ extraProperties: "true");
+ ReplaceFile(Path.Combine("Common", "Program.cs"), Path.Combine(BuildEnvironment.TestAssetsPath, "EntryPoints", "PInvoke", "UnsupportedOSPlatform.cs"));
+ (_, string output) = BuildProject(info, config, new BuildOptions(AssertAppBundle: false, AOT: aot), isNativeBuild: true);
+ Assert.DoesNotContain("WASM0001", output);
+ Assert.DoesNotContain("WASM0060", output);
+ Assert.DoesNotContain("WASM0062", output);
+ }
+
[Theory]
[BuildAndRun(aot: true, config: Configuration.Release)]
[TestCategory("native-mono")]
diff --git a/src/mono/wasm/build/WasmApp.Common.targets b/src/mono/wasm/build/WasmApp.Common.targets
index a8e6211aab2741..5ab9c6323cc81f 100644
--- a/src/mono/wasm/build/WasmApp.Common.targets
+++ b/src/mono/wasm/build/WasmApp.Common.targets
@@ -792,6 +792,7 @@
PInvokeOutputPath="$(_WasmPInvokeTablePath)"
InterpToNativeOutputPath="$(_WasmInterpToNativeTablePath)"
CacheFilePath="$(_WasmM2NCachePath)"
+ TargetOS="$(TargetOS)"
IsLibraryMode="$(_IsLibraryMode)">
diff --git a/src/mono/wasm/testassets/EntryPoints/PInvoke/UnsupportedOSPlatform.cs b/src/mono/wasm/testassets/EntryPoints/PInvoke/UnsupportedOSPlatform.cs
new file mode 100644
index 00000000000000..9932fa33995814
--- /dev/null
+++ b/src/mono/wasm/testassets/EntryPoints/PInvoke/UnsupportedOSPlatform.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+public class Test
+{
+ public static int Main(string[] argv)
+ {
+ Console.WriteLine("TestOutput -> Main running");
+ return 42;
+ }
+}
+
+// Regression coverage for https://github.com/dotnet/runtime/issues/110870:
+// a Windows-only P/Invoke with non-blittable parameters must be skipped by the wasm
+// pinvoke collector when building for the browser, and must not emit WASM0060/WASM0062/WASM0001.
+[SupportedOSPlatform("windows")]
+internal static class Win32Interop
+{
+ [DllImport("gdi32.dll")]
+ public static extern int GetCharABCWidthsFloat(HandleRef hdc, uint iFirst, uint iLast, [Out] ABCFLOAT[] lpABCF);
+
+ [DllImport("user32.dll")]
+ public static extern int MethodWithNonBlittableObject(object arg, HandleRef handle);
+}
+
+internal struct ABCFLOAT
+{
+#pragma warning disable CS0649 // fields are only used to describe the native struct layout
+ public float abcfA;
+ public float abcfB;
+ public float abcfC;
+#pragma warning restore CS0649
+}
diff --git a/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs b/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs
index 047f0d11169d1e..6169c9dccf5e13 100644
--- a/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs
+++ b/src/tasks/WasmAppBuilder/mono/ManagedToNativeGenerator.cs
@@ -34,6 +34,10 @@ public class ManagedToNativeGenerator : Task
public bool IsLibraryMode { get; set; }
+ public string TargetOS { get; set; } = "browser";
+
+ private static readonly string[] s_knownTargetOSes = new[] { "browser", "wasi" };
+
[Output]
public string[]? FileWrites { get; private set; }
@@ -51,6 +55,19 @@ public override bool Execute()
return false;
}
+ if (string.IsNullOrWhiteSpace(TargetOS))
+ {
+ Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(TargetOS)} cannot be empty; expected one of: {string.Join(", ", s_knownTargetOSes)}");
+ return false;
+ }
+
+ TargetOS = TargetOS.Trim().ToLowerInvariant();
+ if (Array.IndexOf(s_knownTargetOSes, TargetOS) < 0)
+ {
+ Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(TargetOS)} '{TargetOS}' is not recognized; expected one of: {string.Join(", ", s_knownTargetOSes)}");
+ return false;
+ }
+
try
{
var logAdapter = new LogAdapter(Log);
@@ -70,7 +87,7 @@ private void ExecuteInternal(LogAdapter log)
List managedAssemblies = FilterOutUnmanagedBinaries(Assemblies);
if (ShouldRun(managedAssemblies))
{
- var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log, IsLibraryMode);
+ var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log, IsLibraryMode, TargetOS);
var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, log, isCoreClr: false);
var resolver = new PathAssemblyResolver(managedAssemblies);
diff --git a/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs b/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs
index 1c774e60441a43..28d0f0f680a6d9 100644
--- a/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs
+++ b/src/tasks/WasmAppBuilder/mono/PInvokeCollector.cs
@@ -60,11 +60,15 @@ public int GetHashCode(PInvoke pinvoke)
internal sealed class PInvokeCollector {
private readonly Dictionary _assemblyDisableRuntimeMarshallingAttributeCache = new();
+ private readonly Dictionary _typeUnsupportedOnPlatformCache = new();
+ private readonly Dictionary _assemblyUnsupportedOnPlatformCache = new();
+ private readonly string _targetOS;
private LogAdapter Log { get; init; }
- public PInvokeCollector(LogAdapter log)
+ public PInvokeCollector(LogAdapter log, string targetOS)
{
Log = log;
+ _targetOS = targetOS;
}
public void CollectPInvokes(List pinvokes, List callbacks, HashSet signatures, Type type)
@@ -102,6 +106,9 @@ void CollectPInvokesForMethod(MethodInfo method)
{
if ((method.Attributes & MethodAttributes.PinvokeImpl) != 0)
{
+ if (IsUnsupportedOnPlatform(method))
+ return;
+
var dllimport = method.CustomAttributes.First(attr => attr.AttributeType.Name == "DllImportAttribute");
var wasmLinkage = method.CustomAttributes.Any(attr => attr.AttributeType.Name == "WasmImportLinkageAttribute");
var module = (string)dllimport.ConstructorArguments[0].Value!;
@@ -124,6 +131,9 @@ bool DoesMethodHaveCallbacks(MethodInfo method, LogAdapter log)
if (!MethodHasCallbackAttributes(method))
return false;
+ if (IsUnsupportedOnPlatform(method))
+ return false;
+
if (TryIsMethodGetParametersUnsupported(method, out string? reason))
{
Log.Warning("WASM0001", $"Skipping callback '{method.DeclaringType!.FullName}::{method.Name}' because '{reason}'.");
@@ -207,6 +217,102 @@ private bool HasAssemblyDisableRuntimeMarshallingAttribute(Assembly assembly)
return value;
}
+
+ private bool IsUnsupportedOnPlatform(MethodInfo method)
+ {
+ PlatformSupport methodResult = EvaluatePlatformAttributes(CustomAttributeData.GetCustomAttributes(method));
+ if (methodResult == PlatformSupport.Unsupported)
+ return true;
+ if (methodResult == PlatformSupport.Supported)
+ return false;
+
+ return IsUnsupportedOnPlatform(method.DeclaringType);
+ }
+
+ private bool IsUnsupportedOnPlatform(Type? type)
+ {
+ if (type is null)
+ return false;
+
+ if (_typeUnsupportedOnPlatformCache.TryGetValue(type, out bool cached))
+ return cached;
+
+ bool value;
+ PlatformSupport typeResult = EvaluatePlatformAttributes(CustomAttributeData.GetCustomAttributes(type));
+ if (typeResult == PlatformSupport.Unsupported)
+ {
+ value = true;
+ }
+ else if (typeResult == PlatformSupport.Supported)
+ {
+ value = false;
+ }
+ else if (type.DeclaringType is not null)
+ {
+ value = IsUnsupportedOnPlatform(type.DeclaringType);
+ }
+ else
+ {
+ value = IsAssemblyUnsupportedOnPlatform(type.Assembly);
+ }
+
+ _typeUnsupportedOnPlatformCache[type] = value;
+ return value;
+ }
+
+ private bool IsAssemblyUnsupportedOnPlatform(Assembly assembly)
+ {
+ if (!_assemblyUnsupportedOnPlatformCache.TryGetValue(assembly, out bool value))
+ {
+ PlatformSupport asmResult = EvaluatePlatformAttributes(assembly.GetCustomAttributesData());
+ value = asmResult == PlatformSupport.Unsupported;
+ _assemblyUnsupportedOnPlatformCache[assembly] = value;
+ }
+
+ return value;
+ }
+
+ private enum PlatformSupport
+ {
+ Unknown, // No platform attributes were observed at this scope
+ Supported, // Explicitly supported here (target appears in a SupportedOSPlatform list)
+ Unsupported, // Explicitly unsupported here (target matches UnsupportedOSPlatform, or
+ // SupportedOSPlatform is present and does not list the target)
+ }
+
+ private PlatformSupport EvaluatePlatformAttributes(IList attrs)
+ {
+ bool hasSupportedOSPlatform = false;
+ bool hasSupportedTarget = false;
+ foreach (CustomAttributeData cattr in attrs)
+ {
+ try
+ {
+ if (cattr.AttributeType.FullName == "System.Runtime.Versioning.UnsupportedOSPlatformAttribute" &&
+ cattr.ConstructorArguments.Count > 0 &&
+ cattr.ConstructorArguments[0].Value?.ToString() == _targetOS)
+ {
+ return PlatformSupport.Unsupported;
+ }
+ if (cattr.AttributeType.FullName == "System.Runtime.Versioning.SupportedOSPlatformAttribute" &&
+ cattr.ConstructorArguments.Count > 0)
+ {
+ hasSupportedOSPlatform = true;
+ if (cattr.ConstructorArguments[0].Value?.ToString() == _targetOS)
+ hasSupportedTarget = true;
+ }
+ }
+ catch
+ {
+ // Assembly not found, ignore
+ }
+ }
+
+ if (hasSupportedOSPlatform)
+ return hasSupportedTarget ? PlatformSupport.Supported : PlatformSupport.Unsupported;
+
+ return PlatformSupport.Unknown;
+ }
}
internal sealed class PInvokeCallbackComparer : IComparer
diff --git a/src/tasks/WasmAppBuilder/mono/PInvokeTableGenerator.cs b/src/tasks/WasmAppBuilder/mono/PInvokeTableGenerator.cs
index e7065ba6f5ab9c..ac9f26c203887f 100644
--- a/src/tasks/WasmAppBuilder/mono/PInvokeTableGenerator.cs
+++ b/src/tasks/WasmAppBuilder/mono/PInvokeTableGenerator.cs
@@ -28,11 +28,11 @@ internal sealed class PInvokeTableGenerator
private readonly PInvokeCollector _pinvokeCollector;
private readonly bool _isLibraryMode;
- public PInvokeTableGenerator(Func fixupSymbolName, LogAdapter log, bool isLibraryMode = false)
+ public PInvokeTableGenerator(Func fixupSymbolName, LogAdapter log, bool isLibraryMode, string targetOS)
{
Log = log;
_fixupSymbolName = fixupSymbolName;
- _pinvokeCollector = new(log);
+ _pinvokeCollector = new(log, targetOS);
_isLibraryMode = isLibraryMode;
}