using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Scriban; namespace QuestPDF.InteropGenerators; /// /// Generates C# UnmanagedCallersOnly bindings for interop using Scriban templates /// public static class CSharpInteropGenerator { private const string CSharpTemplate = @"// #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(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; } /// /// Generates the complete C# interop code /// public static string GenerateInteropCode(List extensionMethods) { var methods = extensionMethods.Select(BuildMethodModel).ToList(); var template = Template.Parse(CSharpTemplate); var output = template.Render(new { methods }); return output; } private static MethodModel BuildMethodModel(IMethodSymbol method) { if (!PublicApiAnalyzer.IsSupported(method)) { // 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 isExtensionMethod = method.IsExtensionMethod; var isInstanceMethod = !method.IsStatic && !isExtensionMethod; var interopReturnType = PublicApiAnalyzer.IsReferenceType(method.ReturnType) ? "nint" : method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var parametersList = new List(); if (isInstanceMethod) parametersList.Add("nint @this"); parametersList.AddRange(method.Parameters.Select(p => { var paramType = PublicApiAnalyzer.IsReferenceType(p.Type) ? "nint" : p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return $"{paramType} {p.Name}"; })); 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); bodySb.AppendLine($" var this_obj = UnboxHandle<{containingType}>(@this);"); } foreach (var param in method.Parameters) { if (PublicApiAnalyzer.IsReferenceType(param.Type)) { var actualType = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); bodySb.AppendLine($" var {param.Name}_obj = UnboxHandle<{actualType}>({param.Name});"); } } var arguments = string.Join(", ", method.Parameters.Select(p => PublicApiAnalyzer.IsReferenceType(p.Type) ? $"{p.Name}_obj" : p.Name)); string callTarget = isInstanceMethod ? $"this_obj.{method.Name}" : $"{method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{method.Name}"; if (method.ReturnsVoid) { bodySb.AppendLine($" {callTarget}({arguments});"); } else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType)) { bodySb.AppendLine($" var result = {callTarget}({arguments});"); bodySb.AppendLine($" return BoxHandle(result);"); } else { bodySb.AppendLine($" return {callTarget}({arguments});"); } 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') }; } /// /// Generates the entry point name for a method /// 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 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"); } }