CSharpInteropGenerator.cs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Text;
  4. using Microsoft.CodeAnalysis;
  5. using Scriban;
  6. namespace QuestPDF.InteropGenerators;
  7. /// <summary>
  8. /// Generates C# UnmanagedCallersOnly bindings for interop using Scriban templates
  9. /// </summary>
  10. public static class CSharpInteropGenerator
  11. {
  12. private const string CSharpTemplate = @"// <auto-generated/>
  13. #nullable enable
  14. using System;
  15. using System.Runtime.CompilerServices;
  16. using System.Runtime.InteropServices;
  17. namespace QuestPDF.Generated;
  18. public static unsafe class GeneratedInterop
  19. {
  20. static IntPtr BoxHandle(object obj)
  21. {
  22. var gch = GCHandle.Alloc(obj, GCHandleType.Normal);
  23. return GCHandle.ToIntPtr(gch);
  24. }
  25. static T UnboxHandle<T>(nint handle) where T : class
  26. {
  27. var gch = GCHandle.FromIntPtr(handle);
  28. return (T)gch.Target!;
  29. }
  30. [UnmanagedCallersOnly(EntryPoint = ""questpdf_free_handle"", CallConvs = new[] { typeof(CallConvCdecl) })]
  31. public static void FreeHandle(nint handle)
  32. {
  33. if (handle == 0) return;
  34. var gch = GCHandle.FromIntPtr(handle);
  35. if (gch.IsAllocated) gch.Free();
  36. }
  37. {{ for m in methods }}
  38. {{~ if m.unsupported ~}}
  39. // UNSUPPORTED: {{ m.unsupported_signature }}
  40. {{~ else ~}}
  41. [UnmanagedCallersOnly(EntryPoint = ""{{ m.entry_point }}"", CallConvs = new[] { typeof(CallConvCdecl) })]
  42. public static {{ m.return_type }} {{ m.method_name }}({{ m.parameters_declaration }})
  43. {
  44. {{ m.body }}
  45. }
  46. {{~ end ~}}
  47. {{ end }}
  48. }
  49. ";
  50. private sealed class MethodModel
  51. {
  52. public bool unsupported { get; set; }
  53. public string unsupported_signature { get; set; } = string.Empty;
  54. public string entry_point { get; set; } = string.Empty;
  55. public string return_type { get; set; } = string.Empty;
  56. public string method_name { get; set; } = string.Empty;
  57. public string parameters_declaration { get; set; } = string.Empty;
  58. public string body { get; set; } = string.Empty;
  59. }
  60. /// <summary>
  61. /// Generates the complete C# interop code
  62. /// </summary>
  63. public static string GenerateInteropCode(List<IMethodSymbol> extensionMethods)
  64. {
  65. var methods = extensionMethods.Select(BuildMethodModel).ToList();
  66. var template = Template.Parse(CSharpTemplate);
  67. var output = template.Render(new { methods });
  68. return output;
  69. }
  70. private static MethodModel BuildMethodModel(IMethodSymbol method)
  71. {
  72. if (!PublicApiAnalyzer.IsSupported(method))
  73. {
  74. // build unsupported signature as before
  75. var returnType = method.ReturnType.ToDisplayString();
  76. var parameters = string.Join(", ", method.Parameters.Select(p =>
  77. {
  78. var refKind = p.RefKind switch
  79. {
  80. RefKind.Ref => "ref ",
  81. RefKind.Out => "out ",
  82. RefKind.In => "in ",
  83. _ => string.Empty
  84. };
  85. return $"{refKind}{p.Type.ToDisplayString()} {p.Name}";
  86. }));
  87. var fullSignature = $"{method.ContainingNamespace}.{method.ContainingType.Name}.{method.Name}({parameters}) : {returnType}";
  88. return new MethodModel
  89. {
  90. unsupported = true,
  91. unsupported_signature = fullSignature
  92. };
  93. }
  94. var isExtensionMethod = method.IsExtensionMethod;
  95. var isInstanceMethod = !method.IsStatic && !isExtensionMethod;
  96. var interopReturnType = PublicApiAnalyzer.IsReferenceType(method.ReturnType)
  97. ? "nint"
  98. : method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  99. var parametersList = new List<string>();
  100. if (isInstanceMethod)
  101. parametersList.Add("nint @this");
  102. parametersList.AddRange(method.Parameters.Select(p =>
  103. {
  104. var paramType = PublicApiAnalyzer.IsReferenceType(p.Type)
  105. ? "nint"
  106. : p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  107. return $"{paramType} {p.Name}";
  108. }));
  109. var interopParameters = string.Join(", ", parametersList);
  110. // Build body
  111. var bodySb = new StringBuilder();
  112. // indent 8 spaces inside method
  113. if (isInstanceMethod)
  114. {
  115. var containingType = method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  116. bodySb.AppendLine($" var this_obj = UnboxHandle<{containingType}>(@this);");
  117. }
  118. foreach (var param in method.Parameters)
  119. {
  120. if (PublicApiAnalyzer.IsReferenceType(param.Type))
  121. {
  122. var actualType = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  123. bodySb.AppendLine($" var {param.Name}_obj = UnboxHandle<{actualType}>({param.Name});");
  124. }
  125. }
  126. var arguments = string.Join(", ", method.Parameters.Select(p =>
  127. PublicApiAnalyzer.IsReferenceType(p.Type) ? $"{p.Name}_obj" : p.Name));
  128. string callTarget = isInstanceMethod
  129. ? $"this_obj.{method.Name}"
  130. : $"{method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{method.Name}";
  131. if (method.ReturnsVoid)
  132. {
  133. bodySb.AppendLine($" {callTarget}({arguments});");
  134. }
  135. else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType))
  136. {
  137. bodySb.AppendLine($" var result = {callTarget}({arguments});");
  138. bodySb.AppendLine($" return BoxHandle(result);");
  139. }
  140. else
  141. {
  142. bodySb.AppendLine($" return {callTarget}({arguments});");
  143. }
  144. return new MethodModel
  145. {
  146. unsupported = false,
  147. entry_point = GenerateEntryPointName(method),
  148. return_type = interopReturnType,
  149. method_name = GenerateMethodName(method),
  150. parameters_declaration = interopParameters,
  151. body = bodySb.ToString().TrimEnd('\r', '\n')
  152. };
  153. }
  154. /// <summary>
  155. /// Generates the entry point name for a method
  156. /// </summary>
  157. public static string GenerateEntryPointName(IMethodSymbol method)
  158. {
  159. var namespaceParts = method.ContainingNamespace.ToDisplayString().ToLowerInvariant().Replace(".", "_");
  160. var typeName = method.ContainingType.Name.ToLowerInvariant();
  161. var methodName = method.Name.ToLowerInvariant();
  162. return $"{namespaceParts}_{typeName}_{methodName}";
  163. }
  164. private static string GenerateMethodName(IMethodSymbol method)
  165. {
  166. var name = $"{method.ContainingType.Name}_{method.Name}";
  167. if (method.Parameters.Length > 0)
  168. {
  169. var paramTypes = string.Join("_", method.Parameters.Select(p => SanitizeTypeName(p.Type.Name)));
  170. name += "_" + paramTypes;
  171. }
  172. return name;
  173. }
  174. private static string SanitizeTypeName(string typeName)
  175. {
  176. return typeName
  177. .Replace("<", "_")
  178. .Replace(">", "_")
  179. .Replace(",", "_")
  180. .Replace(" ", "")
  181. .Replace("?", "Nullable")
  182. .Replace("[]", "Array")
  183. .Replace("*", "Ptr")
  184. .Replace("&", "Ref");
  185. }
  186. }