Browse Source

Further invoke generation improvements

Marcin Ziąbek 1 month ago
parent
commit
b8d919133a

+ 159 - 0
Source/QuestPDF.InteropGenerators.Tests/Run.cs

@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Build.Locator;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.MSBuild;
+
+// TODO: point to your generator type:
+using QuestPDF.InteropGenerators; // e.g., using QuestPDF.InteropGenerators;
+
+internal static class Program
+{
+    // Adjust these values if needed:
+    private static readonly string ProjectName  = "QuestPDF"; // target project in the solution
+    private static readonly string OutputDir    = Path.GetFullPath("./_generated");
+    private const string Configuration = "Debug"; // or "Release"
+    // Optional: if you need a specific TFM, set: ["TargetFramework"] = "net8.0"
+
+    public static async Task Main()
+    {
+        if (!MSBuildLocator.IsRegistered)
+            MSBuildLocator.RegisterDefaults();
+
+        using var workspace = MSBuildWorkspace.Create(new Dictionary<string, string>
+        {
+            ["Configuration"] = Configuration,
+            ["TargetFramework"] = "net8.0"
+        });
+
+        workspace.WorkspaceFailed += (s, e) => Console.WriteLine($"[WorkspaceFailed] {e.Diagnostic.Kind}: {e.Diagnostic.Message}");
+
+        var solutionPath = FindSolutionPath();
+        Console.WriteLine($"Using solution: {solutionPath}");
+        var solution = await workspace.OpenSolutionAsync(solutionPath);
+
+        // Try by name first (only loaded projects are present)
+        var project = solution.Projects.FirstOrDefault(p => p.Name == ProjectName);
+
+        // Fallback: open the csproj directly (e.g., if the project failed to auto-load in the solution)
+        if (project is null)
+        {
+            var solutionDir = Path.GetDirectoryName(solutionPath)!;
+            var csprojPath = Path.Combine(solutionDir, ProjectName, $"{ProjectName}.csproj");
+            if (File.Exists(csprojPath))
+            {
+                Console.WriteLine($"Project '{ProjectName}' not loaded from solution; opening directly: {csprojPath}");
+                project = await workspace.OpenProjectAsync(csprojPath);
+            }
+        }
+
+        if (project is null)
+            throw new InvalidOperationException($"Project '{ProjectName}' not found or failed to load.");
+
+        var compilation = (CSharpCompilation)(await project.GetCompilationAsync()
+                         ?? throw new Exception("Compilation failed (null)."));
+
+        // Create your incremental generator instance here:
+        var generator = new PublicApiGenerator(); // <--- replace with your IIncrementalGenerator
+
+        var parseOptions = (CSharpParseOptions)project.ParseOptions!;
+        var additionalTexts = project.AdditionalDocuments
+            .Select(d => (AdditionalText)new AdditionalTextDocumentAdapter(d));
+
+        var driver = CSharpGeneratorDriver.Create(
+            generators: new ISourceGenerator[] { generator.AsSourceGenerator() },
+            additionalTexts: additionalTexts,
+            parseOptions: parseOptions,
+            optionsProvider: project.AnalyzerOptions.AnalyzerConfigOptionsProvider
+        );
+
+        var driver2 = driver.RunGenerators(compilation);
+        var runResult = driver2.GetRunResult();
+
+        Directory.CreateDirectory(OutputDir);
+
+        foreach (var diag in runResult.Diagnostics)
+            Console.WriteLine(diag.ToString());
+
+        foreach (var result in runResult.Results)
+        {
+            Console.WriteLine($"Generator: {result.Generator.GetType().Name}");
+
+            foreach (var gen in result.GeneratedSources)
+            {
+                var hintName = gen.HintName;
+                var sourceText = gen.SourceText;
+                var file = Path.Combine(OutputDir, Sanitize(hintName));
+                await File.WriteAllTextAsync(file, sourceText.ToString());
+                Console.WriteLine($"  Emitted: {file}");
+            }
+
+            foreach (var d in result.Diagnostics)
+                Console.WriteLine($"  {d}");
+        }
+
+        Console.WriteLine("Done.");
+    }
+
+    private static string Sanitize(string name)
+    {
+        foreach (var c in Path.GetInvalidFileNameChars())
+            name = name.Replace(c, '_');
+        return name;
+    }
+
+    private static string FindSolutionPath()
+    {
+        // 1) Try upward search from current directory
+        string? dir = Directory.GetCurrentDirectory();
+        var tried = new List<string>();
+        for (int i = 0; i < 10 && !string.IsNullOrEmpty(dir); i++)
+        {
+            var candidate = Path.Combine(dir, "QuestPDF.sln");
+            tried.Add(candidate);
+            if (File.Exists(candidate))
+                return candidate;
+            dir = Path.GetDirectoryName(dir);
+        }
+
+        // 2) Try from assembly base directory
+        dir = AppContext.BaseDirectory;
+        for (int i = 0; i < 10 && !string.IsNullOrEmpty(dir); i++)
+        {
+            var candidate = Path.Combine(dir, "QuestPDF.sln");
+            tried.Add(candidate);
+            if (File.Exists(candidate))
+                return candidate;
+            dir = Path.GetDirectoryName(dir);
+        }
+
+        // 3) Try known relative from this source file location (developer environment)
+        // This file lives at: Source/QuestPDF.InteropGenerators.Tests/Run.cs
+        // Solution resides at: Source/QuestPDF.sln
+        var sourceRepoRootGuess = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, ".."));
+        var candidate3 = Path.Combine(sourceRepoRootGuess, "QuestPDF.sln");
+        tried.Add(candidate3);
+        if (File.Exists(candidate3))
+            return candidate3;
+
+        throw new FileNotFoundException("Cannot locate 'QuestPDF.sln'. Tried:\n" + string.Join("\n", tried));
+    }
+}
+
+// Tiny adapter so AdditionalFiles work exactly like in a real build.
+internal sealed class AdditionalTextDocumentAdapter : AdditionalText
+{
+    private readonly TextDocument _doc;
+    public AdditionalTextDocumentAdapter(TextDocument doc) => _doc = doc;
+
+    public override string Path => _doc.FilePath ?? _doc.Name;
+
+    public override Microsoft.CodeAnalysis.Text.SourceText GetText(
+        System.Threading.CancellationToken cancellationToken = default) =>
+        _doc.GetTextAsync(cancellationToken).GetAwaiter().GetResult();
+}

+ 123 - 127
Source/QuestPDF.InteropGenerators/CSharpInteropGenerator.cs

@@ -2,160 +2,176 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using Microsoft.CodeAnalysis;
+using Scriban;
 
 namespace QuestPDF.InteropGenerators;
 
 /// <summary>
-/// Generates C# UnmanagedCallersOnly bindings for interop
+/// Generates C# UnmanagedCallersOnly bindings for interop using Scriban templates
 /// </summary>
 public static class CSharpInteropGenerator
 {
+    private const string CSharpTemplate = @"// <auto-generated/>
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace QuestPDF.Generated;
+
+public static unsafe class GeneratedInterop
+{
+    static IntPtr BoxHandle(object obj)
+    {
+        var gch = GCHandle.Alloc(obj, GCHandleType.Normal);
+        return GCHandle.ToIntPtr(gch);
+    }
+
+    static T UnboxHandle<T>(nint handle) where T : class
+    {
+        var gch = GCHandle.FromIntPtr(handle);
+        return (T)gch.Target!;
+    }
+
+    [UnmanagedCallersOnly(EntryPoint = ""questpdf_free_handle"", CallConvs = new[] { typeof(CallConvCdecl) })]
+    public static void FreeHandle(nint handle)
+    {
+        if (handle == 0) return;
+        var gch = GCHandle.FromIntPtr(handle);
+        if (gch.IsAllocated) gch.Free();
+    }
+
+    {{ for m in methods }}
+    {{~ if m.unsupported ~}}
+    // UNSUPPORTED: {{ m.unsupported_signature }}
+    {{~ else ~}}
+    [UnmanagedCallersOnly(EntryPoint = ""{{ m.entry_point }}"", CallConvs = new[] { typeof(CallConvCdecl) })]
+    public static {{ m.return_type }} {{ m.method_name }}({{ m.parameters_declaration }})
+    {
+{{ m.body }}
+    }
+    {{~ end ~}}
+
+    {{ end }}
+}
+";
+
+    private sealed class MethodModel
+    {
+        public bool unsupported { get; set; }
+        public string unsupported_signature { get; set; } = string.Empty;
+        public string entry_point { get; set; } = string.Empty;
+        public string return_type { get; set; } = string.Empty;
+        public string method_name { get; set; } = string.Empty;
+        public string parameters_declaration { get; set; } = string.Empty;
+        public string body { get; set; } = string.Empty;
+    }
+
     /// <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();
+        var methods = extensionMethods.Select(BuildMethodModel).ToList();
+        var template = Template.Parse(CSharpTemplate);
+        var output = template.Render(new { methods });
+        return output;
     }
 
-    private static void GenerateInteropMethod(StringBuilder sb, IMethodSymbol method)
+    private static MethodModel BuildMethodModel(IMethodSymbol method)
     {
-        sb.AppendLine();
-        
         if (!PublicApiAnalyzer.IsSupported(method))
         {
-            GenerateUnsupportedMethodComment(sb, method);
-            return;
+            // build unsupported signature as before
+            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 ",
+                    _ => string.Empty
+                };
+                return $"{refKind}{p.Type.ToDisplayString()} {p.Name}";
+            }));
+            var fullSignature = $"{method.ContainingNamespace}.{method.ContainingType.Name}.{method.Name}({parameters}) : {returnType}";
+            return new MethodModel
+            {
+                unsupported = true,
+                unsupported_signature = fullSignature
+            };
         }
-        
-        var methodName = GenerateMethodName(method);
+
         var isExtensionMethod = method.IsExtensionMethod;
         var isInstanceMethod = !method.IsStatic && !isExtensionMethod;
-        
-        // Determine interop signature (reference types become nint handles)
-        var interopReturnType = PublicApiAnalyzer.IsReferenceType(method.ReturnType) 
-            ? "nint" 
+
+        var interopReturnType = PublicApiAnalyzer.IsReferenceType(method.ReturnType)
+            ? "nint"
             : method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
-        
-        // For instance methods, add 'this' parameter as first parameter
-        var interopParametersList = new List<string>();
-        
+
+        var parametersList = new List<string>();
         if (isInstanceMethod)
+            parametersList.Add("nint @this");
+
+        parametersList.AddRange(method.Parameters.Select(p =>
         {
-            // Instance methods need the 'this' object as first parameter
-            interopParametersList.Add("nint @this");
-        }
-        
-        interopParametersList.AddRange(method.Parameters.Select(p =>
-        {
-            var paramType = PublicApiAnalyzer.IsReferenceType(p.Type) 
-                ? "nint" 
+            var paramType = PublicApiAnalyzer.IsReferenceType(p.Type)
+                ? "nint"
                 : p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
             return $"{paramType} {p.Name}";
         }));
-        
-        var interopParameters = string.Join(", ", interopParametersList);
-        
-        sb.AppendLine($"    [UnmanagedCallersOnly(EntryPoint = \"{GenerateEntryPointName(method)}\", CallConvs = new[] {{ typeof(CallConvCdecl) }})]");
-        sb.AppendLine($"    public static {interopReturnType} {methodName}({interopParameters})");
-        sb.AppendLine("    {");
-        
-        // For instance methods, unbox the 'this' parameter
+        var interopParameters = string.Join(", ", parametersList);
+
+        // Build body
+        var bodySb = new StringBuilder();
+        // indent 8 spaces inside method
         if (isInstanceMethod)
         {
             var containingType = method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
-            sb.AppendLine($"        var this_obj = UnboxHandle<{containingType}>(@this);");
+            bodySb.AppendLine($"        var this_obj = UnboxHandle<{containingType}>(@this);");
         }
-        
-        // 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});");
+                bodySb.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 => 
+
+        var arguments = string.Join(", ", method.Parameters.Select(p =>
             PublicApiAnalyzer.IsReferenceType(p.Type) ? $"{p.Name}_obj" : p.Name));
-        
-        // Build the call target
-        string callTarget;
-        if (isInstanceMethod)
-        {
-            callTarget = $"this_obj.{method.Name}";
-        }
-        else
-        {
-            callTarget = $"{method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{method.Name}";
-        }
-        
-        // Call the method and handle the result
+
+        string callTarget = isInstanceMethod
+            ? $"this_obj.{method.Name}"
+            : $"{method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{method.Name}";
+
         if (method.ReturnsVoid)
         {
-            sb.AppendLine($"        {callTarget}({arguments});");
+            bodySb.AppendLine($"        {callTarget}({arguments});");
         }
         else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType))
         {
-            sb.AppendLine($"        var result = {callTarget}({arguments});");
-            sb.AppendLine($"        return BoxHandle(result);");
+            bodySb.AppendLine($"        var result = {callTarget}({arguments});");
+            bodySb.AppendLine($"        return BoxHandle(result);");
         }
         else
         {
-            sb.AppendLine($"        return {callTarget}({arguments});");
+            bodySb.AppendLine($"        return {callTarget}({arguments});");
         }
-        
-        sb.AppendLine("    }");
+
+        return new MethodModel
+        {
+            unsupported = false,
+            entry_point = GenerateEntryPointName(method),
+            return_type = interopReturnType,
+            method_name = GenerateMethodName(method),
+            parameters_declaration = interopParameters,
+            body = bodySb.ToString().TrimEnd('\r', '\n')
+        };
     }
     
     /// <summary>
@@ -169,26 +185,6 @@ public static class CSharpInteropGenerator
         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}";

+ 378 - 0
Source/QuestPDF.InteropGenerators/NewGenerator.cs

@@ -0,0 +1,378 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+
+namespace QuestPDF.InteropGenerators;
+
+public static class NewGenerator
+{
+    private const string PythonTemplate = @"# auto-generated
+
+from cffi import FFI
+from typing import Callable, Optional, Tuple, Any, Self
+
+
+";
+    
+    private const string CsharpInteropTemplate = @"// <auto-generated/>
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using QuestPDF.Fluent;
+using QuestPDF.Infrastructure;
+using QuestPDF.Helpers;
+using QuestPDF.Companion;
+
+namespace QuestPDF;
+
+internal unsafe partial class Interop
+{
+    {{ methods }}
+}
+";
+    
+    public static string AnalyzeAndGenerate(INamespaceSymbol namespaceSymbol)
+    {
+        var methods = CollectExtensionMethods(namespaceSymbol);
+        
+        var targetMethods = methods
+            .Where(x => !IsObsolete(x.Item2))
+            .ToList();
+        
+        var supportedExtensionMethods = targetMethods
+            .Where(x => IsMethodSupported(x.Item2))
+            .ToList();
+
+        var simpleMethodHeaders = supportedExtensionMethods
+            .Select(x => ConvertMethodToHeader(x.Item1, x.Item2))
+            .ToList();
+        
+        var excluded = targetMethods.Except(supportedExtensionMethods).ToList();
+
+        var pythonCHeaders = string.Join("\n", simpleMethodHeaders);
+        var pythonContainerClass = supportedExtensionMethods
+            .GroupBy(x => x.Item2.IsExtensionMethod ? x.Item2.Parameters.First().Type.Name : x.Item1.Name)
+            .Select(x => ConvertToPythonContainerClient(x.Key, x))
+            .ToList();
+        
+        var interopTemplateMethods = string.Join("\n\n", supportedExtensionMethods.Select(x => ConvertToUnmanagedCallersOnlyDefinition(x.Item1, x.Item2)));
+        return CsharpInteropTemplate.Replace("{{ methods }}", interopTemplateMethods);
+    }
+
+    private static bool IsObsolete(IMethodSymbol method)
+    {
+        return method.GetAttributes().Any(attr => attr.AttributeClass?.ToDisplayString() == "System.ObsoleteAttribute");
+    }
+    
+    private static bool IsMethodSupported(IMethodSymbol method)
+    {
+        if (method.IsGenericMethod)
+            return false;
+        
+        if (method.Parameters.Any(x => IsTaskRelatedType(x.Type)))
+            return false;
+        
+        // method has lambdas
+        if (method.Parameters.Any(x => x.Type.TypeKind == TypeKind.Delegate))
+            return false;
+
+        var problematicMethods = new[] {"Border", "GeneratePdf", "GenerateXps", "Fallback", "DebugArea"};
+        
+        if (problematicMethods.Contains(method.Name))
+            return false;
+        
+        if (method.IsExtensionMethod)
+        {
+            return method.Parameters.Skip(1).All(x => IsValueType(x.Type) || x.Type.SpecialType == SpecialType.System_String || IsColorType(x.Type) || IsUnitType(x.Type));
+        }
+        
+        return true;
+    }
+    
+    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;
+    }
+    
+    #region Method Discovery
+    
+    private static IEnumerable<(INamedTypeSymbol, IMethodSymbol)> CollectExtensionMethods(INamespaceSymbol root)
+    {
+        var result = new List<(INamedTypeSymbol, IMethodSymbol)>();
+        TraverseNamespaces(root);
+        return result;
+
+        void TraverseNamespaces(INamespaceSymbol namespaceSymbol)
+        {
+            foreach (var member in namespaceSymbol.GetMembers())
+            {
+                if (member is INamespaceSymbol childNs)
+                {
+                    TraverseNamespaces(childNs);
+                }
+                else if (member is INamedTypeSymbol type)
+                {
+                    HandleType(type);
+                }
+            }
+        }
+        
+        void HandleType(INamedTypeSymbol type)
+        {
+            if (type.DeclaredAccessibility != Accessibility.Public || type.IsImplicitlyDeclared)
+                return;
+
+            foreach (var member in type.GetMembers())
+            {
+                if (member is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsImplicitlyDeclared: false } method)
+                {
+                    result.Add((type, method));
+                }
+            }
+        }
+    }
+    
+    #endregion
+
+    #region Method Conversion
+    
+    private static string ConvertMethodToHeader(INamedTypeSymbol typeSymbol, IMethodSymbol method)
+    {
+        var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
+        var returnType = GetCReturnType(method.ReturnType);
+        var parameters = GetCParameters(method);
+                
+        return $"{returnType} {methodName}({string.Join(", ", parameters)});";
+        
+        static List<string> GetCParameters(IMethodSymbol method)
+        {
+            var parameters = new List<string>();
+            
+            foreach (var param in method.Parameters)
+            {
+                var paramType = GetCReturnType(param.Type);
+                var paramName = ToSnakeCase(param.Name);
+                parameters.Add($"{paramType} {paramName}");
+            }
+            
+            return parameters;
+        }
+    }
+
+    static string ToSnakeCase(string text)
+    {
+        return string.Concat(text.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x : x.ToString())).ToLowerInvariant();
+    }
+    
+    static string ConvertToInterfaceFunctionName(INamedTypeSymbol typeSymbol, IMethodSymbol method)
+    {
+        var typeName = method.IsExtensionMethod ? method.Parameters.First().Type.Name : typeSymbol.Name;
+
+        if (typeName.StartsWith("I") && typeName.Length > 1 && char.IsUpper(typeName[1]))
+            typeName = typeName.Substring(1);
+
+        return $"questpdf_{ToSnakeCase(typeName)}_{ToSnakeCase(method.Name)}";
+    }
+
+    private static string ConvertToPythonContainerClient(string groupName, IEnumerable<(INamedTypeSymbol, IMethodSymbol)> methodSymbols)
+    {
+        var sb = new StringBuilder();
+
+        sb.AppendLine($"class {groupName}:");
+        sb.AppendLine("\tdef __init__(self, handler_pointer: \"ffi.CData\"):");
+        sb.AppendLine("\t\tself.handler_pointer = handler_pointer");
+            
+        foreach (var (typeSymbol, methodSymbol) in methodSymbols)
+        {
+            if (!methodSymbol.IsExtensionMethod)
+            {
+                sb.AppendLine($"\t\t# Conversion not supported");
+                continue;
+            }
+            
+            var additionalParameters = string.Join(", ", methodSymbol.Parameters.Skip(1).Select(ParamToInvocationArgument));
+            
+            if (additionalParameters.Length > 0)
+                additionalParameters = ", " + additionalParameters;
+            
+            sb.AppendLine();
+            sb.AppendLine($"\tdef {ToSnakeCase(methodSymbol.Name)}(self{additionalParameters}) -> Self:");
+            sb.AppendLine($"\t\tresult = lib.{ConvertToInterfaceFunctionName(typeSymbol, methodSymbol)}(self.handler_pointer{additionalParameters})");
+            sb.AppendLine("\t\treturn Container(result)");
+        }    
+        
+        
+        
+        
+        return sb.ToString();
+        
+        static string ParamToInvocationArgument(IParameterSymbol param)
+        {
+            var type = "";
+
+            return $"{ToSnakeCase(param.Name)}{type}";
+        }
+    }
+    
+    private static string ConvertToUnmanagedCallersOnlyDefinition(INamedTypeSymbol typeSymbol, IMethodSymbol method)
+    {
+        var methodName = ConvertToInterfaceFunctionName(typeSymbol, method);
+        var returnType = GetCSharpInteropReturnType(method.ReturnType);
+        var parameters = string.Join(", ", GetMethodParams(method));
+
+        var additionalParameters = string.Join(", ", method.Parameters.Skip(method.IsExtensionMethod ? 1 : 0).Select(ParamToInvocationArgument));
+        var typePointer = (method.IsExtensionMethod ? method.Parameters.First().Type : typeSymbol).ToDisplayString();
+
+        if (!method.IsExtensionMethod)
+        {
+            parameters = parameters.Any() ? $"nint nativePointer, {parameters}" : "nint nativePointer";
+            additionalParameters = additionalParameters.Any() ? $"nativePointer, {additionalParameters}" : "nativePointer";
+        }
+        
+        var typePointerInputName = method.IsExtensionMethod ? method.Parameters.First().Name : "nativePointer";
+        
+        var sb = new StringBuilder();
+        sb.AppendLine($"[UnmanagedCallersOnly(EntryPoint = \"{methodName}\", CallConvs = new[] {{ typeof(CallConvCdecl) }})]");
+        sb.AppendLine($"public static {returnType} {typeSymbol.Name}{method.Name}({parameters})");
+        sb.AppendLine("{");
+
+        if (method.ReturnsVoid)
+        {
+            sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
+            sb.AppendLine($"extendedTypePointer.{method.Name}({additionalParameters});");
+        }
+        else
+        {
+            sb.AppendLine($"var extendedTypePointer = UnboxHandle<{typePointer}>({typePointerInputName});");
+            sb.AppendLine($"var result = extendedTypePointer.{method.Name}({additionalParameters});");
+            sb.AppendLine("return BoxHandle(result);");
+        }
+        
+        sb.AppendLine("}");
+        
+        return sb.ToString();
+
+        static string ParamToInvocationArgument(IParameterSymbol param)
+        {
+            if (param.Type.SpecialType == SpecialType.System_String)
+                return $"Marshal.PtrToStringUTF8((IntPtr){param.Name}) ?? \"\"";
+            
+            if (param.Type.TypeKind == TypeKind.Enum)
+                return $"({param.Type.ToDisplayString()}){param.Name}";
+
+            if (IsColorType(param.Type))
+                return $"(QuestPDF.Infrastructure.Color){param.Name}";
+            
+            return param.Name;
+        }
+    }
+
+    static IEnumerable<string> GetMethodParams(IMethodSymbol method)
+    {
+        foreach (var param in method.Parameters)
+        {
+            var paramType = GetCSharpInteropReturnType(param.Type);
+            yield return $"{paramType} {param.Name}";
+        }
+    }
+    
+    static string GetCSharpInteropReturnType(ITypeSymbol type)
+    {
+        if (type.SpecialType == SpecialType.System_Void)
+            return "void";
+        if (type.SpecialType == SpecialType.System_Int32)
+            return "int";
+        if (type.SpecialType == SpecialType.System_UInt32)
+            return "uint";
+        if (type.SpecialType == SpecialType.System_Boolean)
+            return "bool";
+        if (type.SpecialType == SpecialType.System_Single)
+            return "float";
+        if (type.SpecialType == SpecialType.System_Double)
+            return "double";
+        
+        
+            
+        if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
+            return "uint";
+                
+        if (type.TypeKind == TypeKind.Enum)
+            return "int";
+            
+        // For object types, return IntPtr
+        return "nint";
+    }
+    
+    static string GetCReturnType(ITypeSymbol type)
+    {
+        if (type.SpecialType == SpecialType.System_Void)
+            return "void";
+        if (type.SpecialType == SpecialType.System_Int32)
+            return "int32_t";
+        if (type.SpecialType == SpecialType.System_UInt32)
+            return "uint32_t";
+        if (type.SpecialType == SpecialType.System_Boolean)
+            return "bool";
+        if (type.SpecialType == SpecialType.System_Single)
+            return "float";
+        if (type.SpecialType == SpecialType.System_Double)
+            return "double";
+        
+        if (type.SpecialType == SpecialType.System_String)
+            return "char*";
+                
+        if (type.ToDisplayString() == "QuestPDF.Infrastructure.Color")
+            return "uint32_t";
+
+        if (type.TypeKind == TypeKind.Enum)
+            return "int32_t";
+                
+        // For object types, return void pointer
+        return "void*";
+    }
+    
+    static bool IsValueType(ITypeSymbol type)
+    {
+        return type.SpecialType switch
+        {
+            SpecialType.System_Int32 => true,
+            SpecialType.System_UInt32 => true,
+            SpecialType.System_Boolean => true,
+            SpecialType.System_Single => true,
+            SpecialType.System_Double => true,
+            _ => type.IsValueType
+        };
+    }
+    
+    static bool IsColorType(ITypeSymbol type)
+    {
+        return type.ToDisplayString() == "QuestPDF.Infrastructure.Color";
+    }
+    
+    static bool IsUnitType(ITypeSymbol type)
+    {
+        return type.ToDisplayString() == "QuestPDF.Infrastructure.Unit";
+    }
+        
+    #endregion
+}

+ 28 - 13
Source/QuestPDF.InteropGenerators/PublicApiGenerator.cs

@@ -1,4 +1,7 @@
+using System.Runtime.Serialization;
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
 
 namespace QuestPDF.InteropGenerators;
 
@@ -13,21 +16,33 @@ public sealed class PublicApiGenerator : IIncrementalGenerator
     {
         context.RegisterSourceOutput(context.CompilationProvider, static (spc, compilation) =>
         {
-            // Step 1: Analyze the public API and collect all interop methods
-            // This includes both extension methods AND public methods from Fluent API classes
-            // (descriptors, configurations, handlers, etc.)
-            var allMethods = PublicApiAnalyzer.CollectAllInteropMethods(compilation.Assembly.GlobalNamespace);
+            var content = NewGenerator.AnalyzeAndGenerate(compilation.Assembly.GlobalNamespace);
+            
+            var syntaxTree = CSharpSyntaxTree.ParseText(content);
+            var root = syntaxTree
+                .GetRoot()
+                .NormalizeWhitespace(elasticTrivia: true); // auto-indents and cleans up
+
+            var newCode = root.ToFullString();
+            
+            spc.AddSource("Interop.g.cs", newCode);
             
-            // Step 2: Generate C# UnmanagedCallersOnly interop code
-            var csharpInteropCode = CSharpInteropGenerator.GenerateInteropCode(allMethods);
-            spc.AddSource("GeneratedInterop.g.cs", csharpInteropCode);
             
-            // Step 3: Generate Python ctypes bindings
-            // Python bindings strictly follow all C# interop functionalities
-            var pythonBindingsCode = PythonBindingsGenerator.GeneratePythonBindings(allMethods);
-            // 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);
+            // // Step 1: Analyze the public API and collect all interop methods
+            // // This includes both extension methods AND public methods from Fluent API classes
+            // // (descriptors, configurations, handlers, etc.)
+            // var allMethods = PublicApiAnalyzer.CollectAllInteropMethods(compilation.Assembly.GlobalNamespace);
+            //
+            // // Step 2: Generate C# UnmanagedCallersOnly interop code
+            // var csharpInteropCode = CSharpInteropGenerator.GenerateInteropCode(allMethods);
+            // spc.AddSource("GeneratedInterop.g.cs", csharpInteropCode);
+            //
+            // // Step 3: Generate Python ctypes bindings
+            // // Python bindings strictly follow all C# interop functionalities
+            // var pythonBindingsCode = PythonBindingsGenerator.GeneratePythonBindings(allMethods);
+            // // 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);
         });
     }
 }

+ 33 - 11
Source/QuestPDF.InteropGenerators/PythonBindingsGenerator.cs

@@ -2,35 +2,36 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using Microsoft.CodeAnalysis;
+using Scriban;
 
 namespace QuestPDF.InteropGenerators;
 
 /// <summary>
-/// Generates Python ctypes bindings for interop
+/// Generates Python ctypes bindings for interop using Scriban templates
 /// </summary>
 public static class PythonBindingsGenerator
 {
     /// <summary>
     /// Generates the complete Python bindings code
     /// </summary>
+    private const string PythonTemplate = @"{{ header }}{{ library }}{{ wrappers }}";
+
     public static string GeneratePythonBindings(List<IMethodSymbol> extensionMethods)
     {
-        var sb = new StringBuilder();
-        
-        // Generate header and base classes
-        GeneratePythonHeader(sb);
-        
         // Group methods by their containing type
         var methodsByType = GroupMethodsByType(extensionMethods);
         
-        // Generate the main library class
-        GenerateLibraryClass(sb, extensionMethods);
+        // Build pieces using existing logic
+        var header = GeneratePythonHeaderText();
+        var library = GenerateLibraryClassText(extensionMethods);
+        var wrappers = GeneratePythonWrapperClassesText(methodsByType);
         
-        // Generate Python wrapper classes for each C# type
-        GeneratePythonWrapperClasses(sb, 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(sb.ToString());
+        return CommentOutPythonCode(rendered);
     }
     
     /// <summary>
@@ -42,6 +43,27 @@ public static class PythonBindingsGenerator
         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<IMethodSymbol> extensionMethods)
+    {
+        var sb = new StringBuilder();
+        GenerateLibraryClass(sb, extensionMethods);
+        return sb.ToString();
+    }
+
+    private static string GeneratePythonWrapperClassesText(Dictionary<string, List<IMethodSymbol>> methodsByType)
+    {
+        var sb = new StringBuilder();
+        GeneratePythonWrapperClasses(sb, methodsByType);
+        return sb.ToString();
+    }
     
     private static void GeneratePythonHeader(StringBuilder sb)
     {

+ 1 - 0
Source/QuestPDF.InteropGenerators/QuestPDF.InteropGenerators.csproj

@@ -16,6 +16,7 @@
     <ItemGroup>
         <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.*" PrivateAssets="all" />
         <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
+        <PackageReference Include="Scriban" Version="5.*" />
     </ItemGroup>
 
     <ItemGroup>

+ 6 - 0
Source/QuestPDF.sln

@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.VisualTests", "Que
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.InteropGenerators", "QuestPDF.InteropGenerators\QuestPDF.InteropGenerators.csproj", "{2C61E019-7959-40C5-9C55-3B5B4F7E5107}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.InteropGenerators.Tests", "QuestPDF.InteropGenerators.Tests\QuestPDF.InteropGenerators.Tests.csproj", "{DE8A9B13-88EF-4EC5-AC77-3B9453E3D48C}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -65,5 +67,9 @@ Global
 		{2C61E019-7959-40C5-9C55-3B5B4F7E5107}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2C61E019-7959-40C5-9C55-3B5B4F7E5107}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2C61E019-7959-40C5-9C55-3B5B4F7E5107}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DE8A9B13-88EF-4EC5-AC77-3B9453E3D48C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{DE8A9B13-88EF-4EC5-AC77-3B9453E3D48C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DE8A9B13-88EF-4EC5-AC77-3B9453E3D48C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DE8A9B13-88EF-4EC5-AC77-3B9453E3D48C}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 EndGlobal

+ 4 - 34
Source/QuestPDF/Interop.cs

@@ -1,9 +1,7 @@
 using System;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
-using System.Text;
 using QuestPDF.Fluent;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF;
@@ -16,7 +14,7 @@ public unsafe struct Buffer
 }
 
 
-public unsafe class Interop
+internal unsafe partial class Interop
 {
     static Interop()
     {
@@ -120,21 +118,6 @@ public unsafe class Interop
             descriptor(pagePointer);
         });
     }
-
-    [UnmanagedCallersOnly(EntryPoint = "questpdf_document_container_add_page_faster", CallConvs = new[] { typeof(CallConvCdecl) })]
-    public static IntPtr Document_ContainerAddPage_Faster(nint documentContainerPointer) // returns opaque handle
-    {
-        var documentContainer = UnboxHandle<IDocumentContainer>(documentContainerPointer);
-        
-        IntPtr pagePointer = default;
-        
-        documentContainer.Page(page =>
-        {
-            pagePointer = BoxHandle(page);
-        });
-
-        return pagePointer;
-    }
     
     
     
@@ -149,29 +132,16 @@ public unsafe class Interop
     }
 
     [UnmanagedCallersOnly(EntryPoint = "questpdf_page_set_content", CallConvs = new[] { typeof(CallConvCdecl) })]
-    public static void Page_AddContent(nint handle, byte* textPtr)
+    public static IntPtr Page_AddContent(nint handle)
     {
         var thing = UnboxHandle<PageDescriptor>(handle);
-        var textFromOutside = Marshal.PtrToStringUTF8((IntPtr)textPtr) ?? "";
-        
-        thing.Content().Width(200).Height(100).Background(Colors.Red.Lighten3);
-            
-        //     .Text(text =>
-        // {
-        //     text.DefaultTextStyle(x => x.FontSize(20));
-        //
-        //     text.Span("Hello World... from ");
-        //     text.Span(textFromOutside).FontColor(Colors.Blue.Darken1);
-        //     text.Span("!");
-        // });
+        var result = thing.Content();
+        return BoxHandle(result);
     }
     
     
     
     
-    
-    
-    
 
     [UnmanagedCallersOnly(EntryPoint = "questpdf_free_bytes", CallConvs = new[] { typeof(CallConvCdecl) })]
     public static void FreeBytes(byte* ptr)

+ 1 - 1
Source/QuestPDF/QuestPDF.csproj

@@ -20,7 +20,7 @@
         <Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
         <PackageTags>pdf generation creation export merge edit html</PackageTags>
         <Nullable>enable</Nullable>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net10.0</TargetFramework>
         <IncludeSymbols>true</IncludeSymbols>
         <SymbolPackageFormat>snupkg</SymbolPackageFormat>
         <GenerateDocumentationFile>True</GenerateDocumentationFile>

+ 1 - 1
Source/global.json

@@ -1,6 +1,6 @@
 {
   "sdk": {
-    "version": "8.0.0",
+    "version": "10.0.0",
     "rollForward": "latestMinor",
     "allowPrerelease": true
   }