using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; namespace QuestPDF.InteropGenerators; /// /// Generates native AOT/C ABI FFI code and Python bindings for QuestPDF IContainer extension methods. /// This enables fluent API usage in Python through C# interop. /// public class ContainerSourceGenerator : ISourceGenerator { /// /// Generates C# UnmanagedCallersOnly methods for native AOT compilation with C ABI compatibility. /// Each IContainer extension method gets a corresponding FFI wrapper. /// public string GenerateCSharpCode(INamespaceSymbol namespaceSymbol) { var containerMethods = FindContainerExtensionMethods(namespaceSymbol); if (!containerMethods.Any()) return "// No IContainer extension methods found"; var code = new StringBuilder(); // Generate header code.AppendLine("using System;"); code.AppendLine("using System.Runtime.InteropServices;"); code.AppendLine("using System.Runtime.CompilerServices;"); code.AppendLine("using QuestPDF.Infrastructure;"); code.AppendLine("using QuestPDF.Fluent;"); code.AppendLine(); code.AppendLine("namespace QuestPDF.InteropBindings"); code.AppendLine("{"); code.AppendLine(" /// "); code.AppendLine(" /// Native AOT FFI bindings for IContainer extension methods"); code.AppendLine(" /// "); code.AppendLine(" public static class ContainerInterop"); code.AppendLine(" {"); // Generate handle management code.AppendLine(" private static readonly Dictionary ContainerHandles = new();"); code.AppendLine(" private static IntPtr _nextHandle = (IntPtr)1;"); code.AppendLine(); code.AppendLine(" private static IntPtr AllocateHandle(IContainer container)"); code.AppendLine(" {"); code.AppendLine(" var handle = _nextHandle;"); code.AppendLine(" _nextHandle = (IntPtr)((long)_nextHandle + 1);"); code.AppendLine(" ContainerHandles[handle] = container;"); code.AppendLine(" return handle;"); code.AppendLine(" }"); code.AppendLine(); code.AppendLine(" private static IContainer GetContainer(IntPtr handle)"); code.AppendLine(" {"); code.AppendLine(" if (!ContainerHandles.TryGetValue(handle, out var container))"); code.AppendLine(" throw new InvalidOperationException($\"Invalid container handle: {handle}\");"); code.AppendLine(" return container;"); code.AppendLine(" }"); code.AppendLine(); code.AppendLine(" [UnmanagedCallersOnly(EntryPoint = \"container_release\")]"); code.AppendLine(" public static void ReleaseContainer(IntPtr handle)"); code.AppendLine(" {"); code.AppendLine(" ContainerHandles.Remove(handle);"); code.AppendLine(" }"); code.AppendLine(); // Generate FFI methods for each extension method foreach (var method in containerMethods) { GenerateCSharpMethod(code, method); } code.AppendLine(" }"); code.AppendLine("}"); return code.ToString(); } /// /// Generates Python bindings using CFFI for FFI calls to the C# native AOT library. /// Creates a Python Container class with fluent API methods. /// public string GeneratePythonCode(INamespaceSymbol namespaceSymbol) { var containerMethods = FindContainerExtensionMethods(namespaceSymbol); if (!containerMethods.Any()) return "# No IContainer extension methods found"; var code = new StringBuilder(); // Generate imports and setup code.AppendLine("from cffi import FFI"); code.AppendLine("from typing import Optional, Callable, Any"); code.AppendLine("from enum import IntEnum"); code.AppendLine(); code.AppendLine("# Initialize CFFI"); code.AppendLine("ffi = FFI()"); code.AppendLine(); code.AppendLine("# Define C function signatures"); code.AppendLine("ffi.cdef(\"\"\""); // Generate C function declarations for CFFI code.AppendLine(" void container_release(void* handle);"); foreach (var method in containerMethods) { GenerateCFFISignature(code, method); } code.AppendLine("\"\"\")"); code.AppendLine(); code.AppendLine("# Load the native library"); code.AppendLine("_lib = ffi.dlopen('./QuestPDF.Native.dll') # Adjust path as needed"); code.AppendLine(); code.AppendLine("class Container:"); code.AppendLine(" \"\"\""); code.AppendLine(" Represents a layout structure with exactly one child element."); code.AppendLine(" Provides fluent API for building QuestPDF documents."); code.AppendLine(" \"\"\""); code.AppendLine(); code.AppendLine(" def __init__(self, handle):"); code.AppendLine(" \"\"\"Initialize container with native handle\"\"\""); code.AppendLine(" self._handle = handle"); code.AppendLine(); code.AppendLine(" def __del__(self):"); code.AppendLine(" \"\"\"Release native resources\"\"\""); code.AppendLine(" if hasattr(self, '_handle') and self._handle:"); code.AppendLine(" _lib.container_release(self._handle)"); code.AppendLine(); code.AppendLine(" @property"); code.AppendLine(" def handle(self):"); code.AppendLine(" \"\"\"Get the native handle\"\"\""); code.AppendLine(" return self._handle"); code.AppendLine(); // Generate Python methods for each extension method foreach (var method in containerMethods) { GeneratePythonMethod(code, method); } return code.ToString(); } private List FindContainerExtensionMethods(INamespaceSymbol namespaceSymbol) { var methods = new List(); FindExtensionMethodsRecursive(namespaceSymbol, methods); return methods.Where(m => IsContainerExtensionMethod(m)).ToList(); } private void FindExtensionMethodsRecursive(INamespaceSymbol namespaceSymbol, List methods) { // Search in current namespace types foreach (var type in namespaceSymbol.GetTypeMembers()) { if (type.IsStatic) { foreach (var member in type.GetMembers().OfType()) { if (member.IsExtensionMethod) { methods.Add(member); } } } } // Recursively search child namespaces foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers()) { FindExtensionMethodsRecursive(childNamespace, methods); } } private bool IsContainerExtensionMethod(IMethodSymbol method) { if (!method.IsExtensionMethod) return false; var firstParam = method.Parameters.FirstOrDefault(); if (firstParam == null) return false; // Check if the first parameter is IContainer var paramType = firstParam.Type; return paramType.Name == "IContainer" && paramType.ContainingNamespace?.ToDisplayString() == "QuestPDF.Infrastructure"; } private void GenerateCSharpMethod(StringBuilder code, IMethodSymbol method) { var methodName = ToSnakeCaseLower(method.Name); var entryPoint = $"container_{methodName}"; code.AppendLine($" [UnmanagedCallersOnly(EntryPoint = \"{entryPoint}\")]"); // Generate method signature var returnType = method.ReturnsVoid ? "void" : "IntPtr"; code.Append($" public static {returnType} {ToPascalCase(method.Name)}(IntPtr containerHandle"); // Add parameters (skip the first one as it's the extension method's 'this' parameter) foreach (var param in method.Parameters.Skip(1)) { code.Append($", {GetCSharpFFIType(param.Type)} {param.Name}"); } code.AppendLine(")"); code.AppendLine(" {"); // Generate method body code.AppendLine(" try"); code.AppendLine(" {"); code.AppendLine(" var container = GetContainer(containerHandle);"); // Generate the actual method call var callParams = string.Join(", ", method.Parameters.Skip(1).Select(p => ConvertFromFFI(p))); if (method.ReturnsVoid) { code.AppendLine($" container.{method.Name}({callParams});"); } else if (IsContainerReturnType(method.ReturnType)) { code.AppendLine($" var result = container.{method.Name}({callParams});"); code.AppendLine(" return AllocateHandle(result);"); } else { code.AppendLine($" return container.{method.Name}({callParams});"); } code.AppendLine(" }"); code.AppendLine(" catch"); code.AppendLine(" {"); code.AppendLine(method.ReturnsVoid ? " return;" : " return IntPtr.Zero;"); code.AppendLine(" }"); code.AppendLine(" }"); code.AppendLine(); } private void GenerateCFFISignature(StringBuilder code, IMethodSymbol method) { var cFunctionName = $"container_{ToSnakeCaseLower(method.Name)}"; // Generate return type var returnType = method.ReturnsVoid ? "void" : GetCFFIType(method.ReturnType); code.Append($" {returnType} {cFunctionName}(void* handle"); // Add parameters (skip the first one as it's the extension method's 'this' parameter) foreach (var param in method.Parameters.Skip(1)) { code.Append($", {GetCFFIType(param.Type)} {ToSnakeCaseLower(param.Name)}"); } code.AppendLine(");"); } private void GeneratePythonMethod(StringBuilder code, IMethodSymbol method) { var pythonMethodName = ToSnakeCaseLower(method.Name); var doc = DocumentationHelper.ExtractDocumentation(method.GetDocumentationCommentXml()); // Generate method signature code.Append($" def {pythonMethodName}(self"); // Add parameters foreach (var param in method.Parameters.Skip(1)) { var paramName = ToSnakeCaseLower(param.Name); var pythonType = GetPythonType(param.Type); var defaultValue = GetPythonDefaultValue(param); code.Append($", {paramName}: {pythonType}{defaultValue}"); } code.AppendLine("):"); // Add docstring if (!string.IsNullOrEmpty(doc)) { code.AppendLine(" \"\"\""); code.AppendLine($" {doc}"); // Add parameter documentation if (method.Parameters.Length > 1) { code.AppendLine(); code.AppendLine(" Args:"); foreach (var param in method.Parameters.Skip(1)) { var paramName = ToSnakeCaseLower(param.Name); code.AppendLine($" {paramName}: {GetPythonType(param.Type)}"); } } // Add return documentation if (!method.ReturnsVoid && IsContainerReturnType(method.ReturnType)) { code.AppendLine(); code.AppendLine(" Returns:"); code.AppendLine(" Container: Self for method chaining"); } code.AppendLine(" \"\"\""); } // Generate method body var cFunctionName = $"container_{ToSnakeCaseLower(method.Name)}"; var callParams = "self._handle"; foreach (var param in method.Parameters.Skip(1)) { var paramName = ToSnakeCaseLower(param.Name); callParams += $", {ConvertToCFFI(param, paramName)}"; } if (method.ReturnsVoid) { code.AppendLine($" _lib.{cFunctionName}({callParams})"); code.AppendLine(" return self"); } else if (IsContainerReturnType(method.ReturnType)) { code.AppendLine($" new_handle = _lib.{cFunctionName}({callParams})"); code.AppendLine(" if new_handle != ffi.NULL:"); code.AppendLine(" return Container(new_handle)"); code.AppendLine(" return self"); } else { code.AppendLine($" return _lib.{cFunctionName}({callParams})"); } code.AppendLine(); } private string GetCFFIType(ITypeSymbol type) { if (IsContainerReturnType(type)) return "void*"; return type.SpecialType switch { SpecialType.System_Boolean => "bool", SpecialType.System_Int32 => "int", SpecialType.System_Single => "float", SpecialType.System_Double => "double", SpecialType.System_String => "char*", _ when type.TypeKind == TypeKind.Enum => "int", _ => "void*" }; } private string GetCSharpFFIType(ITypeSymbol type) { return type.SpecialType switch { SpecialType.System_Boolean => "bool", SpecialType.System_Int32 => "int", SpecialType.System_Single => "float", SpecialType.System_Double => "double", SpecialType.System_String => "IntPtr", // Marshalled as char* _ when type.TypeKind == TypeKind.Enum => "int", _ => "IntPtr" }; } private string GetPythonType(ITypeSymbol type) { return type.SpecialType switch { SpecialType.System_Boolean => "bool", SpecialType.System_Int32 => "int", SpecialType.System_Single => "float", SpecialType.System_Double => "float", SpecialType.System_String => "str", _ when type.TypeKind == TypeKind.Enum => "int", _ when type.Name == "Action" || type.Name == "Func" => "Callable", _ => "Any" }; } private string ConvertToCFFI(IParameterSymbol param, string paramName) { if (param.Type.SpecialType == SpecialType.System_String) { return $"{paramName}.encode('utf-8') if isinstance({paramName}, str) else {paramName}"; } return paramName; } private string GetPythonDefaultValue(IParameterSymbol param) { if (!param.HasExplicitDefaultValue) return ""; if (param.ExplicitDefaultValue == null) return " = None"; return param.Type.SpecialType switch { SpecialType.System_Boolean => $" = {param.ExplicitDefaultValue.ToString().ToLower()}", SpecialType.System_Int32 or SpecialType.System_Single or SpecialType.System_Double => $" = {param.ExplicitDefaultValue}", SpecialType.System_String => $" = \"{param.ExplicitDefaultValue}\"", _ => "" }; } private string ConvertFromFFI(IParameterSymbol param) { if (param.Type.SpecialType == SpecialType.System_String) { return $"Marshal.PtrToStringUTF8({param.Name})"; } if (param.Type.TypeKind == TypeKind.Enum) { return $"({param.Type.Name}){param.Name}"; } return param.Name; } private bool IsContainerReturnType(ITypeSymbol type) { return type.Name == "IContainer" && type.ContainingNamespace?.ToDisplayString() == "QuestPDF.Infrastructure"; } private string ToSnakeCaseLower(string pascalCase) { if (string.IsNullOrEmpty(pascalCase)) return pascalCase; var result = new StringBuilder(); result.Append(char.ToLower(pascalCase[0])); for (int i = 1; i < pascalCase.Length; i++) { if (char.IsUpper(pascalCase[i])) { result.Append('_'); result.Append(char.ToLower(pascalCase[i])); } else { result.Append(pascalCase[i]); } } return result.ToString(); } private string ToPascalCase(string input) { if (string.IsNullOrEmpty(input)) return input; return char.ToUpper(input[0]) + input.Substring(1); } }