From 6905e9f761da71bfc4634557c8f806e6e2a20950 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Mon, 13 Jul 2026 09:10:42 -0700 Subject: [PATCH 1/3] Replace System.CommandLine with a custom argument parser System.CommandLine has repeatedly broken the CLI, most recently in #554 where the beta4 response-file tokenizer splits each line on whitespace before options are matched, corrupting `name=value` values that contain spaces (such as a `--remap` to a type with a space in it). The stable package also changed too much to adopt cleanly, so roll a small parser we control instead. The new parser is a plain, reflection-free (AOT-friendly) implementation that reads each response-file line as a single token, preserving embedded spaces, while still supporting multiple pairs across separate tokens, the inline `--opt=value` form, and `#` comments. Drop the System.CommandLine package reference and the CustomHelpBuilder, and regenerate the README help blocks to match the new output. Fixes #554 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Directory.Packages.props | 1 - README.md | 92 ++- .../ClangSharpPInvokeGenerator.csproj | 6 +- .../ClangSharpPInvokeGenerator/CommandLine.cs | 334 ++++++++ .../CustomHelpBuilder.cs | 35 - .../Program.Options.cs | 782 ++++-------------- sources/ClangSharpPInvokeGenerator/Program.cs | 199 ++--- 7 files changed, 651 insertions(+), 798 deletions(-) create mode 100644 sources/ClangSharpPInvokeGenerator/CommandLine.cs delete mode 100644 sources/ClangSharpPInvokeGenerator/CustomHelpBuilder.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 59deeab5..c9dc746d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,6 @@ - diff --git a/README.md b/README.md index be44db44..35bd4435 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Options: -r, --remap A declaration name to be remapped to another name during binding generation. [] -rt, --remap-type A type (record or enum) declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name. [] -rf, --remap-field A field declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name. [] - -std Language standard to compile for. [] + -std, --std Language standard to compile for. [] -to, --test-output The output location to write the generated tests to. [] -t, --traverse A file name included either directly or indirectly by -f that should be traversed during binding generation. [] -v, --version Prints the current version information for the tool and its native dependencies. @@ -183,6 +183,7 @@ Options: -wmi, --with-manual-import A remapped function name to be treated as a manual import during binding generation. Supports wildcards. [] -wn, --with-namespace A namespace to be used for the given remapped declaration name during binding generation. Supports wildcards. [] -wp, --with-packing Overrides the StructLayoutAttribute.Pack property for the given type. Supports wildcards. [] + -wro, --with-readonly Add the readonly modifier to a given instance method. Supports wildcards. [] -wsle, --with-setlasterror Add the SetLastError=true modifier or SetsSystemLastError attribute to a given DllImport or UnmanagedFunctionPointer. Supports wildcards. [] -wsgct, --with-suppressgctransition Add the SuppressGCTransition calling convention to a given DllImport or UnmanagedFunctionPointer. Supports wildcards. [] -wts, --with-transparent-struct A remapped type name to be treated as a transparent wrapper during binding generation. Supports wildcards. [] @@ -196,79 +197,80 @@ You can use * as catch-all rule for remapping procedures. For example if you wan The available configuration options (visible with `-c help`) are: ``` ---config, -c A configuration option that controls how the bindings are generated. Specify 'help' to see the available options. +--config, -c A configuration option that controls how the bindings are generated. Specify 'help' to see the available options. Options: - ?, h, help Show help and usage information for -c, --config + ?, h, help Show help and usage information for -c, --config # Codegen Options - compatible-codegen Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation. - default-codegen Bindings should be generated for the previous LTS version of .NET/C#. This is currently .NET 8/C# 12. - latest-codegen Bindings should be generated for the current LTS/STS version of .NET/C#. This is currently .NET 10/C# 14. - preview-codegen Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14. + compatible-codegen Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation. + default-codegen Bindings should be generated for the current LTS version of .NET/C#. This is currently .NET 8/C# 12. + latest-codegen Bindings should be generated for the current STS version of .NET/C#. This is currently .NET 10/C# 14. + preview-codegen Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14. # File Options - single-file Bindings should be generated to a single output file. This is the default. - multi-file Bindings should be generated so there is approximately one type per file. + single-file Bindings should be generated to a single output file. This is the default. + multi-file Bindings should be generated so there is approximately one type per file. # Type Options - unix-types Bindings should be generated assuming Unix defaults. This is the default on Unix platforms. - windows-types Bindings should be generated assuming Windows defaults. This is the default on Windows platforms. + unix-types Bindings should be generated assuming Unix defaults. This is the default on Unix platforms. + windows-types Bindings should be generated assuming Windows defaults. This is the default on Windows platforms. # Exclusion Options - exclude-anonymous-field-helpers The helper ref properties generated for fields in nested anonymous structs and unions should not be generated. - exclude-com-proxies Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub. - exclude-default-remappings Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t - exclude-empty-records Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND. - exclude-enum-operators Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default. - exclude-fnptr-codegen Generated bindings for latest or preview codegen should not use function pointers. - exclude-funcs-with-body Bindings for functions with bodies should not be generated. - exclude-using-statics-for-enums Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;' + exclude-anonymous-field-helpers The helper ref properties generated for fields in nested anonymous structs and unions should not be generated. + exclude-com-proxies Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub. + exclude-default-remappings Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t + exclude-empty-records Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND. + exclude-enum-operators Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default. + exclude-fnptr-codegen Generated bindings for latest or preview codegen should not use function pointers. + exclude-funcs-with-body Bindings for functions with bodies should not be generated. + exclude-using-statics-for-enums Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;' # Vtbl Options - explicit-vtbls VTBLs should have an explicit type generated with named fields per entry. - implicit-vtbls VTBLs should be implicit to reduce metadata bloat. This is the current default - trimmable-vtbls VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming. + explicit-vtbls VTBLs should have an explicit type generated with named fields per entry. + implicit-vtbls VTBLs should be implicit to reduce metadata bloat. This is the current default + trimmable-vtbls VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming. # Test Options - generate-tests-nunit Basic tests validating size, blittability, and associated metadata should be generated for NUnit. - generate-tests-xunit Basic tests validating size, blittability, and associated metadata should be generated for XUnit. + generate-tests-nunit Basic tests validating size, blittability, and associated metadata should be generated for NUnit. + generate-tests-xunit Basic tests validating size, blittability, and associated metadata should be generated for XUnit. # Generation Options - generate-aggressive-inlining [MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions. - generate-callconv-member-function Instance function pointers should use [CallConvMemberFunction] where applicable. - generate-cpp-attributes [CppAttributeList("")] should be generated to document the encountered C++ attributes. - generate-disable-runtime-marshalling [assembly: DisableRuntimeMarshalling] should be generated. - generate-doc-includes xml documentation tags should be generated for declarations. - generate-file-scoped-namespaces Namespaces should be scoped to the file to reduce nesting. - generate-guid-member Types with an associated GUID should have a corresponding member generated. - generate-helper-types Code files should be generated for various helper attributes and declared transparent structs. - generate-macro-bindings Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones. - generate-marker-interfaces Bindings for marker interfaces representing native inheritance hierarchies should be generated. - generate-native-bitfield-attribute [NativeBitfield("", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout. - generate-native-inheritance-attribute [NativeInheritance("")] attribute should be generated to document the encountered C++ base type. - generate-generic-pointer-wrapper Pointer should be used for limited generic type support. - generate-setslastsystemerror-attribute [SetsLastSystemError] attribute should be generated rather than using SetLastError = true. - generate-template-bindings Bindings for template-definitions should be generated. This is currently experimental. - generate-unmanaged-constants Unmanaged constants should be generated using static ref readonly properties. This is currently experimental. - generate-vtbl-index-attribute [VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method. + generate-aggressive-inlining [MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions. + generate-callconv-member-function Instance function pointers should use [CallConvMemberFunction] where applicable. + generate-cpp-attributes [CppAttributeList("")] should be generated to document the encountered C++ attributes. + generate-disable-runtime-marshalling [assembly: DisableRuntimeMarshalling] should be generated. + generate-doc-includes xml documentation tags should be generated for declarations. + generate-file-scoped-namespaces Namespaces should be scoped to the file to reduce nesting. + generate-fixed-buffer-indexer-overloads Fixed sized buffer helper types should generate additional uint, nint, and nuint indexer overloads. + generate-guid-member Types with an associated GUID should have a corresponding member generated. + generate-helper-types Code files should be generated for various helper attributes and declared transparent structs. + generate-macro-bindings Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones. + generate-marker-interfaces Bindings for marker interfaces representing native inheritance hierarchies should be generated. + generate-native-bitfield-attribute [NativeBitfield("", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout. + generate-native-inheritance-attribute [NativeInheritance("")] attribute should be generated to document the encountered C++ base type. + generate-generic-pointer-wrapper Pointer should be used for limited generic type support. + generate-setslastsystemerror-attribute [SetsLastSystemError] attribute should be generated rather than using SetLastError = true. + generate-template-bindings Bindings for template-definitions should be generated. This is currently experimental. + generate-unmanaged-constants Unmanaged constants should be generated using static ref readonly properties. This is currently experimental. + generate-vtbl-index-attribute [VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method. # Stripping Options - strip-enum-member-type-name Strips the enum type name from the beginning of its member names. + strip-enum-member-type-name Strips the enum type name from the beginning of its member names. # Logging Options - log-exclusions A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match. - log-potential-typedef-remappings A list of potential typedef remappings should be generated. This can help identify missing remappings. - log-visited-files A list of the visited files should be generated. This can help identify traversal issues. + log-exclusions A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match. + log-potential-typedef-remappings A list of potential typedef remappings should be generated. This can help identify missing remappings. + log-visited-files A list of the visited files should be generated. This can help identify traversal issues. ``` ### Using locally built versions diff --git a/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj b/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj index 73f6008d..25261c91 100644 --- a/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj +++ b/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj @@ -8,6 +8,8 @@ true linux-arm64;linux-x64;osx-arm64;win-arm64;win-x64 net10.0 + + $(NoWarn);CA1303 @@ -23,10 +25,6 @@ - - - - diff --git a/sources/ClangSharpPInvokeGenerator/CommandLine.cs b/sources/ClangSharpPInvokeGenerator/CommandLine.cs new file mode 100644 index 00000000..41442b46 --- /dev/null +++ b/sources/ClangSharpPInvokeGenerator/CommandLine.cs @@ -0,0 +1,334 @@ +// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ClangSharp; + +internal enum CommandLineOptionKind +{ + Flag, + SingleValue, + MultipleValue, +} + +internal sealed class CommandLineOption +{ + private readonly List _values; + + public CommandLineOption(string[] aliases, string description, CommandLineOptionKind kind, string? valueName = null, string? defaultValue = null, string[]? allowedValues = null) + { + Aliases = aliases; + Description = description; + Kind = kind; + ValueName = valueName ?? aliases[0].TrimStart('-'); + DefaultValue = defaultValue; + AllowedValues = allowedValues; + _values = []; + } + + public string[] Aliases { get; } + + public string[]? AllowedValues { get; } + + // The canonical name used in diagnostics; the first alias is always the long form. + public string CanonicalName => Aliases[0]; + + public string? DefaultValue { get; } + + public string Description { get; } + + public bool IsPresent { get; set; } + + public CommandLineOptionKind Kind { get; } + + public string ValueName { get; } + + // The effective value for a single-value option: the last one specified or the default. + public string SingleValue => (_values.Count != 0) ? _values[^1] : (DefaultValue ?? ""); + + public void AddValue(string value) => _values.Add(value); + + public string[] GetValues() => [.. _values]; +} + +internal readonly struct HelpRow(string term, string description) +{ + public string Term { get; } = term; + + public string Description { get; } = description; +} + +internal sealed class CommandLineParser +{ + private const int MaxResponseFileDepth = 32; + + private readonly IReadOnlyList _options; + private readonly Dictionary _optionsByAlias; + private readonly List _errors; + + public CommandLineParser(IReadOnlyList options) + { + _options = options; + _optionsByAlias = new Dictionary(StringComparer.Ordinal); + _errors = []; + + foreach (var option in options) + { + foreach (var alias in option.Aliases) + { + _optionsByAlias.Add(alias, option); + } + } + } + + public IReadOnlyList Errors => _errors; + + public void Parse(IReadOnlyList args) + { + var tokens = new List(); + + foreach (var arg in args) + { + ExpandToken(arg, tokens, depth: 0); + } + + var index = 0; + + while (index < tokens.Count) + { + var token = tokens[index]; + + if (!TryMatchOption(token, out var option, out var inlineValue)) + { + _errors.Add($"Error: Unrecognized command or argument '{token}'."); + index++; + continue; + } + + index++; + option.IsPresent = true; + + if (option.Kind == CommandLineOptionKind.Flag) + { + if (inlineValue is not null) + { + _errors.Add($"Error: Option '{option.CanonicalName}' does not accept an argument."); + } + continue; + } + + if (inlineValue is not null) + { + AddValue(option, inlineValue); + continue; + } + + var count = 0; + + while ((index < tokens.Count) && !TryMatchOption(tokens[index], out _, out _)) + { + AddValue(option, tokens[index]); + index++; + count++; + + if (option.Kind == CommandLineOptionKind.SingleValue) + { + break; + } + } + + if (count == 0) + { + _errors.Add($"Error: Required argument missing for option: '{option.CanonicalName}'."); + } + } + } + + private void AddValue(CommandLineOption option, string value) + { + if ((option.AllowedValues is not null) && !option.AllowedValues.Contains(value, StringComparer.Ordinal)) + { + _errors.Add($"Error: Argument '{value}' not recognized for option '{option.CanonicalName}'. Must be one of: {string.Join(", ", option.AllowedValues)}."); + return; + } + + option.AddValue(value); + } + + private bool TryMatchOption(string token, out CommandLineOption option, out string? inlineValue) + { + inlineValue = null; + + if (_optionsByAlias.TryGetValue(token, out option!)) + { + return true; + } + + // Support the `--alias=value` (or `-alias=value`) inline form, but only when the text + // before the first '=' is itself a known alias. This keeps bare `name=value` tokens + // (such as a `--remap` value) intact even when the value contains '=' or spaces. + + if ((token.Length != 0) && (token[0] == '-')) + { + var equalsIndex = token.IndexOf('=', StringComparison.Ordinal); + + if (equalsIndex > 0) + { + var alias = token[..equalsIndex]; + + if (_optionsByAlias.TryGetValue(alias, out option!)) + { + inlineValue = token[(equalsIndex + 1)..]; + return true; + } + } + } + + option = null!; + return false; + } + + private void ExpandToken(string token, List result, int depth) + { + if ((token.Length == 0) || (token[0] != '@')) + { + result.Add(token); + return; + } + + var responseFilePath = token[1..]; + + if (depth > MaxResponseFileDepth) + { + _errors.Add($"Error: Response file recursion limit exceeded at '{responseFilePath}'."); + return; + } + + if (!File.Exists(responseFilePath)) + { + _errors.Add($"Error: Response file not found '{responseFilePath}'."); + return; + } + + string[] lines; + + try + { + lines = File.ReadAllLines(responseFilePath); + } + catch (IOException ex) + { + _errors.Add($"Error: {ex.Message}"); + return; + } + + // Each non-empty, non-comment line is treated as a single token; the contents are + // never split on whitespace so a value containing spaces is preserved as-is. + + foreach (var line in lines) + { + var trimmed = line.Trim(); + + if ((trimmed.Length == 0) || (trimmed[0] == '#')) + { + continue; + } + + ExpandToken(trimmed, result, depth + 1); + } + } + + public void WriteHelp(TextWriter writer, string name, string description, string epilogTitle, string epilog) + { + writer.WriteLine(name); + writer.WriteLine($" {description}"); + writer.WriteLine(); + writer.WriteLine("Usage:"); + writer.WriteLine($" {name} [options]"); + writer.WriteLine(); + writer.WriteLine("Options:"); + + var terms = new string[_options.Count]; + var maxTermLength = 0; + + for (var i = 0; i < _options.Count; i++) + { + var option = _options[i]; + var term = string.Join(", ", GetDisplayAliases(option)); + + if (option.Kind != CommandLineOptionKind.Flag) + { + term += $" <{option.ValueName}>"; + } + + terms[i] = term; + maxTermLength = Math.Max(maxTermLength, term.Length); + } + + for (var i = 0; i < _options.Count; i++) + { + var option = _options[i]; + writer.WriteLine($" {terms[i].PadRight(maxTermLength + 2)}{option.Description}{GetDefaultSuffix(option)}"); + } + + writer.WriteLine(); + writer.WriteLine(epilogTitle); + writer.WriteLine(epilog); + } + + public static void WriteOptionHelp(TextWriter writer, CommandLineOption option, IReadOnlyList rows) + { + writer.WriteLine($"{string.Join(", ", option.Aliases)}\t{option.Description}"); + writer.WriteLine(); + WriteTwoColumn(writer, rows); + } + + public static void WriteTwoColumn(TextWriter writer, IReadOnlyList rows) + { + writer.WriteLine("Options:"); + + var maxTermLength = 0; + + foreach (var row in rows) + { + if (row.Description.Length != 0) + { + maxTermLength = Math.Max(maxTermLength, row.Term.Length); + } + } + + foreach (var row in rows) + { + if (row.Description.Length == 0) + { + writer.WriteLine((row.Term.Length == 0) ? "" : $" {row.Term}"); + } + else + { + writer.WriteLine($" {row.Term.PadRight(maxTermLength + 2)}{row.Description}"); + } + } + + writer.WriteLine(); + } + + private static IEnumerable GetDisplayAliases(CommandLineOption option) + { + // Match the historical help ordering: short (`-x`) aliases first, then long (`--xx`). + return option.Aliases.Where(static alias => !alias.StartsWith("--", StringComparison.Ordinal)) + .Concat(option.Aliases.Where(static alias => alias.StartsWith("--", StringComparison.Ordinal))); + } + + private static string GetDefaultSuffix(CommandLineOption option) + { + if (option.Kind == CommandLineOptionKind.Flag) + { + return ""; + } + + return string.IsNullOrEmpty(option.DefaultValue) ? " []" : $" [default: {option.DefaultValue}]"; + } +} diff --git a/sources/ClangSharpPInvokeGenerator/CustomHelpBuilder.cs b/sources/ClangSharpPInvokeGenerator/CustomHelpBuilder.cs deleted file mode 100644 index 5c6ba64e..00000000 --- a/sources/ClangSharpPInvokeGenerator/CustomHelpBuilder.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. - -// Portions of this code are ported from https://github.com/dotnet/command-line-api -// The original source is Copyright © .NET Foundation and Contributor. All rights reserved. Licensed under the MIT License (MIT). - -using System.CommandLine; -using System.CommandLine.Help; -using System.CommandLine.IO; - -namespace ClangSharp; - -internal sealed class CustomHelpBuilder(IConsole console, LocalizationResources localizationResources, int maxWidth = int.MaxValue) : HelpBuilder(localizationResources, maxWidth) -{ - public void Write(Option option) - { - Write(string.Join(", ", option.Aliases)); - Write("\t"); - Write(option.Description ?? ""); - WriteLine(); - } - - public void Write(string value) => console.Out.Write(value); - - public void Write(params TwoColumnHelpRow[] helpItems) - { - WriteLine("Options:"); - var _ = new Command("unused"); - WriteColumns(helpItems, new HelpContext(this, _, console.Out.CreateTextWriter())); - WriteLine(); - } - - public void WriteLine() => console.Out.WriteLine(); - - public void WriteLine(string value) => console.Out.WriteLine(value); -} diff --git a/sources/ClangSharpPInvokeGenerator/Program.Options.cs b/sources/ClangSharpPInvokeGenerator/Program.Options.cs index eafe38a4..404b7d5e 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.Options.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.Options.cs @@ -1,10 +1,5 @@ // Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. -using System; -using System.CommandLine; -using System.CommandLine.Help; -using System.CommandLine.Invocation; - namespace ClangSharp; internal static partial class Program @@ -49,606 +44,193 @@ internal static partial class Program private static readonly string[] s_withTransparentStructOptionAliases = ["--with-transparent-struct", "-wts"]; private static readonly string[] s_withTypeOptionAliases = ["--with-type", "-wt"]; private static readonly string[] s_withUsingOptionAliases = ["--with-using", "-wu"]; - - private static readonly Option s_additionalOption = GetAdditionalOption(); - private static readonly Option s_configOption = GetConfigOption(); - private static readonly Option s_defineMacros = GetDefineMacroOption(); - private static readonly Option s_excludedNames = GetExcludeOption(); - private static readonly Option s_files = GetFileOption(); - private static readonly Option s_fileDirectory = GetFileDirectoryOption(); - private static readonly Option s_headerFile = GetHeaderOption(); - private static readonly Option s_includedNames = GetIncludeOption(); - private static readonly Option s_includeDirectories = GetIncludeDirectoryOption(); - private static readonly Option s_language = GetLanguageOption(); - private static readonly Option s_libraryPath = GetLibraryOption(); - private static readonly Option s_methodClassName = GetMethodClassNameOption(); - private static readonly Option s_methodPrefixToStrip = GetPrefixStripOption(); - private static readonly Option s_namespaceName = GetNamespaceOption(); - private static readonly Option s_nativeTypeNamesToStrip = GetNativeTypeNamesStripOption(); - private static readonly Option s_outputLocation = GetOutputOption(); - private static readonly Option s_outputMode = GetOutputModeOption(); - private static readonly Option s_remappedNameValuePairs = GetRemapOption(); - private static readonly Option s_remappedTypeNameValuePairs = GetRemapTypeOption(); - private static readonly Option s_remappedFieldNameValuePairs = GetRemapFieldOption(); - private static readonly Option s_std = GetStdOption(); - private static readonly Option s_testOutputLocation = GetTestOutputOption(); - private static readonly Option s_traversalNames = GetTraverseOption(); - private static readonly Option s_versionOption = GetVersionOption(); - private static readonly Option s_withAccessSpecifierNameValuePairs = GetWithAccessSpecifierOption(); - private static readonly Option s_withAttributeNameValuePairs = GetWithAttributeOption(); - private static readonly Option s_withCallConvNameValuePairs = GetWithCallConvOption(); - private static readonly Option s_withClassNameValuePairs = GetWithClassOption(); - private static readonly Option s_withGuidNameValuePairs = GetWithGuidOption(); - private static readonly Option s_withLengthNameValuePairs = GetWithLengthOption(); - private static readonly Option s_withLibraryPathNameValuePairs = GetWithLibraryPathOption(); - private static readonly Option s_withManualImports = GetWithManualImportOption(); - private static readonly Option s_withNamespaceNameValuePairs = GetWithNamespaceOption(); - private static readonly Option s_withPackingNameValuePairs = GetWithPackingOption(); - private static readonly Option s_withReadonlys = GetWithReadonlyOption(); - private static readonly Option s_withSetLastErrors = GetWithSetLastErrorOption(); - private static readonly Option s_withSuppressGCTransitions = GetWithSuppressGCTransitionOption(); - private static readonly Option s_withTransparentStructNameValuePairs = GetWithTransparentStructOption(); - private static readonly Option s_withTypeNameValuePairs = GetWithTypeOption(); - private static readonly Option s_withUsingNameValuePairs = GetWithUsingOption(); - - private static readonly RootCommand s_rootCommand = GetRootCommand(); - - private static readonly TwoColumnHelpRow[] s_configOptions = + private static readonly string[] s_helpOptionAliases = ["--help", "-?", "-h"]; + + private static readonly CommandLineOption s_additionalOption = Multi(s_additionalOptionAliases, "An argument to pass to Clang when parsing the input files."); + private static readonly CommandLineOption s_configOption = Multi(s_configOptionAliases, "A configuration option that controls how the bindings are generated. Specify 'help' to see the available options."); + private static readonly CommandLineOption s_defineMacros = Multi(s_defineMacroOptionAliases, "Define to (or 1 if omitted)."); + private static readonly CommandLineOption s_excludedNames = Multi(s_excludeOptionAliases, "A declaration name to exclude from binding generation."); + private static readonly CommandLineOption s_files = Multi(s_fileOptionAliases, "A file to parse and generate bindings for."); + private static readonly CommandLineOption s_fileDirectory = Single(s_fileDirectionOptionAliases, "The base path for files to parse."); + private static readonly CommandLineOption s_headerFile = Single(s_headerOptionAliases, "A file which contains the header to prefix every generated file with."); + private static readonly CommandLineOption s_includedNames = Multi(s_includeOptionAliases, "A declaration name to include in binding generation."); + private static readonly CommandLineOption s_includeDirectories = Multi(s_includeDirectoryOptionAliases, "Add directory to include search path."); + private static readonly CommandLineOption s_language = Single(s_languageOptionAliases, "Treat subsequent input files as having type .", defaultValue: "c++", valueName: "c|c++", allowedValues: ["c", "c++"]); + private static readonly CommandLineOption s_libraryPath = Single(s_libraryOptionAliases, "The string to use in the DllImport attribute used when generating bindings."); + private static readonly CommandLineOption s_methodClassName = Single(s_methodClassNameOptionAliases, "The name of the static class that will contain the generated method bindings.", defaultValue: "Methods"); + private static readonly CommandLineOption s_namespaceName = Single(s_namespaceOptionAliases, "The namespace in which to place the generated bindings."); + private static readonly CommandLineOption s_nativeTypeNamesToStrip = Multi(s_nativeTypeNamesStripOptionAliases, "The contents to strip from the generated NativeTypeName attributes."); + private static readonly CommandLineOption s_outputMode = Single(s_outputModeOptionAliases, "The mode describing how the information collected from the headers are presented in the resultant bindings.", defaultValue: "CSharp", valueName: "CSharp|Xml"); + private static readonly CommandLineOption s_outputLocation = Single(s_outputOptionAliases, "The output location to write the generated bindings to."); + private static readonly CommandLineOption s_methodPrefixToStrip = Single(s_prefixStripOptionAliases, "The prefix to strip from the generated method bindings."); + private static readonly CommandLineOption s_remappedNameValuePairs = Multi(s_remapOptionAliases, "A declaration name to be remapped to another name during binding generation."); + private static readonly CommandLineOption s_remappedTypeNameValuePairs = Multi(s_remapTypeOptionAliases, "A type (record or enum) declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name."); + private static readonly CommandLineOption s_remappedFieldNameValuePairs = Multi(s_remapFieldOptionAliases, "A field declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name."); + private static readonly CommandLineOption s_std = Single(s_stdOptionAliases, "Language standard to compile for."); + private static readonly CommandLineOption s_testOutputLocation = Single(s_testOutputOptionAliases, "The output location to write the generated tests to."); + private static readonly CommandLineOption s_traversalNames = Multi(s_traverseOptionAliases, "A file name included either directly or indirectly by -f that should be traversed during binding generation."); + private static readonly CommandLineOption s_versionOption = Flag(s_versionOptionAliases, "Prints the current version information for the tool and its native dependencies."); + private static readonly CommandLineOption s_withAccessSpecifierNameValuePairs = Multi(s_withAccessSpecifierOptionAliases, "An access specifier to be used with the given qualified or remapped declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withAttributeNameValuePairs = Multi(s_withAttributeOptionAliases, "An attribute to be added to the given remapped declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withCallConvNameValuePairs = Multi(s_withCallConvOptionAliases, "A calling convention to be used for the given declaration during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withClassNameValuePairs = Multi(s_withClassOptionAliases, "A class to be used for the given remapped constant or function declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withGuidNameValuePairs = Multi(s_withGuidOptionAliases, "A GUID to be used for the given declaration during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withLengthNameValuePairs = Multi(s_withLengthOptionAliases, "A length to be used for the given declaration during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withLibraryPathNameValuePairs = Multi(s_withLibraryPathOptionAliases, "A library path to be used for the given declaration during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withManualImports = Multi(s_withManualImportOptionAliases, "A remapped function name to be treated as a manual import during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withNamespaceNameValuePairs = Multi(s_withNamespaceOptionAliases, "A namespace to be used for the given remapped declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withPackingNameValuePairs = Multi(s_withPackingOptionAliases, "Overrides the StructLayoutAttribute.Pack property for the given type. Supports wildcards."); + private static readonly CommandLineOption s_withReadonlys = Multi(s_withReadonlyOptionAliases, "Add the readonly modifier to a given instance method. Supports wildcards."); + private static readonly CommandLineOption s_withSetLastErrors = Multi(s_withSetLastErrorOptionAliases, "Add the SetLastError=true modifier or SetsSystemLastError attribute to a given DllImport or UnmanagedFunctionPointer. Supports wildcards."); + private static readonly CommandLineOption s_withSuppressGCTransitions = Multi(s_withSuppressGCTransitionOptionAliases, "Add the SuppressGCTransition calling convention to a given DllImport or UnmanagedFunctionPointer. Supports wildcards."); + private static readonly CommandLineOption s_withTransparentStructNameValuePairs = Multi(s_withTransparentStructOptionAliases, "A remapped type name to be treated as a transparent wrapper during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withTypeNameValuePairs = Multi(s_withTypeOptionAliases, "A type to be used for the given enum declaration during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_withUsingNameValuePairs = Multi(s_withUsingOptionAliases, "A using directive to be included for the given remapped declaration name during binding generation. Supports wildcards."); + private static readonly CommandLineOption s_helpOption = Flag(s_helpOptionAliases, "Show help and usage information"); + + private static readonly CommandLineOption[] s_options = [ - new TwoColumnHelpRow("?, h, help", "Show help and usage information for -c, --config"), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Codegen Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("compatible-codegen", "Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation."), - new TwoColumnHelpRow("default-codegen", "Bindings should be generated for the current LTS version of .NET/C#. This is currently .NET 8/C# 12."), - new TwoColumnHelpRow("latest-codegen", "Bindings should be generated for the current STS version of .NET/C#. This is currently .NET 10/C# 14."), - new TwoColumnHelpRow("preview-codegen", "Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# File Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("single-file", "Bindings should be generated to a single output file. This is the default."), - new TwoColumnHelpRow("multi-file", "Bindings should be generated so there is approximately one type per file."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Type Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("unix-types", "Bindings should be generated assuming Unix defaults. This is the default on Unix platforms."), - new TwoColumnHelpRow("windows-types", "Bindings should be generated assuming Windows defaults. This is the default on Windows platforms."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Exclusion Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("exclude-anonymous-field-helpers", "The helper ref properties generated for fields in nested anonymous structs and unions should not be generated."), - new TwoColumnHelpRow("exclude-com-proxies", "Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub."), - new TwoColumnHelpRow("exclude-default-remappings", "Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t"), - new TwoColumnHelpRow("exclude-empty-records", "Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND."), - new TwoColumnHelpRow("exclude-enum-operators", "Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default."), - new TwoColumnHelpRow("exclude-fnptr-codegen", "Generated bindings for latest or preview codegen should not use function pointers."), - new TwoColumnHelpRow("exclude-funcs-with-body", "Bindings for functions with bodies should not be generated."), - new TwoColumnHelpRow("exclude-using-statics-for-enums", "Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;'"), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Vtbl Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("explicit-vtbls", "VTBLs should have an explicit type generated with named fields per entry."), - new TwoColumnHelpRow("implicit-vtbls", "VTBLs should be implicit to reduce metadata bloat. This is the current default"), - new TwoColumnHelpRow("trimmable-vtbls", "VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Test Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("generate-tests-nunit", "Basic tests validating size, blittability, and associated metadata should be generated for NUnit."), - new TwoColumnHelpRow("generate-tests-xunit", "Basic tests validating size, blittability, and associated metadata should be generated for XUnit."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Generation Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("generate-aggressive-inlining", "[MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions."), - new TwoColumnHelpRow("generate-callconv-member-function", "Instance function pointers should use [CallConvMemberFunction] where applicable."), - new TwoColumnHelpRow("generate-cpp-attributes", "[CppAttributeList(\"\")] should be generated to document the encountered C++ attributes."), - new TwoColumnHelpRow("generate-disable-runtime-marshalling", "[assembly: DisableRuntimeMarshalling] should be generated."), - new TwoColumnHelpRow("generate-doc-includes", " xml documentation tags should be generated for declarations."), - new TwoColumnHelpRow("generate-file-scoped-namespaces", "Namespaces should be scoped to the file to reduce nesting."), - new TwoColumnHelpRow("generate-fixed-buffer-indexer-overloads", "Fixed sized buffer helper types should generate additional uint, nint, and nuint indexer overloads."), - new TwoColumnHelpRow("generate-guid-member", "Types with an associated GUID should have a corresponding member generated."), - new TwoColumnHelpRow("generate-helper-types", "Code files should be generated for various helper attributes and declared transparent structs."), - new TwoColumnHelpRow("generate-macro-bindings", "Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones."), - new TwoColumnHelpRow("generate-marker-interfaces", "Bindings for marker interfaces representing native inheritance hierarchies should be generated."), - new TwoColumnHelpRow("generate-native-bitfield-attribute", "[NativeBitfield(\"\", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout."), - new TwoColumnHelpRow("generate-native-inheritance-attribute", "[NativeInheritance(\"\")] attribute should be generated to document the encountered C++ base type."), - new TwoColumnHelpRow("generate-generic-pointer-wrapper", "Pointer should be used for limited generic type support."), - new TwoColumnHelpRow("generate-setslastsystemerror-attribute", "[SetsLastSystemError] attribute should be generated rather than using SetLastError = true."), - new TwoColumnHelpRow("generate-template-bindings", "Bindings for template-definitions should be generated. This is currently experimental."), - new TwoColumnHelpRow("generate-unmanaged-constants", "Unmanaged constants should be generated using static ref readonly properties. This is currently experimental."), - new TwoColumnHelpRow("generate-vtbl-index-attribute", "[VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Stripping Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("strip-enum-member-type-name", "Strips the enum type name from the beginning of its member names."), - - new TwoColumnHelpRow("", ""), - new TwoColumnHelpRow("# Logging Options", ""), - new TwoColumnHelpRow("", ""), - - new TwoColumnHelpRow("log-exclusions", "A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match."), - new TwoColumnHelpRow("log-potential-typedef-remappings", "A list of potential typedef remappings should be generated. This can help identify missing remappings."), - new TwoColumnHelpRow("log-visited-files", "A list of the visited files should be generated. This can help identify traversal issues."), + s_additionalOption, + s_configOption, + s_defineMacros, + s_excludedNames, + s_files, + s_fileDirectory, + s_headerFile, + s_includedNames, + s_includeDirectories, + s_language, + s_libraryPath, + s_methodClassName, + s_namespaceName, + s_outputMode, + s_outputLocation, + s_methodPrefixToStrip, + s_nativeTypeNamesToStrip, + s_remappedNameValuePairs, + s_remappedTypeNameValuePairs, + s_remappedFieldNameValuePairs, + s_std, + s_testOutputLocation, + s_traversalNames, + s_versionOption, + s_withAccessSpecifierNameValuePairs, + s_withAttributeNameValuePairs, + s_withCallConvNameValuePairs, + s_withClassNameValuePairs, + s_withGuidNameValuePairs, + s_withLengthNameValuePairs, + s_withLibraryPathNameValuePairs, + s_withManualImports, + s_withNamespaceNameValuePairs, + s_withPackingNameValuePairs, + s_withReadonlys, + s_withSetLastErrors, + s_withSuppressGCTransitions, + s_withTransparentStructNameValuePairs, + s_withTypeNameValuePairs, + s_withUsingNameValuePairs, + s_helpOption, ]; - private static Option GetAdditionalOption() - { - return new Option( - aliases: s_additionalOptionAliases, - description: "An argument to pass to Clang when parsing the input files.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetConfigOption() - { - return new Option( - aliases: s_configOptionAliases, - description: "A configuration option that controls how the bindings are generated. Specify 'help' to see the available options.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetDefineMacroOption() - { - return new Option( - aliases: s_defineMacroOptionAliases, - description: "Define to (or 1 if omitted).", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetExcludeOption() - { - return new Option( - aliases: s_excludeOptionAliases, - description: "A declaration name to exclude from binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetFileOption() - { - return new Option( - aliases: s_fileOptionAliases, - description: "A file to parse and generate bindings for.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetFileDirectoryOption() - { - return new Option( - aliases: s_fileDirectionOptionAliases, - description: "The base path for files to parse.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetHeaderOption() - { - return new Option( - aliases: s_headerOptionAliases, - description: "A file which contains the header to prefix every generated file with.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetIncludeOption() - { - return new Option( - aliases: s_includeOptionAliases, - description: "A declaration name to include in binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetIncludeDirectoryOption() - { - return new Option( - aliases: s_includeDirectoryOptionAliases, - description: "Add directory to include search path.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static readonly CommandLineParser s_parser = new(s_options); - private static Option GetLanguageOption() - { - return new Option( - aliases: s_languageOptionAliases, - description: "Treat subsequent input files as having type .", - getDefaultValue: () => "c++" - ).FromAmong("c", "c++"); - } - - private static Option GetLibraryOption() - { - return new Option( - aliases: s_libraryOptionAliases, - description: "The string to use in the DllImport attribute used when generating bindings.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetMethodClassNameOption() - { - return new Option( - aliases: s_methodClassNameOptionAliases, - description: "The name of the static class that will contain the generated method bindings.", - getDefaultValue: () => "Methods" - ); - } - - private static Option GetNamespaceOption() - { - return new Option( - aliases: s_namespaceOptionAliases, - description: "The namespace in which to place the generated bindings.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetNativeTypeNamesStripOption() - { - return new Option( - aliases: s_nativeTypeNamesStripOptionAliases, - description: "The contents to strip from the generated NativeTypeName attributes.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetOutputModeOption() - { - return new Option( - aliases: s_outputModeOptionAliases, - description: "The mode describing how the information collected from the headers are presented in the resultant bindings.", - getDefaultValue: () => PInvokeGeneratorOutputMode.CSharp - ); - } - - private static Option GetOutputOption() - { - return new Option( - aliases: s_outputOptionAliases, - description: "The output location to write the generated bindings to.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetPrefixStripOption() - { - return new Option( - aliases: s_prefixStripOptionAliases, - description: "The prefix to strip from the generated method bindings.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetRemapOption() - { - return new Option( - aliases: s_remapOptionAliases, - description: "A declaration name to be remapped to another name during binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetRemapTypeOption() - { - return new Option( - aliases: s_remapTypeOptionAliases, - description: "A type (record or enum) declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetRemapFieldOption() - { - return new Option( - aliases: s_remapFieldOptionAliases, - description: "A field declaration name to be remapped to another name during binding generation. Takes precedence over --remap and is useful when a type and field share a name.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static RootCommand GetRootCommand() - { - var rootCommand = new RootCommand("ClangSharp P/Invoke Binding Generator") - { - s_additionalOption, - s_configOption, - s_defineMacros, - s_excludedNames, - s_files, - s_fileDirectory, - s_headerFile, - s_includedNames, - s_includeDirectories, - s_language, - s_libraryPath, - s_methodClassName, - s_namespaceName, - s_outputMode, - s_outputLocation, - s_methodPrefixToStrip, - s_nativeTypeNamesToStrip, - s_remappedNameValuePairs, - s_remappedTypeNameValuePairs, - s_remappedFieldNameValuePairs, - s_std, - s_testOutputLocation, - s_traversalNames, - s_versionOption, - s_withAccessSpecifierNameValuePairs, - s_withAttributeNameValuePairs, - s_withCallConvNameValuePairs, - s_withClassNameValuePairs, - s_withGuidNameValuePairs, - s_withLengthNameValuePairs, - s_withLibraryPathNameValuePairs, - s_withManualImports, - s_withNamespaceNameValuePairs, - s_withPackingNameValuePairs, - s_withReadonlys, - s_withSetLastErrors, - s_withSuppressGCTransitions, - s_withTransparentStructNameValuePairs, - s_withTypeNameValuePairs, - s_withUsingNameValuePairs - }; - Handler.SetHandler(rootCommand, (Action)Run); - return rootCommand; - } - - private static Option GetStdOption() - { - return new Option( - aliases: s_stdOptionAliases, - description: "Language standard to compile for.", - getDefaultValue: () => "" - ); - } - - private static Option GetTestOutputOption() - { - return new Option( - aliases: s_testOutputOptionAliases, - description: "The output location to write the generated tests to.", - getDefaultValue: () => string.Empty - ); - } - - private static Option GetVersionOption() - { - return new Option( - aliases: s_versionOptionAliases, - description: "Prints the current version information for the tool and its native dependencies." - ) { - Arity = ArgumentArity.Zero - }; - } - - private static Option GetTraverseOption() - { - return new Option( - aliases: s_traverseOptionAliases, - description: "A file name included either directly or indirectly by -f that should be traversed during binding generation.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithAccessSpecifierOption() - { - return new Option( - aliases: s_withAccessSpecifierOptionAliases, - description: "An access specifier to be used with the given qualified or remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithAttributeOption() - { - return new Option( - aliases: s_withAttributeOptionAliases, - description: "An attribute to be added to the given remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithCallConvOption() - { - return new Option( - aliases: s_withCallConvOptionAliases, - description: "A calling convention to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithClassOption() - { - return new Option( - aliases: s_withClassOptionAliases, - description: "A class to be used for the given remapped constant or function declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithGuidOption() - { - return new Option( - aliases: s_withGuidOptionAliases, - description: "A GUID to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithLengthOption() - { - return new Option( - aliases: s_withLengthOptionAliases, - description: "A length to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithLibraryPathOption() - { - return new Option( - aliases: s_withLibraryPathOptionAliases, - description: "A library path to be used for the given declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithManualImportOption() - { - return new Option( - aliases: s_withManualImportOptionAliases, - description: "A remapped function name to be treated as a manual import during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithNamespaceOption() - { - return new Option( - aliases: s_withNamespaceOptionAliases, - description: "A namespace to be used for the given remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithReadonlyOption() - { - return new Option( - aliases: s_withReadonlyOptionAliases, - description: "Add the readonly modifier to a given instance method. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithSetLastErrorOption() - { - return new Option( - aliases: s_withSetLastErrorOptionAliases, - description: "Add the SetLastError=true modifier or SetsSystemLastError attribute to a given DllImport or UnmanagedFunctionPointer. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithSuppressGCTransitionOption() - { - return new Option( - aliases: s_withSuppressGCTransitionOptionAliases, - description: "Add the SuppressGCTransition calling convention to a given DllImport or UnmanagedFunctionPointer. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } - - private static Option GetWithTransparentStructOption() - { - return new Option( - aliases: s_withTransparentStructOptionAliases, - description: "A remapped type name to be treated as a transparent wrapper during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static readonly HelpRow[] s_configOptions = + [ + new HelpRow("?, h, help", "Show help and usage information for -c, --config"), + + new HelpRow("", ""), + new HelpRow("# Codegen Options", ""), + new HelpRow("", ""), + + new HelpRow("compatible-codegen", "Bindings should be generated with .NET Standard 2.0 compatibility. Setting this disables preview code generation."), + new HelpRow("default-codegen", "Bindings should be generated for the current LTS version of .NET/C#. This is currently .NET 8/C# 12."), + new HelpRow("latest-codegen", "Bindings should be generated for the current STS version of .NET/C#. This is currently .NET 10/C# 14."), + new HelpRow("preview-codegen", "Bindings should be generated for the preview version of .NET/C#. This is currently .NET 10/C# 14."), + + new HelpRow("", ""), + new HelpRow("# File Options", ""), + new HelpRow("", ""), + + new HelpRow("single-file", "Bindings should be generated to a single output file. This is the default."), + new HelpRow("multi-file", "Bindings should be generated so there is approximately one type per file."), + + new HelpRow("", ""), + new HelpRow("# Type Options", ""), + new HelpRow("", ""), + + new HelpRow("unix-types", "Bindings should be generated assuming Unix defaults. This is the default on Unix platforms."), + new HelpRow("windows-types", "Bindings should be generated assuming Windows defaults. This is the default on Windows platforms."), + + new HelpRow("", ""), + new HelpRow("# Exclusion Options", ""), + new HelpRow("", ""), + + new HelpRow("exclude-anonymous-field-helpers", "The helper ref properties generated for fields in nested anonymous structs and unions should not be generated."), + new HelpRow("exclude-com-proxies", "Types recognized as COM proxies should not have bindings generated. These are currently function declarations ending with _UserFree, _UserMarshal, _UserSize, _UserUnmarshal, _Proxy, or _Stub."), + new HelpRow("exclude-default-remappings", "Default remappings for well known types should not be added. This currently includes intptr_t, ptrdiff_t, size_t, and uintptr_t"), + new HelpRow("exclude-empty-records", "Bindings for records that contain no members should not be generated. These are commonly encountered for opaque handle like types such as HWND."), + new HelpRow("exclude-enum-operators", "Bindings for operators over enum types should not be generated. These are largely unnecessary in C# as the operators are available by default."), + new HelpRow("exclude-fnptr-codegen", "Generated bindings for latest or preview codegen should not use function pointers."), + new HelpRow("exclude-funcs-with-body", "Bindings for functions with bodies should not be generated."), + new HelpRow("exclude-using-statics-for-enums", "Enum usages should be fully qualified and should not include a corresponding 'using static EnumName;'"), + + new HelpRow("", ""), + new HelpRow("# Vtbl Options", ""), + new HelpRow("", ""), + + new HelpRow("explicit-vtbls", "VTBLs should have an explicit type generated with named fields per entry."), + new HelpRow("implicit-vtbls", "VTBLs should be implicit to reduce metadata bloat. This is the current default"), + new HelpRow("trimmable-vtbls", "VTBLs should be defined but not used in helper methods to reduce metadata bloat when trimming."), + + new HelpRow("", ""), + new HelpRow("# Test Options", ""), + new HelpRow("", ""), + + new HelpRow("generate-tests-nunit", "Basic tests validating size, blittability, and associated metadata should be generated for NUnit."), + new HelpRow("generate-tests-xunit", "Basic tests validating size, blittability, and associated metadata should be generated for XUnit."), + + new HelpRow("", ""), + new HelpRow("# Generation Options", ""), + new HelpRow("", ""), + + new HelpRow("generate-aggressive-inlining", "[MethodImpl(MethodImplOptions.AggressiveInlining)] should be added to generated helper functions."), + new HelpRow("generate-callconv-member-function", "Instance function pointers should use [CallConvMemberFunction] where applicable."), + new HelpRow("generate-cpp-attributes", "[CppAttributeList(\"\")] should be generated to document the encountered C++ attributes."), + new HelpRow("generate-disable-runtime-marshalling", "[assembly: DisableRuntimeMarshalling] should be generated."), + new HelpRow("generate-doc-includes", " xml documentation tags should be generated for declarations."), + new HelpRow("generate-file-scoped-namespaces", "Namespaces should be scoped to the file to reduce nesting."), + new HelpRow("generate-fixed-buffer-indexer-overloads", "Fixed sized buffer helper types should generate additional uint, nint, and nuint indexer overloads."), + new HelpRow("generate-guid-member", "Types with an associated GUID should have a corresponding member generated."), + new HelpRow("generate-helper-types", "Code files should be generated for various helper attributes and declared transparent structs."), + new HelpRow("generate-macro-bindings", "Bindings for macro-definitions should be generated. This currently only works with value like macros and not function-like ones."), + new HelpRow("generate-marker-interfaces", "Bindings for marker interfaces representing native inheritance hierarchies should be generated."), + new HelpRow("generate-native-bitfield-attribute", "[NativeBitfield(\"\", offset: #, length: #)] attribute should be generated to document the encountered bitfield layout."), + new HelpRow("generate-native-inheritance-attribute", "[NativeInheritance(\"\")] attribute should be generated to document the encountered C++ base type."), + new HelpRow("generate-generic-pointer-wrapper", "Pointer should be used for limited generic type support."), + new HelpRow("generate-setslastsystemerror-attribute", "[SetsLastSystemError] attribute should be generated rather than using SetLastError = true."), + new HelpRow("generate-template-bindings", "Bindings for template-definitions should be generated. This is currently experimental."), + new HelpRow("generate-unmanaged-constants", "Unmanaged constants should be generated using static ref readonly properties. This is currently experimental."), + new HelpRow("generate-vtbl-index-attribute", "[VtblIndex(#)] attribute should be generated to document the underlying VTBL index for a helper method."), + + new HelpRow("", ""), + new HelpRow("# Stripping Options", ""), + new HelpRow("", ""), + + new HelpRow("strip-enum-member-type-name", "Strips the enum type name from the beginning of its member names."), + + new HelpRow("", ""), + new HelpRow("# Logging Options", ""), + new HelpRow("", ""), + + new HelpRow("log-exclusions", "A list of excluded declaration types should be generated. This will also log if the exclusion was due to an exact or partial match."), + new HelpRow("log-potential-typedef-remappings", "A list of potential typedef remappings should be generated. This can help identify missing remappings."), + new HelpRow("log-visited-files", "A list of the visited files should be generated. This can help identify traversal issues."), + ]; - private static Option GetWithTypeOption() - { - return new Option( - aliases: s_withTypeOptionAliases, - description: "A type to be used for the given enum declaration during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static CommandLineOption Multi(string[] aliases, string description) => new(aliases, description, CommandLineOptionKind.MultipleValue); - private static Option GetWithUsingOption() - { - return new Option( - aliases: s_withUsingOptionAliases, - description: "A using directive to be included for the given remapped declaration name during binding generation. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static CommandLineOption Single(string[] aliases, string description, string defaultValue = "", string? valueName = null, string[]? allowedValues = null) => new(aliases, description, CommandLineOptionKind.SingleValue, valueName, defaultValue, allowedValues); - private static Option GetWithPackingOption() - { - return new Option( - aliases: s_withPackingOptionAliases, - description: "Overrides the StructLayoutAttribute.Pack property for the given type. Supports wildcards.", - getDefaultValue: Array.Empty - ) { - AllowMultipleArgumentsPerToken = true - }; - } + private static CommandLineOption Flag(string[] aliases, string description) => new(aliases, description, CommandLineOptionKind.Flag); } diff --git a/sources/ClangSharpPInvokeGenerator/Program.cs b/sources/ClangSharpPInvokeGenerator/Program.cs index eb3e58a6..b6ca2a08 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.cs @@ -2,17 +2,10 @@ using System; using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Help; -using System.CommandLine.Invocation; -using System.CommandLine.IO; -using System.CommandLine.Parsing; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Threading.Tasks; using ClangSharp.Interop; using static ClangSharp.Interop.CXDiagnosticSeverity; using static ClangSharp.Interop.CXErrorCode; @@ -22,92 +15,82 @@ namespace ClangSharp; internal static partial class Program { - public static IEnumerable GetExtendedHelp(HelpContext context) + private const string Name = "ClangSharpPInvokeGenerator"; + private const string Description = "ClangSharp P/Invoke Binding Generator"; + private const string Version = "21.1.8"; + + private const string WildcardsTitle = "Wildcards:"; + private const string Wildcards = "You can use * as catch-all rule for remapping procedures. For example if you want make all of your generated code internal you can use --with-access-specifier *=Internal."; + + public static int Main(params string[] args) { - foreach (var sectionDelegate in HelpBuilder.Default.GetLayout()) + s_parser.Parse(args); + + if (s_helpOption.IsPresent) { - yield return sectionDelegate; + s_parser.WriteHelp(Console.Out, Name, Description, WildcardsTitle, Wildcards); + return 0; } - yield return _ => { - Console.WriteLine("Wildcards:"); - Console.WriteLine("You can use * as catch-all rule for remapping procedures. For example if you want make all of your generated code internal you can use --with-access-specifier *=Internal."); - }; - } + if (s_versionOption.IsPresent) + { + Console.WriteLine($"{Description} version {Version}"); + Console.WriteLine($" {clang.getClangVersion()}"); + Console.WriteLine($" {clangsharp.getVersion()}"); + return 0; + } - public static async Task Main(params string[] args) - { - var parser = new CommandLineBuilder(s_rootCommand) - .UseHelp(context => context.HelpBuilder.CustomizeLayout(GetExtendedHelp)) - .UseEnvironmentVariableDirective() - .UseParseDirective() - .UseSuggestDirective() - .RegisterWithDotnetSuggest() - .UseTypoCorrections() - .UseParseErrorReporting() - .UseExceptionHandler() - .CancelOnProcessTermination() - .Build(); - return await parser.InvokeAsync(args).ConfigureAwait(false); + return Run(); } - public static void Run(InvocationContext context) + public static int Run() { - ArgumentNullException.ThrowIfNull(context); - - var additionalArgs = context.ParseResult.GetValueForOption(s_additionalOption) ?? []; - var configSwitches = context.ParseResult.GetValueForOption(s_configOption) ?? []; - var defineMacros = context.ParseResult.GetValueForOption(s_defineMacros) ?? []; - var excludedNames = context.ParseResult.GetValueForOption(s_excludedNames) ?? []; - var files = context.ParseResult.GetValueForOption(s_files) ?? []; - var fileDirectory = context.ParseResult.GetValueForOption(s_fileDirectory) ?? ""; - var headerFile = context.ParseResult.GetValueForOption(s_headerFile) ?? ""; - var includedNames = context.ParseResult.GetValueForOption(s_includedNames) ?? []; - var includeDirectories = context.ParseResult.GetValueForOption(s_includeDirectories) ?? []; - var language = context.ParseResult.GetValueForOption(s_language) ?? ""; - var libraryPath = context.ParseResult.GetValueForOption(s_libraryPath) ?? ""; - var methodClassName = context.ParseResult.GetValueForOption(s_methodClassName) ?? ""; - var methodPrefixToStrip = context.ParseResult.GetValueForOption(s_methodPrefixToStrip) ?? ""; - var nativeTypeNamesToStrip = context.ParseResult.GetValueForOption(s_nativeTypeNamesToStrip) ?? []; - var namespaceName = context.ParseResult.GetValueForOption(s_namespaceName) ?? ""; - var outputLocation = context.ParseResult.GetValueForOption(s_outputLocation) ?? ""; - var outputMode = context.ParseResult.GetValueForOption(s_outputMode); - var remappedNameValuePairs = context.ParseResult.GetValueForOption(s_remappedNameValuePairs) ?? []; - var remappedTypeNameValuePairs = context.ParseResult.GetValueForOption(s_remappedTypeNameValuePairs) ?? []; - var remappedFieldNameValuePairs = context.ParseResult.GetValueForOption(s_remappedFieldNameValuePairs) ?? []; - var std = context.ParseResult.GetValueForOption(s_std) ?? ""; - var testOutputLocation = context.ParseResult.GetValueForOption(s_testOutputLocation) ?? ""; - var traversalNames = context.ParseResult.GetValueForOption(s_traversalNames) ?? []; - var withAccessSpecifierNameValuePairs = context.ParseResult.GetValueForOption(s_withAccessSpecifierNameValuePairs) ?? []; - var withAttributeNameValuePairs = context.ParseResult.GetValueForOption(s_withAttributeNameValuePairs) ?? []; - var withCallConvNameValuePairs = context.ParseResult.GetValueForOption(s_withCallConvNameValuePairs) ?? []; - var withClassNameValuePairs = context.ParseResult.GetValueForOption(s_withClassNameValuePairs) ?? []; - var withGuidNameValuePairs = context.ParseResult.GetValueForOption(s_withGuidNameValuePairs) ?? []; - var withLengthNameValuePairs = context.ParseResult.GetValueForOption(s_withLengthNameValuePairs) ?? []; - var withLibraryPathNameValuePairs = context.ParseResult.GetValueForOption(s_withLibraryPathNameValuePairs) ?? []; - var withManualImports = context.ParseResult.GetValueForOption(s_withManualImports) ?? []; - var withNamespaceNameValuePairs = context.ParseResult.GetValueForOption(s_withNamespaceNameValuePairs) ?? []; - var withReadonlys = context.ParseResult.GetValueForOption(s_withReadonlys) ?? []; - var withSetLastErrors = context.ParseResult.GetValueForOption(s_withSetLastErrors) ?? []; - var withSuppressGCTransitions = context.ParseResult.GetValueForOption(s_withSuppressGCTransitions) ?? []; - var withTransparentStructNameValuePairs = context.ParseResult.GetValueForOption(s_withTransparentStructNameValuePairs) ?? []; - var withTypeNameValuePairs = context.ParseResult.GetValueForOption(s_withTypeNameValuePairs) ?? []; - var withUsingNameValuePairs = context.ParseResult.GetValueForOption(s_withUsingNameValuePairs) ?? []; - var withPackingNameValuePairs = context.ParseResult.GetValueForOption(s_withPackingNameValuePairs) ?? []; - - var versionResult = context.ParseResult.FindResultFor(s_versionOption); - - if (versionResult is not null) + var errorList = new List(s_parser.Errors); + + var additionalArgs = s_additionalOption.GetValues(); + var configSwitches = s_configOption.GetValues(); + var defineMacros = s_defineMacros.GetValues(); + var excludedNames = s_excludedNames.GetValues(); + var files = s_files.GetValues(); + var fileDirectory = s_fileDirectory.SingleValue; + var headerFile = s_headerFile.SingleValue; + var includedNames = s_includedNames.GetValues(); + var includeDirectories = s_includeDirectories.GetValues(); + var language = s_language.SingleValue; + var libraryPath = s_libraryPath.SingleValue; + var methodClassName = s_methodClassName.SingleValue; + var methodPrefixToStrip = s_methodPrefixToStrip.SingleValue; + var nativeTypeNamesToStrip = s_nativeTypeNamesToStrip.GetValues(); + var namespaceName = s_namespaceName.SingleValue; + var outputLocation = s_outputLocation.SingleValue; + var remappedNameValuePairs = s_remappedNameValuePairs.GetValues(); + var remappedTypeNameValuePairs = s_remappedTypeNameValuePairs.GetValues(); + var remappedFieldNameValuePairs = s_remappedFieldNameValuePairs.GetValues(); + var std = s_std.SingleValue; + var testOutputLocation = s_testOutputLocation.SingleValue; + var traversalNames = s_traversalNames.GetValues(); + var withAccessSpecifierNameValuePairs = s_withAccessSpecifierNameValuePairs.GetValues(); + var withAttributeNameValuePairs = s_withAttributeNameValuePairs.GetValues(); + var withCallConvNameValuePairs = s_withCallConvNameValuePairs.GetValues(); + var withClassNameValuePairs = s_withClassNameValuePairs.GetValues(); + var withGuidNameValuePairs = s_withGuidNameValuePairs.GetValues(); + var withLengthNameValuePairs = s_withLengthNameValuePairs.GetValues(); + var withLibraryPathNameValuePairs = s_withLibraryPathNameValuePairs.GetValues(); + var withManualImports = s_withManualImports.GetValues(); + var withNamespaceNameValuePairs = s_withNamespaceNameValuePairs.GetValues(); + var withReadonlys = s_withReadonlys.GetValues(); + var withSetLastErrors = s_withSetLastErrors.GetValues(); + var withSuppressGCTransitions = s_withSuppressGCTransitions.GetValues(); + var withTransparentStructNameValuePairs = s_withTransparentStructNameValuePairs.GetValues(); + var withTypeNameValuePairs = s_withTypeNameValuePairs.GetValues(); + var withUsingNameValuePairs = s_withUsingNameValuePairs.GetValues(); + var withPackingNameValuePairs = s_withPackingNameValuePairs.GetValues(); + + if (!Enum.TryParse(s_outputMode.SingleValue, ignoreCase: true, out var outputMode)) { - context.Console.WriteLine($"{s_rootCommand.Description} version 21.1.8"); - context.Console.WriteLine($" {clang.getClangVersion()}"); - context.Console.WriteLine($" {clangsharp.getVersion()}"); - context.ExitCode = 0; - return; + errorList.Add($"Error: Unrecognized output mode: {s_outputMode.SingleValue}. Must be one of CSharp or Xml"); } - var errorList = new List(); - if (files.Length == 0) { errorList.Add("Error: No input C/C++ files provided. Use --file or -f"); @@ -502,34 +485,24 @@ public static void Run(InvocationContext context) if (printConfigHelp) { - var helpBuilder = new CustomHelpBuilder(context.Console, context.LocalizationResources); - - helpBuilder.Write(s_configOption); - helpBuilder.WriteLine(); - helpBuilder.Write(s_configOptions); - - context.ExitCode = -1; - return; + CommandLineParser.WriteOptionHelp(Console.Out, s_configOption, s_configOptions); + return -1; } if (errorList.Count != 0) { - context.Console.Error.Write($"Error in args for '{files.FirstOrDefault()}'"); - context.Console.Error.Write(Environment.NewLine); + Console.Error.Write($"Error in args for '{files.FirstOrDefault()}'"); + Console.Error.Write(Environment.NewLine); foreach (var error in errorList) { - context.Console.Error.Write(error); - context.Console.Error.Write(Environment.NewLine); + Console.Error.Write(error); + Console.Error.Write(Environment.NewLine); } - context.Console.Error.Write(Environment.NewLine); - - using var textWriter = context.Console.Out.CreateTextWriter(); - var customHelpBuilder = new CustomHelpBuilder(context.Console, context.LocalizationResources); - customHelpBuilder.Write(s_rootCommand, textWriter); + Console.Error.Write(Environment.NewLine); - context.ExitCode = -1; - return; + s_parser.WriteHelp(Console.Out, Name, Description, WildcardsTitle, Wildcards); + return -1; } var clangCommandLineArgs = string.IsNullOrWhiteSpace(std) @@ -599,19 +572,19 @@ public static void Run(InvocationContext context) if (translationUnitError != CXError_Success) { - context.Console.WriteLine($"Error: Parsing failed for '{filePath}' due to '{translationUnitError}'."); + Console.WriteLine($"Error: Parsing failed for '{filePath}' due to '{translationUnitError}'."); skipProcessing = true; } else if (handle.NumDiagnostics != 0) { - context.Console.WriteLine($"Diagnostics for '{filePath}':"); + Console.WriteLine($"Diagnostics for '{filePath}':"); for (uint i = 0; i < handle.NumDiagnostics; ++i) { using var diagnostic = handle.GetDiagnostic(i); - context.Console.Write(" "); - context.Console.WriteLine(diagnostic.Format(CXDiagnostic.DefaultDisplayOptions).ToString()); + Console.Write(" "); + Console.WriteLine(diagnostic.Format(CXDiagnostic.DefaultDisplayOptions).ToString()); skipProcessing |= diagnostic.Severity == CXDiagnostic_Error; skipProcessing |= diagnostic.Severity == CXDiagnostic_Fatal; @@ -620,8 +593,8 @@ public static void Run(InvocationContext context) if (skipProcessing) { - context.Console.WriteLine($"Skipping '{filePath}' due to one or more errors listed above."); - context.Console.WriteLine(""); + Console.WriteLine($"Skipping '{filePath}' due to one or more errors listed above."); + Console.WriteLine(""); exitCode = -1; continue; @@ -634,12 +607,12 @@ public static void Run(InvocationContext context) using var translationUnit = TranslationUnit.GetOrCreate(handle); Debug.Assert(translationUnit is not null); - context.Console.WriteLine($"Processing '{filePath}'"); + Console.WriteLine($"Processing '{filePath}'"); pinvokeGenerator.GenerateBindings(translationUnit, filePath, clangCommandLineArgs, translationFlags); } catch (Exception e) { - context.Console.WriteLine(e.ToString()); + Console.WriteLine(e.ToString()); } #pragma warning restore CA1031 // Do not catch general exception types @@ -647,12 +620,12 @@ public static void Run(InvocationContext context) if (pinvokeGenerator.Diagnostics.Count != 0) { - context.Console.WriteLine($"Diagnostics for binding generation of {pinvokeGenerator.FilePath}:"); + Console.WriteLine($"Diagnostics for binding generation of {pinvokeGenerator.FilePath}:"); foreach (var diagnostic in pinvokeGenerator.Diagnostics) { - context.Console.Write(" "); - context.Console.WriteLine(diagnostic.ToString()); + Console.Write(" "); + Console.WriteLine(diagnostic.ToString()); if (diagnostic.Level == DiagnosticLevel.Warning) { @@ -676,7 +649,7 @@ public static void Run(InvocationContext context) } } - context.ExitCode = exitCode; + return exitCode; } private static void ParseKeyValuePairs(IEnumerable keyValuePairs, List errorList, out Dictionary result) From 449aef26960be861818acca3931b769fb3cc9b29 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Mon, 13 Jul 2026 09:11:44 -0700 Subject: [PATCH 2/3] Derive the tool version from the assembly instead of hardcoding it The version shown by --version was a hardcoded "21.1.8" that had to be manually kept in sync with VersionPrefix and the targeted clang release. Read it from the assembly version (major.minor.build) so it tracks VersionPrefix automatically and stays aligned with the clang/clangsharp version lines printed alongside it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sources/ClangSharpPInvokeGenerator/Program.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sources/ClangSharpPInvokeGenerator/Program.cs b/sources/ClangSharpPInvokeGenerator/Program.cs index b6ca2a08..fc8bd19d 100644 --- a/sources/ClangSharpPInvokeGenerator/Program.cs +++ b/sources/ClangSharpPInvokeGenerator/Program.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; using ClangSharp.Interop; using static ClangSharp.Interop.CXDiagnosticSeverity; @@ -17,7 +18,10 @@ internal static partial class Program { private const string Name = "ClangSharpPInvokeGenerator"; private const string Description = "ClangSharp P/Invoke Binding Generator"; - private const string Version = "21.1.8"; + + // The clang release the tool targets, tracked via the assembly version (major.minor.build) + // so it stays in sync with VersionPrefix and matches the clang/clangsharp lines below it. + private static readonly string Version = GetVersion(); private const string WildcardsTitle = "Wildcards:"; private const string Wildcards = "You can use * as catch-all rule for remapping procedures. For example if you want make all of your generated code internal you can use --with-access-specifier *=Internal."; @@ -43,6 +47,12 @@ public static int Main(params string[] args) return Run(); } + private static string GetVersion() + { + var version = Assembly.GetExecutingAssembly().GetName().Version; + return (version is not null) ? $"{version.Major}.{version.Minor}.{version.Build}" : ""; + } + public static int Run() { var errorList = new List(s_parser.Errors); From 38618be8eb43b5a37f5e7684da67fb4ee8529d08 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Mon, 13 Jul 2026 09:17:06 -0700 Subject: [PATCH 3/3] Add unit tests for the command-line parser Cover the custom `CommandLineParser` directly, including the dotnet/clangsharp#554 regression: a `name=value` token whose value contains spaces (bare, inline `--opt=`, and via a response file) is preserved as a single value rather than split on whitespace. Also cover multi-pair tokens, single/flag arity, `AllowedValues`, unknown options, and response-file handling. Wire the exe up with `InternalsVisibleTo` so the internal parser types are testable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ClangSharp.slnx | 1 + .../ClangSharpPInvokeGenerator.csproj | 4 + ...langSharpPInvokeGenerator.UnitTests.csproj | 22 +++ .../CommandLineParserTests.cs | 179 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 tests/ClangSharpPInvokeGenerator.UnitTests/ClangSharpPInvokeGenerator.UnitTests.csproj create mode 100644 tests/ClangSharpPInvokeGenerator.UnitTests/CommandLineParserTests.cs diff --git a/ClangSharp.slnx b/ClangSharp.slnx index 443546c4..9b682777 100644 --- a/ClangSharp.slnx +++ b/ClangSharp.slnx @@ -113,5 +113,6 @@ + diff --git a/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj b/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj index 25261c91..c13ea9c7 100644 --- a/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj +++ b/sources/ClangSharpPInvokeGenerator/ClangSharpPInvokeGenerator.csproj @@ -29,6 +29,10 @@ + + + + libclang libClangSharp diff --git a/tests/ClangSharpPInvokeGenerator.UnitTests/ClangSharpPInvokeGenerator.UnitTests.csproj b/tests/ClangSharpPInvokeGenerator.UnitTests/ClangSharpPInvokeGenerator.UnitTests.csproj new file mode 100644 index 00000000..160a63ea --- /dev/null +++ b/tests/ClangSharpPInvokeGenerator.UnitTests/ClangSharpPInvokeGenerator.UnitTests.csproj @@ -0,0 +1,22 @@ + + + + + ClangSharp.UnitTests + net10.0 + + + + + + + + + $(NoWarn);CA1515;CA1707;CA1711;CA1861;IDE0130 + + + + + + + diff --git a/tests/ClangSharpPInvokeGenerator.UnitTests/CommandLineParserTests.cs b/tests/ClangSharpPInvokeGenerator.UnitTests/CommandLineParserTests.cs new file mode 100644 index 00000000..34e8755c --- /dev/null +++ b/tests/ClangSharpPInvokeGenerator.UnitTests/CommandLineParserTests.cs @@ -0,0 +1,179 @@ +// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information. + +using System.IO; +using NUnit.Framework; + +namespace ClangSharp.UnitTests; + +public sealed class CommandLineParserTests +{ + private static CommandLineOption Multi(params string[] aliases) + => new(aliases, "", CommandLineOptionKind.MultipleValue); + + private static CommandLineOption Single(params string[] aliases) + => new(aliases, "", CommandLineOptionKind.SingleValue); + + private static CommandLineOption Flag(params string[] aliases) + => new(aliases, "", CommandLineOptionKind.Flag); + + // Regression test for dotnet/clangsharp#554: a `name=value` token whose value contains + // spaces must be preserved as a single value rather than split on whitespace. + [Test] + public void SpacedValueIsPreservedAsSingleValue() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap", "__arglist=@params string[] args"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "__arglist=@params string[] args" })); + } + + [Test] + public void MultiplePairsAcrossSeparateTokensAreDistinctValues() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap", "A=B", "C=D"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "A=B", "C=D" })); + } + + [Test] + public void InlineEqualsFormIsSplitOnlyOnTheAliasBoundary() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap=Foo=System.String bar"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "Foo=System.String bar" })); + } + + [Test] + public void BareNameValueTokenIsTreatedAsValueNotOption() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--remap", "-Foo=Bar"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "-Foo=Bar" })); + } + + // A value that happens to start with a known short alias prefix (e.g. `-m64` after + // `--additional`, where `-m` is a distinct option) must be kept as a value. + [Test] + public void ValueStartingWithAnotherOptionPrefixIsNotMatchedAsThatOption() + { + var additional = Multi("--additional", "-a"); + var methodClass = Single("--methodClassName", "-m"); + var parser = new CommandLineParser([additional, methodClass]); + + parser.Parse(["--additional", "-m64"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(additional.GetValues(), Is.EqualTo(new[] { "-m64" })); + Assert.That(methodClass.IsPresent, Is.False); + } + + [Test] + public void SingleValueOptionConsumesExactlyOneToken() + { + var output = Single("--output", "-o"); + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([output, remap]); + + parser.Parse(["--output", "out.cs", "--remap", "A=B"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(output.SingleValue, Is.EqualTo("out.cs")); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "A=B" })); + } + + [Test] + public void FlagOptionTakesNoArgument() + { + var version = Flag("--version"); + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([version, remap]); + + parser.Parse(["--version", "--remap", "A=B"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(version.IsPresent, Is.True); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "A=B" })); + } + + [Test] + public void UnrecognizedOptionProducesAnError() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["--unknown"]); + + Assert.That(parser.Errors, Has.Count.EqualTo(1)); + Assert.That(parser.Errors[0], Does.Contain("--unknown")); + } + + [Test] + public void AllowedValuesRejectsUnexpectedValue() + { + var language = new CommandLineOption(["--language", "-x"], "", CommandLineOptionKind.SingleValue, allowedValues: ["c", "c++"]); + var parser = new CommandLineParser([language]); + + parser.Parse(["--language", "rust"]); + + Assert.That(parser.Errors, Has.Count.EqualTo(1)); + Assert.That(parser.Errors[0], Does.Contain("rust")); + } + + // A response file must yield one token per non-empty, non-comment line so that a value + // containing spaces survives intact (the core of the #554 fix). + [Test] + public void ResponseFileTreatsEachLineAsASingleToken() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + var responseFile = Path.GetTempFileName(); + + try + { + File.WriteAllLines(responseFile, [ + "# a comment", + "--remap", + "__arglist=@params string[] args", + "", + "Foo=System.String bar", + ]); + + parser.Parse([$"@{responseFile}"]); + + Assert.That(parser.Errors, Is.Empty); + Assert.That(remap.GetValues(), Is.EqualTo(new[] { "__arglist=@params string[] args", "Foo=System.String bar" })); + } + finally + { + File.Delete(responseFile); + } + } + + [Test] + public void MissingResponseFileProducesAnError() + { + var remap = Multi("--remap", "-r"); + var parser = new CommandLineParser([remap]); + + parser.Parse(["@does-not-exist.rsp"]); + + Assert.That(parser.Errors, Has.Count.EqualTo(1)); + Assert.That(parser.Errors[0], Does.Contain("Response file not found")); + } +}