PythonBindingsGenerator.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. sb.AppendLine("# Auto-generated Python bindings for QuestPDF");
  18. sb.AppendLine("# This file provides ctypes-based wrapper for the QuestPDF interop layer");
  19. sb.AppendLine();
  20. sb.AppendLine("import ctypes");
  21. sb.AppendLine("import platform");
  22. sb.AppendLine("from typing import Optional");
  23. sb.AppendLine("from pathlib import Path");
  24. sb.AppendLine();
  25. sb.AppendLine();
  26. sb.AppendLine("class QuestPDFException(Exception):");
  27. sb.AppendLine(" \"\"\"Base exception for QuestPDF errors\"\"\"");
  28. sb.AppendLine(" pass");
  29. sb.AppendLine();
  30. sb.AppendLine();
  31. sb.AppendLine("class QuestPDFLibrary:");
  32. sb.AppendLine(" \"\"\"Wrapper for QuestPDF native library\"\"\"");
  33. sb.AppendLine(" ");
  34. sb.AppendLine(" def __init__(self, library_path: Optional[str] = None):");
  35. sb.AppendLine(" \"\"\"");
  36. sb.AppendLine(" Initialize QuestPDF library.");
  37. sb.AppendLine(" ");
  38. sb.AppendLine(" Args:");
  39. sb.AppendLine(" library_path: Path to the native library. If None, will search in standard locations.");
  40. sb.AppendLine(" \"\"\"");
  41. sb.AppendLine(" if library_path is None:");
  42. sb.AppendLine(" library_path = self._find_library()");
  43. sb.AppendLine(" ");
  44. sb.AppendLine(" self._lib = ctypes.CDLL(library_path)");
  45. sb.AppendLine(" self._setup_functions()");
  46. sb.AppendLine(" ");
  47. sb.AppendLine(" @staticmethod");
  48. sb.AppendLine(" def _find_library() -> str:");
  49. sb.AppendLine(" \"\"\"Find the QuestPDF library in standard locations\"\"\"");
  50. sb.AppendLine(" system = platform.system()");
  51. sb.AppendLine(" ");
  52. sb.AppendLine(" if system == 'Windows':");
  53. sb.AppendLine(" lib_name = 'QuestPDF.dll'");
  54. sb.AppendLine(" elif system == 'Darwin':");
  55. sb.AppendLine(" lib_name = 'QuestPDF.dylib'");
  56. sb.AppendLine(" else:");
  57. sb.AppendLine(" lib_name = 'QuestPDF.so'");
  58. sb.AppendLine(" ");
  59. sb.AppendLine(" # Search in common locations");
  60. sb.AppendLine(" search_paths = [");
  61. sb.AppendLine(" Path.cwd() / lib_name,");
  62. sb.AppendLine(" Path(__file__).parent / lib_name,");
  63. sb.AppendLine(" Path(__file__).parent / 'bin' / lib_name,");
  64. sb.AppendLine(" ]");
  65. sb.AppendLine(" ");
  66. sb.AppendLine(" for path in search_paths:");
  67. sb.AppendLine(" if path.exists():");
  68. sb.AppendLine(" return str(path)");
  69. sb.AppendLine(" ");
  70. sb.AppendLine(" raise QuestPDFException(f'Could not find {lib_name} in any standard location')");
  71. sb.AppendLine(" ");
  72. sb.AppendLine(" def _setup_functions(self):");
  73. sb.AppendLine(" \"\"\"Setup function signatures for all exported functions\"\"\"");
  74. sb.AppendLine(" ");
  75. sb.AppendLine(" # Setup free_handle");
  76. sb.AppendLine(" self._lib.questpdf_free_handle.argtypes = [ctypes.c_void_p]");
  77. sb.AppendLine(" self._lib.questpdf_free_handle.restype = None");
  78. sb.AppendLine();
  79. // Generate function setup for each method
  80. foreach (var method in extensionMethods)
  81. {
  82. if (!PublicApiAnalyzer.IsSupported(method))
  83. continue;
  84. GeneratePythonFunctionSetup(sb, method);
  85. }
  86. sb.AppendLine();
  87. sb.AppendLine(" def free_handle(self, handle: int):");
  88. sb.AppendLine(" \"\"\"Free a handle to a managed object\"\"\"");
  89. sb.AppendLine(" if handle != 0:");
  90. sb.AppendLine(" self._lib.questpdf_free_handle(handle)");
  91. sb.AppendLine();
  92. // Generate Python wrapper methods
  93. foreach (var method in extensionMethods)
  94. {
  95. if (!PublicApiAnalyzer.IsSupported(method))
  96. continue;
  97. GeneratePythonWrapperMethod(sb, method);
  98. }
  99. sb.AppendLine();
  100. sb.AppendLine();
  101. sb.AppendLine("class Handle:");
  102. sb.AppendLine(" \"\"\"Wrapper for a managed object handle with automatic cleanup\"\"\"");
  103. sb.AppendLine(" ");
  104. sb.AppendLine(" def __init__(self, lib: QuestPDFLibrary, handle: int):");
  105. sb.AppendLine(" self._lib = lib");
  106. sb.AppendLine(" self._handle = handle");
  107. sb.AppendLine(" ");
  108. sb.AppendLine(" @property");
  109. sb.AppendLine(" def value(self) -> int:");
  110. sb.AppendLine(" return self._handle");
  111. sb.AppendLine(" ");
  112. sb.AppendLine(" def __del__(self):");
  113. sb.AppendLine(" if hasattr(self, '_handle') and self._handle != 0:");
  114. sb.AppendLine(" try:");
  115. sb.AppendLine(" self._lib.free_handle(self._handle)");
  116. sb.AppendLine(" except:");
  117. sb.AppendLine(" pass # Ignore errors during cleanup");
  118. sb.AppendLine(" ");
  119. sb.AppendLine(" def __enter__(self):");
  120. sb.AppendLine(" return self");
  121. sb.AppendLine(" ");
  122. sb.AppendLine(" def __exit__(self, exc_type, exc_val, exc_tb):");
  123. sb.AppendLine(" self._lib.free_handle(self._handle)");
  124. sb.AppendLine(" self._handle = 0");
  125. return sb.ToString();
  126. }
  127. private static void GeneratePythonFunctionSetup(StringBuilder sb, IMethodSymbol method)
  128. {
  129. var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method);
  130. sb.AppendLine($" # {method.ContainingType.Name}.{method.Name}");
  131. sb.Append($" self._lib.{entryPoint}.argtypes = [");
  132. var argTypes = new List<string>();
  133. foreach (var param in method.Parameters)
  134. {
  135. argTypes.Add(GetPythonCType(param.Type));
  136. }
  137. sb.Append(string.Join(", ", argTypes));
  138. sb.AppendLine("]");
  139. sb.AppendLine($" self._lib.{entryPoint}.restype = {GetPythonCType(method.ReturnType)}");
  140. sb.AppendLine();
  141. }
  142. private static void GeneratePythonWrapperMethod(StringBuilder sb, IMethodSymbol method)
  143. {
  144. var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method);
  145. var pythonName = ToPythonMethodName(method.Name);
  146. // Build parameter list
  147. var parameters = new List<string> { "self" };
  148. foreach (var param in method.Parameters)
  149. {
  150. var paramName = ToPythonParamName(param.Name);
  151. var pythonType = GetPythonTypeHint(param.Type);
  152. parameters.Add($"{paramName}: {pythonType}");
  153. }
  154. var returnTypeHint = GetPythonTypeHint(method.ReturnType);
  155. sb.AppendLine($" def {pythonName}({string.Join(", ", parameters)}) -> {returnTypeHint}:");
  156. sb.AppendLine($" \"\"\"");
  157. sb.AppendLine($" {method.ContainingType.Name}.{method.Name}");
  158. sb.AppendLine($" \"\"\"");
  159. // Build argument list for the call
  160. var callArgs = new List<string>();
  161. foreach (var param in method.Parameters)
  162. {
  163. var paramName = ToPythonParamName(param.Name);
  164. if (PublicApiAnalyzer.IsReferenceType(param.Type))
  165. {
  166. callArgs.Add($"{paramName}.value if isinstance({paramName}, Handle) else {paramName}");
  167. }
  168. else
  169. {
  170. callArgs.Add(paramName);
  171. }
  172. }
  173. if (method.ReturnsVoid)
  174. {
  175. sb.AppendLine($" self._lib.{entryPoint}({string.Join(", ", callArgs)})");
  176. }
  177. else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType))
  178. {
  179. sb.AppendLine($" result = self._lib.{entryPoint}({string.Join(", ", callArgs)})");
  180. sb.AppendLine($" return Handle(self, result)");
  181. }
  182. else
  183. {
  184. sb.AppendLine($" return self._lib.{entryPoint}({string.Join(", ", callArgs)})");
  185. }
  186. sb.AppendLine();
  187. }
  188. private static string GetPythonCType(ITypeSymbol type)
  189. {
  190. if (type.SpecialType == SpecialType.System_Void)
  191. return "None";
  192. if (PublicApiAnalyzer.IsReferenceType(type))
  193. return "ctypes.c_void_p";
  194. return type.SpecialType switch
  195. {
  196. SpecialType.System_Boolean => "ctypes.c_bool",
  197. SpecialType.System_Byte => "ctypes.c_uint8",
  198. SpecialType.System_SByte => "ctypes.c_int8",
  199. SpecialType.System_Int16 => "ctypes.c_int16",
  200. SpecialType.System_UInt16 => "ctypes.c_uint16",
  201. SpecialType.System_Int32 => "ctypes.c_int32",
  202. SpecialType.System_UInt32 => "ctypes.c_uint32",
  203. SpecialType.System_Int64 => "ctypes.c_int64",
  204. SpecialType.System_UInt64 => "ctypes.c_uint64",
  205. SpecialType.System_Single => "ctypes.c_float",
  206. SpecialType.System_Double => "ctypes.c_double",
  207. SpecialType.System_IntPtr => "ctypes.c_void_p",
  208. SpecialType.System_UIntPtr => "ctypes.c_void_p",
  209. _ => "ctypes.c_void_p"
  210. };
  211. }
  212. private static string GetPythonTypeHint(ITypeSymbol type)
  213. {
  214. if (type.SpecialType == SpecialType.System_Void)
  215. return "None";
  216. if (PublicApiAnalyzer.IsReferenceType(type))
  217. return "Handle";
  218. return type.SpecialType switch
  219. {
  220. SpecialType.System_Boolean => "bool",
  221. SpecialType.System_Byte => "int",
  222. SpecialType.System_SByte => "int",
  223. SpecialType.System_Int16 => "int",
  224. SpecialType.System_UInt16 => "int",
  225. SpecialType.System_Int32 => "int",
  226. SpecialType.System_UInt32 => "int",
  227. SpecialType.System_Int64 => "int",
  228. SpecialType.System_UInt64 => "int",
  229. SpecialType.System_Single => "float",
  230. SpecialType.System_Double => "float",
  231. SpecialType.System_IntPtr => "int",
  232. SpecialType.System_UIntPtr => "int",
  233. _ => "int"
  234. };
  235. }
  236. private static string ToPythonMethodName(string name)
  237. {
  238. // Convert PascalCase to snake_case
  239. var result = new StringBuilder();
  240. for (int i = 0; i < name.Length; i++)
  241. {
  242. if (i > 0 && char.IsUpper(name[i]) && !char.IsUpper(name[i - 1]))
  243. result.Append('_');
  244. result.Append(char.ToLowerInvariant(name[i]));
  245. }
  246. return result.ToString();
  247. }
  248. private static string ToPythonParamName(string name)
  249. {
  250. // Convert to snake_case and avoid Python keywords
  251. var result = ToPythonMethodName(name);
  252. // Avoid Python keywords
  253. if (result == "class" || result == "from" || result == "import" || result == "def" ||
  254. result == "return" || result == "if" || result == "else" || result == "for" ||
  255. result == "while" || result == "try" || result == "except" || result == "with")
  256. {
  257. result += "_";
  258. }
  259. return result;
  260. }
  261. }