CSharpInteropGenerator.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Text;
  4. using Microsoft.CodeAnalysis;
  5. namespace QuestPDF.InteropGenerators;
  6. /// <summary>
  7. /// Generates C# UnmanagedCallersOnly bindings for interop
  8. /// </summary>
  9. public static class CSharpInteropGenerator
  10. {
  11. /// <summary>
  12. /// Generates the complete C# interop code
  13. /// </summary>
  14. public static string GenerateInteropCode(List<IMethodSymbol> extensionMethods)
  15. {
  16. var sb = new StringBuilder();
  17. sb.AppendLine("// <auto-generated/>");
  18. sb.AppendLine("#nullable enable");
  19. sb.AppendLine();
  20. sb.AppendLine("using System;");
  21. sb.AppendLine("using System.Runtime.CompilerServices;");
  22. sb.AppendLine("using System.Runtime.InteropServices;");
  23. sb.AppendLine();
  24. sb.AppendLine("namespace QuestPDF.Generated;");
  25. sb.AppendLine();
  26. sb.AppendLine("public static unsafe class GeneratedInterop");
  27. sb.AppendLine("{");
  28. // Add helper methods
  29. GenerateHelperMethods(sb);
  30. foreach (var method in extensionMethods)
  31. {
  32. GenerateInteropMethod(sb, method);
  33. }
  34. sb.AppendLine("}");
  35. return sb.ToString();
  36. }
  37. private static void GenerateHelperMethods(StringBuilder sb)
  38. {
  39. sb.AppendLine(" static IntPtr BoxHandle(object obj)");
  40. sb.AppendLine(" {");
  41. sb.AppendLine(" var gch = GCHandle.Alloc(obj, GCHandleType.Normal);");
  42. sb.AppendLine(" return GCHandle.ToIntPtr(gch);");
  43. sb.AppendLine(" }");
  44. sb.AppendLine();
  45. sb.AppendLine(" static T UnboxHandle<T>(nint handle) where T : class");
  46. sb.AppendLine(" {");
  47. sb.AppendLine(" var gch = GCHandle.FromIntPtr(handle);");
  48. sb.AppendLine(" return (T)gch.Target!;");
  49. sb.AppendLine(" }");
  50. sb.AppendLine();
  51. sb.AppendLine(" [UnmanagedCallersOnly(EntryPoint = \"questpdf_free_handle\", CallConvs = new[] { typeof(CallConvCdecl) })]");
  52. sb.AppendLine(" public static void FreeHandle(nint handle)");
  53. sb.AppendLine(" {");
  54. sb.AppendLine(" if (handle == 0) return;");
  55. sb.AppendLine(" var gch = GCHandle.FromIntPtr(handle);");
  56. sb.AppendLine(" if (gch.IsAllocated) gch.Free();");
  57. sb.AppendLine(" }");
  58. sb.AppendLine();
  59. }
  60. private static void GenerateInteropMethod(StringBuilder sb, IMethodSymbol method)
  61. {
  62. sb.AppendLine();
  63. if (!PublicApiAnalyzer.IsSupported(method))
  64. {
  65. GenerateUnsupportedMethodComment(sb, method);
  66. return;
  67. }
  68. var methodName = GenerateMethodName(method);
  69. var isExtensionMethod = method.IsExtensionMethod;
  70. var isInstanceMethod = !method.IsStatic && !isExtensionMethod;
  71. // Determine interop signature (reference types become nint handles)
  72. var interopReturnType = PublicApiAnalyzer.IsReferenceType(method.ReturnType)
  73. ? "nint"
  74. : method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  75. // For instance methods, add 'this' parameter as first parameter
  76. var interopParametersList = new List<string>();
  77. if (isInstanceMethod)
  78. {
  79. // Instance methods need the 'this' object as first parameter
  80. interopParametersList.Add("nint @this");
  81. }
  82. interopParametersList.AddRange(method.Parameters.Select(p =>
  83. {
  84. var paramType = PublicApiAnalyzer.IsReferenceType(p.Type)
  85. ? "nint"
  86. : p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  87. return $"{paramType} {p.Name}";
  88. }));
  89. var interopParameters = string.Join(", ", interopParametersList);
  90. sb.AppendLine($" [UnmanagedCallersOnly(EntryPoint = \"{GenerateEntryPointName(method)}\", CallConvs = new[] {{ typeof(CallConvCdecl) }})]");
  91. sb.AppendLine($" public static {interopReturnType} {methodName}({interopParameters})");
  92. sb.AppendLine(" {");
  93. // For instance methods, unbox the 'this' parameter
  94. if (isInstanceMethod)
  95. {
  96. var containingType = method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  97. sb.AppendLine($" var this_obj = UnboxHandle<{containingType}>(@this);");
  98. }
  99. // Unbox reference type parameters
  100. foreach (var param in method.Parameters)
  101. {
  102. if (PublicApiAnalyzer.IsReferenceType(param.Type))
  103. {
  104. var actualType = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
  105. sb.AppendLine($" var {param.Name}_obj = UnboxHandle<{actualType}>({param.Name});");
  106. }
  107. }
  108. // Build argument list (use unboxed versions for reference types)
  109. var arguments = string.Join(", ", method.Parameters.Select(p =>
  110. PublicApiAnalyzer.IsReferenceType(p.Type) ? $"{p.Name}_obj" : p.Name));
  111. // Build the call target
  112. string callTarget;
  113. if (isInstanceMethod)
  114. {
  115. callTarget = $"this_obj.{method.Name}";
  116. }
  117. else
  118. {
  119. callTarget = $"{method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{method.Name}";
  120. }
  121. // Call the method and handle the result
  122. if (method.ReturnsVoid)
  123. {
  124. sb.AppendLine($" {callTarget}({arguments});");
  125. }
  126. else if (PublicApiAnalyzer.IsReferenceType(method.ReturnType))
  127. {
  128. sb.AppendLine($" var result = {callTarget}({arguments});");
  129. sb.AppendLine($" return BoxHandle(result);");
  130. }
  131. else
  132. {
  133. sb.AppendLine($" return {callTarget}({arguments});");
  134. }
  135. sb.AppendLine(" }");
  136. }
  137. /// <summary>
  138. /// Generates the entry point name for a method
  139. /// </summary>
  140. public static string GenerateEntryPointName(IMethodSymbol method)
  141. {
  142. var namespaceParts = method.ContainingNamespace.ToDisplayString().ToLowerInvariant().Replace(".", "_");
  143. var typeName = method.ContainingType.Name.ToLowerInvariant();
  144. var methodName = method.Name.ToLowerInvariant();
  145. return $"{namespaceParts}_{typeName}_{methodName}";
  146. }
  147. private static void GenerateUnsupportedMethodComment(StringBuilder sb, IMethodSymbol method)
  148. {
  149. var returnType = method.ReturnType.ToDisplayString();
  150. var parameters = string.Join(", ", method.Parameters.Select(p =>
  151. {
  152. var refKind = p.RefKind switch
  153. {
  154. RefKind.Ref => "ref ",
  155. RefKind.Out => "out ",
  156. RefKind.In => "in ",
  157. _ => ""
  158. };
  159. return $"{refKind}{p.Type.ToDisplayString()} {p.Name}";
  160. }));
  161. var fullSignature = $"{method.ContainingNamespace}.{method.ContainingType.Name}.{method.Name}({parameters}) : {returnType}";
  162. sb.AppendLine($" // UNSUPPORTED: {fullSignature}");
  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. }