PythonBindingsGenerator.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Text;
  4. using Microsoft.CodeAnalysis;
  5. using Scriban;
  6. namespace QuestPDF.InteropGenerators;
  7. /// <summary>
  8. /// Generates Python ctypes bindings for interop using Scriban templates
  9. /// </summary>
  10. public static class PythonBindingsGenerator
  11. {
  12. /// <summary>
  13. /// Generates the complete Python bindings code
  14. /// </summary>
  15. private const string PythonTemplate = @"{{ header }}{{ library }}{{ wrappers }}";
  16. public static string GeneratePythonBindings(List<IMethodSymbol> extensionMethods)
  17. {
  18. // Group methods by their containing type
  19. var methodsByType = GroupMethodsByType(extensionMethods);
  20. // Build pieces using existing logic
  21. var header = GeneratePythonHeaderText();
  22. var library = GenerateLibraryClassText(extensionMethods);
  23. var wrappers = GeneratePythonWrapperClassesText(methodsByType);
  24. // Render via Scriban
  25. var template = Template.Parse(PythonTemplate);
  26. var rendered = template.Render(new { header, library, wrappers });
  27. // Comment out all lines with "//" to avoid C# compilation issues
  28. return CommentOutPythonCode(rendered);
  29. }
  30. /// <summary>
  31. /// Comments out all Python code lines with "//" prefix to avoid C# compilation issues
  32. /// </summary>
  33. private static string CommentOutPythonCode(string pythonCode)
  34. {
  35. var lines = pythonCode.Split(new[] { "\r\n", "\r", "\n" }, System.StringSplitOptions.None);
  36. var commentedLines = lines.Select(line => "// " + line);
  37. return string.Join("\n", commentedLines);
  38. }
  39. private static string GeneratePythonHeaderText()
  40. {
  41. var sb = new StringBuilder();
  42. GeneratePythonHeader(sb);
  43. return sb.ToString();
  44. }
  45. private static string GenerateLibraryClassText(List<IMethodSymbol> extensionMethods)
  46. {
  47. var sb = new StringBuilder();
  48. GenerateLibraryClass(sb, extensionMethods);
  49. return sb.ToString();
  50. }
  51. private static string GeneratePythonWrapperClassesText(Dictionary<string, List<IMethodSymbol>> methodsByType)
  52. {
  53. var sb = new StringBuilder();
  54. GeneratePythonWrapperClasses(sb, methodsByType);
  55. return sb.ToString();
  56. }
  57. private static void GeneratePythonHeader(StringBuilder sb)
  58. {
  59. sb.AppendLine("# Auto-generated Python bindings for QuestPDF");
  60. sb.AppendLine("# This file provides ctypes-based wrapper for the QuestPDF interop layer");
  61. sb.AppendLine();
  62. sb.AppendLine("import ctypes");
  63. sb.AppendLine("import platform");
  64. sb.AppendLine("from typing import Optional, TYPE_CHECKING");
  65. sb.AppendLine("from pathlib import Path");
  66. sb.AppendLine();
  67. sb.AppendLine("if TYPE_CHECKING:");
  68. sb.AppendLine(" from typing import Any");
  69. sb.AppendLine();
  70. sb.AppendLine();
  71. sb.AppendLine("class QuestPDFException(Exception):");
  72. sb.AppendLine(" \"\"\"Base exception for QuestPDF errors\"\"\"");
  73. sb.AppendLine(" pass");
  74. sb.AppendLine();
  75. sb.AppendLine();
  76. }
  77. private static Dictionary<string, List<IMethodSymbol>> GroupMethodsByType(List<IMethodSymbol> methods)
  78. {
  79. var result = new Dictionary<string, List<IMethodSymbol>>();
  80. foreach (var method in methods)
  81. {
  82. if (!PublicApiAnalyzer.IsSupported(method))
  83. continue;
  84. // For extension methods, use the first parameter type (the extended type)
  85. // For instance methods, use the containing type
  86. string typeName;
  87. if (method.IsExtensionMethod)
  88. {
  89. typeName = method.Parameters[0].Type.Name;
  90. }
  91. else
  92. {
  93. typeName = method.ContainingType.Name;
  94. }
  95. if (!result.ContainsKey(typeName))
  96. result[typeName] = new List<IMethodSymbol>();
  97. result[typeName].Add(method);
  98. }
  99. return result;
  100. }
  101. private static void GenerateLibraryClass(StringBuilder sb, List<IMethodSymbol> extensionMethods)
  102. {
  103. sb.AppendLine("class QuestPDFLibrary:");
  104. sb.AppendLine(" \"\"\"Wrapper for QuestPDF native library\"\"\"");
  105. sb.AppendLine(" ");
  106. sb.AppendLine(" def __init__(self, library_path: Optional[str] = None):");
  107. sb.AppendLine(" \"\"\"");
  108. sb.AppendLine(" Initialize QuestPDF library.");
  109. sb.AppendLine(" ");
  110. sb.AppendLine(" Args:");
  111. sb.AppendLine(" library_path: Path to the native library. If None, will search in standard locations.");
  112. sb.AppendLine(" \"\"\"");
  113. sb.AppendLine(" if library_path is None:");
  114. sb.AppendLine(" library_path = self._find_library()");
  115. sb.AppendLine(" ");
  116. sb.AppendLine(" self._lib = ctypes.CDLL(library_path)");
  117. sb.AppendLine(" self._setup_functions()");
  118. sb.AppendLine(" ");
  119. sb.AppendLine(" @staticmethod");
  120. sb.AppendLine(" def _find_library() -> str:");
  121. sb.AppendLine(" \"\"\"Find the QuestPDF library in standard locations\"\"\"");
  122. sb.AppendLine(" system = platform.system()");
  123. sb.AppendLine(" ");
  124. sb.AppendLine(" if system == 'Windows':");
  125. sb.AppendLine(" lib_name = 'QuestPDF.dll'");
  126. sb.AppendLine(" elif system == 'Darwin':");
  127. sb.AppendLine(" lib_name = 'QuestPDF.dylib'");
  128. sb.AppendLine(" else:");
  129. sb.AppendLine(" lib_name = 'QuestPDF.so'");
  130. sb.AppendLine(" ");
  131. sb.AppendLine(" # Search in common locations");
  132. sb.AppendLine(" search_paths = [");
  133. sb.AppendLine(" Path.cwd() / lib_name,");
  134. sb.AppendLine(" Path(__file__).parent / lib_name,");
  135. sb.AppendLine(" Path(__file__).parent / 'bin' / lib_name,");
  136. sb.AppendLine(" ]");
  137. sb.AppendLine(" ");
  138. sb.AppendLine(" for path in search_paths:");
  139. sb.AppendLine(" if path.exists():");
  140. sb.AppendLine(" return str(path)");
  141. sb.AppendLine(" ");
  142. sb.AppendLine(" raise QuestPDFException(f'Could not find {lib_name} in any standard location')");
  143. sb.AppendLine(" ");
  144. sb.AppendLine(" def _setup_functions(self):");
  145. sb.AppendLine(" \"\"\"Setup function signatures for all exported functions\"\"\"");
  146. sb.AppendLine(" ");
  147. sb.AppendLine(" # Setup free_handle");
  148. sb.AppendLine(" self._lib.questpdf_free_handle.argtypes = [ctypes.c_void_p]");
  149. sb.AppendLine(" self._lib.questpdf_free_handle.restype = None");
  150. sb.AppendLine();
  151. // Generate function setup for each method
  152. foreach (var method in extensionMethods)
  153. {
  154. if (!PublicApiAnalyzer.IsSupported(method))
  155. continue;
  156. GeneratePythonFunctionSetup(sb, method);
  157. }
  158. sb.AppendLine();
  159. sb.AppendLine(" def free_handle(self, handle: int):");
  160. sb.AppendLine(" \"\"\"Free a handle to a managed object\"\"\"");
  161. sb.AppendLine(" if handle != 0:");
  162. sb.AppendLine(" self._lib.questpdf_free_handle(handle)");
  163. sb.AppendLine();
  164. sb.AppendLine();
  165. }
  166. private static void GeneratePythonWrapperClasses(StringBuilder sb, Dictionary<string, List<IMethodSymbol>> methodsByType)
  167. {
  168. // Sort types alphabetically for consistent output
  169. var sortedTypes = methodsByType.Keys.OrderBy(k => k).ToList();
  170. foreach (var typeName in sortedTypes)
  171. {
  172. var methods = methodsByType[typeName];
  173. var pythonClassName = ToPythonClassName(typeName);
  174. sb.AppendLine($"class {pythonClassName}:");
  175. sb.AppendLine($" \"\"\"Python wrapper for {typeName}\"\"\"");
  176. sb.AppendLine(" ");
  177. sb.AppendLine(" def __init__(self, lib: QuestPDFLibrary, handle: int):");
  178. sb.AppendLine(" self._lib = lib");
  179. sb.AppendLine(" self._handle = handle");
  180. sb.AppendLine(" ");
  181. sb.AppendLine(" @property");
  182. sb.AppendLine(" def handle(self) -> int:");
  183. sb.AppendLine(" \"\"\"Get the underlying native handle\"\"\"");
  184. sb.AppendLine(" return self._handle");
  185. sb.AppendLine(" ");
  186. sb.AppendLine(" def __del__(self):");
  187. sb.AppendLine(" if hasattr(self, '_handle') and self._handle != 0:");
  188. sb.AppendLine(" try:");
  189. sb.AppendLine(" self._lib.free_handle(self._handle)");
  190. sb.AppendLine(" except:");
  191. sb.AppendLine(" pass # Ignore errors during cleanup");
  192. sb.AppendLine(" ");
  193. sb.AppendLine(" def __enter__(self):");
  194. sb.AppendLine(" return self");
  195. sb.AppendLine(" ");
  196. sb.AppendLine(" def __exit__(self, exc_type, exc_val, exc_tb):");
  197. sb.AppendLine(" self._lib.free_handle(self._handle)");
  198. sb.AppendLine(" self._handle = 0");
  199. sb.AppendLine();
  200. // Generate methods for this class
  201. foreach (var method in methods.OrderBy(m => m.Name))
  202. {
  203. GeneratePythonClassMethod(sb, method, pythonClassName);
  204. }
  205. sb.AppendLine();
  206. }
  207. }
  208. private static void GeneratePythonFunctionSetup(StringBuilder sb, IMethodSymbol method)
  209. {
  210. var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method);
  211. var isInstanceMethod = !method.IsStatic && !method.IsExtensionMethod;
  212. sb.AppendLine($" # {method.ContainingType.Name}.{method.Name}");
  213. sb.Append($" self._lib.{entryPoint}.argtypes = [");
  214. var argTypes = new List<string>();
  215. // For instance methods, add 'this' parameter
  216. if (isInstanceMethod)
  217. {
  218. argTypes.Add("ctypes.c_void_p");
  219. }
  220. foreach (var param in method.Parameters)
  221. {
  222. argTypes.Add(GetPythonCType(param.Type));
  223. }
  224. sb.Append(string.Join(", ", argTypes));
  225. sb.AppendLine("]");
  226. sb.AppendLine($" self._lib.{entryPoint}.restype = {GetPythonCType(method.ReturnType)}");
  227. sb.AppendLine();
  228. }
  229. private static void GeneratePythonClassMethod(StringBuilder sb, IMethodSymbol method, string pythonClassName)
  230. {
  231. var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method);
  232. var pythonName = ToPythonMethodName(method.Name);
  233. var isExtensionMethod = method.IsExtensionMethod;
  234. // Build parameter list
  235. var parameters = new List<string> { "self" };
  236. // For extension methods, skip the first parameter (the extended type - that's 'self')
  237. var paramsToProcess = isExtensionMethod ? method.Parameters.Skip(1) : method.Parameters;
  238. foreach (var param in paramsToProcess)
  239. {
  240. var paramName = ToPythonParamName(param.Name);
  241. var pythonType = GetPythonTypeHint(param.Type, isParameter: true);
  242. parameters.Add($"{paramName}: {pythonType}");
  243. }
  244. var returnTypeHint = GetPythonTypeHint(method.ReturnType, isParameter: false);
  245. sb.AppendLine($" def {pythonName}({string.Join(", ", parameters)}) -> '{returnTypeHint}':");
  246. sb.AppendLine($" \"\"\"");
  247. sb.AppendLine($" {method.Name}");
  248. if (!string.IsNullOrEmpty(method.GetDocumentationCommentXml()))
  249. {
  250. // Could extract summary from XML here if needed
  251. }
  252. sb.AppendLine($" \"\"\"");
  253. // Build argument list for the call
  254. var callArgs = new List<string>();
  255. // Always pass 'self._handle' as the first argument (either for extension method or instance method)
  256. callArgs.Add("self._handle");
  257. foreach (var param in paramsToProcess)
  258. {
  259. var paramName = ToPythonParamName(param.Name);
  260. if (PublicApiAnalyzer.IsReferenceType(param.Type))
  261. {
  262. callArgs.Add($"{paramName}.handle if hasattr({paramName}, 'handle') else {paramName}");
  263. }
  264. else
  265. {
  266. callArgs.Add(paramName);
  267. }
  268. }
  269. if (method.ReturnsVoid)
  270. {
  271. sb.AppendLine($" self._lib._lib.{entryPoint}({string.Join(", ", callArgs)})");
  272. }
  273. else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType))
  274. {
  275. sb.AppendLine($" result = self._lib._lib.{entryPoint}({string.Join(", ", callArgs)})");
  276. var returnPythonClass = ToPythonClassName(method.ReturnType.Name);
  277. sb.AppendLine($" return {returnPythonClass}(self._lib, result)");
  278. }
  279. else
  280. {
  281. sb.AppendLine($" return self._lib._lib.{entryPoint}({string.Join(", ", callArgs)})");
  282. }
  283. sb.AppendLine();
  284. }
  285. private static string GetPythonCType(ITypeSymbol type)
  286. {
  287. if (type.SpecialType == SpecialType.System_Void)
  288. return "None";
  289. if (PublicApiAnalyzer.IsReferenceType(type))
  290. return "ctypes.c_void_p";
  291. return type.SpecialType switch
  292. {
  293. SpecialType.System_Boolean => "ctypes.c_bool",
  294. SpecialType.System_Byte => "ctypes.c_uint8",
  295. SpecialType.System_SByte => "ctypes.c_int8",
  296. SpecialType.System_Int16 => "ctypes.c_int16",
  297. SpecialType.System_UInt16 => "ctypes.c_uint16",
  298. SpecialType.System_Int32 => "ctypes.c_int32",
  299. SpecialType.System_UInt32 => "ctypes.c_uint32",
  300. SpecialType.System_Int64 => "ctypes.c_int64",
  301. SpecialType.System_UInt64 => "ctypes.c_uint64",
  302. SpecialType.System_Single => "ctypes.c_float",
  303. SpecialType.System_Double => "ctypes.c_double",
  304. SpecialType.System_IntPtr => "ctypes.c_void_p",
  305. SpecialType.System_UIntPtr => "ctypes.c_void_p",
  306. _ => "ctypes.c_void_p"
  307. };
  308. }
  309. private static string GetPythonTypeHint(ITypeSymbol type, bool isParameter)
  310. {
  311. if (type.SpecialType == SpecialType.System_Void)
  312. return "None";
  313. if (PublicApiAnalyzer.IsReferenceType(type))
  314. {
  315. // Return the appropriate Python class name for reference types
  316. return ToPythonClassName(type.Name);
  317. }
  318. return type.SpecialType switch
  319. {
  320. SpecialType.System_Boolean => "bool",
  321. SpecialType.System_Byte => "int",
  322. SpecialType.System_SByte => "int",
  323. SpecialType.System_Int16 => "int",
  324. SpecialType.System_UInt16 => "int",
  325. SpecialType.System_Int32 => "int",
  326. SpecialType.System_UInt32 => "int",
  327. SpecialType.System_Int64 => "int",
  328. SpecialType.System_UInt64 => "int",
  329. SpecialType.System_Single => "float",
  330. SpecialType.System_Double => "float",
  331. SpecialType.System_IntPtr => "int",
  332. SpecialType.System_UIntPtr => "int",
  333. _ => "int"
  334. };
  335. }
  336. private static string ToPythonClassName(string csharpTypeName)
  337. {
  338. // Convert C# type name to Python class name
  339. // Remove generic markers and sanitize
  340. var cleanName = csharpTypeName
  341. .Replace("`", "")
  342. .Replace("<", "_")
  343. .Replace(">", "_")
  344. .Replace(",", "_")
  345. .Trim('_');
  346. // Return as-is (PascalCase is acceptable in Python for class names)
  347. return cleanName;
  348. }
  349. private static string ToPythonMethodName(string name)
  350. {
  351. // Convert PascalCase to snake_case
  352. var result = new StringBuilder();
  353. for (int i = 0; i < name.Length; i++)
  354. {
  355. if (i > 0 && char.IsUpper(name[i]) && !char.IsUpper(name[i - 1]))
  356. result.Append('_');
  357. result.Append(char.ToLowerInvariant(name[i]));
  358. }
  359. return result.ToString();
  360. }
  361. private static string ToPythonParamName(string name)
  362. {
  363. // Convert to snake_case and avoid Python keywords
  364. var result = ToPythonMethodName(name);
  365. // Avoid Python keywords
  366. if (result == "class" || result == "from" || result == "import" || result == "def" ||
  367. result == "return" || result == "if" || result == "else" || result == "for" ||
  368. result == "while" || result == "try" || result == "except" || result == "with")
  369. {
  370. result += "_";
  371. }
  372. return result;
  373. }
  374. }