Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Options:
-wa, --with-attribute <with-attribute> An attribute to be added to the given remapped declaration name during binding generation. Supports wildcards. []
-wcc, --with-callconv <with-callconv> A calling convention to be used for the given declaration during binding generation. Supports wildcards. []
-wc, --with-class <with-class> A class to be used for the given remapped constant or function declaration name during binding generation. Supports wildcards. []
-wems, --with-enum-member-strip <with-enum-member-strip> How to strip a prefix or suffix from the members of the given remapped enum name during binding generation. Mode is one of `none`, `common-prefix`, `common-suffix`, `type-name`, `prefix:<str>`, or `suffix:<str>`. Supports wildcards. []
-wg, --with-guid <with-guid> A GUID to be used for the given declaration during binding generation. Supports wildcards. []
-wl, --with-length <with-length> A length to be used for the given declaration during binding generation. Supports wildcards. []
-wlb, --with-librarypath <with-librarypath> A library path to be used for the given declaration during binding generation. Supports wildcards. []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ private void VisitEnumConstantDecl(EnumConstantDecl enumConstantDecl)
parentName = _outputBuilder.Name;
}

var escapedName = EscapeAndStripEnumMemberName(name, parentName);
var escapedName = EscapeAndStripEnumMemberName(name, parentName, enumConstantDecl.DeclContext as EnumDecl);

var kind = isAnonymousEnum ? ValueKind.Primitive : ValueKind.Enumerator;
var flags = ValueFlags.Constant;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,7 @@ private void VisitDeclRefExpr(DeclRefExpr declRefExpr)

if (!IsAnonymousEnum(enumTypeName))
{
escapedName = EscapeAndStripEnumMemberName(name, enumTypeName);
escapedName = EscapeAndStripEnumMemberName(name, enumTypeName, enumConstantDecl.DeclContext as EnumDecl);
}
}

Expand Down
202 changes: 197 additions & 5 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public sealed partial class PInvokeGenerator : IDisposable
private readonly HashSet<string> _topLevelClassNames;
private readonly HashSet<string> _usedRemappings;
private readonly HashSet<RecordDecl> _declashedRecordNames;
private readonly Dictionary<EnumDecl, (EnumMemberStripKind Kind, string Text)> _enumMemberStrips;
private readonly string _placeholderMacroType;

private string _filePath;
Expand Down Expand Up @@ -199,6 +200,7 @@ public PInvokeGenerator(PInvokeGeneratorConfiguration config, Func<string, Strea
_topLevelClassUsings = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
_usedRemappings = new HashSet<string>(StringComparer.Ordinal);
_declashedRecordNames = [];
_enumMemberStrips = [];
_filePath = "";
_clangCommandLineArgs = [];
_placeholderMacroType = GetPlaceholderMacroType();
Expand Down Expand Up @@ -1027,8 +1029,43 @@ private string EscapeAndStripMethodName(string name)
return EscapeName(name);
}

private string EscapeAndStripEnumMemberName(string name, string enumTypeName)
private string EscapeAndStripEnumMemberName(string name, string enumTypeName, EnumDecl? enumDecl = null)
{
if ((enumDecl is not null) &&
(_config.WithEnumMemberStrip.TryGetValue(enumTypeName, out var mode) || _config.WithEnumMemberStrip.TryGetValue("*", out mode)))
{
// An explicit mode was configured for this enum (or via the `*` default) and fully determines
// how the member is stripped, taking precedence over the legacy type-name/type-prefix behavior.
switch (mode)
{
case "none":
{
return EscapeName(name);
}

case "type-name":
{
return StripEnumMemberTypeName(name, enumTypeName);
}

default:
{
var (kind, text) = GetEnumMemberStrip(enumDecl, mode);

if (kind == EnumMemberStripKind.None)
{
return EscapeName(name);
}

var strippedName = (kind == EnumMemberStripKind.Suffix)
? StripSuffix(name, text)
: PrefixAndStrip(name, text);

return GuardLeadingDigit(strippedName);
}
}
}

var typePrefixToStrip = _config.TypePrefixToStrip;

if (Config.StripEnumMemberTypeName || (typePrefixToStrip.Length != 0))
Expand All @@ -1042,15 +1079,170 @@ private string EscapeAndStripEnumMemberName(string name, string enumTypeName)
strippedName = PrefixAndStrip(strippedName, enumTypeName, trimChar: '_');
}

if (strippedName.Length > 0 && char.IsAsciiDigit(strippedName[0]))
return GuardLeadingDigit(strippedName);
}
return EscapeName(name);
}

private static string StripEnumMemberTypeName(string name, string enumTypeName)
{
var strippedName = PrefixAndStrip(name, enumTypeName, trimChar: '_');
return GuardLeadingDigit(strippedName);
}

private static string GuardLeadingDigit(string name)
{
// A leading digit is not a valid C# identifier start, so guard it with an underscore.
if (name.Length > 0 && char.IsAsciiDigit(name[0]))
{
name = '_' + name;
}
return name;
}

private static string StripSuffix(string name, string suffix)
{
var nameSpan = name.AsSpan();
if (nameSpan.EndsWith(suffix, StringComparison.Ordinal))
{
return nameSpan[..^suffix.Length].ToString();
}
return name;
}

// Resolves, and caches per enum, the prefix/suffix that should be stripped from the members of an enum
// for the `common-prefix`, `common-suffix`, `prefix:<str>`, and `suffix:<str>` modes. The result is
// all-or-nothing: `EnumMemberStripKind.None` is returned when no valid strip exists so the whole enum
// keeps its original member names and stays internally consistent.
private (EnumMemberStripKind Kind, string Text) GetEnumMemberStrip(EnumDecl enumDecl, string mode)
{
if (_enumMemberStrips.TryGetValue(enumDecl, out var cached))
{
return cached;
}

var result = ComputeEnumMemberStrip(enumDecl, mode);
_enumMemberStrips.Add(enumDecl, result);
return result;
}

private (EnumMemberStripKind Kind, string Text) ComputeEnumMemberStrip(EnumDecl enumDecl, string mode)
{
EnumMemberStripKind kind;
string text;

if (mode.StartsWith("prefix:", StringComparison.Ordinal))
{
kind = EnumMemberStripKind.Prefix;
text = mode["prefix:".Length..];
}
else if (mode.StartsWith("suffix:", StringComparison.Ordinal))
{
kind = EnumMemberStripKind.Suffix;
text = mode["suffix:".Length..];
}
else if (mode == "common-prefix")
{
kind = EnumMemberStripKind.Prefix;
text = ComputeEnumMemberCommonAffix(enumDecl, suffix: false);
}
else if (mode == "common-suffix")
{
kind = EnumMemberStripKind.Suffix;
text = ComputeEnumMemberCommonAffix(enumDecl, suffix: true);
}
else
{
AddDiagnostic(DiagnosticLevel.Warning, $"Unrecognized enum member strip mode '{mode}' for enum '{GetCursorName(enumDecl)}'.", enumDecl);
return (EnumMemberStripKind.None, "");
}

if (text.Length == 0)
{
return (EnumMemberStripKind.None, "");
}

// Validate all-or-nothing: every member must remain a distinct, non-empty identifier after stripping.
var strippedNames = new HashSet<string>(StringComparer.Ordinal);

foreach (var enumerator in enumDecl.Enumerators)
{
var name = GetRemappedCursorName(enumerator);
var strippedName = (kind == EnumMemberStripKind.Suffix) ? StripSuffix(name, text) : PrefixAndStrip(name, text);
strippedName = GuardLeadingDigit(strippedName);

if (strippedName.Length == 0)
{
AddDiagnostic(DiagnosticLevel.Info, $"Not stripping {(kind == EnumMemberStripKind.Suffix ? "suffix" : "prefix")} '{text}' from enum '{GetCursorName(enumDecl)}' because it would leave an empty member name.", enumDecl);
return (EnumMemberStripKind.None, "");
}

if (!strippedNames.Add(strippedName))
{
strippedName = '_' + strippedName;
AddDiagnostic(DiagnosticLevel.Info, $"Not stripping {(kind == EnumMemberStripKind.Suffix ? "suffix" : "prefix")} '{text}' from enum '{GetCursorName(enumDecl)}' because it would cause member name collisions.", enumDecl);
return (EnumMemberStripKind.None, "");
}
return strippedName;
}
return EscapeName(name);

return (kind, text);
}

// Auto-detects the longest common prefix (or suffix) shared by an enum's member names, trimmed back to
// the nearest `_` token boundary so a partial token is never cut. Returns "" when no valid affix exists
// (fewer than two members, no shared characters, or no `_` boundary within the shared characters).
private string ComputeEnumMemberCommonAffix(EnumDecl enumDecl, bool suffix)
{
var enumerators = enumDecl.Enumerators;

// The longest common affix of a single name is the whole name, which is useless, so require at least two.
if (enumerators.Count < 2)
{
return "";
}

var commonAffix = GetRemappedCursorName(enumerators[0]).AsSpan();

for (var i = 1; i < enumerators.Count; i++)
{
var name = GetRemappedCursorName(enumerators[i]).AsSpan();
var length = Math.Min(commonAffix.Length, name.Length);
var matched = 0;

while ((matched < length) && (suffix ? (commonAffix[^(matched + 1)] == name[^(matched + 1)]) : (commonAffix[matched] == name[matched])))
{
matched++;
}

commonAffix = suffix ? commonAffix[^matched..] : commonAffix[..matched];

if (commonAffix.Length == 0)
{
return "";
}
}

// Trim the affix back to the nearest `_` (inclusive) so we strip whole tokens only. Without this,
// `abc_some_enum_key1`/`key2` would share the prefix `abc_some_enum_key` and strip to `1`/`2`.
if (suffix)
{
var firstSeparator = commonAffix.IndexOf('_');
return (firstSeparator < 0) ? "" : commonAffix[firstSeparator..].ToString();
}
else
{
var lastSeparator = commonAffix.LastIndexOf('_');
return (lastSeparator < 0) ? "" : commonAffix[..(lastSeparator + 1)].ToString();
}
}

private enum EnumMemberStripKind
{
None,
Prefix,
Suffix,
}


internal static string EscapeCharacter(char value) => value switch {
'\0' => @"\0",
'\\' => @"\\",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public sealed class PInvokeGeneratorConfiguration
internal readonly Dictionary<string, IReadOnlyList<string>> _withAttributes;
internal readonly Dictionary<string, string> _withCallConvs;
internal readonly Dictionary<string, string> _withClasses;
internal readonly Dictionary<string, string> _withEnumMemberStrip;
internal readonly Dictionary<string, Guid> _withGuids;
internal readonly Dictionary<string, string> _withLengths;
private readonly Dictionary<string, string> _withLibraryPaths;
Expand Down Expand Up @@ -104,6 +105,7 @@ public PInvokeGeneratorConfiguration(string language, string languageStandard, s
_withAttributes = new Dictionary<string, IReadOnlyList<string>>(QualifiedNameComparer.Default);
_withCallConvs = new Dictionary<string, string>(QualifiedNameComparer.Default);
_withClasses = new Dictionary<string, string>(StringComparer.Ordinal);
_withEnumMemberStrip = new Dictionary<string, string>(QualifiedNameComparer.Default);
_withGuids = new Dictionary<string, Guid>(QualifiedNameComparer.Default);
_withLengths = new Dictionary<string, string>(QualifiedNameComparer.Default);
_withLibraryPaths = new Dictionary<string, string>(StringComparer.Ordinal);
Expand Down Expand Up @@ -577,6 +579,20 @@ public IReadOnlyDictionary<string, string> WithClasses
}
}

[AllowNull]
public IReadOnlyDictionary<string, string> WithEnumMemberStrip
{
get
{
return _withEnumMemberStrip;
}

init
{
AddRange(_withEnumMemberStrip, value);
}
}

[AllowNull]
public IReadOnlyDictionary<string, Guid> WithGuids
{
Expand Down
3 changes: 3 additions & 0 deletions sources/ClangSharpPInvokeGenerator/Program.Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal static partial class Program
private static readonly string[] s_withAttributeOptionAliases = ["--with-attribute", "-wa"];
private static readonly string[] s_withCallConvOptionAliases = ["--with-callconv", "-wcc"];
private static readonly string[] s_withClassOptionAliases = ["--with-class", "-wc"];
private static readonly string[] s_withEnumMemberStripOptionAliases = ["--with-enum-member-strip", "-wems"];
private static readonly string[] s_withGuidOptionAliases = ["--with-guid", "-wg"];
private static readonly string[] s_withLengthOptionAliases = ["--with-length", "-wl"];
private static readonly string[] s_withLibraryPathOptionAliases = ["--with-librarypath", "-wlb"];
Expand Down Expand Up @@ -80,6 +81,7 @@ internal static partial class Program
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_withEnumMemberStripNameValuePairs = Multi(s_withEnumMemberStripOptionAliases, "How to strip a prefix or suffix from the members of the given remapped enum name during binding generation. Mode is one of `none`, `common-prefix`, `common-suffix`, `type-name`, `prefix:<str>`, or `suffix:<str>`. 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.");
Expand Down Expand Up @@ -127,6 +129,7 @@ internal static partial class Program
s_withAttributeNameValuePairs,
s_withCallConvNameValuePairs,
s_withClassNameValuePairs,
s_withEnumMemberStripNameValuePairs,
s_withGuidNameValuePairs,
s_withLengthNameValuePairs,
s_withLibraryPathNameValuePairs,
Expand Down
3 changes: 3 additions & 0 deletions sources/ClangSharpPInvokeGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public static int Run()
var withAttributeNameValuePairs = s_withAttributeNameValuePairs.GetValues();
var withCallConvNameValuePairs = s_withCallConvNameValuePairs.GetValues();
var withClassNameValuePairs = s_withClassNameValuePairs.GetValues();
var withEnumMemberStripNameValuePairs = s_withEnumMemberStripNameValuePairs.GetValues();
var withGuidNameValuePairs = s_withGuidNameValuePairs.GetValues();
var withLengthNameValuePairs = s_withLengthNameValuePairs.GetValues();
var withLibraryPathNameValuePairs = s_withLibraryPathNameValuePairs.GetValues();
Expand Down Expand Up @@ -127,6 +128,7 @@ public static int Run()
ParseKeyValuePairs(withAttributeNameValuePairs, errorList, out Dictionary<string, IReadOnlyList<string>> withAttributes);
ParseKeyValuePairs(withCallConvNameValuePairs, errorList, out Dictionary<string, string> withCallConvs);
ParseKeyValuePairs(withClassNameValuePairs, errorList, out Dictionary<string, string> withClasses);
ParseKeyValuePairs(withEnumMemberStripNameValuePairs, errorList, out Dictionary<string, string> withEnumMemberStrip);
ParseKeyValuePairs(withGuidNameValuePairs, errorList, out Dictionary<string, Guid> withGuids);
ParseKeyValuePairs(withLengthNameValuePairs, errorList, out Dictionary<string, string> withLengths);
ParseKeyValuePairs(withLibraryPathNameValuePairs, errorList, out Dictionary<string, string> withLibraryPaths);
Expand Down Expand Up @@ -598,6 +600,7 @@ public static int Run()
WithAttributes = withAttributes,
WithCallConvs = withCallConvs,
WithClasses = withClasses,
WithEnumMemberStrip = withEnumMemberStrip,
WithGuids = withGuids,
WithLengths = withLengths,
WithLibraryPaths = withLibraryPaths,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ClangSharp.Test
{
public enum abc_flags
{
abc_flags_1,
abc_flags__1,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ClangSharp.Test
{
public enum abc_some_enum
{
first,
second,
third,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ClangSharp.Test
{
public enum render_modes
{
fast,
slow,
idle,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ClangSharp.Test
{
public enum abc_version
{
_1_0,
_2_0,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ClangSharp.Test
{
public enum colors
{
RED,
GREEN,
BLUE,
}
}
Loading
Loading