NewGenerator.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Microsoft.CodeAnalysis;
  6. namespace QuestPDF.InteropGenerators;
  7. public static class NewGenerator
  8. {
  9. private const string PythonTemplate = @"# auto-generated
  10. from cffi import FFI
  11. from typing import Callable, Optional, Tuple, Any, Self
  12. ";
  13. private const string CsharpInteropTemplate = @"// <auto-generated/>
  14. #nullable enable
  15. using System;
  16. using System.Runtime.CompilerServices;
  17. using System.Runtime.InteropServices;
  18. using QuestPDF.Fluent;
  19. using QuestPDF.Infrastructure;
  20. using QuestPDF.Helpers;
  21. using QuestPDF.Companion;
  22. namespace QuestPDF;
  23. internal unsafe partial class Interop
  24. {
  25. {{ methods }}
  26. }
  27. ";
  28. public static string AnalyzeAndGenerate(INamespaceSymbol namespaceSymbol)
  29. {
  30. var methods = CollectExtensionMethods(namespaceSymbol);
  31. var targetMethods = methods
  32. .Where(x => !IsObsolete(x.Item2))
  33. .ToList();
  34. var supportedExtensionMethods = targetMethods
  35. .Where(x => IsMethodSupported(x.Item2))
  36. .ToList();
  37. var simpleMethodHeaders = supportedExtensionMethods
  38. .Select(x => ConvertMethodToHeader(x.Item1, x.Item2))
  39. .ToList();
  40. var excluded = targetMethods.Except(supportedExtensionMethods).ToList();
  41. var pythonCHeaders = string.Join("\n", simpleMethodHeaders);
  42. var pythonContainerClass = supportedExtensionMethods
  43. .GroupBy(x => x.Item2.IsExtensionMethod ? x.Item2.Parameters.First().Type.Name : x.Item1.Name)
  44. .Select(x => ConvertToPythonContainerClient(x.Key, x))
  45. .ToList();
  46. var interopTemplateMethods = string.Join("\n\n", supportedExtensionMethods.Select(x => ConvertToUnmanagedCallersOnlyDefinition(x.Item1, x.Item2)));
  47. return CsharpInteropTemplate.Replace("{{ methods }}", interopTemplateMethods);
  48. }
  49. private static bool IsObsolete(IMethodSymbol method)
  50. {
  51. return method.GetAttributes().Any(attr => attr.AttributeClass?.ToDisplayString() == "System.ObsoleteAttribute");
  52. }
  53. private static bool IsMethodSupported(IMethodSymbol method)
  54. {
  55. if (method.IsGenericMethod)
  56. return false;
  57. if (method.Parameters.Any(x => IsTaskRelatedType(x.Type)))
  58. return false;
  59. // method has lambdas
  60. if (method.Parameters.Any(x => x.Type.TypeKind == TypeKind.Delegate))
  61. return false;
  62. var problematicMethods = new[] {"Border", "GeneratePdf", "GenerateXps", "Fallback", "DebugArea"};
  63. if (problematicMethods.Contains(method.Name))
  64. return false;
  65. if (method.IsExtensionMethod)
  66. {
  67. return method.Parameters.Skip(1).All(x => IsValueType(x.Type) || x.Type.SpecialType == SpecialType.System_String || IsColorType(x.Type) || IsUnitType(x.Type));
  68. }
  69. return true;
  70. }
  71. public static bool IsTaskRelatedType(ITypeSymbol type)
  72. {
  73. var fullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  74. // Check for Task, Task<T>, ValueTask, ValueTask<T>
  75. if (fullName.StartsWith("global::System.Threading.Tasks.Task") ||
  76. fullName.StartsWith("global::System.Threading.Tasks.ValueTask"))
  77. return true;
  78. // Check for CancellationToken
  79. if (fullName == "global::System.Threading.CancellationToken")
  80. return true;
  81. // Check for IAsyncEnumerable<T> and IAsyncEnumerator<T>
  82. if (fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerable") ||
  83. fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerator"))
  84. return true;
  85. return false;
  86. }
  87. #region Method Discovery
  88. private static IEnumerable<(INamedTypeSymbol, IMethodSymbol)> CollectExtensionMethods(INamespaceSymbol root)
  89. {
  90. var result = new List<(INamedTypeSymbol, IMethodSymbol)>();
  91. TraverseNamespaces(root);
  92. return result;
  93. void TraverseNamespaces(INamespaceSymbol namespaceSymbol)
  94. {
  95. foreach (var member in namespaceSymbol.GetMembers())
  96. {
  97. if (member is INamespaceSymbol childNs)
  98. {
  99. TraverseNamespaces(childNs);
  100. }
  101. else if (member is INamedTypeSymbol type)
  102. {
  103. HandleType(type);
  104. }
  105. }
  106. }
  107. void HandleType(INamedTypeSymbol type)
  108. {
  109. if (type.DeclaredAccessibility != Accessibility.Public || type.IsImplicitlyDeclared)
  110. return;
  111. foreach (var member in type.GetMembers())
  112. {
  113. if (member is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsImplicitlyDeclared: false } method)
  114. {
  115. result.Add((type, method));
  116. }
  117. }
  118. }
  119. }
  120. #endregion
  121. #region Method Conversion
  122. private static string ConvertMethodToHeader(INamedTypeSymbol typeSymbol, IMethodSymbol method)
  123. {
  124. var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
  125. var returnType = GetCReturnType(method.ReturnType);
  126. var parameters = GetCParameters(method);
  127. return $"{returnType} {methodName}({string.Join(", ", parameters)});";
  128. static List<string> GetCParameters(IMethodSymbol method)
  129. {
  130. var parameters = new List<string>();
  131. foreach (var param in method.Parameters)
  132. {
  133. var paramType = GetCReturnType(param.Type);
  134. var paramName = ToSnakeCase(param.Name);
  135. parameters.Add($"{paramType} {paramName}");
  136. }
  137. return parameters;
  138. }
  139. }
  140. static string ToSnakeCase(string text)
  141. {
  142. return string.Concat(text.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())).ToLowerInvariant();
  143. }
  144. static string ConvertToInterfaceFunctionName(INamedTypeSymbol typeSymbol, IMethodSymbol method)
  145. {
  146. var typeName = method.IsExtensionMethod ? method.Parameters.First().Type.Name : typeSymbol.Name;
  147. if (typeName.StartsWith("I") && typeName.Length > 1 && char.IsUpper(typeName[1]))
  148. typeName = typeName.Substring(1);
  149. return $"questpdf_{ToSnakeCase(typeName)}_{ToSnakeCase(method.Name)}";
  150. }
  151. private static string ConvertToPythonContainerClient(string groupName, IEnumerable<(INamedTypeSymbol, IMethodSymbol)> methodSymbols)
  152. {
  153. var sb = new StringBuilder();
  154. sb.AppendLine($"class {groupName}:");
  155. sb.AppendLine("\tdef __init__(self, handler_pointer: \"ffi.CData\"):");
  156. sb.AppendLine("\t\tself.handler_pointer = handler_pointer");
  157. foreach (var (typeSymbol, methodSymbol) in methodSymbols)
  158. {
  159. if (!methodSymbol.IsExtensionMethod)
  160. {
  161. sb.AppendLine($"\t\t# Conversion not supported");
  162. continue;
  163. }
  164. var additionalParameters = string.Join(", ", methodSymbol.Parameters.Skip(1).Select(ParamToInvocationArgument));
  165. if (additionalParameters.Length > 0)
  166. additionalParameters = ", " + additionalParameters;
  167. sb.AppendLine();
  168. sb.AppendLine($"\tdef {ToSnakeCase(methodSymbol.Name)}(self{additionalParameters}) -> Self:");
  169. sb.AppendLine($"\t\tresult = lib.{ConvertToInterfaceFunctionName(typeSymbol, methodSymbol)}(self.handler_pointer{additionalParameters})");
  170. sb.AppendLine("\t\treturn Container(result)");
  171. }
  172. return sb.ToString();
  173. static string ParamToInvocationArgument(IParameterSymbol param)
  174. {
  175. var type = "";
  176. return $"{ToSnakeCase(param.Name)}{type}";
  177. }
  178. }
  179. private static string ConvertToUnmanagedCallersOnlyDefinition(INamedTypeSymbol typeSymbol, IMethodSymbol method)
  180. {
  181. var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
  182. var returnType = GetCSharpInteropReturnType(method.ReturnType);
  183. var parameters = string.Join(", ", GetMethodParams(method));
  184. var additionalParameters = string.Join(", ", method.Parameters.Skip(method.IsExtensionMethod ? 1 : 0).Select(ParamToInvocationArgument));
  185. var typePointer = (method.IsExtensionMethod ? method.Parameters.First().Type : typeSymbol).ToDisplayString();
  186. if (!method.IsExtensionMethod)
  187. {
  188. parameters = parameters.Any() ? $"nint nativePointer, {parameters}" : "nint nativePointer";
  189. additionalParameters = additionalParameters.Any() ? $"nativePointer, {additionalParameters}" : "nativePointer";
  190. }
  191. var typePointerInputName = method.IsExtensionMethod ? method.Parameters.First().Name : "nativePointer";
  192. var sb = new StringBuilder();
  193. sb.AppendLine($"[UnmanagedCallersOnly(EntryPoint = \"{methodName}\", CallConvs = new[] {{ typeof(CallConvCdecl) }})]");
  194. sb.AppendLine($"public static {returnType} {typeSymbol.Name}{method.Name}({parameters})");
  195. sb.AppendLine("{");
  196. if (method.ReturnsVoid)
  197. {
  198. sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
  199. sb.AppendLine($"extendedTypePointer.{method.Name}({additionalParameters});");
  200. }
  201. else
  202. {
  203. sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
  204. sb.AppendLine($"var result = extendedTypePointer.{method.Name}({additionalParameters});");
  205. sb.AppendLine("return BoxHandle(result);");
  206. }
  207. sb.AppendLine("}");
  208. return sb.ToString();
  209. static string ParamToInvocationArgument(IParameterSymbol param)
  210. {
  211. if (param.Type.SpecialType == SpecialType.System_String)
  212. return $"Marshal.PtrToStringUTF8((IntPtr){param.Name}) ?? \"\"";
  213. if (param.Type.TypeKind == TypeKind.Enum)
  214. return $"({param.Type.ToDisplayString()}){param.Name}";
  215. if (IsColorType(param.Type))
  216. return $"(QuestPDF.Infrastructure.Color){param.Name}";
  217. return param.Name;
  218. }
  219. }
  220. static IEnumerable<string> GetMethodParams(IMethodSymbol method)
  221. {
  222. foreach (var param in method.Parameters)
  223. {
  224. var paramType = GetCSharpInteropReturnType(param.Type);
  225. yield return $"{paramType} {param.Name}";
  226. }
  227. }
  228. static string GetCSharpInteropReturnType(ITypeSymbol type)
  229. {
  230. if (type.SpecialType == SpecialType.System_Void)
  231. return "void";
  232. if (type.SpecialType == SpecialType.System_Int32)
  233. return "int";
  234. if (type.SpecialType == SpecialType.System_UInt32)
  235. return "uint";
  236. if (type.SpecialType == SpecialType.System_Boolean)
  237. return "bool";
  238. if (type.SpecialType == SpecialType.System_Single)
  239. return "float";
  240. if (type.SpecialType == SpecialType.System_Double)
  241. return "double";
  242. if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
  243. return "uint";
  244. if (type.TypeKind == TypeKind.Enum)
  245. return "int";
  246. // For object types, return IntPtr
  247. return "nint";
  248. }
  249. static string GetCReturnType(ITypeSymbol type)
  250. {
  251. if (type.SpecialType == SpecialType.System_Void)
  252. return "void";
  253. if (type.SpecialType == SpecialType.System_Int32)
  254. return "int32_t";
  255. if (type.SpecialType == SpecialType.System_UInt32)
  256. return "uint32_t";
  257. if (type.SpecialType == SpecialType.System_Boolean)
  258. return "bool";
  259. if (type.SpecialType == SpecialType.System_Single)
  260. return "float";
  261. if (type.SpecialType == SpecialType.System_Double)
  262. return "double";
  263. if (type.SpecialType == SpecialType.System_String)
  264. return "char*";
  265. if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
  266. return "uint32_t";
  267. if (type.TypeKind == TypeKind.Enum)
  268. return "int32_t";
  269. // For object types, return void pointer
  270. return "void*";
  271. }
  272. static bool IsValueType(ITypeSymbol type)
  273. {
  274. return type.SpecialType switch
  275. {
  276. SpecialType.System_Int32 => true,
  277. SpecialType.System_UInt32 => true,
  278. SpecialType.System_Boolean => true,
  279. SpecialType.System_Single => true,
  280. SpecialType.System_Double => true,
  281. _ => type.IsValueType
  282. };
  283. }
  284. static bool IsColorType(ITypeSymbol type)
  285. {
  286. return type.ToDisplayString() == "QuestPDF.Infrastructure.Color";
  287. }
  288. static bool IsUnitType(ITypeSymbol type)
  289. {
  290. return type.ToDisplayString() == "QuestPDF.Infrastructure.Unit";
  291. }
  292. #endregion
  293. }