utils/SkiaSharpGenerator/Cookies/CookieDetector.cs (159 lines of code) (raw):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Mono.Cecil;
namespace SkiaSharpGenerator
{
public class CookieDetector
{
private const string SourceUrl = "https://github.com/mono/mono/raw/{0}/mcs/tools/wasm-tuner/InterpToNativeGenerator.cs";
private readonly string branchUrl;
private readonly string assemblyPath;
private readonly string type;
private CSharpSyntaxTree? compilation;
public CookieDetector(string assembly, string interopType, string branchName)
{
assemblyPath = assembly;
type = interopType;
branchUrl = string.Format(SourceUrl, branchName);
}
public ILogger? Log { get; set; }
public bool HasErrors =>
compilation?.GetDiagnostics()?.Any(d => d.Severity == DiagnosticSeverity.Error) ?? false;
public IEnumerable<Diagnostic> Messages =>
compilation?.GetDiagnostics() ?? Array.Empty<Diagnostic>();
public async Task DetectAsync()
{
Log?.LogVerbose("Downloading source...");
var code = await DownloadLatestCodeAsync();
if (string.IsNullOrEmpty(code))
{
Log?.LogError("Downloading source failed.");
throw new Exception("Downloading source failed.");
}
compilation = (CSharpSyntaxTree)CSharpSyntaxTree.ParseText(code);
if (compilation == null || HasErrors)
{
Log?.LogError("Parsing source failed.");
throw new Exception("Parsing source failed.");
}
Log?.LogVerbose("Parsing cookies...");
var cookies = GetExistingCookies();
if (cookies == null || cookies.Length == 0)
{
Log?.LogError("Retreiving cookies failed.");
throw new Exception("Retreiving cookies failed.");
}
Log?.LogVerbose($"Found {cookies.Length} cookies.");
Log?.LogVerbose("Loading .NET assembly...");
var signatures = ParseAssembly(type);
Log?.LogVerbose($"Found {signatures.Length} signatures.");
var newSignatures = signatures
.Where(s => !cookies.Contains(s.Signature))
.ToArray();
if (newSignatures.Length > 0)
{
Log?.LogWarning($"Found {newSignatures.Length} NEW signatures.");
foreach (var sig in newSignatures)
{
Log?.LogVerbose($"{sig}");
Log?.LogVerbose($"Potential matches: {string.Join(", ", GetMatches(sig.Signature))}");
}
Log?.Log(string.Join(Environment.NewLine, newSignatures.Select(s => s.Signature).Distinct()));
}
else
{
Log?.LogVerbose($"Found NO new signatures.");
}
IEnumerable<string> GetMatches(string signature)
{
return cookies.Where(c =>
c.Length == signature.Length &&
c[0] == signature[0] &&
string.Concat(c.Substring(1).ToCharArray().OrderBy(x => x)) == string.Concat(signature.Substring(1).ToCharArray().OrderBy(x => x)));
}
}
private (string Method, string Signature)[] ParseAssembly(string typeFullName)
{
var resolver = new DefaultAssemblyResolver();
var param = new ReaderParameters
{
AssemblyResolver = resolver
};
var module = AssemblyDefinition.ReadAssembly(assemblyPath, param);
var type = module.MainModule.GetType(typeFullName);
var methods = type.Methods.Select(m =>
{
var returnSig = GetSignature(m, m.ReturnType);
var paramsSig = string.Concat(m.Parameters.Select(p => GetSignature(m, p.ParameterType)));
return (Method: m.Name, Signature: returnSig + paramsSig);
});
return methods.OrderBy(s => s.Signature).ToArray();
static string GetSignature(MethodDefinition method, TypeReference ret)
{
if (ret.IsByReference || ret.IsArray || ret.IsPointer)
return "I";
return GetTypeSignature(method, ret.Resolve());
}
}
private static string GetTypeSignature(MethodDefinition method, TypeDefinition type)
{
if (type.IsEnum || type.IsPointer || type.IsArray)
return "I";
// special delegates
if ((type.FullName.StartsWith("SkiaSharp.") || type.FullName.StartsWith("HarfBuzzSharp.")) && type.FullName.EndsWith("ProxyDelegate"))
return "I";
switch (type.FullName)
{
case "System.String":
case "System.UInt16":
case "System.Int64":
case "System.UInt32":
case "System.Int32":
case "System.IntPtr":
case "System.Byte":
case "System.Boolean":
case "System.Void*":
case "SkiaSharp.SKManagedDrawableDelegates":
case "SkiaSharp.SKManagedStreamDelegates":
case "SkiaSharp.SKManagedWStreamDelegates":
return "I";
case "System.Double":
return "D";
case "System.Single":
return "F";
case "SkiaSharp.GRVkBackendContextNative":
return "O";
case "System.Void":
return "V";
default:
throw new NotSupportedException($"Unsupported parameter type for {method.FullName}: {type}");
}
}
private string[]? GetExistingCookies()
{
var root = compilation!.GetCompilationUnitRoot();
var generatorClass = root.Members
.OfType<ClassDeclarationSyntax>()
.Single(m => m.Identifier.ValueText == "InterpToNativeGenerator");
var cookiesField = generatorClass.Members
.OfType<FieldDeclarationSyntax>()
.SelectMany(m => m.Declaration.Variables)
.Single(m => m.Identifier.ValueText == "cookies");
var cookiesDeclaration = cookiesField.Initializer?.Value as ArrayCreationExpressionSyntax;
return cookiesDeclaration?.Initializer?.Expressions
.OfType<LiteralExpressionSyntax>()
.Select(e => e.Token.ValueText)
.ToArray();
}
private async Task<string?> DownloadLatestCodeAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync(branchUrl);
}
}
}