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 = @"// #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, ValueTask, ValueTask 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 and IAsyncEnumerator 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 GetCParameters(IMethodSymbol method) { var parameters = new List(); 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 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 }