diff --git a/Source/MochaTool.InteropGen/CodeGen/BaseCodeGenerator.cs b/Source/MochaTool.InteropGen/CodeGen/BaseCodeGenerator.cs deleted file mode 100644 index 7587c39c..00000000 --- a/Source/MochaTool.InteropGen/CodeGen/BaseCodeGenerator.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace MochaTool.InteropGen; - -internal class BaseCodeGenerator -{ - protected List Units { get; } = new(); - - public BaseCodeGenerator( List units ) - { - Units = units; - } - - protected string GetHeader() - { - return $""" - //------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // InteropGen generated on {DateTime.Now} - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - //------------------------------------------------------------------------------ - """; - } -} diff --git a/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs b/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs index 6cfb0fa1..eeb6a576 100644 --- a/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs +++ b/Source/MochaTool.InteropGen/CodeGen/ManagedCodeGenerator.cs @@ -1,31 +1,103 @@ -using System.CodeDom.Compiler; +using MochaTool.InteropGen.Parsing; +using System.CodeDom.Compiler; +using System.Collections.Immutable; -namespace MochaTool.InteropGen; +namespace MochaTool.InteropGen.CodeGen; -sealed class ManagedCodeGenerator : BaseCodeGenerator +/// +/// Contains functionality for generating C# code. +/// +internal static class ManagedCodeGenerator { - public ManagedCodeGenerator( List units ) : base( units ) + /// + /// The namespace that all generated code will be under. + /// + private const string Namespace = "Mocha.Glue"; + + /// + /// An array containing all using declarations for generated code. + /// + private static readonly string[] Usings = new[] { - } - - private List GetUsings() + "System.Runtime.InteropServices", + "System.Runtime.Serialization", + "Mocha.Common" + }; + + /// + /// The header to be used at the top of generated code. + /// + private static string Header => $""" + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // InteropGen generated on {DateTime.Now} + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + """; + + /// + /// Generates and returns C# code for a set of s. + /// + /// An enumerable list of s to generate code for. + /// C# code representing the set of s passed. + internal static string GenerateCode( IEnumerable units ) { - return new() { "System.Runtime.InteropServices", "System.Runtime.Serialization", "Mocha.Common" }; - } + var (baseTextWriter, writer) = Utils.CreateWriter(); - private string GetNamespace() - { - return "Mocha.Glue"; + // Write header. + writer.WriteLine( Header ); + writer.WriteLine(); + + // Write using statements. + foreach ( var usingStatement in Usings ) + writer.WriteLine( $"using {usingStatement};" ); + + // Write namespace. + writer.WriteLine(); + writer.WriteLine( $"namespace {Namespace};" ); + writer.WriteLine(); + + // Write each unit. + foreach ( var unit in units ) + { + switch ( unit ) + { + case Class c when c.IsNamespace: + GenerateNamespaceCode( writer, c ); + break; + case Class c: + GenerateClassCode( writer, c ); + break; + case Struct s: + GenerateStructCode( writer, s ); + break; + default: + continue; + } + + writer.WriteLine(); + } + + return baseTextWriter.ToString(); } - private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) + /// + /// Generates C# code for a class. + /// + /// The writer to append the code to. + /// The class to write code for. + private static void GenerateClassCode( IndentedTextWriter writer, Class c ) { // // Gather everything we need into nice lists // List decls = new(); - foreach ( var method in sel.Methods ) + foreach ( var method in c.Methods ) { var returnType = Utils.GetManagedType( method.ReturnType ); var name = method.Name; @@ -56,13 +128,13 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) // any parameters. The return type is the last type argument passed to // the delegate. // - decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{sel.Name}_{name}MethodPtr;" ); + decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{c.Name}_{name}MethodPtr;" ); } // // Write shit // - writer.WriteLine( $"public unsafe class {sel.Name} : INativeGlue" ); + writer.WriteLine( $"public unsafe class {c.Name} : INativeGlue" ); writer.WriteLine( "{" ); writer.Indent++; @@ -71,18 +143,16 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) // Decls writer.WriteLine(); foreach ( var decl in decls ) - { writer.WriteLine( decl ); - } writer.WriteLine(); // Ctor - if ( sel.Methods.Any( x => x.IsConstructor ) ) + if ( c.Methods.Any( x => x.IsConstructor ) ) { - var ctor = sel.Methods.First( x => x.IsConstructor ); + var ctor = c.Methods.First( x => x.IsConstructor ); var managedCtorArgs = string.Join( ", ", ctor.Parameters.Select( x => $"{Utils.GetManagedType( x.Type )} {x.Name}" ) ); - writer.WriteLine( $"public {sel.Name}( {managedCtorArgs} )" ); + writer.WriteLine( $"public {c.Name}( {managedCtorArgs} )" ); writer.WriteLine( "{" ); writer.Indent++; @@ -94,7 +164,7 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) } // Methods - foreach ( var method in sel.Methods ) + foreach ( var method in c.Methods ) { writer.WriteLine(); @@ -122,7 +192,7 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) writer.Indent++; // Spin up a MemoryContext instance - writer.WriteLine( $"using var ctx = new MemoryContext( \"{sel.Name}.{name}\" );" ); + writer.WriteLine( $"using var ctx = new MemoryContext( \"{c.Name}.{name}\" );" ); // // Gather function body @@ -131,7 +201,7 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) // We need to pass the instance in if this is not a static method if ( !method.IsStatic ) - paramsAndInstance = paramsAndInstance.Prepend( new Variable( "NativePtr", "IntPtr" ) ).ToList(); + paramsAndInstance = paramsAndInstance.Prepend( new Variable( "NativePtr", "IntPtr" ) ).ToImmutableArray(); // Gather function call arguments. Make sure that we're passing in a pointer for everything var paramNames = paramsAndInstance.Select( x => "ctx.GetPtr( " + x.Name + " )" ); @@ -175,30 +245,38 @@ private void GenerateClassCode( ref IndentedTextWriter writer, Class sel ) writer.WriteLine( "}" ); } - private void GenerateStructCode( ref IndentedTextWriter writer, Structure sel ) + /// + /// Generates C# code for a struct. + /// + /// The writer to append the code to. + /// The struct to write code for. + private static void GenerateStructCode( IndentedTextWriter writer, Struct s ) { writer.WriteLine( $"[StructLayout( LayoutKind.Sequential )]" ); - writer.WriteLine( $"public struct {sel.Name}" ); + writer.WriteLine( $"public struct {s.Name}" ); writer.WriteLine( "{" ); writer.Indent++; - foreach ( var field in sel.Fields ) - { + foreach ( var field in s.Fields ) writer.WriteLine( $"public {Utils.GetManagedType( field.Type )} {field.Name};" ); - } writer.Indent--; writer.WriteLine( "}" ); } - private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) + /// + /// Generates C# code for a namespace. + /// + /// The writer to append the code to. + /// The namespace to write code for. + private static void GenerateNamespaceCode( IndentedTextWriter writer, Class ns ) { // // Gather everything we need into nice lists // List decls = new(); - foreach ( var method in sel.Methods ) + foreach ( var method in ns.Methods ) { var returnType = Utils.GetManagedType( method.ReturnType ); var name = method.Name; @@ -220,24 +298,22 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) // any parameters. The return type is the last type argument passed to // the delegate. // - decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{sel.Name}_{name}MethodPtr;" ); + decls.Add( $"private static {delegateSignature} _{name} = ({delegateSignature})Mocha.Common.Global.UnmanagedArgs.__{ns.Name}_{name}MethodPtr;" ); } // // Write shit // - writer.WriteLine( $"public static unsafe class {sel.Name}" ); + writer.WriteLine( $"public static unsafe class {ns.Name}" ); writer.WriteLine( "{" ); writer.Indent++; writer.WriteLine(); foreach ( var decl in decls ) - { writer.WriteLine( decl ); - } // Methods - foreach ( var method in sel.Methods ) + foreach ( var method in ns.Methods ) { writer.WriteLine(); @@ -252,10 +328,9 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) writer.Indent++; // Spin up a MemoryContext instance - writer.WriteLine( $"using var ctx = new MemoryContext( \"{sel.Name}.{name}\" );" ); + writer.WriteLine( $"using var ctx = new MemoryContext( \"{ns.Name}.{name}\" );" ); - var @params = method.Parameters; - var paramNames = @params.Select( x => "ctx.GetPtr( " + x.Name + " )" ); + var paramNames = method.Parameters.Select( x => "ctx.GetPtr( " + x.Name + " )" ); var functionCallArgs = string.Join( ", ", paramNames ); if ( returnsPointer ) @@ -293,39 +368,4 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class sel ) writer.Indent--; writer.WriteLine( "}" ); } - - public string GenerateManagedCode() - { - var (baseTextWriter, writer) = Utils.CreateWriter(); - - writer.WriteLine( GetHeader() ); - writer.WriteLine(); - - foreach ( var usingStatement in GetUsings() ) - writer.WriteLine( $"using {usingStatement};" ); - - writer.WriteLine(); - writer.WriteLine( $"namespace {GetNamespace()};" ); - writer.WriteLine(); - - foreach ( var unit in Units ) - { - if ( unit is Class c ) - { - if ( c.IsNamespace ) - GenerateNamespaceCode( ref writer, c ); - else - GenerateClassCode( ref writer, c ); - } - - if ( unit is Structure s ) - { - GenerateStructCode( ref writer, s ); - } - - writer.WriteLine(); - } - - return baseTextWriter.ToString(); - } } diff --git a/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs b/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs index 645f06ec..5721ae72 100644 --- a/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs +++ b/Source/MochaTool.InteropGen/CodeGen/NativeCodeGenerator.cs @@ -1,18 +1,40 @@ -using System.CodeDom.Compiler; +using MochaTool.InteropGen.Parsing; +using System.CodeDom.Compiler; +using System.Collections.Immutable; -namespace MochaTool.InteropGen; +namespace MochaTool.InteropGen.CodeGen; -sealed class NativeCodeGenerator : BaseCodeGenerator +/// +/// Contains functionality for generating C++ code. +/// +internal static class NativeCodeGenerator { - public NativeCodeGenerator( List units ) : base( units ) - { - } - - public string GenerateNativeCode( string headerPath ) + /// + /// The header to be used at the top of generated code. + /// + private static string Header => $""" + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // InteropGen generated on {DateTime.Now} + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + """; + + /// + /// Generates and returns C++ code for a set of s. + /// + /// The path to the header file that contained the units. + /// An enumerable list of s to generate code for. + /// C++ code representing the set of s passed. + internal static string GenerateCode( string headerPath, IEnumerable units ) { var (baseTextWriter, writer) = Utils.CreateWriter(); - writer.WriteLine( GetHeader() ); + writer.WriteLine( Header ); writer.WriteLine(); writer.WriteLine( "#pragma once" ); @@ -20,15 +42,15 @@ public string GenerateNativeCode( string headerPath ) writer.WriteLine(); - foreach ( var unit in Units ) + foreach ( var unit in units ) { - if ( unit is Class c ) - { - if ( c.IsNamespace ) - GenerateNamespaceCode( ref writer, c ); - else - GenerateClassCode( ref writer, c ); - } + if ( unit is not Class c ) + continue; + + if ( c.IsNamespace ) + GenerateNamespaceCode( writer, c ); + else + GenerateClassCode( writer, c ); writer.WriteLine(); } @@ -36,34 +58,47 @@ public string GenerateNativeCode( string headerPath ) return baseTextWriter.ToString(); } - private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class c ) + /// + /// Generates C++ code for a class. + /// + /// The writer to append the code to. + /// The class to write code for. + private static void GenerateClassCode( IndentedTextWriter writer, Class c ) { foreach ( var method in c.Methods ) { var args = method.Parameters; + if ( !method.IsStatic ) + args = args.Prepend( new Variable( "instance", $"{c.Name}*" ) ).ToImmutableArray(); + var argStr = string.Join( ", ", args.Select( x => { if ( x.Type == "std::string" ) - { return $"const char* {x.Name}"; - } return $"{x.Type} {x.Name}"; } ) ); var signature = $"extern \"C\" inline {method.ReturnType} __{c.Name}_{method.Name}( {argStr} )"; var body = ""; - var @params = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); - - var accessor = $"{c.Name}::"; + var parameters = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); - if ( method.ReturnType == "void" ) - body += $"{accessor}{method.Name}( {@params} );"; - else if ( method.ReturnType == "std::string" ) - body += $"std::string text = {accessor}{method.Name}( {@params} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; + if ( method.IsConstructor ) + body += $"return new {c.Name}( {parameters} );"; + else if ( method.IsDestructor ) + body += $"instance->~{c.Name}( {parameters} );"; else - body += $"return {accessor}{method.Name}( {@params} );"; + { + var accessor = method.IsStatic ? $"{c.Name}::" : "instance->"; + + if ( method.ReturnType == "void" ) + body += $"{accessor}{method.Name}( {parameters} );"; + else if ( method.ReturnType == "std::string" ) + body += $"std::string text = {accessor}{method.Name}( {parameters} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; + else + body += $"return {accessor}{method.Name}( {parameters} );"; + } writer.WriteLine( signature ); writer.WriteLine( "{" ); @@ -76,48 +111,37 @@ private void GenerateNamespaceCode( ref IndentedTextWriter writer, Class c ) } } - private void GenerateClassCode( ref IndentedTextWriter writer, Class c ) + /// + /// Generates C++ code for a namespace. + /// + /// The writer to append the code to. + /// The namespace to write code for. + private static void GenerateNamespaceCode( IndentedTextWriter writer, Class ns ) { - foreach ( var method in c.Methods ) + foreach ( var method in ns.Methods ) { var args = method.Parameters; - if ( !method.IsStatic ) - args = args.Prepend( new Variable( "instance", $"{c.Name}*" ) ).ToList(); - var argStr = string.Join( ", ", args.Select( x => { if ( x.Type == "std::string" ) - { return $"const char* {x.Name}"; - } return $"{x.Type} {x.Name}"; } ) ); - var signature = $"extern \"C\" inline {method.ReturnType} __{c.Name}_{method.Name}( {argStr} )"; + var signature = $"extern \"C\" inline {method.ReturnType} __{ns.Name}_{method.Name}( {argStr} )"; var body = ""; - var @params = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); + var parameters = string.Join( ", ", method.Parameters.Select( x => x.Name ) ); - if ( method.IsConstructor ) - { - body += $"return new {c.Name}( {@params} );"; - } - else if ( method.IsDestructor ) - { - body += $"instance->~{c.Name}( {@params} );"; - } - else - { - var accessor = method.IsStatic ? $"{c.Name}::" : "instance->"; + var accessor = $"{ns.Name}::"; - if ( method.ReturnType == "void" ) - body += $"{accessor}{method.Name}( {@params} );"; - else if ( method.ReturnType == "std::string" ) - body += $"std::string text = {accessor}{method.Name}( {@params} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; - else - body += $"return {accessor}{method.Name}( {@params} );"; - } + if ( method.ReturnType == "void" ) + body += $"{accessor}{method.Name}( {parameters} );"; + else if ( method.ReturnType == "std::string" ) + body += $"std::string text = {accessor}{method.Name}( {parameters} );\r\nconst char* cstr = text.c_str();\r\nchar* dup = _strdup(cstr);\r\nreturn dup;"; + else + body += $"return {accessor}{method.Name}( {parameters} );"; writer.WriteLine( signature ); writer.WriteLine( "{" ); diff --git a/Source/MochaTool.InteropGen/Extensions/CXCursorExtensions.cs b/Source/MochaTool.InteropGen/Extensions/CXCursorExtensions.cs new file mode 100644 index 00000000..0d385ad2 --- /dev/null +++ b/Source/MochaTool.InteropGen/Extensions/CXCursorExtensions.cs @@ -0,0 +1,26 @@ +using ClangSharp.Interop; + +namespace MochaTool.InteropGen.Extensions; + +/// +/// Contains extension methods for the . +/// +internal static class CXCursorExtensions +{ + /// + /// Returns whether or not the current item the cursor is over has the "generate_bindings" attribute on it. + /// + /// The cursor to check. + /// Whether or not the current item the cursor is over has the "generate_bindings" attribute on it. + internal static bool HasGenerateBindingsAttribute( this CXCursor cursor ) + { + if ( !cursor.HasAttrs ) + return false; + + var attr = cursor.GetAttr( 0 ); + if ( attr.Spelling.CString != "generate_bindings" ) + return false; + + return true; + } +} diff --git a/Source/MochaTool.InteropGen/Extensions/ILoggerExtensions.cs b/Source/MochaTool.InteropGen/Extensions/ILoggerExtensions.cs new file mode 100644 index 00000000..0a0056b9 --- /dev/null +++ b/Source/MochaTool.InteropGen/Extensions/ILoggerExtensions.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Logging; + +namespace MochaTool.InteropGen.Extensions; + +/// +/// Contains extension methods for s. +/// +internal static partial class ILoggerExtensions +{ + /// + /// Logs the first message to the user. + /// + /// The instance to log to. + [LoggerMessage( EventId = 0, + Level = LogLevel.Information, + Message = "Generating C# <--> C++ interop code..." )] + internal static partial void LogIntro( this ILogger logger ); + + /// + /// Logs a timed operation to the user. + /// + /// The instance to log to. + /// The name of the timed operation. + /// The time in seconds that it took to complete the operation. + [LoggerMessage( EventId = 1, + Message = "{name} took {seconds} seconds." )] + internal static partial void ReportTime( this ILogger logger, LogLevel logLevel, string name, double seconds ); + + /// + /// Logs to the user that a header is being processed by the parser. + /// + /// The instance to log to. + /// The absolute path to the header file being processed. + [LoggerMessage( EventId = 2, + Level = LogLevel.Debug, + Message = "Processing header {path}..." )] + internal static partial void ProcessingHeader( this ILogger logger, string path ); + + /// + /// Logs a fatal C++ diagnostic to the user. + /// + /// The instance to log to. + /// The diagnostic to show. + [LoggerMessage( EventId = 3, + Level = LogLevel.Warning, + Message = "{diagnostic}" )] + internal static partial void FatalDiagnostic( this ILogger logger, string diagnostic ); + + /// + /// Logs an error C++ diagnostic to the user. + /// + /// The instance to log to. + /// The diagnostic to show. + [LoggerMessage( EventId = 4, + Level = LogLevel.Warning, + Message = "{diagnostic}" )] + internal static partial void ErrorDiagnostic( this ILogger logger, string diagnostic ); + + /// + /// Logs a warning C++ diagnostic to the user. + /// + /// The instance to log to. + /// The diagnostic to show. + [LoggerMessage( EventId = 5, + Level = LogLevel.Warning, + Message = "{diagnostic}" )] + internal static partial void WarnDiagnostic( this ILogger logger, string diagnostic ); +} diff --git a/Source/MochaTool.InteropGen/Global.cs b/Source/MochaTool.InteropGen/Global.cs new file mode 100644 index 00000000..9049ac6e --- /dev/null +++ b/Source/MochaTool.InteropGen/Global.cs @@ -0,0 +1,26 @@ +global using static MochaTool.InteropGen.Global; +using Microsoft.Extensions.Logging; + +namespace MochaTool.InteropGen; + +/// +/// Contains globally used items in the project. +/// +internal static class Global +{ + /// + /// The instance of to use when logging. + /// + internal static readonly ILogger Log; + + /// + /// Initializes the instance. + /// + static Global() + { + using var factory = LoggerFactory.Create( builder => builder + .AddConsole() + .SetMinimumLevel( LogLevel.Information ) ); + Log = factory.CreateLogger( "InteropGen" ); + } +} diff --git a/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj b/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj index 1ff8a9e9..8a1951e0 100644 --- a/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj +++ b/Source/MochaTool.InteropGen/MochaTool.InteropGen.csproj @@ -15,6 +15,9 @@ + + + diff --git a/Source/MochaTool.InteropGen/Parser.cs b/Source/MochaTool.InteropGen/Parser.cs deleted file mode 100644 index 05a6904f..00000000 --- a/Source/MochaTool.InteropGen/Parser.cs +++ /dev/null @@ -1,195 +0,0 @@ -using ClangSharp.Interop; -namespace MochaTool.InteropGen; - -public static class Parser -{ - /// - /// Cached launch arguments so that we don't have to regenerate them every time - /// - private static string[] s_launchArgs = GetLaunchArgs(); - private static string[] GetLaunchArgs() - { - // Generate includes from vcxproj - var includeDirs = VcxprojParser.ParseIncludes( "../Mocha.Host/Mocha.Host.vcxproj" ); - - var args = new List - { - "-x", - "c++", - "-fparse-all-comments", - "-std=c++20", - "-DVK_NO_PROTOTYPES", - "-DNOMINMAX", - "-DVK_USE_PLATFORM_WIN32_KHR" - }; - - args.AddRange( includeDirs.Select( x => "-I" + x ) ); - - return args.ToArray(); - } - - public unsafe static List GetUnits( string path ) - { - List units = new(); - - using var index = CXIndex.Create(); - using var unit = CXTranslationUnit.Parse( index, path, s_launchArgs, ReadOnlySpan.Empty, CXTranslationUnit_Flags.CXTranslationUnit_None ); - - for ( int i = 0; i < unit.NumDiagnostics; ++i ) - { - var diagnostics = unit.GetDiagnostic( (uint)i ); - Console.WriteLine( $"{diagnostics.Format( CXDiagnostic.DefaultDisplayOptions )}" ); - } - - var cursor = unit.Cursor; - - CXCursorVisitor cursorVisitor = ( CXCursor cursor, CXCursor parent, void* data ) => - { - if ( !cursor.Location.IsFromMainFile ) - return CXChildVisitResult.CXChildVisit_Continue; - - bool HasGenerateBindingsAttribute() - { - if ( !cursor.HasAttrs ) - return false; - - var attr = cursor.GetAttr( 0 ); - if ( attr.Spelling.CString != "generate_bindings" ) - return false; - - return true; - } - - switch ( cursor.Kind ) - { - // - // Struct / class / namespace - // - case CXCursorKind.CXCursor_ClassDecl: - units.Add( new Class( cursor.Spelling.ToString() ) ); - break; - case CXCursorKind.CXCursor_StructDecl: - units.Add( new Structure( cursor.Spelling.ToString() ) ); - break; - case CXCursorKind.CXCursor_Namespace: - units.Add( new Class( cursor.Spelling.ToString() ) - { - IsNamespace = true - } ); - break; - - // - // Methods - // - case CXCursorKind.CXCursor_Constructor: - case CXCursorKind.CXCursor_CXXMethod: - case CXCursorKind.CXCursor_FunctionDecl: - { - if ( !HasGenerateBindingsAttribute() ) - return CXChildVisitResult.CXChildVisit_Continue; - - var oName = cursor.LexicalParent.Spelling.ToString(); - var o = units.FirstOrDefault( x => x.Name == oName ); - var m = new Method( cursor.Spelling.ToString(), cursor.ReturnType.Spelling.ToString() ) - { - IsStatic = cursor.IsStatic - }; - - if ( o == null ) - { - Console.WriteLine( "No unit" ); - break; - } - - CXCursorVisitor methodChildVisitor = ( CXCursor cursor, CXCursor parent, void* data ) => - { - if ( cursor.Kind == CXCursorKind.CXCursor_ParmDecl ) - { - var type = cursor.Type.ToString(); - var name = cursor.Spelling.ToString(); - - var parameter = new Variable( name, type ); - - m.Parameters.Add( parameter ); - } - - return CXChildVisitResult.CXChildVisit_Recurse; - }; - - cursor.VisitChildren( methodChildVisitor, default ); - - if ( cursor.Kind == CXCursorKind.CXCursor_Constructor ) - { - // Constructor specific stuff here - m.ReturnType = $"{o.Name}*"; - m.Name = "Ctor"; - m.IsConstructor = true; - } - - if ( cursor.CXXAccessSpecifier == CX_CXXAccessSpecifier.CX_CXXPublic || cursor.Kind == CXCursorKind.CXCursor_FunctionDecl ) - o.Methods.Add( m ); - - break; - } - - // - // Field - // - case CXCursorKind.CXCursor_FieldDecl: - { - if ( !HasGenerateBindingsAttribute() ) - return CXChildVisitResult.CXChildVisit_Continue; - - var oName = cursor.LexicalParent.Spelling.ToString(); - var s = units.FirstOrDefault( x => x.Name == oName ); - - if ( s == null ) - break; - - s.Fields.Add( new Variable( cursor.Spelling.ToString(), cursor.Type.ToString() ) ); - break; - } - - default: - break; - } - - return CXChildVisitResult.CXChildVisit_Recurse; - }; - - cursor.VisitChildren( cursorVisitor, default ); - - // - // Remove all items with duplicate names - // - for ( int i = 0; i < units.Count; i++ ) - { - var o = units[i]; - o.Methods = o.Methods.GroupBy( x => x.Name ).Select( x => x.First() ).ToList(); - o.Fields = o.Fields.GroupBy( x => x.Name ).Select( x => x.First() ).ToList(); - } - - // - // Remove any units that have no methods or fields - // - units = units.Where( x => x.Methods.Count > 0 || x.Fields.Count > 0 ).ToList(); - - // - // Post-processing - // - //foreach ( var o in units ) - //{ - // // Create a default constructor if one wasn't already defined - // if ( !o.Methods.Any( x => x.IsConstructor ) && o is not Class { IsNamespace: true } ) - // { - // Console.WriteLine( $"Creating default ctor for {o.Name}" ); - // o.Methods.Add( new Method( "Ctor", $"{o.Name}*" ) - // { - // IsConstructor = true - // } ); - // } - //} - - return units; - } -} diff --git a/Source/MochaTool.InteropGen/Parsing/Class.cs b/Source/MochaTool.InteropGen/Parsing/Class.cs new file mode 100644 index 00000000..0ec9c643 --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Class.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a class or namespace in C++. +/// +internal sealed class Class : IUnit +{ + /// + public string Name { get; } + /// + public bool IsNamespace { get; } + + /// + public ImmutableArray Fields { get; } + /// + public ImmutableArray Methods { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the class or namespace. + /// Whether or not it is a class or namespace. + /// All of the fields that are contained. + /// All of the methods that are contained. + private Class( string name, bool isNamespace, in ImmutableArray fields, in ImmutableArray methods ) + { + Name = name; + IsNamespace = isNamespace; + + Fields = fields; + Methods = methods; + } + + /// + /// Returns a new instance of the with the fields given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the fields given. + internal Class WithFields( in ImmutableArray fields ) + { + return new Class( Name, IsNamespace, fields, Methods ); + } + + /// + /// Returns a new instance of the with the methods given. + /// + /// The new methods to place in the instance. + /// A new instance of the with the methods given. + internal Class WithMethods( in ImmutableArray methods ) + { + return new Class( Name, IsNamespace, Fields, methods ); + } + + /// + public override string ToString() + { + return Name; + } + + /// + IUnit IUnit.WithFields( in ImmutableArray fields ) => WithFields( fields ); + /// + IUnit IUnit.WithMethods( in ImmutableArray methods ) => WithMethods( methods ); + + /// + /// Returns a new instance of . + /// + /// The name of the class. + /// The fields contained in the class. + /// The methods contained in the class. + /// A new instance of . + internal static Class NewClass( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + return new Class( name, false, fields, methods ); + } + + /// + /// Returns a new instance of as a namespace. + /// + /// The name of the namespace. + /// The fields contained in the namespace. + /// The methods contained in the namespace. + /// A new instance of as a namespace. + internal static Class NewNamespace( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + return new Class( name, true, fields, methods ); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/IUnit.cs b/Source/MochaTool.InteropGen/Parsing/IUnit.cs new file mode 100644 index 00000000..ad8b68fb --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/IUnit.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Defines a container for fields and methods defined in C++. +/// +internal interface IUnit +{ + /// + /// The name of the . + /// + string Name { get; } + + /// + /// All of the fields contained in the . + /// + ImmutableArray Fields { get; } + /// + /// All of the methods contained in the . + /// + ImmutableArray Methods { get; } + + /// + /// Returns a new instance of the with the fields given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the fields given. + IUnit WithFields( in ImmutableArray fields ); + /// + /// Returns a new instance of the with the methods given. + /// + /// The new methods to place in the instance. + /// A new instance of the with the methods given. + IUnit WithMethods( in ImmutableArray methods ); +} diff --git a/Source/MochaTool.InteropGen/Parsing/Method.cs b/Source/MochaTool.InteropGen/Parsing/Method.cs new file mode 100644 index 00000000..8b9540cc --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Method.cs @@ -0,0 +1,111 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a method in C++. +/// +internal sealed class Method +{ + /// + /// The name of the method. + /// + internal string Name { get; } + /// + /// The literal string containing the return type of the method. + /// + internal string ReturnType { get; } + + /// + /// Whether or not the method is a constructor. + /// + internal bool IsConstructor { get; } = false; + /// + /// Whether or not the method is a destructor. + /// + internal bool IsDestructor { get; } = false; + /// + /// Whether or not the method is static. + /// + internal bool IsStatic { get; } = false; + + /// + /// An array of all the parameters in the method. + /// + internal ImmutableArray Parameters { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// Whether or not the method is a constructor. + /// Whether or not the method is a destructor. + /// Whether or not the method is static. + /// An array of all the parameters in the method. + private Method( string name, string returnType, bool isConstructor, bool isDestructor, bool isStatic, in ImmutableArray parameters ) + { + Name = name; + ReturnType = returnType; + + IsConstructor = isConstructor; + IsDestructor = isDestructor; + IsStatic = isStatic; + + Parameters = parameters; + } + + /// + /// Returns a new instance of the with the parameters given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the parameters given. + internal Method WithParameters( in ImmutableArray parameters ) + { + return new( Name, ReturnType, IsConstructor, IsDestructor, IsStatic, parameters ); + } + + /// + public override string ToString() + { + var p = string.Join( ", ", Parameters ); + return $"{ReturnType} {Name}( {p} )"; + } + + /// + /// Returns a new instance of as a constructor. + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// An array of all the parameters in the method. + /// A new instance of as a constructor. + internal static Method NewConstructor( string name, string returnType, in ImmutableArray parameters ) + { + return new( name, returnType, true, false, false, parameters ); + } + + /// + /// Returns a new instance of as a destructor. + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// An array of all the parameters in the method. + /// A new instance of as a destructor. + internal static Method NewDestructor( string name, string returnType, in ImmutableArray parameters ) + { + return new( name, returnType, false, true, false, parameters ); + } + + /// + /// Returns a new instance of . + /// + /// The name of the method. + /// The literal string containing the return type of the method. + /// Whether or not the method is static. + /// An array of all the parameters in the method. + /// A new instance of . + internal static Method NewMethod( string name, string returnType, bool isStatic, in ImmutableArray parameters ) + { + return new( name, returnType, false, false, isStatic, parameters ); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/Parser.cs b/Source/MochaTool.InteropGen/Parsing/Parser.cs new file mode 100644 index 00000000..14810da4 --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Parser.cs @@ -0,0 +1,249 @@ +using ClangSharp.Interop; +using Microsoft.Extensions.Logging; +using MochaTool.InteropGen.Extensions; +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Contains all parsing functionality for C++ header files. +/// +internal static class Parser +{ + /// + /// Cached launch arguments so that we don't have to regenerate them every time + /// + private static readonly string[] s_launchArgs = GetLaunchArgs(); + + /// + /// Parses a header file and returns all of the s contained inside. + /// + /// The absolute path to the header file to parse. + /// All of the s contained inside the header file. + internal unsafe static IEnumerable GetUnits( string path ) + { + var units = new List(); + + using var index = CXIndex.Create(); + using var unit = CXTranslationUnit.Parse( index, path, s_launchArgs, ReadOnlySpan.Empty, CXTranslationUnit_Flags.CXTranslationUnit_None ); + + // Only start walking diagnostics if logging is enabled to the minimum level. + if ( Log.IsEnabled( LogLevel.Warning ) ) + { + for ( var i = 0; i < unit.NumDiagnostics; i++ ) + { + var diagnostics = unit.GetDiagnostic( (uint)i ); + switch ( diagnostics.Severity ) + { + case CXDiagnosticSeverity.CXDiagnostic_Fatal: + Log.FatalDiagnostic( diagnostics.Format( CXDiagnostic.DefaultDisplayOptions ).CString ); + break; + case CXDiagnosticSeverity.CXDiagnostic_Error: + Log.ErrorDiagnostic( diagnostics.Format( CXDiagnostic.DefaultDisplayOptions ).CString ); + break; + case CXDiagnosticSeverity.CXDiagnostic_Warning: + Log.WarnDiagnostic( diagnostics.Format( CXDiagnostic.DefaultDisplayOptions ).CString ); + break; + } + } + } + + CXChildVisitResult cursorVisitor( CXCursor cursor, CXCursor parent, void* data ) + { + if ( !cursor.Location.IsFromMainFile ) + return CXChildVisitResult.CXChildVisit_Continue; + + switch ( cursor.Kind ) + { + // + // Struct / class / namespace + // + case CXCursorKind.CXCursor_ClassDecl: + units.Add( Class.NewClass( cursor.Spelling.ToString(), ImmutableArray.Empty, ImmutableArray.Empty ) ); + break; + case CXCursorKind.CXCursor_StructDecl: + units.Add( Struct.NewStructure( cursor.Spelling.ToString(), ImmutableArray.Empty, ImmutableArray.Empty ) ); + break; + case CXCursorKind.CXCursor_Namespace: + units.Add( Class.NewNamespace( cursor.Spelling.ToString(), ImmutableArray.Empty, ImmutableArray.Empty ) ); + break; + + // + // Methods + // + case CXCursorKind.CXCursor_Constructor: + case CXCursorKind.CXCursor_CXXMethod: + case CXCursorKind.CXCursor_FunctionDecl: + return VisitMethod( cursor, units ); + + // + // Field + // + case CXCursorKind.CXCursor_FieldDecl: + return VisitField( cursor, units ); + } + + return CXChildVisitResult.CXChildVisit_Recurse; + } + + unit.Cursor.VisitChildren( cursorVisitor, default ); + + // + // Remove all items with duplicate names + // + for ( int i = 0; i < units.Count; i++ ) + { + var item = units[i]; + item = item.WithFields( item.Fields.GroupBy( x => x.Name ).Select( x => x.First() ).ToImmutableArray() ) + .WithMethods( item.Methods.GroupBy( x => x.Name ).Select( x => x.First() ).ToImmutableArray() ); + + units[i] = item; + } + + // + // Remove any units that have no methods or fields + // + units = units.Where( x => x.Methods.Length > 0 || x.Fields.Length > 0 ).ToList(); + + return units; + } + + /// + /// The visitor method for walking a method declaration. + /// + /// The cursor that is traversing the method. + /// The collection to fetch method owners from. + /// The next action the cursor should take in traversal. + private static unsafe CXChildVisitResult VisitMethod( in CXCursor cursor, ICollection units ) + { + // Early bails. + if ( !cursor.HasGenerateBindingsAttribute() ) + return CXChildVisitResult.CXChildVisit_Continue; + if ( cursor.CXXAccessSpecifier != CX_CXXAccessSpecifier.CX_CXXPublic && cursor.Kind != CXCursorKind.CXCursor_FunctionDecl ) + return CXChildVisitResult.CXChildVisit_Continue; + + // Verify that the method has an owner. + var ownerName = cursor.LexicalParent.Spelling.ToString(); + var owner = units.FirstOrDefault( x => x.Name == ownerName ); + if ( owner is null ) + return CXChildVisitResult.CXChildVisit_Continue; + + string name; + string returnType; + bool isStatic; + bool isConstructor; + bool isDestructor; + + var parametersBuilder = ImmutableArray.CreateBuilder(); + // We're traversing a constructor. + if ( cursor.Kind == CXCursorKind.CXCursor_Constructor ) + { + name = "Ctor"; + returnType = owner.Name + '*'; + isStatic = false; + isConstructor = true; + isDestructor = false; + } + // We're traversing a destructor. + else if ( cursor.Kind == CXCursorKind.CXCursor_Destructor ) + { + name = "DeCtor"; + returnType = '~' + owner.Name; + isStatic = false; + isConstructor = false; + isDestructor = true; + } + // We're traversing a standard method. + else + { + name = cursor.Spelling.ToString(); + returnType = cursor.ReturnType.Spelling.ToString(); + isStatic = cursor.IsStatic; + isConstructor = false; + isDestructor = false; + } + + // Visitor for parameter delcarations. + CXChildVisitResult methodChildVisitor( CXCursor cursor, CXCursor parent, void* data ) + { + if ( cursor.Kind != CXCursorKind.CXCursor_ParmDecl ) + return CXChildVisitResult.CXChildVisit_Continue; + + var name = cursor.Spelling.ToString(); + var type = cursor.Type.ToString(); + + parametersBuilder.Add( new Variable( name, type ) ); + + return CXChildVisitResult.CXChildVisit_Recurse; + } + + cursor.VisitChildren( methodChildVisitor, default ); + + // Construct the method. + Method method; + if ( isConstructor ) + method = Method.NewConstructor( name, returnType, parametersBuilder.ToImmutable() ); + else if ( isDestructor ) + method = Method.NewDestructor( name, returnType, parametersBuilder.ToImmutable() ); + else + method = Method.NewMethod( name, returnType, isStatic, parametersBuilder.ToImmutable() ); + + // Update owner with new method. + var newOwner = owner.WithMethods( owner.Methods.Add( method ) ); + units.Remove( owner ); + units.Add( newOwner ); + + return CXChildVisitResult.CXChildVisit_Continue; + } + + /// + /// The visitor method for walking a field declaration. + /// + /// The cursor that is traversing the method. + /// The collection to fetch method owners from. + /// The next action the cursor should take in traversal. + private static CXChildVisitResult VisitField( in CXCursor cursor, ICollection units ) + { + // Early bail. + if ( !cursor.HasGenerateBindingsAttribute() ) + return CXChildVisitResult.CXChildVisit_Continue; + + // Verify that the field has an owner. + var ownerName = cursor.LexicalParent.Spelling.ToString(); + var owner = units.FirstOrDefault( x => x.Name == ownerName ); + if ( owner is null ) + return CXChildVisitResult.CXChildVisit_Recurse; + + // Update owner with new field. + var newOwner = owner.WithFields( owner.Fields.Add( new Variable( cursor.Spelling.ToString(), cursor.Type.ToString() ) ) ); + units.Remove( owner ); + units.Add( newOwner ); + + return CXChildVisitResult.CXChildVisit_Recurse; + } + + /// + /// Returns a compiled array of launch arguments to pass to the C++ parser. + /// + /// A compiled array of launch arguments to pass to the C++ parser. + private static string[] GetLaunchArgs() + { + // Generate includes from vcxproj + var includeDirs = VcxprojParser.ParseIncludes( "../Mocha.Host/Mocha.Host.vcxproj" ); + + var args = new List + { + "-x", + "c++", + "-fparse-all-comments", + "-std=c++20", + "-DVK_NO_PROTOTYPES", + "-DNOMINMAX", + "-DVK_USE_PLATFORM_WIN32_KHR" + }; + + args.AddRange( includeDirs.Select( x => "-I" + x ) ); + + return args.ToArray(); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/Struct.cs b/Source/MochaTool.InteropGen/Parsing/Struct.cs new file mode 100644 index 00000000..d48fd6ba --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Struct.cs @@ -0,0 +1,74 @@ +using System.Collections.Immutable; + +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a struct in C++. +/// +internal sealed class Struct : IUnit +{ + /// + public string Name { get; } + + /// + public ImmutableArray Fields { get; } + /// + public ImmutableArray Methods { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the struct. + /// The fields contained in the struct. + /// The methods contained in the struct. + private Struct( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + Name = name; + + Fields = fields; + Methods = methods; + } + + /// + /// Returns a new instance of the with the fields given. + /// + /// The new fields to place in the instance. + /// A new instance of the with the fields given. + internal Struct WithFields( in ImmutableArray fields ) + { + return new( Name, fields, Methods ); + } + + /// + /// Returns a new instance of the with the methods given. + /// + /// The new methods to place in the instance. + /// A new instance of the with the methods given. + internal Struct WithMethods( in ImmutableArray methods ) + { + return new( Name, Fields, methods ); + } + + /// + public override string ToString() + { + return Name; + } + + /// + IUnit IUnit.WithFields( in ImmutableArray fields ) => WithFields( fields ); + /// + IUnit IUnit.WithMethods( in ImmutableArray methods ) => WithMethods( methods ); + + /// + /// Returns a new instance of . + /// + /// The name of the struct. + /// The fields contained in the struct. + /// The methods contained in the struct. + /// A new instance of . + internal static Struct NewStructure( string name, in ImmutableArray fields, in ImmutableArray methods ) + { + return new( name, fields, methods ); + } +} diff --git a/Source/MochaTool.InteropGen/Parsing/Variable.cs b/Source/MochaTool.InteropGen/Parsing/Variable.cs new file mode 100644 index 00000000..60c5e1e8 --- /dev/null +++ b/Source/MochaTool.InteropGen/Parsing/Variable.cs @@ -0,0 +1,33 @@ +namespace MochaTool.InteropGen.Parsing; + +/// +/// Represents a variable in C++. This can be a field, parameter, etc. +/// +internal sealed class Variable +{ + /// + /// The name of the variable. + /// + internal string Name { get; } + /// + /// The literal string containing the type of the variable. + /// + internal string Type { get; } + + /// + /// Initializes a new instance of . + /// + /// The name of the variable. + /// The literal string containing the type of the variable. + internal Variable( string name, string type ) + { + Name = name; + Type = type; + } + + /// + public override string ToString() + { + return $"{Type} {Name}"; + } +} diff --git a/Source/MochaTool.InteropGen/VcxprojParser.cs b/Source/MochaTool.InteropGen/Parsing/VcxprojParser.cs similarity index 81% rename from Source/MochaTool.InteropGen/VcxprojParser.cs rename to Source/MochaTool.InteropGen/Parsing/VcxprojParser.cs index 4ddaef8f..d7317fca 100644 --- a/Source/MochaTool.InteropGen/VcxprojParser.cs +++ b/Source/MochaTool.InteropGen/Parsing/VcxprojParser.cs @@ -1,7 +1,10 @@ using System.Xml; -namespace MochaTool.InteropGen; +namespace MochaTool.InteropGen.Parsing; +/// +/// Contains functionality for parsing vcxproj files. +/// internal static class VcxprojParser { // Note that these paths only work for the windows x64 platforms right now. @@ -11,19 +14,6 @@ internal static class VcxprojParser private const string ExternalIncludePath = "/rs:Project/rs:PropertyGroup[@Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\"]/rs:ExternalIncludePath"; private const string IncludePath = "/rs:Project/rs:PropertyGroup[@Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\"]/rs:IncludePath"; - private static string GetNodeContents( XmlNode root, string xpath, XmlNamespaceManager namespaceManager ) - { - var nodeList = root.SelectNodes( xpath, namespaceManager ); - if ( nodeList?.Count == 0 || nodeList?[0] == null ) - throw new Exception( "Couldn't find IncludePath!" ); - -#pragma warning disable CS8602 // Dereference of a possibly null reference. - var includeStr = nodeList[0].InnerText; -#pragma warning restore CS8602 // Dereference of a possibly null reference. - - return includeStr; - } - /// /// Parse the include list from a vcxproj file. /// @@ -31,20 +21,20 @@ private static string GetNodeContents( XmlNode root, string xpath, XmlNamespaceM /// This currently only supports x64-windows, so any different includes for other platforms /// will not be reflected here. /// - public static List ParseIncludes( string path ) + internal static List ParseIncludes( string path ) { - XmlDocument doc = new XmlDocument(); + var doc = new XmlDocument(); doc.Load( path ); - XmlNamespaceManager namespaceManager = new XmlNamespaceManager( doc.NameTable ); + var namespaceManager = new XmlNamespaceManager( doc.NameTable ); namespaceManager.AddNamespace( "rs", "http://schemas.microsoft.com/developer/msbuild/2003" ); - if ( doc.DocumentElement == null ) + if ( doc.DocumentElement is null ) throw new Exception( "Failed to parse root node!" ); - XmlNode root = doc.DocumentElement; + var root = doc.DocumentElement; - List includes = new(); + var includes = new List(); // Select Project -> PropertyGroup -> ExternalIncludePath { @@ -70,7 +60,7 @@ public static List ParseIncludes( string path ) { "ExternalIncludePath", "" } }; - List parsedIncludes = new(); + var parsedIncludes = new List(); // Simple find-and-replace for macros and environment variables foreach ( var include in includes ) @@ -78,13 +68,22 @@ public static List ParseIncludes( string path ) var processedInclude = include; foreach ( var environmentVariable in environmentVariables ) - { processedInclude = processedInclude.Replace( $"$({environmentVariable.Key})", environmentVariable.Value ); - } parsedIncludes.Add( processedInclude ); } return parsedIncludes; } + + private static string GetNodeContents( XmlNode root, string xpath, XmlNamespaceManager namespaceManager ) + { + var nodeList = root.SelectNodes( xpath, namespaceManager ); + if ( nodeList?.Count == 0 || nodeList?[0] is null ) + throw new Exception( "Couldn't find IncludePath!" ); + + var includeStr = nodeList[0]!.InnerText; + + return includeStr; + } } diff --git a/Source/MochaTool.InteropGen/Program.cs b/Source/MochaTool.InteropGen/Program.cs index d4dce2f7..6e815d58 100644 --- a/Source/MochaTool.InteropGen/Program.cs +++ b/Source/MochaTool.InteropGen/Program.cs @@ -1,86 +1,110 @@ -namespace MochaTool.InteropGen; +using Microsoft.Extensions.Logging; +using MochaTool.InteropGen.CodeGen; +using MochaTool.InteropGen.Extensions; +using MochaTool.InteropGen.Parsing; +namespace MochaTool.InteropGen; + +/// +/// The main entry point to the IntropGen program. +/// public static class Program { - internal static List s_generatedPaths { get; set; } = new(); - internal static List s_units { get; set; } = new(); - internal static List s_files { get; set; } = new(); - - private static void ProcessHeader( string baseDir, string path ) + /// + /// Contains all of the parsed units to generate bindings for. + /// + private static readonly List s_units = new(); + /// + /// Contains all of the files that need to be generated. + /// + private static readonly List s_files = new(); + + /// + /// The entry point to the program. + /// + /// The command-line arguments given to the program. + public static void Main( string[] args ) { - Console.WriteLine( $"Processing header {path}..." ); + using var _totalTime = new StopwatchLog( "InteropGen", LogLevel.Information ); - var units = Parser.GetUnits( path ); - var fileName = Path.GetFileNameWithoutExtension( path ); + var baseDir = args[0]; + Log.LogIntro(); - var managedGenerator = new ManagedCodeGenerator( units ); - var managedCode = managedGenerator.GenerateManagedCode(); - File.WriteAllText( $"{baseDir}Mocha.Common\\Glue\\{fileName}.generated.cs", managedCode ); + // + // Prep + // + DeleteExistingFiles( baseDir ); - var nativeGenerator = new NativeCodeGenerator( units ); - var relativePath = Path.GetRelativePath( $"{baseDir}/Mocha.Host/", path ); - var nativeCode = nativeGenerator.GenerateNativeCode( relativePath ); + using ( var _parseTime = new StopwatchLog( "Parsing" ) ) + Parse( baseDir ); - Console.WriteLine( $"{baseDir}Mocha.Host\\generated\\{fileName}.generated.h" ); - File.WriteAllText( $"{baseDir}Mocha.Host\\generated\\{fileName}.generated.h", nativeCode ); + // + // Expand methods out into list of (method name, method) + // + var methods = s_units.OfType().SelectMany( unit => unit.Methods, ( unit, method ) => (unit.Name, method) ).ToList(); - s_files.Add( fileName ); - s_units.AddRange( units ); + // + // Write files + // + WriteManagedStruct( baseDir, methods ); + WriteNativeStruct( baseDir, methods ); + WriteNativeIncludes( baseDir ); } - private static void QueueDirectory( ref List queue, string directory ) + /// + /// Deletes and re-creates the generated file directories. + /// + /// The base directory that contains the source projects. + private static void DeleteExistingFiles( string baseDir ) { - foreach ( var file in Directory.GetFiles( directory ) ) - { - if ( file.EndsWith( ".h" ) && !file.EndsWith( ".generated.h" ) ) - { - var fileContents = File.ReadAllText( file ); + var destCsDir = $"{baseDir}\\Mocha.Common\\Glue"; + var destHeaderDir = $"{baseDir}\\Mocha.Host\\generated"; - if ( !fileContents.Contains( "GENERATE_BINDINGS", StringComparison.CurrentCultureIgnoreCase ) ) - continue; // Fast early bail - - QueueFile( ref queue, file ); - } - } - - foreach ( var subDirectory in Directory.GetDirectories( directory ) ) - { - QueueDirectory( ref queue, subDirectory ); - } - } + if ( Directory.Exists( destHeaderDir ) ) + Directory.Delete( destHeaderDir, true ); + if ( Directory.Exists( destCsDir ) ) + Directory.Delete( destCsDir, true ); - private static void QueueFile( ref List queue, string path ) - { - queue.Add( path ); + Directory.CreateDirectory( destHeaderDir ); + Directory.CreateDirectory( destCsDir ); } + /// + /// Parses all header files in the Mocha.Host project for interop generation. + /// + /// The base directory that contains the source projects. private static void Parse( string baseDir ) { - List queue = new(); - QueueDirectory( ref queue, baseDir ); + // Find and queue all of the header files to parse. + var queue = new List(); + QueueDirectory( queue, baseDir + "\\Mocha.Host" ); - var dispatcher = new ThreadDispatcher( ( files ) => + // Dispatch jobs to parse all files. + var dispatcher = new ThreadDispatcher( async ( files ) => { foreach ( var path in files ) - { - ProcessHeader( baseDir, path ); - } + await ProcessHeaderAsync( baseDir, path ); }, queue ); // Wait for all threads to finish... while ( !dispatcher.IsComplete ) - Thread.Sleep( 500 ); + Thread.Sleep( 1 ); } - private static void WriteManagedStruct( string baseDir, ref List<(string Name, Method method)> methods ) + /// + /// Writes the C# unmanaged arguments. + /// + /// The base directory that contains the source projects. + /// An enumerable list of all of the methods to write in the struct. + private static void WriteManagedStruct( string baseDir, IEnumerable<(string Name, Method method)> methods ) { var (baseManagedStructWriter, managedStructWriter) = Utils.CreateWriter(); - managedStructWriter.WriteLine( $"using System.Runtime.InteropServices;" ); + managedStructWriter.WriteLine( "using System.Runtime.InteropServices;" ); managedStructWriter.WriteLine(); - managedStructWriter.WriteLine( $"[StructLayout( LayoutKind.Sequential )]" ); - managedStructWriter.WriteLine( $"public struct UnmanagedArgs" ); - managedStructWriter.WriteLine( $"{{" ); + managedStructWriter.WriteLine( "[StructLayout( LayoutKind.Sequential )]" ); + managedStructWriter.WriteLine( "public struct UnmanagedArgs" ); + managedStructWriter.WriteLine( '{' ); managedStructWriter.Indent++; @@ -92,13 +116,18 @@ private static void WriteManagedStruct( string baseDir, ref List<(string Name, M managedStructWriter.Indent--; - managedStructWriter.WriteLine( $"}}" ); + managedStructWriter.WriteLine( '}' ); managedStructWriter.Dispose(); File.WriteAllText( $"{baseDir}/Mocha.Common/Glue/UnmanagedArgs.cs", baseManagedStructWriter.ToString() ); } - private static void WriteNativeStruct( string baseDir, ref List<(string Name, Method method)> methods ) + /// + /// Writes the C++ unmanaged arguments. + /// + /// The base directory that contains the source projects. + /// An enumerable list of all of the methods to write in the struct. + private static void WriteNativeStruct( string baseDir, IEnumerable<(string Name, Method method)> methods ) { var (baseNativeStructWriter, nativeStructWriter) = Utils.CreateWriter(); @@ -107,7 +136,7 @@ private static void WriteNativeStruct( string baseDir, ref List<(string Name, Me nativeStructWriter.WriteLine( "#include \"InteropList.generated.h\"" ); nativeStructWriter.WriteLine(); nativeStructWriter.WriteLine( "struct UnmanagedArgs" ); - nativeStructWriter.WriteLine( $"{{" ); + nativeStructWriter.WriteLine( '{' ); nativeStructWriter.Indent++; nativeStructWriter.WriteLine( "void* __Root;" ); @@ -117,11 +146,11 @@ private static void WriteNativeStruct( string baseDir, ref List<(string Name, Me nativeStructWriter.WriteLine(); nativeStructWriter.Indent--; - nativeStructWriter.WriteLine( $"}};" ); + nativeStructWriter.WriteLine( "};" ); nativeStructWriter.WriteLine(); nativeStructWriter.WriteLine( "inline UnmanagedArgs args" ); - nativeStructWriter.WriteLine( $"{{" ); + nativeStructWriter.WriteLine( '{' ); nativeStructWriter.Indent++; nativeStructWriter.WriteLine( "Root::GetInstance()," ); @@ -131,15 +160,19 @@ private static void WriteNativeStruct( string baseDir, ref List<(string Name, Me nativeStructWriter.WriteLine(); nativeStructWriter.Indent--; - nativeStructWriter.WriteLine( $"}};" ); + nativeStructWriter.WriteLine( "};" ); nativeStructWriter.WriteLine(); - nativeStructWriter.WriteLine( $"#endif // __GENERATED_UNMANAGED_ARGS_H" ); + nativeStructWriter.WriteLine( "#endif // __GENERATED_UNMANAGED_ARGS_H" ); nativeStructWriter.Dispose(); File.WriteAllText( $"{baseDir}Mocha.Host\\generated\\UnmanagedArgs.generated.h", baseNativeStructWriter.ToString() ); } + /// + /// Writes the C++ includes for the host project. + /// + /// The base directory that contains the source projects. private static void WriteNativeIncludes( string baseDir ) { var (baseNativeListWriter, nativeListWriter) = Utils.CreateWriter(); @@ -160,48 +193,56 @@ private static void WriteNativeIncludes( string baseDir ) File.WriteAllText( $"{baseDir}Mocha.Host\\generated\\InteropList.generated.h", baseNativeListWriter.ToString() ); } - private static void DeleteExistingFiles( string baseDir ) + /// + /// Parses a header file and generates its C# and C++ interop code. + /// + /// The base directory that contains the source projects. + /// + /// A task that represents the asynchronous operation. + private static async Task ProcessHeaderAsync( string baseDir, string path ) { - var destCsDir = $"{baseDir}\\Mocha.Common\\Glue\\"; - var destHeaderDir = $"{baseDir}\\Mocha.Host\\generated\\"; + Log.ProcessingHeader( path ); - if ( Directory.Exists( destHeaderDir ) ) - Directory.Delete( destHeaderDir, true ); - if ( Directory.Exists( destCsDir ) ) - Directory.Delete( destCsDir, true ); + // Parse header. + var units = Parser.GetUnits( path ); - Directory.CreateDirectory( destHeaderDir ); - Directory.CreateDirectory( destCsDir ); - } + // Generate interop code. + var managedCode = ManagedCodeGenerator.GenerateCode( units ); + var relativePath = Path.GetRelativePath( $"{baseDir}/Mocha.Host/", path ); + var nativeCode = NativeCodeGenerator.GenerateCode( relativePath, units ); - public static void Main( string[] args ) - { - var baseDir = args[0]; - var start = DateTime.Now; + // Write interop code. + var fileName = Path.GetFileNameWithoutExtension( path ); + var csTask = File.WriteAllTextAsync( $"{baseDir}Mocha.Common\\Glue\\{fileName}.generated.cs", managedCode ); + var nativeTask = File.WriteAllTextAsync( $"{baseDir}Mocha.Host\\generated\\{fileName}.generated.h", nativeCode ); - Console.WriteLine( "Generating C# <--> C++ interop code..." ); + // Wait for writing to finish. + await Task.WhenAll( csTask, nativeTask ); - // - // Prep - // - DeleteExistingFiles( baseDir ); - Parse( baseDir ); + s_files.Add( fileName ); + s_units.AddRange( units ); + } - // - // Expand methods out into list of (method name, method) - // - var methods = s_units.OfType().SelectMany( unit => unit.Methods, ( unit, method ) => (unit.Name, method) ).ToList(); + /// + /// Searches the directory for any header files that should be parsed. + /// + /// The queue collection to append to. + /// The absolute path to the directory to search for files. + private static void QueueDirectory( ICollection queue, string directory ) + { + foreach ( var file in Directory.GetFiles( directory, "*.h" ) ) + { + if ( file.EndsWith( ".generated.h" ) ) + continue; - // - // Write files - // - WriteManagedStruct( baseDir, ref methods ); - WriteNativeStruct( baseDir, ref methods ); - WriteNativeIncludes( baseDir ); + var fileContents = File.ReadAllText( file ); + if ( !fileContents.Contains( "GENERATE_BINDINGS", StringComparison.CurrentCultureIgnoreCase ) ) + continue; // Fast early bail + + queue.Add( file ); + } - // Track time & output total duration - var end = DateTime.Now; - var totalTime = end - start; - Console.WriteLine( $"-- Took {totalTime.TotalSeconds} seconds." ); + foreach ( var subDirectory in Directory.GetDirectories( directory ) ) + QueueDirectory( queue, subDirectory ); } } diff --git a/Source/MochaTool.InteropGen/Properties/launchSettings.json b/Source/MochaTool.InteropGen/Properties/launchSettings.json index 4443d61e..32179963 100644 --- a/Source/MochaTool.InteropGen/Properties/launchSettings.json +++ b/Source/MochaTool.InteropGen/Properties/launchSettings.json @@ -3,7 +3,7 @@ "Run InteropGen": { "commandName": "Project", "commandLineArgs": "$(SolutionDir)", - "workingDirectory": "F:\\Projects\\mocha-native\\Source\\MochaTool.InteropGen" + "workingDirectory": "$(ProjectDir)" } } } \ No newline at end of file diff --git a/Source/MochaTool.InteropGen/StopwatchLog.cs b/Source/MochaTool.InteropGen/StopwatchLog.cs new file mode 100644 index 00000000..e9ae7f09 --- /dev/null +++ b/Source/MochaTool.InteropGen/StopwatchLog.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using MochaTool.InteropGen.Extensions; +using System.Diagnostics; + +namespace MochaTool.InteropGen; + +/// +/// Represents a scoped logger for recording time taken to complete an operation. +/// +internal readonly struct StopwatchLog : IDisposable +{ + /// + /// The name of the operation being completed. + /// + private readonly string name = "Unknown"; + /// + /// The level at which to log the operation. + /// + private readonly LogLevel logLevel = LogLevel.Debug; + /// + /// The timestamp at which the operation started. + /// + private readonly long startTimestamp = Stopwatch.GetTimestamp(); + + /// + /// Initializes a new instance of . + /// + /// The name of the operation being completed. + /// The level at which to log the operation. + /// The timestamp at which the operation started. + internal StopwatchLog( string name, LogLevel logLevel = LogLevel.Debug, long? startTimestamp = null ) + { + this.name = name; + this.logLevel = logLevel; + this.startTimestamp = startTimestamp ?? Stopwatch.GetTimestamp(); + } + + /// + public void Dispose() + { + // Log the time taken to complete the operation. + Log.ReportTime( logLevel, name, Stopwatch.GetElapsedTime( startTimestamp ).TotalSeconds ); + } +} diff --git a/Source/MochaTool.InteropGen/ThreadDispatcher.cs b/Source/MochaTool.InteropGen/ThreadDispatcher.cs index ee0cfeb7..2c46e44f 100644 --- a/Source/MochaTool.InteropGen/ThreadDispatcher.cs +++ b/Source/MochaTool.InteropGen/ThreadDispatcher.cs @@ -1,24 +1,31 @@ namespace MochaTool.InteropGen; -public class ThreadDispatcher +internal class ThreadDispatcher { - public delegate void ThreadCallback( List threadQueue ); + internal delegate void ThreadCallback( List threadQueue ); + internal delegate Task AsyncThreadCallback( List threadQueue ); - private int _threadCount = 16; + internal bool IsComplete => _threadsCompleted >= _threadCount; + + private int _threadCount = (int)Math.Ceiling( Environment.ProcessorCount * 0.75 ); private int _threadsCompleted = 0; - public bool IsComplete => _threadsCompleted == _threadCount; - public ThreadDispatcher( ThreadCallback threadStart, List queue ) + internal ThreadDispatcher( ThreadCallback threadStart, List queue ) { - var batchSize = queue.Count / _threadCount; + Setup( queue, threadQueue => threadStart( threadQueue ) ); + } - if ( batchSize == 0 ) - return; // Bail to avoid division by zero + internal ThreadDispatcher( AsyncThreadCallback threadStart, List queue ) + { + Setup( queue, threadQueue => threadStart( threadQueue ).Wait() ); + } - var remainder = queue.Count % _threadCount; + private void Setup( List queue, Action> threadStart ) + { + var batchSize = queue.Count / _threadCount - 1; - if ( remainder != 0 ) - batchSize++; + if ( batchSize == 0 ) + return; // Bail to avoid division by zero var batched = queue .Select( ( Value, Index ) => new { Value, Index } ) @@ -26,8 +33,7 @@ public ThreadDispatcher( ThreadCallback threadStart, List queue ) .Select( g => g.Select( p => p.Value ).ToList() ) .ToList(); - if ( batched.Count < _threadCount ) - _threadCount = batched.Count; // Min. 1 per thread + _threadCount = batched.Count; for ( int i = 0; i < batched.Count; i++ ) { @@ -35,7 +41,6 @@ public ThreadDispatcher( ThreadCallback threadStart, List queue ) var thread = new Thread( () => { threadStart( threadQueue ); - _threadsCompleted++; } ); diff --git a/Source/MochaTool.InteropGen/Units/Class.cs b/Source/MochaTool.InteropGen/Units/Class.cs deleted file mode 100644 index 7e436e65..00000000 --- a/Source/MochaTool.InteropGen/Units/Class.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Class : IUnit -{ - public Class( string name ) : this() - { - Name = name; - - Fields = new(); - Methods = new(); - } - - public string Name { get; set; } - public List Methods { get; set; } - public List Fields { get; set; } - public bool IsNamespace { get; set; } - - public override string ToString() - { - return Name; - } -} diff --git a/Source/MochaTool.InteropGen/Units/Field.cs b/Source/MochaTool.InteropGen/Units/Field.cs deleted file mode 100644 index e17a2781..00000000 --- a/Source/MochaTool.InteropGen/Units/Field.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Variable -{ - public Variable( string name, string type ) : this() - { - Name = name; - Type = type; - } - - public string Name { get; set; } - public string Type { get; set; } - - public override string ToString() - { - return $"{Type} {Name}"; - } -} diff --git a/Source/MochaTool.InteropGen/Units/IUnit.cs b/Source/MochaTool.InteropGen/Units/IUnit.cs deleted file mode 100644 index 71f88a6c..00000000 --- a/Source/MochaTool.InteropGen/Units/IUnit.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MochaTool.InteropGen; - -public interface IUnit -{ - public string Name { get; set; } - public List Fields { get; set; } - public List Methods { get; set; } -} diff --git a/Source/MochaTool.InteropGen/Units/Method.cs b/Source/MochaTool.InteropGen/Units/Method.cs deleted file mode 100644 index 92ffe8b9..00000000 --- a/Source/MochaTool.InteropGen/Units/Method.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Method -{ - public Method( string name, string returnType ) - { - Name = name; - ReturnType = returnType; - Parameters = new(); - } - - public bool IsConstructor { get; set; } = false; - public bool IsDestructor { get; set; } = false; - public bool IsStatic { get; set; } = false; - - public string Name { get; set; } - public string ReturnType { get; set; } - public List Parameters { get; set; } - - public override string ToString() - { - var p = string.Join( ", ", Parameters ); - return $"{ReturnType} {Name}( {p} )"; - } -} diff --git a/Source/MochaTool.InteropGen/Units/Structure.cs b/Source/MochaTool.InteropGen/Units/Structure.cs deleted file mode 100644 index 4e2f150f..00000000 --- a/Source/MochaTool.InteropGen/Units/Structure.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MochaTool.InteropGen; - -public struct Structure : IUnit -{ - public Structure( string name ) : this() - { - Name = name; - - Fields = new(); - Methods = new(); - } - - public string Name { get; set; } - public List Methods { get; set; } - public List Fields { get; set; } - - public override string ToString() - { - return Name; - } -} diff --git a/Source/MochaTool.InteropGen/Utils.cs b/Source/MochaTool.InteropGen/Utils.cs index 77f528d1..e7153969 100644 --- a/Source/MochaTool.InteropGen/Utils.cs +++ b/Source/MochaTool.InteropGen/Utils.cs @@ -1,16 +1,64 @@ -using System.CodeDom.Compiler; +using MochaTool.InteropGen.Extensions; +using System.CodeDom.Compiler; namespace MochaTool.InteropGen; -static class Utils +/// +/// Contains a number of utility methods. +/// +internal static class Utils { - public static bool IsPointer( string nativeType ) + /// + /// Used as a lookup table for mapping native types to managed ones. + /// + private static readonly Dictionary s_lookupTable = new() + { + // Native type Managed type + //------------------------------- + { "void", "void" }, + { "uint32_t", "uint" }, + { "int32_t", "int" }, + { "size_t", "uint" }, + + { "char**", "ref string" }, + { "char **", "ref string" }, + { "char*", "string" }, + { "char *", "string" }, + { "void*", "IntPtr" }, + { "void *", "IntPtr" }, + + // STL + { "std::string", "/* UNSUPPORTED */ string" }, + + // GLM + { "glm::vec2", "Vector2" }, + { "glm::vec3", "Vector3" }, + { "glm::mat4", "Matrix4x4" }, + { "glm::quat", "Rotation" }, + + // Custom + { "Quaternion", "Rotation" }, + { "InteropStruct", "IInteropArray" }, + { "Handle", "uint" } + }; + + /// + /// Returns whether or not the string represents a pointer. + /// + /// The native type to check. + /// Whether or not the string represents a pointer. + internal static bool IsPointer( string nativeType ) { var managedType = GetManagedType( nativeType ); return nativeType.Trim().EndsWith( "*" ) && managedType != "string" && managedType != "IntPtr"; } - public static string GetManagedType( string nativeType ) + /// + /// Returns the C# version of a native type. + /// + /// The native type to check. + /// The C# verison of a native type. + internal static string GetManagedType( string nativeType ) { // Trim whitespace from beginning / end (if it exists) nativeType = nativeType.Trim(); @@ -19,54 +67,22 @@ public static string GetManagedType( string nativeType ) if ( nativeType.StartsWith( "const" ) ) nativeType = nativeType[5..].Trim(); - // Create a dictionary to hold the mapping between native and managed types - var lookupTable = new Dictionary() - { - // Native type Managed type - //------------------------------- - { "void", "void" }, - { "uint32_t", "uint" }, - { "int32_t", "int" }, - { "size_t", "uint" }, - - { "char**", "ref string" }, - { "char **", "ref string" }, - { "char*", "string" }, - { "char *", "string" }, - { "void*", "IntPtr" }, - { "void *", "IntPtr" }, - - // STL - { "std::string", "/* UNSUPPORTED */ string" }, - - // GLM - { "glm::vec2", "Vector2" }, - { "glm::vec3", "Vector3" }, - { "glm::mat4", "Matrix4x4" }, - { "glm::quat", "Rotation" }, - - // Custom - { "Quaternion", "Rotation" }, - { "InteropStruct", "IInteropArray" }, - { "Handle", "uint" } - }; - // Check if the native type is a reference if ( nativeType.EndsWith( "&" ) ) return GetManagedType( nativeType[0..^1] ); // Check if the native type is in the lookup table - if ( lookupTable.ContainsKey( nativeType ) ) + if ( s_lookupTable.TryGetValue( nativeType, out var value ) ) { // Bonus: Emit a compiler warning if the native type is std::string if ( nativeType == "std::string" ) { // There's a better API that does this but I can't remember what it is // TODO: Show position of the warning (line number, file name) - Console.WriteLine( "warning IG0001: std::string is not supported in managed code. Use a C string instead." ); + Log.WarnDiagnostic( "warning IG0001: std::string is not supported in managed code. Use a C string instead." ); } - return lookupTable[nativeType]; + return value; } // Check if the native type is a pointer @@ -77,7 +93,11 @@ public static string GetManagedType( string nativeType ) return nativeType; } - public static (StringWriter StringWriter, IndentedTextWriter TextWriter) CreateWriter() + /// + /// Creates and returns the text writer for writing formatted files. + /// + /// The created text writer. + internal static (StringWriter StringWriter, IndentedTextWriter TextWriter) CreateWriter() { var baseTextWriter = new StringWriter();