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");
}
}