|
|
@@ -0,0 +1,378 @@
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using System.Text;
|
|
|
+using Microsoft.CodeAnalysis;
|
|
|
+
|
|
|
+namespace QuestPDF.InteropGenerators;
|
|
|
+
|
|
|
+public static class NewGenerator
|
|
|
+{
|
|
|
+ private const string PythonTemplate = @"# auto-generated
|
|
|
+
|
|
|
+from cffi import FFI
|
|
|
+from typing import Callable, Optional, Tuple, Any, Self
|
|
|
+
|
|
|
+
|
|
|
+";
|
|
|
+
|
|
|
+ private const string CsharpInteropTemplate = @"// <auto-generated/>
|
|
|
+#nullable enable
|
|
|
+
|
|
|
+using System;
|
|
|
+using System.Runtime.CompilerServices;
|
|
|
+using System.Runtime.InteropServices;
|
|
|
+using QuestPDF.Fluent;
|
|
|
+using QuestPDF.Infrastructure;
|
|
|
+using QuestPDF.Helpers;
|
|
|
+using QuestPDF.Companion;
|
|
|
+
|
|
|
+namespace QuestPDF;
|
|
|
+
|
|
|
+internal unsafe partial class Interop
|
|
|
+{
|
|
|
+ {{ methods }}
|
|
|
+}
|
|
|
+";
|
|
|
+
|
|
|
+ public static string AnalyzeAndGenerate(INamespaceSymbol namespaceSymbol)
|
|
|
+ {
|
|
|
+ var methods = CollectExtensionMethods(namespaceSymbol);
|
|
|
+
|
|
|
+ var targetMethods = methods
|
|
|
+ .Where(x => !IsObsolete(x.Item2))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ var supportedExtensionMethods = targetMethods
|
|
|
+ .Where(x => IsMethodSupported(x.Item2))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ var simpleMethodHeaders = supportedExtensionMethods
|
|
|
+ .Select(x => ConvertMethodToHeader(x.Item1, x.Item2))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ var excluded = targetMethods.Except(supportedExtensionMethods).ToList();
|
|
|
+
|
|
|
+ var pythonCHeaders = string.Join("\n", simpleMethodHeaders);
|
|
|
+ var pythonContainerClass = supportedExtensionMethods
|
|
|
+ .GroupBy(x => x.Item2.IsExtensionMethod ? x.Item2.Parameters.First().Type.Name : x.Item1.Name)
|
|
|
+ .Select(x => ConvertToPythonContainerClient(x.Key, x))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ var interopTemplateMethods = string.Join("\n\n", supportedExtensionMethods.Select(x => ConvertToUnmanagedCallersOnlyDefinition(x.Item1, x.Item2)));
|
|
|
+ return CsharpInteropTemplate.Replace("{{ methods }}", interopTemplateMethods);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool IsObsolete(IMethodSymbol method)
|
|
|
+ {
|
|
|
+ return method.GetAttributes().Any(attr => attr.AttributeClass?.ToDisplayString() == "System.ObsoleteAttribute");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool IsMethodSupported(IMethodSymbol method)
|
|
|
+ {
|
|
|
+ if (method.IsGenericMethod)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (method.Parameters.Any(x => IsTaskRelatedType(x.Type)))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // method has lambdas
|
|
|
+ if (method.Parameters.Any(x => x.Type.TypeKind == TypeKind.Delegate))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ var problematicMethods = new[] {"Border", "GeneratePdf", "GenerateXps", "Fallback", "DebugArea"};
|
|
|
+
|
|
|
+ if (problematicMethods.Contains(method.Name))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (method.IsExtensionMethod)
|
|
|
+ {
|
|
|
+ return method.Parameters.Skip(1).All(x => IsValueType(x.Type) || x.Type.SpecialType == SpecialType.System_String || IsColorType(x.Type) || IsUnitType(x.Type));
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static bool IsTaskRelatedType(ITypeSymbol type)
|
|
|
+ {
|
|
|
+ var fullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
|
+
|
|
|
+ // Check for Task, Task<T>, ValueTask, ValueTask<T>
|
|
|
+ if (fullName.StartsWith("global::System.Threading.Tasks.Task") ||
|
|
|
+ fullName.StartsWith("global::System.Threading.Tasks.ValueTask"))
|
|
|
+ return true;
|
|
|
+
|
|
|
+ // Check for CancellationToken
|
|
|
+ if (fullName == "global::System.Threading.CancellationToken")
|
|
|
+ return true;
|
|
|
+
|
|
|
+ // Check for IAsyncEnumerable<T> and IAsyncEnumerator<T>
|
|
|
+ if (fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerable") ||
|
|
|
+ fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerator"))
|
|
|
+ return true;
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ #region Method Discovery
|
|
|
+
|
|
|
+ private static IEnumerable<(INamedTypeSymbol, IMethodSymbol)> CollectExtensionMethods(INamespaceSymbol root)
|
|
|
+ {
|
|
|
+ var result = new List<(INamedTypeSymbol, IMethodSymbol)>();
|
|
|
+ TraverseNamespaces(root);
|
|
|
+ return result;
|
|
|
+
|
|
|
+ void TraverseNamespaces(INamespaceSymbol namespaceSymbol)
|
|
|
+ {
|
|
|
+ foreach (var member in namespaceSymbol.GetMembers())
|
|
|
+ {
|
|
|
+ if (member is INamespaceSymbol childNs)
|
|
|
+ {
|
|
|
+ TraverseNamespaces(childNs);
|
|
|
+ }
|
|
|
+ else if (member is INamedTypeSymbol type)
|
|
|
+ {
|
|
|
+ HandleType(type);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void HandleType(INamedTypeSymbol type)
|
|
|
+ {
|
|
|
+ if (type.DeclaredAccessibility != Accessibility.Public || type.IsImplicitlyDeclared)
|
|
|
+ return;
|
|
|
+
|
|
|
+ foreach (var member in type.GetMembers())
|
|
|
+ {
|
|
|
+ if (member is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsImplicitlyDeclared: false } method)
|
|
|
+ {
|
|
|
+ result.Add((type, method));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Method Conversion
|
|
|
+
|
|
|
+ private static string ConvertMethodToHeader(INamedTypeSymbol typeSymbol, IMethodSymbol method)
|
|
|
+ {
|
|
|
+ var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
|
|
|
+ var returnType = GetCReturnType(method.ReturnType);
|
|
|
+ var parameters = GetCParameters(method);
|
|
|
+
|
|
|
+ return $"{returnType} {methodName}({string.Join(", ", parameters)});";
|
|
|
+
|
|
|
+ static List<string> GetCParameters(IMethodSymbol method)
|
|
|
+ {
|
|
|
+ var parameters = new List<string>();
|
|
|
+
|
|
|
+ foreach (var param in method.Parameters)
|
|
|
+ {
|
|
|
+ var paramType = GetCReturnType(param.Type);
|
|
|
+ var paramName = ToSnakeCase(param.Name);
|
|
|
+ parameters.Add($"{paramType} {paramName}");
|
|
|
+ }
|
|
|
+
|
|
|
+ return parameters;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static string ToSnakeCase(string text)
|
|
|
+ {
|
|
|
+ return string.Concat(text.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())).ToLowerInvariant();
|
|
|
+ }
|
|
|
+
|
|
|
+ static string ConvertToInterfaceFunctionName(INamedTypeSymbol typeSymbol, IMethodSymbol method)
|
|
|
+ {
|
|
|
+ var typeName = method.IsExtensionMethod ? method.Parameters.First().Type.Name : typeSymbol.Name;
|
|
|
+
|
|
|
+ if (typeName.StartsWith("I") && typeName.Length > 1 && char.IsUpper(typeName[1]))
|
|
|
+ typeName = typeName.Substring(1);
|
|
|
+
|
|
|
+ return $"questpdf_{ToSnakeCase(typeName)}_{ToSnakeCase(method.Name)}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string ConvertToPythonContainerClient(string groupName, IEnumerable<(INamedTypeSymbol, IMethodSymbol)> methodSymbols)
|
|
|
+ {
|
|
|
+ var sb = new StringBuilder();
|
|
|
+
|
|
|
+ sb.AppendLine($"class {groupName}:");
|
|
|
+ sb.AppendLine("\tdef __init__(self, handler_pointer: \"ffi.CData\"):");
|
|
|
+ sb.AppendLine("\t\tself.handler_pointer = handler_pointer");
|
|
|
+
|
|
|
+ foreach (var (typeSymbol, methodSymbol) in methodSymbols)
|
|
|
+ {
|
|
|
+ if (!methodSymbol.IsExtensionMethod)
|
|
|
+ {
|
|
|
+ sb.AppendLine($"\t\t# Conversion not supported");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ var additionalParameters = string.Join(", ", methodSymbol.Parameters.Skip(1).Select(ParamToInvocationArgument));
|
|
|
+
|
|
|
+ if (additionalParameters.Length > 0)
|
|
|
+ additionalParameters = ", " + additionalParameters;
|
|
|
+
|
|
|
+ sb.AppendLine();
|
|
|
+ sb.AppendLine($"\tdef {ToSnakeCase(methodSymbol.Name)}(self{additionalParameters}) -> Self:");
|
|
|
+ sb.AppendLine($"\t\tresult = lib.{ConvertToInterfaceFunctionName(typeSymbol, methodSymbol)}(self.handler_pointer{additionalParameters})");
|
|
|
+ sb.AppendLine("\t\treturn Container(result)");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ return sb.ToString();
|
|
|
+
|
|
|
+ static string ParamToInvocationArgument(IParameterSymbol param)
|
|
|
+ {
|
|
|
+ var type = "";
|
|
|
+
|
|
|
+ return $"{ToSnakeCase(param.Name)}{type}";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string ConvertToUnmanagedCallersOnlyDefinition(INamedTypeSymbol typeSymbol, IMethodSymbol method)
|
|
|
+ {
|
|
|
+ var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
|
|
|
+ var returnType = GetCSharpInteropReturnType(method.ReturnType);
|
|
|
+ var parameters = string.Join(", ", GetMethodParams(method));
|
|
|
+
|
|
|
+ var additionalParameters = string.Join(", ", method.Parameters.Skip(method.IsExtensionMethod ? 1 : 0).Select(ParamToInvocationArgument));
|
|
|
+ var typePointer = (method.IsExtensionMethod ? method.Parameters.First().Type : typeSymbol).ToDisplayString();
|
|
|
+
|
|
|
+ if (!method.IsExtensionMethod)
|
|
|
+ {
|
|
|
+ parameters = parameters.Any() ? $"nint nativePointer, {parameters}" : "nint nativePointer";
|
|
|
+ additionalParameters = additionalParameters.Any() ? $"nativePointer, {additionalParameters}" : "nativePointer";
|
|
|
+ }
|
|
|
+
|
|
|
+ var typePointerInputName = method.IsExtensionMethod ? method.Parameters.First().Name : "nativePointer";
|
|
|
+
|
|
|
+ var sb = new StringBuilder();
|
|
|
+ sb.AppendLine($"[UnmanagedCallersOnly(EntryPoint = \"{methodName}\", CallConvs = new[] {{ typeof(CallConvCdecl) }})]");
|
|
|
+ sb.AppendLine($"public static {returnType} {typeSymbol.Name}{method.Name}({parameters})");
|
|
|
+ sb.AppendLine("{");
|
|
|
+
|
|
|
+ if (method.ReturnsVoid)
|
|
|
+ {
|
|
|
+ sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
|
|
|
+ sb.AppendLine($"extendedTypePointer.{method.Name}({additionalParameters});");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
|
|
|
+ sb.AppendLine($"var result = extendedTypePointer.{method.Name}({additionalParameters});");
|
|
|
+ sb.AppendLine("return BoxHandle(result);");
|
|
|
+ }
|
|
|
+
|
|
|
+ sb.AppendLine("}");
|
|
|
+
|
|
|
+ return sb.ToString();
|
|
|
+
|
|
|
+ static string ParamToInvocationArgument(IParameterSymbol param)
|
|
|
+ {
|
|
|
+ if (param.Type.SpecialType == SpecialType.System_String)
|
|
|
+ return $"Marshal.PtrToStringUTF8((IntPtr){param.Name}) ?? \"\"";
|
|
|
+
|
|
|
+ if (param.Type.TypeKind == TypeKind.Enum)
|
|
|
+ return $"({param.Type.ToDisplayString()}){param.Name}";
|
|
|
+
|
|
|
+ if (IsColorType(param.Type))
|
|
|
+ return $"(QuestPDF.Infrastructure.Color){param.Name}";
|
|
|
+
|
|
|
+ return param.Name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static IEnumerable<string> GetMethodParams(IMethodSymbol method)
|
|
|
+ {
|
|
|
+ foreach (var param in method.Parameters)
|
|
|
+ {
|
|
|
+ var paramType = GetCSharpInteropReturnType(param.Type);
|
|
|
+ yield return $"{paramType} {param.Name}";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static string GetCSharpInteropReturnType(ITypeSymbol type)
|
|
|
+ {
|
|
|
+ if (type.SpecialType == SpecialType.System_Void)
|
|
|
+ return "void";
|
|
|
+ if (type.SpecialType == SpecialType.System_Int32)
|
|
|
+ return "int";
|
|
|
+ if (type.SpecialType == SpecialType.System_UInt32)
|
|
|
+ return "uint";
|
|
|
+ if (type.SpecialType == SpecialType.System_Boolean)
|
|
|
+ return "bool";
|
|
|
+ if (type.SpecialType == SpecialType.System_Single)
|
|
|
+ return "float";
|
|
|
+ if (type.SpecialType == SpecialType.System_Double)
|
|
|
+ return "double";
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
|
|
|
+ return "uint";
|
|
|
+
|
|
|
+ if (type.TypeKind == TypeKind.Enum)
|
|
|
+ return "int";
|
|
|
+
|
|
|
+ // For object types, return IntPtr
|
|
|
+ return "nint";
|
|
|
+ }
|
|
|
+
|
|
|
+ static string GetCReturnType(ITypeSymbol type)
|
|
|
+ {
|
|
|
+ if (type.SpecialType == SpecialType.System_Void)
|
|
|
+ return "void";
|
|
|
+ if (type.SpecialType == SpecialType.System_Int32)
|
|
|
+ return "int32_t";
|
|
|
+ if (type.SpecialType == SpecialType.System_UInt32)
|
|
|
+ return "uint32_t";
|
|
|
+ if (type.SpecialType == SpecialType.System_Boolean)
|
|
|
+ return "bool";
|
|
|
+ if (type.SpecialType == SpecialType.System_Single)
|
|
|
+ return "float";
|
|
|
+ if (type.SpecialType == SpecialType.System_Double)
|
|
|
+ return "double";
|
|
|
+
|
|
|
+ if (type.SpecialType == SpecialType.System_String)
|
|
|
+ return "char*";
|
|
|
+
|
|
|
+ if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
|
|
|
+ return "uint32_t";
|
|
|
+
|
|
|
+ if (type.TypeKind == TypeKind.Enum)
|
|
|
+ return "int32_t";
|
|
|
+
|
|
|
+ // For object types, return void pointer
|
|
|
+ return "void*";
|
|
|
+ }
|
|
|
+
|
|
|
+ static bool IsValueType(ITypeSymbol type)
|
|
|
+ {
|
|
|
+ return type.SpecialType switch
|
|
|
+ {
|
|
|
+ SpecialType.System_Int32 => true,
|
|
|
+ SpecialType.System_UInt32 => true,
|
|
|
+ SpecialType.System_Boolean => true,
|
|
|
+ SpecialType.System_Single => true,
|
|
|
+ SpecialType.System_Double => true,
|
|
|
+ _ => type.IsValueType
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ static bool IsColorType(ITypeSymbol type)
|
|
|
+ {
|
|
|
+ return type.ToDisplayString() == "QuestPDF.Infrastructure.Color";
|
|
|
+ }
|
|
|
+
|
|
|
+ static bool IsUnitType(ITypeSymbol type)
|
|
|
+ {
|
|
|
+ return type.ToDisplayString() == "QuestPDF.Infrastructure.Unit";
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+}
|