// 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.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Internal.JitInterface;
using Internal.TypeSystem;
using Internal.TypeSystem.Ecma;
namespace ILCompiler.PortableCallHelpers
{
/// <summary>
/// A P/Invoke discovered while scanning the input assemblies.
/// </summary>
internal sealed class PInvokeInfo(string entryPoint, string module, EcmaMethod method, bool wasmLinkage)
: IEquatable<PInvokeInfo>
{
public string EntryPoint { get; } = entryPoint;
public string Module { get; } = module;
public EcmaMethod Method { get; } = method;
public bool WasmLinkage { get; } = wasmLinkage;
public bool Skip { get; set; }
/// <summary>A stable identity for de-duplicating declarations of the same import.</summary>
private string Identity => $"{EntryPoint}!{Module}!{Method.OwningType}::{Method.Name.ToString()}{Method.Signature}";
public bool Equals(PInvokeInfo other)
=> other is not null && string.Equals(Identity, other.Identity, StringComparison.Ordinal);
public override bool Equals(object obj) => Equals(obj as PInvokeInfo);
public override int GetHashCode() => Identity.GetHashCode(StringComparison.Ordinal);
public override string ToString() => $"{{ EntryPoint: {EntryPoint}, Module: {Module}, Method: {Method}, Skip: {Skip} }}";
}
/// <summary>
/// A managed method callable from native code, discovered while scanning the input assemblies.
/// </summary>
internal sealed class PInvokeCallback
{
public PInvokeCallback(EcmaMethod method)
{
Method = method;
var type = (EcmaType)method.OwningType;
TypeName = type.Name.ToString();
TypeFullName = TypeNames.GetFullName(type);
AssemblyName = ((EcmaAssembly)type.Module).GetName().Name;
// Nested types: the runtime reverse-thunk key (vm/wasm/helpers.cpp GetHashCode ->
// GetFullyQualifiedNameInfo) reports an empty namespace for nested types, so match that
// here or the emitted g_ReverseThunks key won't be found at lookup time (#130129).
// This key drops the enclosing-type chain, so nested types with the same simple name in
// different namespaces collide; the duplicate-key check in PInvokeTableGenerator
// (EmitNativeToInterp) turns that into a build error.
// Tracked by https://github.com/dotnet/runtime/issues/130739.
Namespace = type.ContainingType is not null ? string.Empty : type.Namespace.ToString();
MethodName = method.Name.ToString();
ReturnType = method.Signature.ReturnType;
IsVoid = ReturnType.IsVoid;
Token = (uint)MetadataTokens.GetToken(method.Handle);
// FIXME: This name-based key is a temporary workaround. It must match the key generated by
// GetHashCode in vm/wasm/helpers.cpp because LookupThunk uses it to find the corresponding
// entry in g_ReverseThunks. The key must uniquely identify each callback; otherwise a
// collision could select a thunk whose native signature does not match the method.
//
// the key also needs to survive being encoded in C literals, if in doubt
// add something like "\U0001F412" to the key on both the managed and unmanaged side
Key = $"{MethodName}#{method.Signature.Length}:{AssemblyName}:{Namespace}:{TypeName}";
if (method.GetDecodedCustomAttribute("System.Runtime.InteropServices", "UnmanagedCallersOnlyAttribute")
is CustomAttributeValue<TypeDesc> attribute)
{
foreach (var argument in attribute.NamedArguments)
{
if (argument.Name == "EntryPoint" && argument.Value is string entryPoint)
{
EntryPoint = entryPoint;
IsExport = true;
break;
}
}
}
}
/// <summary>
/// The name of the type that declares the callback, as the runtime spells it. Nested types
/// report an empty namespace here, which is what the reverse-thunk key expects.
/// </summary>
public string EntryName => $"{AssemblyName}_{Namespace}_{TypeName}_{MethodName}";
public MethodSignature Parameters => Method.Signature;
public string EntryPoint { get; }
public EcmaMethod Method { get; }
public string EntrySymbol { get; set; }
public string AssemblyName { get; }
public string TypeName { get; }
public string TypeFullName { get; }
public string Namespace { get; }
public string MethodName { get; }
public TypeDesc ReturnType { get; }
public bool IsExport { get; }
public bool IsVoid { get; }
public uint Token { get; }
public string Key { get; }
}
internal sealed class PInvokeCallbackComparer : IComparer<PInvokeCallback>
{
public int Compare(PInvokeCallback x, PInvokeCallback y)
{
int compare = string.Compare(x.Key, y.Key, StringComparison.Ordinal);
return compare != 0 ? compare : x.Token.CompareTo(y.Token);
}
}
/// <summary>
/// Scans assemblies for the interop surface the wasm interpreter needs thunks for: P/Invokes,
/// methods callable from native code, native function pointer signatures, and InternalCalls.
/// </summary>
internal sealed class PInvokeCollector(InteropLogger log, string targetOS)
{
private readonly Dictionary<TypeDesc, bool> _typeUnsupportedOnPlatform = [];
private readonly Dictionary<EcmaAssembly, bool> _assemblyUnsupportedOnPlatform = [];
public void CollectPInvokes(List<PInvokeInfo> pinvokes, List<PInvokeCallback> callbacks, Dictionary<string, MethodDesc> signatures, EcmaType type)
{
foreach (MethodDesc methodDesc in type.GetMethods())
{
var method = (EcmaMethod)methodDesc;
try
{
CollectPInvokesForMethod(method);
if (IsMethodCallback(method))
callbacks.Add(new PInvokeCallback(method));
}
catch (Exception ex) when (ex is not LogAsErrorException)
{
log.Warning("WASM0001", $"Could not get pinvoke, or callbacks for method '{type}::{method.Name.ToString()}' because '{ex}'");
}
}
if (type.HasCustomAttribute("System.Runtime.InteropServices", "UnmanagedFunctionPointerAttribute"))
{
// Each instantiation of an open generic delegate would marshal differently, so there is
// no single native signature to emit a thunk for. The encoding this used to produce came
// from mapping the type parameter itself, which was only ever right by accident.
if (type.HasInstantiation)
{
log.Warning("WASM0001", $"Skipping generic function pointer delegate '{type}', which has no single native signature");
return;
}
MethodDesc invokeMethod = type.GetMethod("Invoke"u8, null);
if (invokeMethod is not null)
AddSignature(signatures, invokeMethod, WasmLowering.LoweringFlags.IsUnmanagedCallersOnly, "pinvoke");
}
void CollectPInvokesForMethod(EcmaMethod method)
{
if (!method.IsPInvoke)
return;
if (IsUnsupportedOnPlatform(method))
return;
PInvokeMetadata metadata = method.GetPInvokeMethodMetadata();
bool wasmLinkage = method.HasCustomAttribute("System.Runtime.InteropServices", "WasmImportLinkageAttribute");
pinvokes.Add(new PInvokeInfo(metadata.Name, metadata.Module, method, wasmLinkage));
AddSignature(signatures, method, WasmLowering.LoweringFlags.IsUnmanagedCallersOnly, "pinvoke");
}
}
private void AddSignature(Dictionary<string, MethodDesc> signatures, MethodDesc method, WasmLowering.LoweringFlags flags, string kind)
{
string signature = InteropSignature.GetMethodSignature(method, flags);
if (signatures.TryAdd(signature, method))
log.Verbose($"Adding {kind} signature {signature} for method '{method.OwningType}.{method.Name.ToString()}'");
}
private bool IsMethodCallback(EcmaMethod method)
{
if (!method.IsUnmanagedCallersOnly)
return false;
if (IsUnsupportedOnPlatform(method))
return false;
return true;
}
private bool IsUnsupportedOnPlatform(EcmaMethod method)
=> EvaluatePlatformAttributes(method.GetDecodedCustomAttributes) switch
{
PlatformSupport.Unsupported => true,
PlatformSupport.Supported => false,
_ => IsUnsupportedOnPlatform(method.OwningType)
};
private bool IsUnsupportedOnPlatform(TypeDesc type)
{
if (type is not EcmaType ecmaType)
return false;
if (_typeUnsupportedOnPlatform.TryGetValue(type, out bool cached))
return cached;
bool value = EvaluatePlatformAttributes(ecmaType.GetDecodedCustomAttributes) switch
{
PlatformSupport.Unsupported => true,
PlatformSupport.Supported => false,
_ when ecmaType.ContainingType is not null => IsUnsupportedOnPlatform(ecmaType.ContainingType),
_ => IsAssemblyUnsupportedOnPlatform((EcmaAssembly)ecmaType.Module)
};
_typeUnsupportedOnPlatform[type] = value;
return value;
}
private bool IsAssemblyUnsupportedOnPlatform(EcmaAssembly assembly)
{
if (!_assemblyUnsupportedOnPlatform.TryGetValue(assembly, out bool value))
{
_assemblyUnsupportedOnPlatform[assembly] =
value = EvaluatePlatformAttributes(assembly.GetDecodedCustomAttributes) == PlatformSupport.Unsupported;
}
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(
Func<string, string, IEnumerable<CustomAttributeValue<TypeDesc>>> getAttributes)
{
const string Namespace = "System.Runtime.Versioning";
foreach (var attribute in getAttributes(Namespace, "UnsupportedOSPlatformAttribute"))
{
if (MatchesTargetOS(attribute))
return PlatformSupport.Unsupported;
}
bool hasSupportedOSPlatform = false;
foreach (var attribute in getAttributes(Namespace, "SupportedOSPlatformAttribute"))
{
if (attribute.FixedArguments.Length == 0)
continue;
hasSupportedOSPlatform = true;
if (MatchesTargetOS(attribute))
return PlatformSupport.Supported;
}
return hasSupportedOSPlatform ? PlatformSupport.Unsupported : PlatformSupport.Unknown;
bool MatchesTargetOS(CustomAttributeValue<TypeDesc> attribute)
{
if (attribute.FixedArguments.Length == 0)
return false;
string platformName = attribute.FixedArguments[0].Value?.ToString();
if (string.Equals(platformName, targetOS, StringComparison.OrdinalIgnoreCase))
return true;
// A platform name may carry a version, as in [SupportedOSPlatform("browser1.0")].
// Nothing generated here varies by platform version, so a versioned name still
// names the target as long as a version is all that follows it.
if (platformName?.StartsWith(targetOS, StringComparison.OrdinalIgnoreCase) != true)
return false;
return Version.TryParse(platformName.AsSpan(targetOS.Length), out _);
}
}
}
}