using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Scriban; namespace QuestPDF.InteropGenerators; /// /// Generates Python ctypes bindings for interop using Scriban templates /// public static class PythonBindingsGenerator { /// /// Generates the complete Python bindings code /// private const string PythonTemplate = @"{{ header }}{{ library }}{{ wrappers }}"; public static string GeneratePythonBindings(List extensionMethods) { // Group methods by their containing type var methodsByType = GroupMethodsByType(extensionMethods); // Build pieces using existing logic var header = GeneratePythonHeaderText(); var library = GenerateLibraryClassText(extensionMethods); var wrappers = GeneratePythonWrapperClassesText(methodsByType); // Render via Scriban var template = Template.Parse(PythonTemplate); var rendered = template.Render(new { header, library, wrappers }); // Comment out all lines with "//" to avoid C# compilation issues return CommentOutPythonCode(rendered); } /// /// Comments out all Python code lines with "//" prefix to avoid C# compilation issues /// private static string CommentOutPythonCode(string pythonCode) { var lines = pythonCode.Split(new[] { "\r\n", "\r", "\n" }, System.StringSplitOptions.None); var commentedLines = lines.Select(line => "// " + line); return string.Join("\n", commentedLines); } private static string GeneratePythonHeaderText() { var sb = new StringBuilder(); GeneratePythonHeader(sb); return sb.ToString(); } private static string GenerateLibraryClassText(List extensionMethods) { var sb = new StringBuilder(); GenerateLibraryClass(sb, extensionMethods); return sb.ToString(); } private static string GeneratePythonWrapperClassesText(Dictionary> methodsByType) { var sb = new StringBuilder(); GeneratePythonWrapperClasses(sb, methodsByType); return sb.ToString(); } private static void GeneratePythonHeader(StringBuilder sb) { sb.AppendLine("# Auto-generated Python bindings for QuestPDF"); sb.AppendLine("# This file provides ctypes-based wrapper for the QuestPDF interop layer"); sb.AppendLine(); sb.AppendLine("import ctypes"); sb.AppendLine("import platform"); sb.AppendLine("from typing import Optional, TYPE_CHECKING"); sb.AppendLine("from pathlib import Path"); sb.AppendLine(); sb.AppendLine("if TYPE_CHECKING:"); sb.AppendLine(" from typing import Any"); sb.AppendLine(); sb.AppendLine(); sb.AppendLine("class QuestPDFException(Exception):"); sb.AppendLine(" \"\"\"Base exception for QuestPDF errors\"\"\""); sb.AppendLine(" pass"); sb.AppendLine(); sb.AppendLine(); } private static Dictionary> GroupMethodsByType(List methods) { var result = new Dictionary>(); foreach (var method in methods) { if (!PublicApiAnalyzer.IsSupported(method)) continue; // For extension methods, use the first parameter type (the extended type) // For instance methods, use the containing type string typeName; if (method.IsExtensionMethod) { typeName = method.Parameters[0].Type.Name; } else { typeName = method.ContainingType.Name; } if (!result.ContainsKey(typeName)) result[typeName] = new List(); result[typeName].Add(method); } return result; } private static void GenerateLibraryClass(StringBuilder sb, List extensionMethods) { sb.AppendLine("class QuestPDFLibrary:"); sb.AppendLine(" \"\"\"Wrapper for QuestPDF native library\"\"\""); sb.AppendLine(" "); sb.AppendLine(" def __init__(self, library_path: Optional[str] = None):"); sb.AppendLine(" \"\"\""); sb.AppendLine(" Initialize QuestPDF library."); sb.AppendLine(" "); sb.AppendLine(" Args:"); sb.AppendLine(" library_path: Path to the native library. If None, will search in standard locations."); sb.AppendLine(" \"\"\""); sb.AppendLine(" if library_path is None:"); sb.AppendLine(" library_path = self._find_library()"); sb.AppendLine(" "); sb.AppendLine(" self._lib = ctypes.CDLL(library_path)"); sb.AppendLine(" self._setup_functions()"); sb.AppendLine(" "); sb.AppendLine(" @staticmethod"); sb.AppendLine(" def _find_library() -> str:"); sb.AppendLine(" \"\"\"Find the QuestPDF library in standard locations\"\"\""); sb.AppendLine(" system = platform.system()"); sb.AppendLine(" "); sb.AppendLine(" if system == 'Windows':"); sb.AppendLine(" lib_name = 'QuestPDF.dll'"); sb.AppendLine(" elif system == 'Darwin':"); sb.AppendLine(" lib_name = 'QuestPDF.dylib'"); sb.AppendLine(" else:"); sb.AppendLine(" lib_name = 'QuestPDF.so'"); sb.AppendLine(" "); sb.AppendLine(" # Search in common locations"); sb.AppendLine(" search_paths = ["); sb.AppendLine(" Path.cwd() / lib_name,"); sb.AppendLine(" Path(__file__).parent / lib_name,"); sb.AppendLine(" Path(__file__).parent / 'bin' / lib_name,"); sb.AppendLine(" ]"); sb.AppendLine(" "); sb.AppendLine(" for path in search_paths:"); sb.AppendLine(" if path.exists():"); sb.AppendLine(" return str(path)"); sb.AppendLine(" "); sb.AppendLine(" raise QuestPDFException(f'Could not find {lib_name} in any standard location')"); sb.AppendLine(" "); sb.AppendLine(" def _setup_functions(self):"); sb.AppendLine(" \"\"\"Setup function signatures for all exported functions\"\"\""); sb.AppendLine(" "); sb.AppendLine(" # Setup free_handle"); sb.AppendLine(" self._lib.questpdf_free_handle.argtypes = [ctypes.c_void_p]"); sb.AppendLine(" self._lib.questpdf_free_handle.restype = None"); sb.AppendLine(); // Generate function setup for each method foreach (var method in extensionMethods) { if (!PublicApiAnalyzer.IsSupported(method)) continue; GeneratePythonFunctionSetup(sb, method); } sb.AppendLine(); sb.AppendLine(" def free_handle(self, handle: int):"); sb.AppendLine(" \"\"\"Free a handle to a managed object\"\"\""); sb.AppendLine(" if handle != 0:"); sb.AppendLine(" self._lib.questpdf_free_handle(handle)"); sb.AppendLine(); sb.AppendLine(); } private static void GeneratePythonWrapperClasses(StringBuilder sb, Dictionary> methodsByType) { // Sort types alphabetically for consistent output var sortedTypes = methodsByType.Keys.OrderBy(k => k).ToList(); foreach (var typeName in sortedTypes) { var methods = methodsByType[typeName]; var pythonClassName = ToPythonClassName(typeName); sb.AppendLine($"class {pythonClassName}:"); sb.AppendLine($" \"\"\"Python wrapper for {typeName}\"\"\""); sb.AppendLine(" "); sb.AppendLine(" def __init__(self, lib: QuestPDFLibrary, handle: int):"); sb.AppendLine(" self._lib = lib"); sb.AppendLine(" self._handle = handle"); sb.AppendLine(" "); sb.AppendLine(" @property"); sb.AppendLine(" def handle(self) -> int:"); sb.AppendLine(" \"\"\"Get the underlying native handle\"\"\""); sb.AppendLine(" return self._handle"); sb.AppendLine(" "); sb.AppendLine(" def __del__(self):"); sb.AppendLine(" if hasattr(self, '_handle') and self._handle != 0:"); sb.AppendLine(" try:"); sb.AppendLine(" self._lib.free_handle(self._handle)"); sb.AppendLine(" except:"); sb.AppendLine(" pass # Ignore errors during cleanup"); sb.AppendLine(" "); sb.AppendLine(" def __enter__(self):"); sb.AppendLine(" return self"); sb.AppendLine(" "); sb.AppendLine(" def __exit__(self, exc_type, exc_val, exc_tb):"); sb.AppendLine(" self._lib.free_handle(self._handle)"); sb.AppendLine(" self._handle = 0"); sb.AppendLine(); // Generate methods for this class foreach (var method in methods.OrderBy(m => m.Name)) { GeneratePythonClassMethod(sb, method, pythonClassName); } sb.AppendLine(); } } private static void GeneratePythonFunctionSetup(StringBuilder sb, IMethodSymbol method) { var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method); var isInstanceMethod = !method.IsStatic && !method.IsExtensionMethod; sb.AppendLine($" # {method.ContainingType.Name}.{method.Name}"); sb.Append($" self._lib.{entryPoint}.argtypes = ["); var argTypes = new List(); // For instance methods, add 'this' parameter if (isInstanceMethod) { argTypes.Add("ctypes.c_void_p"); } foreach (var param in method.Parameters) { argTypes.Add(GetPythonCType(param.Type)); } sb.Append(string.Join(", ", argTypes)); sb.AppendLine("]"); sb.AppendLine($" self._lib.{entryPoint}.restype = {GetPythonCType(method.ReturnType)}"); sb.AppendLine(); } private static void GeneratePythonClassMethod(StringBuilder sb, IMethodSymbol method, string pythonClassName) { var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method); var pythonName = ToPythonMethodName(method.Name); var isExtensionMethod = method.IsExtensionMethod; // Build parameter list var parameters = new List { "self" }; // For extension methods, skip the first parameter (the extended type - that's 'self') var paramsToProcess = isExtensionMethod ? method.Parameters.Skip(1) : method.Parameters; foreach (var param in paramsToProcess) { var paramName = ToPythonParamName(param.Name); var pythonType = GetPythonTypeHint(param.Type, isParameter: true); parameters.Add($"{paramName}: {pythonType}"); } var returnTypeHint = GetPythonTypeHint(method.ReturnType, isParameter: false); sb.AppendLine($" def {pythonName}({string.Join(", ", parameters)}) -> '{returnTypeHint}':"); sb.AppendLine($" \"\"\""); sb.AppendLine($" {method.Name}"); if (!string.IsNullOrEmpty(method.GetDocumentationCommentXml())) { // Could extract summary from XML here if needed } sb.AppendLine($" \"\"\""); // Build argument list for the call var callArgs = new List(); // Always pass 'self._handle' as the first argument (either for extension method or instance method) callArgs.Add("self._handle"); foreach (var param in paramsToProcess) { var paramName = ToPythonParamName(param.Name); if (PublicApiAnalyzer.IsReferenceType(param.Type)) { callArgs.Add($"{paramName}.handle if hasattr({paramName}, 'handle') else {paramName}"); } else { callArgs.Add(paramName); } } if (method.ReturnsVoid) { sb.AppendLine($" self._lib._lib.{entryPoint}({string.Join(", ", callArgs)})"); } else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType)) { sb.AppendLine($" result = self._lib._lib.{entryPoint}({string.Join(", ", callArgs)})"); var returnPythonClass = ToPythonClassName(method.ReturnType.Name); sb.AppendLine($" return {returnPythonClass}(self._lib, result)"); } else { sb.AppendLine($" return self._lib._lib.{entryPoint}({string.Join(", ", callArgs)})"); } sb.AppendLine(); } private static string GetPythonCType(ITypeSymbol type) { if (type.SpecialType == SpecialType.System_Void) return "None"; if (PublicApiAnalyzer.IsReferenceType(type)) return "ctypes.c_void_p"; return type.SpecialType switch { SpecialType.System_Boolean => "ctypes.c_bool", SpecialType.System_Byte => "ctypes.c_uint8", SpecialType.System_SByte => "ctypes.c_int8", SpecialType.System_Int16 => "ctypes.c_int16", SpecialType.System_UInt16 => "ctypes.c_uint16", SpecialType.System_Int32 => "ctypes.c_int32", SpecialType.System_UInt32 => "ctypes.c_uint32", SpecialType.System_Int64 => "ctypes.c_int64", SpecialType.System_UInt64 => "ctypes.c_uint64", SpecialType.System_Single => "ctypes.c_float", SpecialType.System_Double => "ctypes.c_double", SpecialType.System_IntPtr => "ctypes.c_void_p", SpecialType.System_UIntPtr => "ctypes.c_void_p", _ => "ctypes.c_void_p" }; } private static string GetPythonTypeHint(ITypeSymbol type, bool isParameter) { if (type.SpecialType == SpecialType.System_Void) return "None"; if (PublicApiAnalyzer.IsReferenceType(type)) { // Return the appropriate Python class name for reference types return ToPythonClassName(type.Name); } return type.SpecialType switch { SpecialType.System_Boolean => "bool", SpecialType.System_Byte => "int", SpecialType.System_SByte => "int", SpecialType.System_Int16 => "int", SpecialType.System_UInt16 => "int", SpecialType.System_Int32 => "int", SpecialType.System_UInt32 => "int", SpecialType.System_Int64 => "int", SpecialType.System_UInt64 => "int", SpecialType.System_Single => "float", SpecialType.System_Double => "float", SpecialType.System_IntPtr => "int", SpecialType.System_UIntPtr => "int", _ => "int" }; } private static string ToPythonClassName(string csharpTypeName) { // Convert C# type name to Python class name // Remove generic markers and sanitize var cleanName = csharpTypeName .Replace("`", "") .Replace("<", "_") .Replace(">", "_") .Replace(",", "_") .Trim('_'); // Return as-is (PascalCase is acceptable in Python for class names) return cleanName; } private static string ToPythonMethodName(string name) { // Convert PascalCase to snake_case var result = new StringBuilder(); for (int i = 0; i < name.Length; i++) { if (i > 0 && char.IsUpper(name[i]) && !char.IsUpper(name[i - 1])) result.Append('_'); result.Append(char.ToLowerInvariant(name[i])); } return result.ToString(); } private static string ToPythonParamName(string name) { // Convert to snake_case and avoid Python keywords var result = ToPythonMethodName(name); // Avoid Python keywords if (result == "class" || result == "from" || result == "import" || result == "def" || result == "return" || result == "if" || result == "else" || result == "for" || result == "while" || result == "try" || result == "except" || result == "with") { result += "_"; } return result; } }