瀏覽代碼

Simple roslyn analyzer/generator

Marcin Ziąbek 1 月之前
父節點
當前提交
bced98918e

+ 0 - 92
Source/QuestPDF.InteropGenerators/AutoBuilderGenerator.cs

@@ -1,92 +0,0 @@
-using System.Diagnostics;
-using System.Text;
-using System.Threading;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-namespace QuestPDF.InteropGenerators;
-
-[Generator]
-public sealed class AutoBuilderGenerator : IIncrementalGenerator
-{
-    public void Initialize(IncrementalGeneratorInitializationContext context)
-    {
-        if (!Debugger.IsAttached)
-            Debugger.Launch();
-        
-        while (!Debugger.IsAttached)
-            Thread.Sleep(100);
-        
-        Debugger.Break();
-        
-        
-        // 1) Find candidate classes with [AutoBuilder]
-        var classesWithAttribute = context.SyntaxProvider
-            .CreateSyntaxProvider(
-                predicate: static (node, _) =>
-                    node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0,
-                transform: static (ctx, _) =>
-                {
-                    var cds = (ClassDeclarationSyntax)ctx.Node;
-                    foreach (var list in cds.AttributeLists)
-                    foreach (var attr in list.Attributes)
-                    {
-                        var model = ctx.SemanticModel;
-                        var symbolInfo = model.GetSymbolInfo(attr).Symbol;
-                        var attrType = symbolInfo switch
-                        {
-                            IMethodSymbol ms when ms.ContainingType is not null => ms.ContainingType,
-                            _ => model.GetTypeInfo(attr).Type
-                        };
-
-                        if (attrType?.ToDisplayString()?.Contains("AutoBuilder") ?? false)
-                            return cds;
-                    }
-                    return null;
-                })
-            .Where(static cds => cds is not null)!;
-
-        // 2) Combine with compilation (to resolve symbols)
-        var compilationAndClasses = context.CompilationProvider
-            .Combine(classesWithAttribute.Collect());
-
-        // 3) Generate
-        context.RegisterSourceOutput(compilationAndClasses, static (spc, pair) =>
-        {
-            var (compilation, classDecls) = (pair.Left, pair.Right);
-            foreach (var cds in classDecls!)
-            {
-                var model = compilation.GetSemanticModel(cds.SyntaxTree);
-                if (model.GetDeclaredSymbol(cds) is not INamedTypeSymbol classSymbol)
-                    continue;
-
-                var ns = classSymbol.ContainingNamespace.IsGlobalNamespace
-                            ? null
-                            : classSymbol.ContainingNamespace.ToDisplayString();
-
-                var className = classSymbol.Name;
-                var builderName = className + "Builder";
-
-                var sb = new StringBuilder();
-                sb.AppendLine("// <auto-generated/>");
-                if (ns is not null) sb.AppendLine($"namespace {ns};");
-                sb.AppendLine($"public partial class {builderName}");
-                sb.AppendLine("{");
-                sb.AppendLine($"    private {className} _instance = new();");
-
-                foreach (var m in classSymbol.GetMembers())
-                {
-                    if (m is IPropertySymbol p && !p.IsReadOnly && p.SetMethod is not null)
-                    {
-                        sb.AppendLine($"    public {builderName} With{p.Name}({p.Type.ToDisplayString()} value) {{ _instance.{p.Name} = value; return this; }}");
-                    }
-                }
-
-                sb.AppendLine($"    public {className} Build() => _instance;");
-                sb.AppendLine("}");
-
-                spc.AddSource($"{builderName}.g.cs", sb.ToString());
-            }
-        });
-    }
-}

+ 188 - 0
Source/QuestPDF.InteropGenerators/CSharpInteropGenerator.cs

@@ -0,0 +1,188 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Generates C# UnmanagedCallersOnly bindings for interop
+/// </summary>
+public static class CSharpInteropGenerator
+{
+    /// <summary>
+    /// Generates the complete C# interop code
+    /// </summary>
+    public static string GenerateInteropCode(List<IMethodSymbol> extensionMethods)
+    {
+        var sb = new StringBuilder();
+        
+        sb.AppendLine("// <auto-generated/>");
+        sb.AppendLine("#nullable enable");
+        sb.AppendLine();
+        sb.AppendLine("using System;");
+        sb.AppendLine("using System.Runtime.CompilerServices;");
+        sb.AppendLine("using System.Runtime.InteropServices;");
+        sb.AppendLine();
+        sb.AppendLine("namespace QuestPDF.Generated;");
+        sb.AppendLine();
+        sb.AppendLine("public static unsafe class GeneratedInterop");
+        sb.AppendLine("{");
+        
+        // Add helper methods
+        GenerateHelperMethods(sb);
+        
+        foreach (var method in extensionMethods)
+        {
+            GenerateInteropMethod(sb, method);
+        }
+        
+        sb.AppendLine("}");
+        
+        return sb.ToString();
+    }
+    
+    private static void GenerateHelperMethods(StringBuilder sb)
+    {
+        sb.AppendLine("    static IntPtr BoxHandle(object obj)");
+        sb.AppendLine("    {");
+        sb.AppendLine("        var gch = GCHandle.Alloc(obj, GCHandleType.Normal);");
+        sb.AppendLine("        return GCHandle.ToIntPtr(gch);");
+        sb.AppendLine("    }");
+        sb.AppendLine();
+        sb.AppendLine("    static T UnboxHandle<T>(nint handle) where T : class");
+        sb.AppendLine("    {");
+        sb.AppendLine("        var gch = GCHandle.FromIntPtr(handle);");
+        sb.AppendLine("        return (T)gch.Target!;");
+        sb.AppendLine("    }");
+        sb.AppendLine();
+        sb.AppendLine("    [UnmanagedCallersOnly(EntryPoint = \"questpdf_free_handle\", CallConvs = new[] { typeof(CallConvCdecl) })]");
+        sb.AppendLine("    public static void FreeHandle(nint handle)");
+        sb.AppendLine("    {");
+        sb.AppendLine("        if (handle == 0) return;");
+        sb.AppendLine("        var gch = GCHandle.FromIntPtr(handle);");
+        sb.AppendLine("        if (gch.IsAllocated) gch.Free();");
+        sb.AppendLine("    }");
+        sb.AppendLine();
+    }
+
+    private static void GenerateInteropMethod(StringBuilder sb, IMethodSymbol method)
+    {
+        sb.AppendLine();
+        
+        if (!PublicApiAnalyzer.IsSupported(method))
+        {
+            GenerateUnsupportedMethodComment(sb, method);
+            return;
+        }
+        
+        var methodName = GenerateMethodName(method);
+        var callTarget = $"{method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{method.Name}";
+        
+        // Determine interop signature (reference types become nint handles)
+        var interopReturnType = PublicApiAnalyzer.IsReferenceType(method.ReturnType) 
+            ? "nint" 
+            : method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+        
+        var interopParameters = string.Join(", ", method.Parameters.Select(p =>
+        {
+            var paramType = PublicApiAnalyzer.IsReferenceType(p.Type) 
+                ? "nint" 
+                : p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+            return $"{paramType} {p.Name}";
+        }));
+        
+        sb.AppendLine($"    [UnmanagedCallersOnly(EntryPoint = \"{GenerateEntryPointName(method)}\", CallConvs = new[] {{ typeof(CallConvCdecl) }})]");
+        sb.AppendLine($"    public static {interopReturnType} {methodName}({interopParameters})");
+        sb.AppendLine("    {");
+        
+        // Unbox reference type parameters
+        foreach (var param in method.Parameters)
+        {
+            if (PublicApiAnalyzer.IsReferenceType(param.Type))
+            {
+                var actualType = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+                sb.AppendLine($"        var {param.Name}_obj = UnboxHandle<{actualType}>({param.Name});");
+            }
+        }
+        
+        // Build argument list (use unboxed versions for reference types)
+        var arguments = string.Join(", ", method.Parameters.Select(p => 
+            PublicApiAnalyzer.IsReferenceType(p.Type) ? $"{p.Name}_obj" : p.Name));
+        
+        // Call the method and handle the result
+        if (method.ReturnsVoid)
+        {
+            sb.AppendLine($"        {callTarget}({arguments});");
+        }
+        else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType))
+        {
+            sb.AppendLine($"        var result = {callTarget}({arguments});");
+            sb.AppendLine($"        return BoxHandle(result);");
+        }
+        else
+        {
+            sb.AppendLine($"        return {callTarget}({arguments});");
+        }
+        
+        sb.AppendLine("    }");
+    }
+    
+    /// <summary>
+    /// Generates the entry point name for a method
+    /// </summary>
+    public static string GenerateEntryPointName(IMethodSymbol method)
+    {
+        var namespaceParts = method.ContainingNamespace.ToDisplayString().ToLowerInvariant().Replace(".", "_");
+        var typeName = method.ContainingType.Name.ToLowerInvariant();
+        var methodName = method.Name.ToLowerInvariant();
+        return $"{namespaceParts}_{typeName}_{methodName}";
+    }
+
+    private static void GenerateUnsupportedMethodComment(StringBuilder sb, IMethodSymbol method)
+    {
+        var returnType = method.ReturnType.ToDisplayString();
+        var parameters = string.Join(", ", method.Parameters.Select(p =>
+        {
+            var refKind = p.RefKind switch
+            {
+                RefKind.Ref => "ref ",
+                RefKind.Out => "out ",
+                RefKind.In => "in ",
+                _ => ""
+            };
+            return $"{refKind}{p.Type.ToDisplayString()} {p.Name}";
+        }));
+        
+        var fullSignature = $"{method.ContainingNamespace}.{method.ContainingType.Name}.{method.Name}({parameters}) : {returnType}";
+        
+        sb.AppendLine($"    // UNSUPPORTED: {fullSignature}");
+    }
+
+    private static string GenerateMethodName(IMethodSymbol method)
+    {
+        var name = $"{method.ContainingType.Name}_{method.Name}";
+        
+        if (method.Parameters.Length > 0)
+        {
+            var paramTypes = string.Join("_", method.Parameters.Select(p => SanitizeTypeName(p.Type.Name)));
+            name += "_" + paramTypes;
+        }
+        
+        return name;
+    }
+
+    private static string SanitizeTypeName(string typeName)
+    {
+        return typeName
+            .Replace("<", "_")
+            .Replace(">", "_")
+            .Replace(",", "_")
+            .Replace(" ", "")
+            .Replace("?", "Nullable")
+            .Replace("[]", "Array")
+            .Replace("*", "Ptr")
+            .Replace("&", "Ref");
+    }
+}
+

+ 188 - 0
Source/QuestPDF.InteropGenerators/PublicApiAnalyzer.cs

@@ -0,0 +1,188 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Analyzes the QuestPDF public API and extracts methods suitable for interop
+/// </summary>
+public static class PublicApiAnalyzer
+{
+    /// <summary>
+    /// Collects all public extension methods from the assembly
+    /// </summary>
+    public static List<IMethodSymbol> CollectExtensionMethods(INamespaceSymbol rootNamespace)
+    {
+        var methods = new List<IMethodSymbol>();
+        CollectExtensionMethodsRecursive(rootNamespace, methods);
+        return methods;
+    }
+
+    private static void CollectExtensionMethodsRecursive(INamespaceSymbol ns, List<IMethodSymbol> methods)
+    {
+        foreach (var member in ns.GetMembers())
+        {
+            if (member is INamespaceSymbol childNs)
+            {
+                CollectExtensionMethodsRecursive(childNs, methods);
+            }
+            else if (member is INamedTypeSymbol type)
+            {
+                CollectFromType(type, methods);
+            }
+        }
+    }
+
+    private static void CollectFromType(INamedTypeSymbol type, List<IMethodSymbol> methods)
+    {
+        if (type.DeclaredAccessibility != Accessibility.Public || type.IsImplicitlyDeclared)
+            return;
+
+        foreach (var member in type.GetMembers())
+        {
+            if (member is IMethodSymbol { MethodKind: MethodKind.Ordinary, IsExtensionMethod: true, DeclaredAccessibility: Accessibility.Public } method 
+                && !method.IsImplicitlyDeclared)
+            {
+                methods.Add(method);
+            }
+        }
+
+        foreach (var nestedType in type.GetTypeMembers())
+        {
+            CollectFromType(nestedType, methods);
+        }
+    }
+
+    /// <summary>
+    /// Checks if a method is supported for interop generation
+    /// </summary>
+    public static bool IsSupported(IMethodSymbol method)
+    {
+        // Async methods are not supported
+        if (method.IsAsync)
+            return false;
+        
+        // Check return type
+        if (!IsSupportedType(method.ReturnType))
+            return false;
+        
+        // Exclude methods that return Task or Task<T>
+        if (IsTaskRelatedType(method.ReturnType))
+            return false;
+        
+        // Check all parameter types
+        foreach (var parameter in method.Parameters)
+        {
+            if (!IsSupportedType(parameter.Type))
+                return false;
+            
+            // Exclude methods with Task-related parameters
+            if (IsTaskRelatedType(parameter.Type))
+                return false;
+            
+            // Ref/out parameters are not supported in UnmanagedCallersOnly
+            if (parameter.RefKind != RefKind.None)
+                return false;
+        }
+        
+        // Generic methods are not supported
+        if (method.IsGenericMethod)
+            return false;
+        
+        return true;
+    }
+
+    /// <summary>
+    /// Checks if a type is supported for interop
+    /// </summary>
+    public static bool IsSupportedType(ITypeSymbol type)
+    {
+        // Void is supported
+        if (type.SpecialType == SpecialType.System_Void)
+            return true;
+        
+        // Blittable primitive types
+        if (type.SpecialType is 
+            SpecialType.System_Boolean or
+            SpecialType.System_Byte or
+            SpecialType.System_SByte or
+            SpecialType.System_Int16 or
+            SpecialType.System_UInt16 or
+            SpecialType.System_Int32 or
+            SpecialType.System_UInt32 or
+            SpecialType.System_Int64 or
+            SpecialType.System_UInt64 or
+            SpecialType.System_Single or
+            SpecialType.System_Double or
+            SpecialType.System_IntPtr or
+            SpecialType.System_UIntPtr)
+            return true;
+        
+        // Pointers are supported
+        if (type.TypeKind == TypeKind.Pointer)
+            return true;
+        
+        // Function pointers are supported
+        if (type.TypeKind == TypeKind.FunctionPointer)
+            return true;
+        
+        // Value types (structs) that are blittable might be supported
+        // For simplicity, we'll allow struct types but developers should ensure they're blittable
+        if (type.TypeKind == TypeKind.Struct && !type.IsRefLikeType)
+            return true;
+        
+        // Enums are supported (backed by primitive types)
+        if (type.TypeKind == TypeKind.Enum)
+            return true;
+        
+        // Classes and interfaces are supported via handle boxing (passed as nint)
+        if (type.TypeKind is TypeKind.Class or TypeKind.Interface)
+            return true;
+        
+        // Generic types, arrays, delegates are not supported
+        if (type is INamedTypeSymbol { IsGenericType: true })
+            return false;
+        
+        if (type.TypeKind == TypeKind.Delegate)
+            return false;
+        
+        if (type.TypeKind == TypeKind.Array)
+            return false;
+        
+        return false;
+    }
+
+    /// <summary>
+    /// Checks if a type is a reference type (class or interface)
+    /// </summary>
+    public static bool IsReferenceType(ITypeSymbol type)
+    {
+        return type.TypeKind is TypeKind.Class or TypeKind.Interface;
+    }
+
+    /// <summary>
+    /// Checks if a type is Task-related (Task, CancellationToken, etc.)
+    /// </summary>
+    public static bool IsTaskRelatedType(ITypeSymbol type)
+    {
+        var fullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+        
+        // Check for Task, Task<T>, ValueTask, ValueTask<T>
+        if (fullName.StartsWith("global::System.Threading.Tasks.Task") ||
+            fullName.StartsWith("global::System.Threading.Tasks.ValueTask"))
+            return true;
+        
+        // Check for CancellationToken
+        if (fullName == "global::System.Threading.CancellationToken")
+            return true;
+        
+        // Check for IAsyncEnumerable<T> and IAsyncEnumerator<T>
+        if (fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerable") ||
+            fullName.StartsWith("global::System.Collections.Generic.IAsyncEnumerator"))
+            return true;
+        
+        return false;
+    }
+}
+

+ 30 - 0
Source/QuestPDF.InteropGenerators/PublicApiGenerator.cs

@@ -0,0 +1,30 @@
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Source generator that creates interop bindings for QuestPDF public API.
+/// Combines analysis, C# generation, and Python generation into a complete pipeline.
+/// </summary>
+[Generator]
+public sealed class PublicApiGenerator : IIncrementalGenerator
+{
+    public void Initialize(IncrementalGeneratorInitializationContext context)
+    {
+        context.RegisterSourceOutput(context.CompilationProvider, static (spc, compilation) =>
+        {
+            // Step 1: Analyze the public API and collect extension methods
+            var extensionMethods = PublicApiAnalyzer.CollectExtensionMethods(compilation.Assembly.GlobalNamespace);
+            
+            // Step 2: Generate C# UnmanagedCallersOnly interop code
+            var csharpInteropCode = CSharpInteropGenerator.GenerateInteropCode(extensionMethods);
+            spc.AddSource("GeneratedInterop.g.cs", csharpInteropCode);
+            
+            // Step 3: Generate Python ctypes bindings
+            var pythonBindingsCode = PythonBindingsGenerator.GeneratePythonBindings(extensionMethods);
+            // Note: Python file is added as .txt so it appears in generated files
+            // Extract and rename to .py for actual use
+            spc.AddSource("GeneratedInterop.g.py.txt", pythonBindingsCode);
+        });
+    }
+}

+ 293 - 0
Source/QuestPDF.InteropGenerators/PythonBindingsGenerator.cs

@@ -0,0 +1,293 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+/// <summary>
+/// Generates Python ctypes bindings for interop
+/// </summary>
+public static class PythonBindingsGenerator
+{
+    /// <summary>
+    /// Generates the complete Python bindings code
+    /// </summary>
+    public static string GeneratePythonBindings(List<IMethodSymbol> extensionMethods)
+    {
+        var sb = new StringBuilder();
+        
+        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");
+        sb.AppendLine("from pathlib import Path");
+        sb.AppendLine();
+        sb.AppendLine();
+        sb.AppendLine("class QuestPDFException(Exception):");
+        sb.AppendLine("    \"\"\"Base exception for QuestPDF errors\"\"\"");
+        sb.AppendLine("    pass");
+        sb.AppendLine();
+        sb.AppendLine();
+        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();
+        
+        // Generate Python wrapper methods
+        foreach (var method in extensionMethods)
+        {
+            if (!PublicApiAnalyzer.IsSupported(method))
+                continue;
+                
+            GeneratePythonWrapperMethod(sb, method);
+        }
+        
+        sb.AppendLine();
+        sb.AppendLine();
+        sb.AppendLine("class Handle:");
+        sb.AppendLine("    \"\"\"Wrapper for a managed object handle with automatic cleanup\"\"\"");
+        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 value(self) -> int:");
+        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");
+        
+        return sb.ToString();
+    }
+    
+    private static void GeneratePythonFunctionSetup(StringBuilder sb, IMethodSymbol method)
+    {
+        var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method);
+        
+        sb.AppendLine($"        # {method.ContainingType.Name}.{method.Name}");
+        sb.Append($"        self._lib.{entryPoint}.argtypes = [");
+        
+        var argTypes = new List<string>();
+        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 GeneratePythonWrapperMethod(StringBuilder sb, IMethodSymbol method)
+    {
+        var entryPoint = CSharpInteropGenerator.GenerateEntryPointName(method);
+        var pythonName = ToPythonMethodName(method.Name);
+        
+        // Build parameter list
+        var parameters = new List<string> { "self" };
+        foreach (var param in method.Parameters)
+        {
+            var paramName = ToPythonParamName(param.Name);
+            var pythonType = GetPythonTypeHint(param.Type);
+            parameters.Add($"{paramName}: {pythonType}");
+        }
+        
+        var returnTypeHint = GetPythonTypeHint(method.ReturnType);
+        
+        sb.AppendLine($"    def {pythonName}({string.Join(", ", parameters)}) -> {returnTypeHint}:");
+        sb.AppendLine($"        \"\"\"");
+        sb.AppendLine($"        {method.ContainingType.Name}.{method.Name}");
+        sb.AppendLine($"        \"\"\"");
+        
+        // Build argument list for the call
+        var callArgs = new List<string>();
+        foreach (var param in method.Parameters)
+        {
+            var paramName = ToPythonParamName(param.Name);
+            if (PublicApiAnalyzer.IsReferenceType(param.Type))
+            {
+                callArgs.Add($"{paramName}.value if isinstance({paramName}, Handle) else {paramName}");
+            }
+            else
+            {
+                callArgs.Add(paramName);
+            }
+        }
+        
+        if (method.ReturnsVoid)
+        {
+            sb.AppendLine($"        self._lib.{entryPoint}({string.Join(", ", callArgs)})");
+        }
+        else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType))
+        {
+            sb.AppendLine($"        result = self._lib.{entryPoint}({string.Join(", ", callArgs)})");
+            sb.AppendLine($"        return Handle(self, result)");
+        }
+        else
+        {
+            sb.AppendLine($"        return self._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)
+    {
+        if (type.SpecialType == SpecialType.System_Void)
+            return "None";
+        
+        if (PublicApiAnalyzer.IsReferenceType(type))
+            return "Handle";
+        
+        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 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;
+    }
+}
+

+ 160 - 0
Source/QuestPDF.InteropGenerators/README.md

@@ -0,0 +1,160 @@
+# QuestPDF Interop Generators
+
+This project contains the source generators that automatically create interop bindings for the QuestPDF public API.
+
+## Architecture
+
+The code is organized into 4 separate, well-defined components:
+
+### 1. PublicApiAnalyzer.cs
+**Purpose:** Analyzes the QuestPDF public API and extracts methods suitable for interop.
+
+**Responsibilities:**
+- Collects all public extension methods from the assembly
+- Determines if a method is supported for interop (checks for async, Task-related types, ref/out parameters, generics)
+- Validates type compatibility (primitives, structs, enums, classes, interfaces)
+- Identifies reference types that need handle boxing
+- Filters out async/await patterns and Task-related types
+
+**Key Methods:**
+- `CollectExtensionMethods()` - Recursively scans namespaces for public extension methods
+- `IsSupported()` - Validates if a method can be exposed via interop
+- `IsSupportedType()` - Checks if a type is compatible with UnmanagedCallersOnly
+- `IsReferenceType()` - Identifies classes/interfaces that need handle boxing
+- `IsTaskRelatedType()` - Detects Task, CancellationToken, and async-related types
+
+### 2. CSharpInteropGenerator.cs
+**Purpose:** Generates C# UnmanagedCallersOnly bindings for native interop.
+
+**Responsibilities:**
+- Creates `GeneratedInterop.g.cs` with all interop methods
+- Generates helper methods for handle boxing/unboxing (BoxHandle, UnboxHandle, FreeHandle)
+- Converts managed method calls to UnmanagedCallersOnly exports
+- Handles parameter marshalling (reference types → nint handles)
+- Generates entry point names (e.g., `questpdf_fluent_alignmentextensions_alignright`)
+
+**Key Methods:**
+- `GenerateInteropCode()` - Creates the complete C# interop file
+- `GenerateEntryPointName()` - Creates consistent entry point naming
+- `GenerateInteropMethod()` - Generates individual method wrappers
+
+**Generated Features:**
+- UnmanagedCallersOnly methods with CDecl calling convention
+- Automatic boxing/unboxing for reference types
+- Support for primitives, structs, enums, classes, and interfaces
+- Memory management via GCHandle
+
+### 3. PythonBindingsGenerator.cs
+**Purpose:** Generates Python ctypes bindings for the interop layer.
+
+**Responsibilities:**
+- Creates `GeneratedInterop.g.py.txt` with Python wrapper classes
+- Generates ctypes function signatures for all exported methods
+- Converts C# types to Python types (nint → Handle, int32 → int, etc.)
+- Creates Python-friendly method names (PascalCase → snake_case)
+- Implements automatic handle cleanup with context managers
+
+**Key Methods:**
+- `GeneratePythonBindings()` - Creates the complete Python bindings file
+- `GetPythonCType()` - Maps C# types to ctypes types
+- `GetPythonTypeHint()` - Creates Python type hints for IDE support
+- `ToPythonMethodName()` - Converts PascalCase to snake_case
+
+**Generated Features:**
+- `QuestPDFLibrary` class - Main wrapper with automatic library discovery
+- `Handle` class - Automatic memory management for managed objects
+- Type hints for full IDE support
+- Context manager support (with statement)
+- Cross-platform library loading (Windows/Linux/macOS)
+
+### 4. PublicApiGenerator.cs
+**Purpose:** Main orchestrator that combines all components.
+
+**Responsibilities:**
+- Implements IIncrementalGenerator for Roslyn
+- Coordinates the 3-step pipeline:
+  1. Analyze API → collect methods
+  2. Generate C# code → add to compilation
+  3. Generate Python code → add as text file
+- Registers source outputs with the compiler
+
+**Pipeline Flow:**
+```
+Compilation
+    ↓
+PublicApiAnalyzer.CollectExtensionMethods()
+    ↓
+[List of IMethodSymbol]
+    ↓
+    ├─→ CSharpInteropGenerator.GenerateInteropCode()
+    │       ↓
+    │   GeneratedInterop.g.cs (added to compilation)
+    │
+    └─→ PythonBindingsGenerator.GeneratePythonBindings()
+            ↓
+        GeneratedInterop.g.py.txt (available in obj folder)
+```
+
+## Type Support
+
+### Supported Types
+- **Primitives:** bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double
+- **Special:** IntPtr, UIntPtr
+- **Value Types:** Structs (must be blittable), Enums
+- **Reference Types:** Classes and Interfaces (passed as handles)
+- **Pointers:** Raw pointers and function pointers
+
+### Unsupported Types
+- **Async:** async methods, Task, Task<T>, ValueTask, CancellationToken
+- **Complex:** Generic methods, Arrays, Delegates
+- **Modifiers:** ref, out, in parameters
+
+## Usage
+
+### Building
+The generators run automatically during compilation:
+```bash
+dotnet build QuestPDF/QuestPDF.csproj
+```
+
+### Output Files
+- **C# Interop:** `QuestPDF/obj/Debug/netX.0/generated/.../GeneratedInterop.g.cs`
+- **Python Bindings:** `QuestPDF/obj/Debug/netX.0/generated/.../GeneratedInterop.g.py.txt`
+
+### Python Example
+```python
+from questpdf import QuestPDFLibrary, Handle
+
+# Initialize library
+pdf = QuestPDFLibrary()
+
+# Use the API (methods converted to snake_case)
+container = pdf.create_container()
+with container as c:
+    aligned = pdf.align_right(c)  # Returns Handle
+    padded = pdf.padding(aligned, 10)
+    # Handles automatically freed when out of scope
+```
+
+## Memory Management
+
+### C# Side
+- Uses GCHandle to pin managed objects
+- Objects kept alive until explicitly freed
+- `questpdf_free_handle` exported for cleanup
+
+### Python Side
+- `Handle` class wraps nint pointers
+- Automatic cleanup via `__del__` and context managers
+- Prevents double-free with handle tracking
+
+## Extension Points
+
+To add support for new types:
+1. Update `PublicApiAnalyzer.IsSupportedType()`
+2. Update `CSharpInteropGenerator` for C# marshalling
+3. Update `PythonBindingsGenerator.GetPythonCType()` for Python mapping
+
+To exclude specific methods:
+1. Add filtering logic to `PublicApiAnalyzer.IsSupported()`
+

+ 0 - 12
Source/QuestPDF/Interop.cs

@@ -70,8 +70,6 @@ public unsafe class Interop
     [UnmanagedCallersOnly(EntryPoint = "questpdf_document_create", CallConvs = new[] { typeof(CallConvCdecl) })]
     public static nint Document_Create(delegate* unmanaged[Cdecl]<nint, void> pageHandler) // returns opaque handle
     {
-        
-
         var thing = Document
             .Create(document =>
             {
@@ -152,14 +150,4 @@ public unsafe class Interop
     {
         Marshal.FreeHGlobal((IntPtr)ptr);
     }
-}
-
-[System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
-public sealed class AutoBuilderAttribute : System.Attribute { }
-
-[AutoBuilder]
-public partial class Order
-{
-    public string Id { get; set; } = "";
-    public int Quantity { get; set; }
 }