diff --git a/src/Uno.Wasm.Tuner/IcallTableGenerator.net5.cs b/src/Uno.Wasm.Tuner/IcallTableGenerator.net5.cs index e1de61361..785a33863 100644 --- a/src/Uno.Wasm.Tuner/IcallTableGenerator.net5.cs +++ b/src/Uno.Wasm.Tuner/IcallTableGenerator.net5.cs @@ -1,25 +1,21 @@ -// Based on https://github.com/dotnet/runtime/commit/4f7a096dce6bb1d69b844b539678fa25ed7b8e20 +// Based on https://github.com/dotnet/runtime/commit/711447a #pragma warning disable CS8632 #pragma warning disable IDE0022 #pragma warning disable IDE0011 #pragma warning disable IDE0007 #pragma warning disable IDE0018 -// Based on https://github.com/dotnet/runtime/commit/7b4b23269b0 +#pragma warning disable IDE0270 // 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.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Reflection; - #if ORIGINAL_NETCORE_SOURCE using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -30,14 +26,12 @@ internal sealed class IcallTableGenerator public string[]? Cookies { get; private set; } private List _icalls = new List(); - private List _signatures = new List(); + private readonly HashSet _signatures = new(); private Dictionary _runtimeIcalls = new Dictionary(); + private object _gate = new(); -#if ORIGINAL_NETCORE_SOURCE private TaskLoggingHelper Log { get; set; } - - public IcallTableGenerator(TaskLoggingHelper log) => Log = log; -#endif + private readonly Func _fixupSymbolName; // // Given the runtime generated icall table, and a set of assemblies, generate @@ -45,23 +39,22 @@ internal sealed class IcallTableGenerator // The runtime icall table should be generated using // mono --print-icall-table // - public IEnumerable Generate(string? runtimeIcallTableFile, string[] assemblies, string? outputPath) + public IcallTableGenerator(string? runtimeIcallTableFile, Func fixupSymbolName, TaskLoggingHelper log) { - _icalls.Clear(); - _signatures.Clear(); - + Log = log; + _fixupSymbolName = fixupSymbolName; if (runtimeIcallTableFile != null) ReadTable(runtimeIcallTableFile); + } - var resolver = new PathAssemblyResolver(assemblies); - using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib"); - foreach (var aname in assemblies) - { - var a = mlc.LoadFromAssemblyPath(aname); - foreach (var type in a.GetTypes()) - ProcessType(type); - } + public void ScanAssembly(Assembly asm) + { + foreach (Type type in asm.GetTypes()) + ProcessType(type); + } + public IEnumerable Generate(string? outputPath) + { if (outputPath != null) { string tmpFileName = Path.GetTempFileName(); @@ -70,12 +63,10 @@ public IEnumerable Generate(string? runtimeIcallTableFile, string[] asse using (var w = File.CreateText(tmpFileName)) EmitTable(w); -#if ORIGINAL_NETCORE_SOURCE if (Utils.CopyIfDifferent(tmpFileName, outputPath, useHash: false)) Log.LogMessage(MessageImportance.Low, $"Generating icall table to '{outputPath}'."); else Log.LogMessage(MessageImportance.Low, $"Icall table in {outputPath} is unchanged."); -#endif } finally { @@ -101,7 +92,7 @@ private void EmitTable(StreamWriter w) if (assembly == "System.Private.CoreLib") aname = "corlib"; else - aname = assembly.Replace(".", "_"); + aname = _fixupSymbolName(assembly); w.WriteLine($"#define ICALL_TABLE_{aname} 1\n"); w.WriteLine($"static int {aname}_icall_indexes [] = {{"); @@ -117,9 +108,9 @@ private void EmitTable(StreamWriter w) w.WriteLine(string.Format("{0},", icall.Func)); } w.WriteLine("};"); - w.WriteLine($"static uint8_t {aname}_icall_handles [] = {{"); + w.WriteLine($"static uint8_t {aname}_icall_flags [] = {{"); foreach (var icall in sorted) - w.WriteLine(string.Format("{0},", icall.Handles ? "1" : "0")); + w.WriteLine(string.Format("{0},", icall.Flags)); w.WriteLine("};"); } } @@ -139,7 +130,12 @@ private void ReadTable(string filename) continue; var icallClass = new IcallClass(className); - _runtimeIcalls[icallClass.Name] = icallClass; + + lock (_gate) + { + _runtimeIcalls[icallClass.Name] = icallClass; + } + foreach (var icall_j in v.GetProperty("icalls").EnumerateArray()) { if (!icall_j.TryGetProperty("name", out var nameElem)) @@ -148,8 +144,9 @@ private void ReadTable(string filename) string name = nameElem.GetString()!; string func = icall_j.GetProperty("func").GetString()!; bool handles = icall_j.GetProperty("handles").GetBoolean(); + int flags = icall_j.TryGetProperty("flags", out var _) ? int.Parse(icall_j.GetProperty("flags").GetString()!) : 0; - icallClass.Icalls.Add(name, new Icall(name, func, handles)); + icallClass.Icalls.Add(name, new Icall(name, func, handles, flags)); } } } @@ -165,42 +162,42 @@ private void ProcessType(Type type) { AddSignature(type, method); } -#if ORIGINAL_NETCORE_SOURCE catch (Exception ex) when (ex is not LogAsErrorException) { Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, $"Could not get icall, or callbacks for method '{type.FullName}::{method.Name}' because '{ex.Message}'"); -#else - catch (Exception) - { -#endif continue; } - var className = method.DeclaringType!.FullName!; - if (!_runtimeIcalls.ContainsKey(className)) - // Registered at runtime - continue; + lock (_gate) + { + var className = method.DeclaringType!.FullName!; + if (!_runtimeIcalls.ContainsKey(className)) + // Registered at runtime + continue; - var icallClass = _runtimeIcalls[className]; + var icallClass = _runtimeIcalls[className]; - Icall? icall = null; + Icall? icall = null; - // Try name first - icallClass.Icalls.TryGetValue(method.Name, out icall); - if (icall == null) - { - string? methodSig = BuildSignature(method, className); - if (methodSig != null && icallClass.Icalls.ContainsKey(methodSig)) - icall = icallClass.Icalls[methodSig]; - } - if (icall == null) - // Registered at runtime - continue; + // Try name first + icallClass.Icalls.TryGetValue(method.Name, out icall); + if (icall == null) + { + string? methodSig = BuildSignature(method, className); + if (methodSig != null) + icallClass.Icalls.TryGetValue(methodSig, out icall); + + if (icall == null) + // Registered at runtime + continue; + } + + icall.Method = method; + icall.TokenIndex = (int)method.MetadataToken & 0xffffff; + icall.Assembly = method.DeclaringType.Module.Assembly.GetName().Name; - icall.Method = method; - icall.TokenIndex = (int)method.MetadataToken & 0xffffff; - icall.Assembly = method.DeclaringType.Module.Assembly.GetName().Name; - _icalls.Add(icall); + _icalls.Add(icall); + } } foreach (var nestedType in type.GetNestedTypes()) @@ -223,13 +220,8 @@ private void ProcessType(Type type) } catch (NotImplementedException nie) { -#if ORIGINAL_NETCORE_SOURCE - Log.LogWarning($"Failed to generate icall function for method '[{method.DeclaringType!.Assembly.GetName().Name}] {className}::{method.Name}'" + - $" because type '{nie.Message}' is not supported for parameter named '{par.Name}'. Ignoring."); -#else - Console.WriteLine($"Failed to generate icall function for method '[{method.DeclaringType!.Assembly.GetName().Name}] {className}::{method.Name}'" + + Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, $"Failed to generate icall function for method '[{method.DeclaringType!.Assembly.GetName().Name}] {className}::{method.Name}'" + $" because type '{nie.Message}' is not supported for parameter named '{par.Name}'. Ignoring."); -#endif return null; } pindex++; @@ -244,17 +236,14 @@ void AddSignature(Type type, MethodInfo method) string? signature = SignatureMapper.MethodToSignature(method); if (signature == null) { -#if ORIGINAL_NETCORE_SOURCE throw new LogAsErrorException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); -#else - throw new InvalidOperationException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); -#endif } -#if ORIGINAL_NETCORE_SOURCE - Log.LogMessage(MessageImportance.Normal, $"[icall] Adding signature {signature} for method '{type.FullName}.{method.Name}'"); -#endif - _signatures.Add(signature); + lock (_gate) + { + if (_signatures.Add(signature)) + Log.LogMessage(MessageImportance.Low, $"Adding icall signature {signature} for method '{type.FullName}.{method.Name}'"); + } } } @@ -346,10 +335,11 @@ private static string GenIcallDecl(Icall icall) private sealed class Icall : IComparable { - public Icall(string name, string func, bool handles) + public Icall(string name, string func, bool handles, int flags) { Name = name; Func = func; + Flags = flags; Handles = handles; TokenIndex = 0; } @@ -358,6 +348,7 @@ public Icall(string name, string func, bool handles) public string Func; public string? Assembly; public bool Handles; + public int Flags; public int TokenIndex; public MethodInfo? Method; diff --git a/src/Uno.Wasm.Tuner/InterpToNativeGenerator.cs b/src/Uno.Wasm.Tuner/InterpToNativeGenerator.cs index a0586cd8c..54b2e56dc 100644 --- a/src/Uno.Wasm.Tuner/InterpToNativeGenerator.cs +++ b/src/Uno.Wasm.Tuner/InterpToNativeGenerator.cs @@ -1,10 +1,11 @@ -// based on https://github.com/dotnet/runtime/commit/4f7a096dce6bb1d69b844b539678fa25ed7b8e20 +// Based on https://github.com/dotnet/runtime/commit/711447a #pragma warning disable CS8632 #pragma warning disable IDE0022 #pragma warning disable IDE0011 #pragma warning disable IDE0007 #pragma warning disable IDE0018 +#pragma warning disable IDE0021 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -15,12 +16,10 @@ using System.Text; using System.Collections.Generic; using System.Globalization; - #if ORIGINAL_NETCORE_SOURCE using Microsoft.Build.Utilities; using Microsoft.Build.Framework; #endif - using System.Diagnostics.CodeAnalysis; // @@ -32,11 +31,9 @@ internal sealed class InterpToNativeGenerator { -#if ORIGINAL_NETCORE_SOURCE private TaskLoggingHelper Log { get; set; } public InterpToNativeGenerator(TaskLoggingHelper log) => Log = log; -#endif public void Generate(IEnumerable cookies, string outputPath) { @@ -49,15 +46,9 @@ public void Generate(IEnumerable cookies, string outputPath) } if (Utils.CopyIfDifferent(tmpFileName, outputPath, useHash: false)) -#if ORIGINAL_NETCORE_SOURCE Log.LogMessage(MessageImportance.Low, $"Generating managed2native table to '{outputPath}'."); else Log.LogMessage(MessageImportance.Low, $"Managed2native table in {outputPath} is unchanged."); -#else - Console.WriteLine($"Generating managed2native table to '{outputPath}'."); - else - Console.WriteLine($"Managed2native table in {outputPath} is unchanged."); -#endif } finally { @@ -67,15 +58,16 @@ public void Generate(IEnumerable cookies, string outputPath) private static void Emit(StreamWriter w, IEnumerable cookies) { - w.WriteLine(""" - /* - * GENERATED FILE, DON'T EDIT - * Generated by InterpToNativeGenerator - */ - - # include "pinvoke.h" - # include - """); + w.WriteLine( + """ + /* + * GENERATED FILE, DON'T EDIT + * Generated by InterpToNativeGenerator + */ + + # include "pinvoke.h" + # include + """); var signatures = cookies.Distinct().ToArray(); foreach (var signature in signatures) @@ -126,11 +118,7 @@ private static void Emit(StreamWriter w, IEnumerable cookies) } catch (InvalidSignatureCharException e) { -#if ORIGINAL_NETCORE_SOURCE throw new LogAsErrorException($"Element '{e.Char}' of signature '{signature}' can't be handled by managed2native generator"); -#else - throw new InvalidOperationException($"Element '{e.Char}' of signature '{signature}' can't be handled by managed2native generator"); -#endif } } diff --git a/src/Uno.Wasm.Tuner/LogAsErrorException.cs b/src/Uno.Wasm.Tuner/LogAsErrorException.cs new file mode 100644 index 000000000..2723263c8 --- /dev/null +++ b/src/Uno.Wasm.Tuner/LogAsErrorException.cs @@ -0,0 +1,11 @@ +// Based on https://github.com/dotnet/runtime/commit/711447a + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +public class LogAsErrorException : System.Exception +{ + public LogAsErrorException(string message) : base(message) + { + } +} diff --git a/src/Uno.Wasm.Tuner/MessageImportance.cs b/src/Uno.Wasm.Tuner/MessageImportance.cs new file mode 100644 index 000000000..19ae72d31 --- /dev/null +++ b/src/Uno.Wasm.Tuner/MessageImportance.cs @@ -0,0 +1,4 @@ +internal enum MessageImportance +{ + Low +} diff --git a/src/Uno.Wasm.Tuner/PInvokeCollector.cs b/src/Uno.Wasm.Tuner/PInvokeCollector.cs new file mode 100644 index 000000000..a99ed3492 --- /dev/null +++ b/src/Uno.Wasm.Tuner/PInvokeCollector.cs @@ -0,0 +1,256 @@ +// Based on https://github.com/dotnet/runtime/commit/711447a +#nullable enable +#pragma warning disable IDE0011 +#pragma warning disable IDE0270 +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System; +using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +#if ORIGINAL_NETCORE_SOURCE +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +#endif + +#pragma warning disable CA1067 +#pragma warning disable CS0649 +internal sealed class PInvoke : IEquatable +#pragma warning restore CA1067 +{ + public PInvoke(string entryPoint, string module, MethodInfo method) + { + EntryPoint = entryPoint; + Module = module; + Method = method; + } + + public string EntryPoint; + public string Module; + public MethodInfo Method; + public bool Skip; + + public bool Equals(PInvoke? other) + => other != null && + string.Equals(EntryPoint, other.EntryPoint, StringComparison.Ordinal) && + string.Equals(Module, other.Module, StringComparison.Ordinal) && + string.Equals(Method.ToString(), other.Method.ToString(), StringComparison.Ordinal); + + public override string ToString() => $"{{ EntryPoint: {EntryPoint}, Module: {Module}, Method: {Method}, Skip: {Skip} }}"; +} +#pragma warning restore CS0649 + +internal sealed class PInvokeComparer : IEqualityComparer +{ + public bool Equals(PInvoke? x, PInvoke? y) + { + if (x == null && y == null) + return true; + if (x == null || y == null) + return false; + + return x.Equals(y); + } + + public int GetHashCode(PInvoke pinvoke) + => $"{pinvoke.EntryPoint}{pinvoke.Module}{pinvoke.Method}".GetHashCode(); +} + + +internal sealed class PInvokeCollector +{ + private readonly Dictionary _assemblyDisableRuntimeMarshallingAttributeCache = new(); + private TaskLoggingHelper Log { get; init; } + + public PInvokeCollector(TaskLoggingHelper log) + { + Log = log; + } + + public void CollectPInvokes(List pinvokes, List callbacks, HashSet signatures, Type type) + { + foreach (var method in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) + { + try + { + CollectPInvokesForMethod(method); + if (DoesMethodHaveCallbacks(method)) + callbacks.Add(new PInvokeCallback(method)); + } + catch (Exception ex) when (ex is not LogAsErrorException) + { + Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, + $"Could not get pinvoke, or callbacks for method '{type.FullName}::{method.Name}' because '{ex.Message}'"); + } + } + + if (HasAttribute(type, "System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute")) + { + var method = type.GetMethod("Invoke"); + + if (method != null) + { + string? signature = SignatureMapper.MethodToSignature(method!); + if (signature == null) + throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); + + if (signatures.Add(signature)) + Log.LogMessage(MessageImportance.Low, $"Adding pinvoke signature {signature} for method '{type.FullName}.{method.Name}'"); + } + } + + void CollectPInvokesForMethod(MethodInfo method) + { + if ((method.Attributes & MethodAttributes.PinvokeImpl) != 0) + { + var dllimport = method.CustomAttributes.First(attr => attr.AttributeType.Name == "DllImportAttribute"); + var module = (string)dllimport.ConstructorArguments[0].Value!; + var entrypoint = (string)dllimport.NamedArguments.First(arg => arg.MemberName == "EntryPoint").TypedValue.Value!; + pinvokes.Add(new PInvoke(entrypoint, module, method)); + + string? signature = SignatureMapper.MethodToSignature(method); + if (signature == null) + { + throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); + } + + if (signatures.Add(signature)) + Log.LogMessage(MessageImportance.Low, $"Adding pinvoke signature {signature} for method '{type.FullName}.{method.Name}'"); + } + } + + bool DoesMethodHaveCallbacks(MethodInfo method) + { + if (!MethodHasCallbackAttributes(method)) + return false; + + if (TryIsMethodGetParametersUnsupported(method, out string? reason)) + { + Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, + $"Skipping callback '{method.DeclaringType!.FullName}::{method.Name}' because '{reason}'."); + return false; + } + + if (method.DeclaringType != null && HasAssemblyDisableRuntimeMarshallingAttribute(method.DeclaringType.Assembly)) + return true; + + // No DisableRuntimeMarshalling attribute, so check if the params/ret-type are + // blittable + bool isVoid = method.ReturnType.FullName == "System.Void"; + if (!isVoid && !IsBlittable(method.ReturnType)) + Error($"The return type '{method.ReturnType.FullName}' of pinvoke callback method '{method}' needs to be blittable."); + + foreach (var p in method.GetParameters()) + { + if (!IsBlittable(p.ParameterType)) + Error("Parameter types of pinvoke callback method '" + method + "' needs to be blittable."); + } + + return true; + } + + static bool MethodHasCallbackAttributes(MethodInfo method) + { + foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(method)) + { + try + { + if (cattr.AttributeType.FullName == "System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute" || + cattr.AttributeType.Name == "MonoPInvokeCallbackAttribute") + { + return true; + } + } + catch + { + // Assembly not found, ignore + } + } + + return false; + } + } + + public static bool IsBlittable(Type type) + { + if (type.IsPrimitive || type.IsByRef || type.IsPointer || type.IsEnum) + return true; + else + return false; + } + + private static void Error(string msg) => throw new LogAsErrorException(msg); + + private static bool HasAttribute(MemberInfo element, params string[] attributeNames) + { + foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(element)) + { + try + { + for (int i = 0; i < attributeNames.Length; ++i) + { + if (cattr.AttributeType.FullName == attributeNames[i] || + cattr.AttributeType.Name == attributeNames[i]) + { + return true; + } + } + } + catch + { + // Assembly not found, ignore + } + } + return false; + } + + private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotNullWhen(true)] out string? reason) + { + try + { + method.GetParameters(); + } + catch (NotSupportedException nse) + { + reason = nse.Message; + return true; + } + catch + { + // not concerned with other exceptions + } + + reason = null; + return false; + } + + private bool HasAssemblyDisableRuntimeMarshallingAttribute(Assembly assembly) + { + if (!_assemblyDisableRuntimeMarshallingAttributeCache.TryGetValue(assembly, out var value)) + { + _assemblyDisableRuntimeMarshallingAttributeCache[assembly] = value = assembly + .GetCustomAttributesData() + .Any(d => d.AttributeType.Name == "DisableRuntimeMarshallingAttribute"); + } + + value = assembly.GetCustomAttributesData().Any(d => d.AttributeType.Name == "DisableRuntimeMarshallingAttribute"); + + return value; + } +} + +#pragma warning disable CS0649 +internal sealed class PInvokeCallback +{ + public PInvokeCallback(MethodInfo method) + { + Method = method; + } + + public MethodInfo Method; + public string? EntryName; +} +#pragma warning restore CS0649 diff --git a/src/Uno.Wasm.Tuner/PInvokeTableGenerator.net5.cs b/src/Uno.Wasm.Tuner/PInvokeTableGenerator.net5.cs index 7ae9e7581..97ce2f928 100644 --- a/src/Uno.Wasm.Tuner/PInvokeTableGenerator.net5.cs +++ b/src/Uno.Wasm.Tuner/PInvokeTableGenerator.net5.cs @@ -1,8 +1,7 @@ -// Based on https://github.com/dotnet/runtime/commit/4f7a096dce6bb1d69b844b539678fa25ed7b8e20 - -#pragma warning disable IDE0011 // Add braces - +// Based on https://github.com/dotnet/runtime/commit/711447a #nullable enable +#pragma warning disable IDE0011 +#pragma warning disable IDE0270 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -14,7 +13,7 @@ using System.Linq; using System.Text; using System.Reflection; - +using System.Runtime.CompilerServices; #if ORIGINAL_NETCORE_SOURCE using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -22,34 +21,49 @@ internal sealed class PInvokeTableGenerator { - private static readonly char[] s_charsToReplace = new[] { '.', '-', '+' }; private readonly Dictionary _assemblyDisableRuntimeMarshallingAttributeCache = new(); -#if ORIGINAL_NETCORE_SOURCE private TaskLoggingHelper Log { get; set; } - - public PInvokeTableGenerator(TaskLoggingHelper log) => Log = log; -#endif - - public IEnumerable Generate(string[] pinvokeModules, string[] assemblies, string outputPath) + private readonly Func _fixupSymbolName; + private readonly HashSet signatures = new(); + private readonly List pinvokes = new(); + private readonly List callbacks = new(); + private readonly PInvokeCollector _pinvokeCollector; + private readonly object _gate = new(); + + public PInvokeTableGenerator(Func fixupSymbolName, TaskLoggingHelper log) { - var modules = new Dictionary(); - foreach (var module in pinvokeModules) - modules[module] = module; + Log = log; + _fixupSymbolName = fixupSymbolName; + _pinvokeCollector = new(log); + } - var signatures = new List(); + public void ScanAssembly(Assembly asm) + { + HashSet localSignatures = new(); + List localPinvokes = new(); + List localCallbacks = new(); - var pinvokes = new List(); - var callbacks = new List(); + foreach (Type type in asm.GetTypes()) + _pinvokeCollector.CollectPInvokes(localPinvokes, localCallbacks, localSignatures, type); - var resolver = new PathAssemblyResolver(assemblies); - using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib"); - foreach (var aname in assemblies) + lock (_gate) { - var a = mlc.LoadFromAssemblyPath(aname); - foreach (var type in a.GetTypes()) - CollectPInvokes(pinvokes, callbacks, signatures, type); + pinvokes.AddRange(localPinvokes); + callbacks.AddRange(localCallbacks); + + foreach(var sig in localSignatures) + { + signatures.Add(sig); + } } + } + + public IEnumerable Generate(string[] pinvokeModules, string outputPath) + { + var modules = new Dictionary(); + foreach (var module in pinvokeModules) + modules[module] = module; string tmpFileName = Path.GetTempFileName(); try @@ -57,20 +71,13 @@ public IEnumerable Generate(string[] pinvokeModules, string[] assemblies using (var w = File.CreateText(tmpFileName)) { EmitPInvokeTable(w, modules, pinvokes); - EmitNativeToInterp(w, ref callbacks); + EmitNativeToInterp(w, callbacks); } -#if ORIGINAL_NETCORE_SOURCE if (Utils.CopyIfDifferent(tmpFileName, outputPath, useHash: false)) Log.LogMessage(MessageImportance.Low, $"Generating pinvoke table to '{outputPath}'."); else Log.LogMessage(MessageImportance.Low, $"PInvoke table in {outputPath} is unchanged."); -#else - if (Utils.CopyIfDifferent(tmpFileName, outputPath, useHash: false)) - Console.WriteLine($"Generating pinvoke table to '{outputPath}'."); - else - Console.WriteLine($"PInvoke table in {outputPath} is unchanged."); -#endif } finally { @@ -80,128 +87,6 @@ public IEnumerable Generate(string[] pinvokeModules, string[] assemblies return signatures; } - private void CollectPInvokes(List pinvokes, List callbacks, List signatures, Type type) - { - foreach (var method in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) - { - try - { - CollectPInvokesForMethod(method); - if (DoesMethodHaveCallbacks(method)) - callbacks.Add(new PInvokeCallback(method)); - } -#if ORIGINAL_NETCORE_SOURCE - catch (Exception ex) when (ex is not LogAsErrorException) - { - Log.LogMessage(MessageImportance.Low, $"Could not get pinvoke, or callbacks for method {method.Name}: {ex}"); -#else - catch(Exception ex) - { - Console.WriteLine($"Could not get pinvoke, or callbacks for method {method.Name}: {ex}"); -#endif - } - } - - if (HasAttribute(type, "System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute")) - { - var method = type.GetMethod("Invoke"); - - if (method != null) - { - string? signature = SignatureMapper.MethodToSignature(method!); - if (signature == null) - throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); - - -#if ORIGINAL_NETCORE_SOURCE - Log.LogMessage(MessageImportance.Low, $"Adding pinvoke signature {signature} for method '{type.FullName}.{method.Name}'"); -#else - Console.WriteLine($"Adding pinvoke signature {signature} for method '{type.FullName}.{method.Name}'"); -#endif - signatures.Add(signature); - } - } - - void CollectPInvokesForMethod(MethodInfo method) - { - if ((method.Attributes & MethodAttributes.PinvokeImpl) != 0) - { - var dllimport = method.CustomAttributes.First(attr => attr.AttributeType.Name == "DllImportAttribute"); - var module = (string)dllimport.ConstructorArguments[0].Value!; - var entrypoint = (string)dllimport.NamedArguments.First(arg => arg.MemberName == "EntryPoint").TypedValue.Value!; - pinvokes.Add(new PInvoke(entrypoint, module, method)); - - string? signature = SignatureMapper.MethodToSignature(method); - if (signature == null) - { - throw new NotSupportedException($"Unsupported parameter type in method '{type.FullName}.{method.Name}'"); - } - -#if ORIGINAL_NETCORE_SOURCE - Log.LogMessage(MessageImportance.Normal, $"[pinvoke] Adding signature {signature} for method '{type.FullName}.{method.Name}'"); -#else - Console.WriteLine($"[pinvoke] Adding signature {signature} for method '{type.FullName}.{method.Name}'"); -#endif - signatures.Add(signature); - } - } - - bool DoesMethodHaveCallbacks(MethodInfo method) - { - if (!MethodHasCallbackAttributes(method)) - return false; - - if (TryIsMethodGetParametersUnsupported(method, out string? reason)) - { -#if ORIGINAL_NETCORE_SOURCE - Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, - $"Skipping callback '{method.DeclaringType!.FullName}::{method.Name}' because '{reason}'."); -#else - Console.WriteLine( - $"Skipping callback '{method.DeclaringType!.FullName}::{method.Name}' because '{reason}'."); -#endif - return false; - } - - if (method.DeclaringType != null && HasAssemblyDisableRuntimeMarshallingAttribute(method.DeclaringType.Assembly)) - return true; - - // No DisableRuntimeMarshalling attribute, so check if the params/ret-type are - // blittable - bool isVoid = method.ReturnType.FullName == "System.Void"; - if (!isVoid && !IsBlittable(method.ReturnType)) - Error($"The return type '{method.ReturnType.FullName}' of pinvoke callback method '{method}' needs to be blittable."); - - foreach (var p in method.GetParameters()) - { - if (!IsBlittable(p.ParameterType)) - Error("Parameter types of pinvoke callback method '" + method + "' needs to be blittable."); - } - - return true; - } - - static bool MethodHasCallbackAttributes(MethodInfo method) - { - foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(method)) - { - try - { - if (cattr.AttributeType.FullName == "System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute" || - cattr.AttributeType.Name == "MonoPInvokeCallbackAttribute") - { - return true; - } - } - catch - { - // Assembly not found, ignore - } - } - - return false; - } - } private static bool HasAttribute(MemberInfo element, params string[] attributeNames) { foreach (CustomAttributeData cattr in CustomAttributeData.GetCustomAttributes(element)) @@ -245,15 +130,9 @@ private void EmitPInvokeTable(StreamWriter w, Dictionary modules string imports = string.Join(Environment.NewLine, candidates.Select( p => $" {p.Method} (in [{p.Method.DeclaringType?.Assembly.GetName().Name}] {p.Method.DeclaringType})")); -#if ORIGINAL_NETCORE_SOURCE - Log.LogWarning($"Found a native function ({first.EntryPoint}) with varargs in {first.Module}." + - " Calling such functions is not supported, and will fail at runtime." + - $" Managed DllImports: {Environment.NewLine}{imports}"); -#else - Console.WriteLine($"Found a native function ({first.EntryPoint}) with varargs in {first.Module}." + + Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, $"Found a native function ({first.EntryPoint}) with varargs in {first.Module}." + " Calling such functions is not supported, and will fail at runtime." + $" Managed DllImports: {Environment.NewLine}{imports}"); -#endif foreach (var c in candidates) c.Skip = true; @@ -275,14 +154,14 @@ private void EmitPInvokeTable(StreamWriter w, Dictionary modules foreach (var module in modules.Keys) { - string symbol = ModuleNameToId(module) + "_imports"; + string symbol = _fixupSymbolName(module) + "_imports"; w.WriteLine("static PinvokeImport " + symbol + " [] = {"); var assemblies_pinvokes = pinvokes. Where(l => l.Module == module && !l.Skip). OrderBy(l => l.EntryPoint). GroupBy(d => d.EntryPoint). - Select(l => "{\"" + FixupSymbolName(l.Key) + "\", " + FixupSymbolName(l.Key) + "}, " + + Select(l => "{\"" + _fixupSymbolName(l.Key) + "\", " + _fixupSymbolName(l.Key) + "}, " + "// " + string.Join(", ", l.Select(c => c.Method.DeclaringType!.Module!.Assembly!.GetName()!.Name!).Distinct().OrderBy(n => n))); foreach (var pinvoke in assemblies_pinvokes) @@ -296,7 +175,7 @@ private void EmitPInvokeTable(StreamWriter w, Dictionary modules w.Write("static void *pinvoke_tables[] = { "); foreach (var module in modules.Keys) { - string symbol = ModuleNameToId(module) + "_imports"; + string symbol = _fixupSymbolName(module) + "_imports"; w.Write(symbol + ","); } w.WriteLine("};"); @@ -307,18 +186,6 @@ private void EmitPInvokeTable(StreamWriter w, Dictionary modules } w.WriteLine("};"); - static string ModuleNameToId(string name) - { - if (name.IndexOfAny(s_charsToReplace) < 0) - return name; - - string fixedName = name; - foreach (char c in s_charsToReplace) - fixedName = fixedName.Replace(c, '_'); - - return fixedName; - } - static bool ShouldTreatAsVariadic(PInvoke[] candidates) { if (candidates.Length < 2) @@ -336,35 +203,7 @@ static bool ShouldTreatAsVariadic(PInvoke[] candidates) } } - private static string FixupSymbolName(string name) - { - UTF8Encoding utf8 = new(); - byte[] bytes = utf8.GetBytes(name); - StringBuilder sb = new(); - - foreach (byte b in bytes) - { - if ((b >= (byte)'0' && b <= (byte)'9') || - (b >= (byte)'a' && b <= (byte)'z') || - (b >= (byte)'A' && b <= (byte)'Z') || - (b == (byte)'_')) - { - sb.Append((char)b); - } - else if (s_charsToReplace.Contains((char)b)) - { - sb.Append('_'); - } - else - { - sb.Append($"_{b:X}_"); - } - } - - return sb.ToString(); - } - - private static string SymbolNameForMethod(MethodInfo method) + private string SymbolNameForMethod(MethodInfo method) { StringBuilder sb = new(); Type? type = method.DeclaringType; @@ -372,7 +211,7 @@ private static string SymbolNameForMethod(MethodInfo method) sb.Append($"{(type!.IsNested ? type!.FullName : type!.Name)}_"); sb.Append(method.Name); - return FixupSymbolName(sb.ToString()); + return _fixupSymbolName(sb.ToString()); } private static string MapType(Type t) => t.Name switch @@ -415,23 +254,23 @@ private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotN { // FIXME: System.Reflection.MetadataLoadContext can't decode function pointer types // https://github.com/dotnet/runtime/issues/43791 - sb.Append($"int {FixupSymbolName(pinvoke.EntryPoint)} (int, int, int, int, int);"); + sb.Append($"int {_fixupSymbolName(pinvoke.EntryPoint)} (int, int, int, int, int);"); return sb.ToString(); } if (TryIsMethodGetParametersUnsupported(pinvoke.Method, out string? reason)) { -#if ORIGINAL_NETCORE_SOURCE - Log.LogWarning($"Skipping the following DllImport because '{reason}'. {Environment.NewLine} {pinvoke.Method}"); -#else - Console.WriteLine($"Skipping the following DllImport because '{reason}'. {Environment.NewLine} {pinvoke.Method}"); -#endif + // Don't use method.ToString() or any of it's parameters, or return type + // because at least one of those are unsupported, and will throw + Log.LogWarning(null, "WASM0001", "", "", 0, 0, 0, 0, + $"Skipping pinvoke '{pinvoke.Method.DeclaringType!.FullName}::{pinvoke.Method.Name}' because '{reason}'."); + pinvoke.Skip = true; return null; } sb.Append(MapType(method.ReturnType)); - sb.Append($" {FixupSymbolName(pinvoke.EntryPoint)} ("); + sb.Append($" {_fixupSymbolName(pinvoke.EntryPoint)} ("); int pindex = 0; var pars = method.GetParameters(); foreach (var p in pars) @@ -445,7 +284,7 @@ private static bool TryIsMethodGetParametersUnsupported(MethodInfo method, [NotN return sb.ToString(); } - private static void EmitNativeToInterp(StreamWriter w, ref List callbacks) + private void EmitNativeToInterp(StreamWriter w, List callbacks) { // Generate native->interp entry functions // These are called by native code, so they need to obtain @@ -491,7 +330,7 @@ private static void EmitNativeToInterp(StreamWriter w, ref List bool is_void = method.ReturnType.Name == "Void"; - string module_symbol = method.DeclaringType!.Module!.Assembly!.GetName()!.Name!.Replace(".", "_"); + string module_symbol = _fixupSymbolName(method.DeclaringType!.Module!.Assembly!.GetName()!.Name!); uint token = (uint)method.MetadataToken; string class_name = method.DeclaringType.Name; string method_name = method.Name; @@ -509,7 +348,7 @@ private static void EmitNativeToInterp(StreamWriter w, ref List { if (pindex > 0) sb.Append(','); - sb.Append(MapType(method.GetParameters()[pindex].ParameterType)); + sb.Append(MapType(p.ParameterType)); sb.Append($" arg{pindex}"); pindex++; } @@ -520,7 +359,7 @@ private static void EmitNativeToInterp(StreamWriter w, ref List pindex = 0; if (!is_void) { - sb.Append("&res"); + sb.Append("(int*)&res"); pindex++; } int aindex = 0; @@ -528,7 +367,7 @@ private static void EmitNativeToInterp(StreamWriter w, ref List { if (pindex > 0) sb.Append(", "); - sb.Append($"&arg{aindex}"); + sb.Append($"(int*)&arg{aindex}"); pindex++; aindex++; } @@ -558,7 +397,7 @@ private static void EmitNativeToInterp(StreamWriter w, ref List foreach (var cb in callbacks) { var method = cb.Method; - string module_symbol = method.DeclaringType!.Module!.Assembly!.GetName()!.Name!.Replace(".", "_"); + string module_symbol = _fixupSymbolName(method.DeclaringType!.Module!.Assembly!.GetName()!.Name!); string class_name = method.DeclaringType.Name; string method_name = method.Name; w.WriteLine($"\"{module_symbol}_{class_name}_{method_name}\","); @@ -586,62 +425,5 @@ private static bool IsBlittable(Type type) return false; } - private static void Error(string msg) -#if ORIGINAL_NETCORE_SOURCE - => throw new LogAsErrorException(msg); -#else - => throw new InvalidOperationException(msg); -#endif -} - -#pragma warning disable CA1067 -internal sealed class PInvoke : IEquatable -#pragma warning restore CA1067 -{ - public PInvoke(string entryPoint, string module, MethodInfo method) - { - EntryPoint = entryPoint; - Module = module; - Method = method; - } - - public string EntryPoint; - public string Module; - public MethodInfo Method; - public bool Skip; - - public bool Equals(PInvoke? other) - => other != null && - string.Equals(EntryPoint, other.EntryPoint, StringComparison.Ordinal) && - string.Equals(Module, other.Module, StringComparison.Ordinal) && - string.Equals(Method.ToString(), other.Method.ToString(), StringComparison.Ordinal); - - public override string ToString() => $"{{ EntryPoint: {EntryPoint}, Module: {Module}, Method: {Method}, Skip: {Skip} }}"; -} - -internal sealed class PInvokeComparer : IEqualityComparer -{ - public bool Equals(PInvoke? x, PInvoke? y) - { - if (x == null && y == null) - return true; - if (x == null || y == null) - return false; - - return x.Equals(y); - } - - public int GetHashCode(PInvoke pinvoke) - => $"{pinvoke.EntryPoint}{pinvoke.Module}{pinvoke.Method}".GetHashCode(); -} - -internal sealed class PInvokeCallback -{ - public PInvokeCallback(MethodInfo method) - { - Method = method; - } - - public MethodInfo Method; - public string? EntryName; + private static void Error(string msg) => throw new LogAsErrorException(msg); } diff --git a/src/Uno.Wasm.Tuner/SignatureMapper.cs b/src/Uno.Wasm.Tuner/SignatureMapper.cs index 75b51ddeb..e858f5091 100644 --- a/src/Uno.Wasm.Tuner/SignatureMapper.cs +++ b/src/Uno.Wasm.Tuner/SignatureMapper.cs @@ -1,4 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Based on https://github.com/dotnet/runtime/commit/711447a + +#nullable enable +#pragma warning disable IDE0011 +#pragma warning disable IDE0270 +#pragma warning disable IDE0021 + +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; diff --git a/src/Uno.Wasm.Tuner/TaskLoggingHelper.cs b/src/Uno.Wasm.Tuner/TaskLoggingHelper.cs new file mode 100644 index 000000000..461b443ed --- /dev/null +++ b/src/Uno.Wasm.Tuner/TaskLoggingHelper.cs @@ -0,0 +1,14 @@ +// 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.Threading.Channels; + +public class TaskLoggingHelper +{ + internal void LogMessage(MessageImportance importance, string message) + => Console.WriteLine($"[{importance}] {message}"); + + internal void LogWarning(string subcategory, string warningCode, string helpKeyword, string file, int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber, string message, params object[] messageArgs) + => Console.WriteLine($"[{subcategory}] {string.Format(message, messageArgs)}"); +} diff --git a/src/Uno.Wasm.Tuner/tuner.cs b/src/Uno.Wasm.Tuner/tuner.cs index f0b36f84f..abd6c7af7 100644 --- a/src/Uno.Wasm.Tuner/tuner.cs +++ b/src/Uno.Wasm.Tuner/tuner.cs @@ -1,3 +1,9 @@ +#nullable enable +#pragma warning disable IDE0011 +#pragma warning disable IDE0270 +#pragma warning disable IDE0021 +#pragma warning disable IDE0022 + // // tuner.cs: WebAssembly build time helpers // @@ -10,10 +16,16 @@ using System.Collections.Generic; using Mono.Cecil; using System.Diagnostics; +using System.Reflection; +using System.Threading; public class WasmTuner { - public static int Main (String[] args) { + // Avoid sharing this cache with all the invocations of this task throughout the build + private readonly Dictionary _symbolNameFixups = new(); + private static readonly char[] s_charsToReplace = new[] { '.', '-', '+' }; + + public static int Main (string[] args) { return new WasmTuner ().Run (args); } @@ -26,7 +38,7 @@ void Usage () { Console.WriteLine ("--gen-empty-assemblies ."); } - int Run (String[] args) + int Run (string[] args) { if (args.Length < 1) { Usage (); @@ -77,30 +89,42 @@ int GenPinvokeTable (string[] args) .ToArray(); } - var outputFile = args[1]; - var icallTable = args[3]; + var PInvokeOutputPath = args[1]; + var RuntimeIcallTableFile = args[3]; + var InterpToNativeOutputPath = Path.Combine(Path.GetDirectoryName(PInvokeOutputPath)!, "wasm_m2n_invoke.g.h"); - var modules = new Dictionary (); + var modules = new List (); foreach (var module in args [2].Split (',')) { - modules [module] = module; + modules.Add(module); } + var PInvokeModules = modules.ToArray(); + + var managedAssemblies = args.Skip(4).ToArray(); - var files = args.Skip(4).ToArray(); + var Log = new TaskLoggingHelper(); - var generator = new PInvokeTableGenerator(); + var pinvoke = new PInvokeTableGenerator(FixupSymbolName, Log); + var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, Log); - Console.WriteLine($"Generating to {outputFile}"); - var PInvokeOutputFile = outputFile; - var pInvokeCookiesList = generator.Generate(modules.Keys.ToArray(), files.ToArray(), PInvokeOutputFile); + var resolver = new PathAssemblyResolver(managedAssemblies); + using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib"); + + managedAssemblies.AsParallel().ForAll( + asmPath => + { + Log.LogMessage(MessageImportance.Low, $"[{Thread.CurrentThread.ManagedThreadId}] Loading {asmPath} to scan for pinvokes, and icalls"); + Assembly asm = mlc.LoadFromAssemblyPath(asmPath); + pinvoke.ScanAssembly(asm); + icall.ScanAssembly(asm); + }); - var icallGenerator = new IcallTableGenerator(); - var iCallCookiesList = icallGenerator.Generate(icallTable, files, Path.GetTempFileName()); + IEnumerable cookies = Enumerable.Concat( + pinvoke.Generate(PInvokeModules, PInvokeOutputPath), + icall.Generate(Path.GetTempFileName())); - var m2nInvoke = Path.Combine(Path.GetDirectoryName(PInvokeOutputFile), "wasm_m2n_invoke.g.h"); - Console.WriteLine($"Generating interp to native to {m2nInvoke}"); - var interpNativeGenerator = new InterpToNativeGenerator(); - interpNativeGenerator.Generate(pInvokeCookiesList.Concat(iCallCookiesList), m2nInvoke); + var m2n = new InterpToNativeGenerator(Log); + m2n.Generate(cookies, InterpToNativeOutputPath); return 0; } @@ -110,23 +134,42 @@ void Error (string msg) { Environment.Exit (1); } + public string FixupSymbolName(string name) + { + if (_symbolNameFixups.TryGetValue(name, out string? fixedName)) + return fixedName; + UTF8Encoding utf8 = new(); + byte[] bytes = utf8.GetBytes(name); + StringBuilder sb = new(); + foreach (byte b in bytes) + { + if ((b >= (byte)'0' && b <= (byte)'9') || + (b >= (byte)'a' && b <= (byte)'z') || + (b >= (byte)'A' && b <= (byte)'Z') || + (b == (byte)'_')) + { + sb.Append((char)b); + } + else if (s_charsToReplace.Contains((char)b)) + { + sb.Append('_'); + } + else + { + sb.Append($"_{b:X}_"); + } + } + fixedName = sb.ToString(); + _symbolNameFixups[name] = fixedName; + return fixedName; + } // // Given the runtime generated icall table, and a set of assemblies, generate // a smaller linked icall table mapping tokens to C function names // int GenIcallTable(string[] args) { - var icall_table_filename = args [2]; - var fileNames = args.Skip (3).ToArray (); - -#if NETFRAMEWORK - throw new NotSupportedException($"icall table generation is not supported for netstandard2.0"); -#else - Console.WriteLine($"Generating to {args[1]}"); - - var generator = new IcallTableGenerator(); - generator.Generate(icall_table_filename, fileNames, args[2]); -#endif + // Unused, work is done in GenPinvokeTable. return 0; }