ContainerSourceGenerator.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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. /// <summary>
  8. /// Generates native AOT/C ABI FFI code and Python bindings for QuestPDF IContainer extension methods.
  9. /// This enables fluent API usage in Python through C# interop.
  10. /// </summary>
  11. public class ContainerSourceGenerator : ISourceGenerator
  12. {
  13. /// <summary>
  14. /// Generates C# UnmanagedCallersOnly methods for native AOT compilation with C ABI compatibility.
  15. /// Each IContainer extension method gets a corresponding FFI wrapper.
  16. /// </summary>
  17. public string GenerateCSharpCode(INamespaceSymbol namespaceSymbol)
  18. {
  19. var containerMethods = FindContainerExtensionMethods(namespaceSymbol);
  20. if (!containerMethods.Any())
  21. return "// No IContainer extension methods found";
  22. var code = new StringBuilder();
  23. // Generate header
  24. code.AppendLine("using System;");
  25. code.AppendLine("using System.Runtime.InteropServices;");
  26. code.AppendLine("using System.Runtime.CompilerServices;");
  27. code.AppendLine("using QuestPDF.Infrastructure;");
  28. code.AppendLine("using QuestPDF.Fluent;");
  29. code.AppendLine();
  30. code.AppendLine("namespace QuestPDF.InteropBindings");
  31. code.AppendLine("{");
  32. code.AppendLine(" /// <summary>");
  33. code.AppendLine(" /// Native AOT FFI bindings for IContainer extension methods");
  34. code.AppendLine(" /// </summary>");
  35. code.AppendLine(" public static class ContainerInterop");
  36. code.AppendLine(" {");
  37. // Generate handle management
  38. code.AppendLine(" private static readonly Dictionary<IntPtr, IContainer> ContainerHandles = new();");
  39. code.AppendLine(" private static IntPtr _nextHandle = (IntPtr)1;");
  40. code.AppendLine();
  41. code.AppendLine(" private static IntPtr AllocateHandle(IContainer container)");
  42. code.AppendLine(" {");
  43. code.AppendLine(" var handle = _nextHandle;");
  44. code.AppendLine(" _nextHandle = (IntPtr)((long)_nextHandle + 1);");
  45. code.AppendLine(" ContainerHandles[handle] = container;");
  46. code.AppendLine(" return handle;");
  47. code.AppendLine(" }");
  48. code.AppendLine();
  49. code.AppendLine(" private static IContainer GetContainer(IntPtr handle)");
  50. code.AppendLine(" {");
  51. code.AppendLine(" if (!ContainerHandles.TryGetValue(handle, out var container))");
  52. code.AppendLine(" throw new InvalidOperationException($\"Invalid container handle: {handle}\");");
  53. code.AppendLine(" return container;");
  54. code.AppendLine(" }");
  55. code.AppendLine();
  56. code.AppendLine(" [UnmanagedCallersOnly(EntryPoint = \"container_release\")]");
  57. code.AppendLine(" public static void ReleaseContainer(IntPtr handle)");
  58. code.AppendLine(" {");
  59. code.AppendLine(" ContainerHandles.Remove(handle);");
  60. code.AppendLine(" }");
  61. code.AppendLine();
  62. // Generate FFI methods for each extension method
  63. foreach (var method in containerMethods)
  64. {
  65. GenerateCSharpMethod(code, method);
  66. }
  67. code.AppendLine(" }");
  68. code.AppendLine("}");
  69. return code.ToString();
  70. }
  71. /// <summary>
  72. /// Generates Python bindings using CFFI for FFI calls to the C# native AOT library.
  73. /// Creates a Python Container class with fluent API methods.
  74. /// </summary>
  75. public string GeneratePythonCode(INamespaceSymbol namespaceSymbol)
  76. {
  77. var containerMethods = FindContainerExtensionMethods(namespaceSymbol);
  78. if (!containerMethods.Any())
  79. return "# No IContainer extension methods found";
  80. var code = new StringBuilder();
  81. // Generate imports and setup
  82. code.AppendLine("from cffi import FFI");
  83. code.AppendLine("from typing import Optional, Callable, Any");
  84. code.AppendLine("from enum import IntEnum");
  85. code.AppendLine();
  86. code.AppendLine("# Initialize CFFI");
  87. code.AppendLine("ffi = FFI()");
  88. code.AppendLine();
  89. code.AppendLine("# Define C function signatures");
  90. code.AppendLine("ffi.cdef(\"\"\"");
  91. // Generate C function declarations for CFFI
  92. code.AppendLine(" void container_release(void* handle);");
  93. foreach (var method in containerMethods)
  94. {
  95. GenerateCFFISignature(code, method);
  96. }
  97. code.AppendLine("\"\"\")");
  98. code.AppendLine();
  99. code.AppendLine("# Load the native library");
  100. code.AppendLine("_lib = ffi.dlopen('./QuestPDF.Native.dll') # Adjust path as needed");
  101. code.AppendLine();
  102. code.AppendLine("class Container:");
  103. code.AppendLine(" \"\"\"");
  104. code.AppendLine(" Represents a layout structure with exactly one child element.");
  105. code.AppendLine(" Provides fluent API for building QuestPDF documents.");
  106. code.AppendLine(" \"\"\"");
  107. code.AppendLine();
  108. code.AppendLine(" def __init__(self, handle):");
  109. code.AppendLine(" \"\"\"Initialize container with native handle\"\"\"");
  110. code.AppendLine(" self._handle = handle");
  111. code.AppendLine();
  112. code.AppendLine(" def __del__(self):");
  113. code.AppendLine(" \"\"\"Release native resources\"\"\"");
  114. code.AppendLine(" if hasattr(self, '_handle') and self._handle:");
  115. code.AppendLine(" _lib.container_release(self._handle)");
  116. code.AppendLine();
  117. code.AppendLine(" @property");
  118. code.AppendLine(" def handle(self):");
  119. code.AppendLine(" \"\"\"Get the native handle\"\"\"");
  120. code.AppendLine(" return self._handle");
  121. code.AppendLine();
  122. // Generate Python methods for each extension method
  123. foreach (var method in containerMethods)
  124. {
  125. GeneratePythonMethod(code, method);
  126. }
  127. return code.ToString();
  128. }
  129. private List<IMethodSymbol> FindContainerExtensionMethods(INamespaceSymbol namespaceSymbol)
  130. {
  131. var methods = new List<IMethodSymbol>();
  132. FindExtensionMethodsRecursive(namespaceSymbol, methods);
  133. return methods.Where(m => IsContainerExtensionMethod(m)).ToList();
  134. }
  135. private void FindExtensionMethodsRecursive(INamespaceSymbol namespaceSymbol, List<IMethodSymbol> methods)
  136. {
  137. // Search in current namespace types
  138. foreach (var type in namespaceSymbol.GetTypeMembers())
  139. {
  140. if (type.IsStatic)
  141. {
  142. foreach (var member in type.GetMembers().OfType<IMethodSymbol>())
  143. {
  144. if (member.IsExtensionMethod)
  145. {
  146. methods.Add(member);
  147. }
  148. }
  149. }
  150. }
  151. // Recursively search child namespaces
  152. foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers())
  153. {
  154. FindExtensionMethodsRecursive(childNamespace, methods);
  155. }
  156. }
  157. private bool IsContainerExtensionMethod(IMethodSymbol method)
  158. {
  159. if (!method.IsExtensionMethod)
  160. return false;
  161. var firstParam = method.Parameters.FirstOrDefault();
  162. if (firstParam == null)
  163. return false;
  164. // Check if the first parameter is IContainer
  165. var paramType = firstParam.Type;
  166. return paramType.Name == "IContainer" &&
  167. paramType.ContainingNamespace?.ToDisplayString() == "QuestPDF.Infrastructure";
  168. }
  169. private void GenerateCSharpMethod(StringBuilder code, IMethodSymbol method)
  170. {
  171. var methodName = ToSnakeCaseLower(method.Name);
  172. var entryPoint = $"container_{methodName}";
  173. code.AppendLine($" [UnmanagedCallersOnly(EntryPoint = \"{entryPoint}\")]");
  174. // Generate method signature
  175. var returnType = method.ReturnsVoid ? "void" : "IntPtr";
  176. code.Append($" public static {returnType} {ToPascalCase(method.Name)}(IntPtr containerHandle");
  177. // Add parameters (skip the first one as it's the extension method's 'this' parameter)
  178. foreach (var param in method.Parameters.Skip(1))
  179. {
  180. code.Append($", {GetCSharpFFIType(param.Type)} {param.Name}");
  181. }
  182. code.AppendLine(")");
  183. code.AppendLine(" {");
  184. // Generate method body
  185. code.AppendLine(" try");
  186. code.AppendLine(" {");
  187. code.AppendLine(" var container = GetContainer(containerHandle);");
  188. // Generate the actual method call
  189. var callParams = string.Join(", ", method.Parameters.Skip(1).Select(p => ConvertFromFFI(p)));
  190. if (method.ReturnsVoid)
  191. {
  192. code.AppendLine($" container.{method.Name}({callParams});");
  193. }
  194. else if (IsContainerReturnType(method.ReturnType))
  195. {
  196. code.AppendLine($" var result = container.{method.Name}({callParams});");
  197. code.AppendLine(" return AllocateHandle(result);");
  198. }
  199. else
  200. {
  201. code.AppendLine($" return container.{method.Name}({callParams});");
  202. }
  203. code.AppendLine(" }");
  204. code.AppendLine(" catch");
  205. code.AppendLine(" {");
  206. code.AppendLine(method.ReturnsVoid ? " return;" : " return IntPtr.Zero;");
  207. code.AppendLine(" }");
  208. code.AppendLine(" }");
  209. code.AppendLine();
  210. }
  211. private void GenerateCFFISignature(StringBuilder code, IMethodSymbol method)
  212. {
  213. var cFunctionName = $"container_{ToSnakeCaseLower(method.Name)}";
  214. // Generate return type
  215. var returnType = method.ReturnsVoid ? "void" : GetCFFIType(method.ReturnType);
  216. code.Append($" {returnType} {cFunctionName}(void* handle");
  217. // Add parameters (skip the first one as it's the extension method's 'this' parameter)
  218. foreach (var param in method.Parameters.Skip(1))
  219. {
  220. code.Append($", {GetCFFIType(param.Type)} {ToSnakeCaseLower(param.Name)}");
  221. }
  222. code.AppendLine(");");
  223. }
  224. private void GeneratePythonMethod(StringBuilder code, IMethodSymbol method)
  225. {
  226. var pythonMethodName = ToSnakeCaseLower(method.Name);
  227. var doc = DocumentationHelper.ExtractDocumentation(method.GetDocumentationCommentXml());
  228. // Generate method signature
  229. code.Append($" def {pythonMethodName}(self");
  230. // Add parameters
  231. foreach (var param in method.Parameters.Skip(1))
  232. {
  233. var paramName = ToSnakeCaseLower(param.Name);
  234. var pythonType = GetPythonType(param.Type);
  235. var defaultValue = GetPythonDefaultValue(param);
  236. code.Append($", {paramName}: {pythonType}{defaultValue}");
  237. }
  238. code.AppendLine("):");
  239. // Add docstring
  240. if (!string.IsNullOrEmpty(doc))
  241. {
  242. code.AppendLine(" \"\"\"");
  243. code.AppendLine($" {doc}");
  244. // Add parameter documentation
  245. if (method.Parameters.Length > 1)
  246. {
  247. code.AppendLine();
  248. code.AppendLine(" Args:");
  249. foreach (var param in method.Parameters.Skip(1))
  250. {
  251. var paramName = ToSnakeCaseLower(param.Name);
  252. code.AppendLine($" {paramName}: {GetPythonType(param.Type)}");
  253. }
  254. }
  255. // Add return documentation
  256. if (!method.ReturnsVoid && IsContainerReturnType(method.ReturnType))
  257. {
  258. code.AppendLine();
  259. code.AppendLine(" Returns:");
  260. code.AppendLine(" Container: Self for method chaining");
  261. }
  262. code.AppendLine(" \"\"\"");
  263. }
  264. // Generate method body
  265. var cFunctionName = $"container_{ToSnakeCaseLower(method.Name)}";
  266. var callParams = "self._handle";
  267. foreach (var param in method.Parameters.Skip(1))
  268. {
  269. var paramName = ToSnakeCaseLower(param.Name);
  270. callParams += $", {ConvertToCFFI(param, paramName)}";
  271. }
  272. if (method.ReturnsVoid)
  273. {
  274. code.AppendLine($" _lib.{cFunctionName}({callParams})");
  275. code.AppendLine(" return self");
  276. }
  277. else if (IsContainerReturnType(method.ReturnType))
  278. {
  279. code.AppendLine($" new_handle = _lib.{cFunctionName}({callParams})");
  280. code.AppendLine(" if new_handle != ffi.NULL:");
  281. code.AppendLine(" return Container(new_handle)");
  282. code.AppendLine(" return self");
  283. }
  284. else
  285. {
  286. code.AppendLine($" return _lib.{cFunctionName}({callParams})");
  287. }
  288. code.AppendLine();
  289. }
  290. private string GetCFFIType(ITypeSymbol type)
  291. {
  292. if (IsContainerReturnType(type))
  293. return "void*";
  294. return type.SpecialType switch
  295. {
  296. SpecialType.System_Boolean => "bool",
  297. SpecialType.System_Int32 => "int",
  298. SpecialType.System_Single => "float",
  299. SpecialType.System_Double => "double",
  300. SpecialType.System_String => "char*",
  301. _ when type.TypeKind == TypeKind.Enum => "int",
  302. _ => "void*"
  303. };
  304. }
  305. private string GetCSharpFFIType(ITypeSymbol type)
  306. {
  307. return type.SpecialType switch
  308. {
  309. SpecialType.System_Boolean => "bool",
  310. SpecialType.System_Int32 => "int",
  311. SpecialType.System_Single => "float",
  312. SpecialType.System_Double => "double",
  313. SpecialType.System_String => "IntPtr", // Marshalled as char*
  314. _ when type.TypeKind == TypeKind.Enum => "int",
  315. _ => "IntPtr"
  316. };
  317. }
  318. private string GetPythonType(ITypeSymbol type)
  319. {
  320. return type.SpecialType switch
  321. {
  322. SpecialType.System_Boolean => "bool",
  323. SpecialType.System_Int32 => "int",
  324. SpecialType.System_Single => "float",
  325. SpecialType.System_Double => "float",
  326. SpecialType.System_String => "str",
  327. _ when type.TypeKind == TypeKind.Enum => "int",
  328. _ when type.Name == "Action" || type.Name == "Func" => "Callable",
  329. _ => "Any"
  330. };
  331. }
  332. private string ConvertToCFFI(IParameterSymbol param, string paramName)
  333. {
  334. if (param.Type.SpecialType == SpecialType.System_String)
  335. {
  336. return $"{paramName}.encode('utf-8') if isinstance({paramName}, str) else {paramName}";
  337. }
  338. return paramName;
  339. }
  340. private string GetPythonDefaultValue(IParameterSymbol param)
  341. {
  342. if (!param.HasExplicitDefaultValue)
  343. return "";
  344. if (param.ExplicitDefaultValue == null)
  345. return " = None";
  346. return param.Type.SpecialType switch
  347. {
  348. SpecialType.System_Boolean => $" = {param.ExplicitDefaultValue.ToString().ToLower()}",
  349. SpecialType.System_Int32 or SpecialType.System_Single or SpecialType.System_Double => $" = {param.ExplicitDefaultValue}",
  350. SpecialType.System_String => $" = \"{param.ExplicitDefaultValue}\"",
  351. _ => ""
  352. };
  353. }
  354. private string ConvertFromFFI(IParameterSymbol param)
  355. {
  356. if (param.Type.SpecialType == SpecialType.System_String)
  357. {
  358. return $"Marshal.PtrToStringUTF8({param.Name})";
  359. }
  360. if (param.Type.TypeKind == TypeKind.Enum)
  361. {
  362. return $"({param.Type.Name}){param.Name}";
  363. }
  364. return param.Name;
  365. }
  366. private bool IsContainerReturnType(ITypeSymbol type)
  367. {
  368. return type.Name == "IContainer" &&
  369. type.ContainingNamespace?.ToDisplayString() == "QuestPDF.Infrastructure";
  370. }
  371. private string ToSnakeCaseLower(string pascalCase)
  372. {
  373. if (string.IsNullOrEmpty(pascalCase))
  374. return pascalCase;
  375. var result = new StringBuilder();
  376. result.Append(char.ToLower(pascalCase[0]));
  377. for (int i = 1; i < pascalCase.Length; i++)
  378. {
  379. if (char.IsUpper(pascalCase[i]))
  380. {
  381. result.Append('_');
  382. result.Append(char.ToLower(pascalCase[i]));
  383. }
  384. else
  385. {
  386. result.Append(pascalCase[i]);
  387. }
  388. }
  389. return result.ToString();
  390. }
  391. private string ToPascalCase(string input)
  392. {
  393. if (string.IsNullOrEmpty(input))
  394. return input;
  395. return char.ToUpper(input[0]) + input.Substring(1);
  396. }
  397. }