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; }