PythonBindingsGenerator.cs 16 KB

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