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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ Options:
-p, --prefixStrip <prefixStrip> The prefix to strip from the generated method bindings. []
--nativeTypeNamesToStrip <nativeTypeNamesToStrip> The contents to strip from the generated NativeTypeName attributes. []
-r, --remap <remap> A declaration name to be remapped to another name during binding generation. []
-rt, --remap-type <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 <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 <std> Language standard to compile for. []
-to, --test-output <test-output> The output location to write the generated tests to. []
-t, --traverse <traverse> A file name included either directly or indirectly by -f that should be traversed during binding generation. []
Expand Down
111 changes: 101 additions & 10 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Naming.cs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,26 @@ private string GetRemappedCursorName(NamedDecl namedDecl, out string nativeTypeN
{
nativeTypeName = GetCursorQualifiedName(namedDecl);

// A `--remap-type` / `--remap-field` entry takes precedence over the general `--remap` so a
// type and a field that share a name (legal in C, but not C#) can be disambiguated. These
// are only consulted here, on the decl's own name, so that resolving the *type* referenced
// by a field (which reuses the field cursor) doesn't pick up a `--remap-field` entry.
var kindSpecificRemappedNames = namedDecl switch {
FieldDecl => _config._remappedFieldNames,
TypeDecl => _config._remappedTypeNames,
_ => null,
};

if ((kindSpecificRemappedNames is not null) && (kindSpecificRemappedNames.Count != 0))
{
var kindSpecificLookup = kindSpecificRemappedNames.GetAlternateLookup<ReadOnlySpan<char>>();

if (kindSpecificLookup.TryGetValue(GetCursorName(namedDecl), out var kindSpecificRemappedName))
{
return AddUsingDirectiveIfNeeded(_outputBuilder, kindSpecificRemappedName, skipUsing);
}
}

var name = nativeTypeName;
var remappedName = GetRemappedName(name, namedDecl, tryRemapOperatorName: true, out var wasRemapped, skipUsing);

Expand Down Expand Up @@ -503,10 +523,81 @@ private string GetRemappedCursorName(NamedDecl namedDecl, out string nativeTypeN
{
remappedName = GetRemappedNameForAnonymousRecord(recordDecl);
}
else if ((namedDecl is RecordDecl clashingRecordDecl) && TryDeclashRecordName(clashingRecordDecl, remappedName, out var declashedName))
{
remappedName = declashedName;
}

return remappedName;
}

private string ApplyTagTypeNameOverrides(TagType tagType, string leafName)
{
// Keep a `--remap-type` override or a de-clashed nested record name (see
// GetRemappedCursorName) consistent between the type declaration and any reference to it,
// so both resolve to the same C# type.

if (_config._remappedTypeNames.Count != 0)
{
var remappedTypeNamesLookup = _config._remappedTypeNames.GetAlternateLookup<ReadOnlySpan<char>>();

if (remappedTypeNamesLookup.TryGetValue(leafName, out var remappedTypeName))
{
return remappedTypeName;
}
}

if ((tagType.Decl is RecordDecl recordDecl) && TryDeclashRecordName(recordDecl, leafName, out var declashedName))
{
return declashedName;
}

return leafName;
}

private bool TryDeclashRecordName(RecordDecl recordDecl, string name, out string declashedName)
{
declashedName = name;

// A named nested record is legal in C even when a sibling field shares its name, but the
// two would map to a nested type and a field of the same name in C#, which is a CS0102
// clash. Rename the type using the anonymous-record naming so both the declaration and any
// field type references resolve consistently through this method.
//
// The lexical parent is used (not the semantic one) since C promotes a nested record to the
// enclosing scope, yet it is still emitted nested based on where it lexically appears.

if (recordDecl.LexicalDeclContext is not RecordDecl parentRecordDecl)
{
return false;
}

var hasClash = false;

foreach (var fieldDecl in parentRecordDecl.Fields)
{
if (GetRemappedCursorName(fieldDecl) == name)
{
hasClash = true;
break;
}
}

if (!hasClash)
{
return false;
}

declashedName = $"_{name}{AnonymousTypeKindTag}{(recordDecl.IsUnion ? "Union" : "Struct")}";

if (_declashedRecordNames.Add(recordDecl))
{
AddDiagnostic(DiagnosticLevel.Warning, $"Renamed nested type '{name}' to '{declashedName}' to avoid a name clash with a field of the same name in '{GetRemappedCursorName(parentRecordDecl)}'. Use '--remap-type {name}=NewName' or '--remap-field {name}=NewName' to control the naming explicitly.", recordDecl);
}

return true;
}

private static int GetAnonymousRecordIndex(RecordDecl recordDecl, RecordDecl parentRecordDecl)
{
var index = -1;
Expand Down Expand Up @@ -648,22 +739,22 @@ private string GetRemappedName(string name, Cursor? cursor, bool tryRemapOperato

wasRemapped = false;
return AddUsingDirectiveIfNeeded(_outputBuilder, remappedName, skipUsingIfNotRemapped);
}

string AddUsingDirectiveIfNeeded(IOutputBuilder? outputBuilder, string remappedName, bool skipUsing)
private string AddUsingDirectiveIfNeeded(IOutputBuilder? outputBuilder, string remappedName, bool skipUsing)
{
if (!skipUsing)
{
if (!skipUsing)
if (NeedsSystemSupportRegex().IsMatch(remappedName))
{
if (NeedsSystemSupportRegex().IsMatch(remappedName))
{
outputBuilder?.EmitSystemSupport();
}

var namespaceName = GetNamespace(remappedName);
AddUsingDirective(outputBuilder, namespaceName);
outputBuilder?.EmitSystemSupport();
}

return remappedName;
var namespaceName = GetNamespace(remappedName);
AddUsingDirective(outputBuilder, namespaceName);
}

return remappedName;
}

private string GetRemappedTypeName(Cursor? cursor, Cursor? context, Type type, out string nativeTypeName, bool skipUsing = false, bool ignoreTransparentStructsWhereRequired = false, bool isTemplate = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ private string GetTypeName(Cursor? cursor, Cursor? context, Type rootType, Type
{
result.typeName = result.typeName.Split(s_doubleColonSeparator, StringSplitOptions.RemoveEmptyEntries).Last();
result.typeName = GetRemappedName(result.typeName, cursor, tryRemapOperatorName: false, out _, skipUsing: true);
result.typeName = ApplyTagTypeNameOverrides(tagType, result.typeName);

// A nested type needs to be qualified by its containing type(s) so it resolves
// when referenced from another scope (e.g. `A::Inner` -> `A.Inner`). Namespaces
Expand All @@ -403,6 +404,10 @@ private string GetTypeName(Cursor? cursor, Cursor? context, Type rootType, Type

result.typeName = qualificationBuilder.Append(result.typeName).ToString();
}
else
{
result.typeName = ApplyTagTypeNameOverrides(tagType, result.typeName);
}
}
else if (type is TemplateSpecializationType templateSpecializationType)
{
Expand Down
2 changes: 2 additions & 0 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public sealed partial class PInvokeGenerator : IDisposable
private readonly Dictionary<CXFile, (string Name, string FullName)> _fileNames;
private readonly HashSet<string> _topLevelClassNames;
private readonly HashSet<string> _usedRemappings;
private readonly HashSet<RecordDecl> _declashedRecordNames;
private readonly string _placeholderMacroType;

private string _filePath;
Expand Down Expand Up @@ -174,6 +175,7 @@ public PInvokeGenerator(PInvokeGeneratorConfiguration config, Func<string, Strea
_fileNames = [];
_topLevelClassUsings = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
_usedRemappings = new HashSet<string>(StringComparer.Ordinal);
_declashedRecordNames = [];
_filePath = "";
_clangCommandLineArgs = [];
_placeholderMacroType = GetPlaceholderMacroType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public sealed class PInvokeGeneratorConfiguration
internal readonly HashSet<string> _withSuppressGCTransitions;

internal readonly Dictionary<string, string> _remappedNames;
internal readonly Dictionary<string, string> _remappedTypeNames;
internal readonly Dictionary<string, string> _remappedFieldNames;
internal readonly Dictionary<string, AccessSpecifier> _withAccessSpecifiers;
internal readonly Dictionary<string, IReadOnlyList<string>> _withAttributes;
internal readonly Dictionary<string, string> _withCallConvs;
Expand Down Expand Up @@ -93,6 +95,8 @@ public PInvokeGeneratorConfiguration(string language, string languageStandard, s
_withSuppressGCTransitions = new HashSet<string>(QualifiedNameComparer.Default);

_remappedNames = new Dictionary<string, string>(QualifiedNameComparer.Default);
_remappedTypeNames = new Dictionary<string, string>(QualifiedNameComparer.Default);
_remappedFieldNames = new Dictionary<string, string>(QualifiedNameComparer.Default);
_withAccessSpecifiers = new Dictionary<string, AccessSpecifier>(QualifiedNameComparer.Default);
_withAttributes = new Dictionary<string, IReadOnlyList<string>>(QualifiedNameComparer.Default);
_withCallConvs = new Dictionary<string, string>(QualifiedNameComparer.Default);
Expand Down Expand Up @@ -363,6 +367,34 @@ public IReadOnlyDictionary<string, string> RemappedNames

public IReadOnlyCollection<string> ForceRemappedNames => _forceRemappedNames;

[AllowNull]
public IReadOnlyDictionary<string, string> RemappedTypeNames
{
get
{
return _remappedTypeNames;
}

init
{
AddRange(_remappedTypeNames, value);
}
}

[AllowNull]
public IReadOnlyDictionary<string, string> RemappedFieldNames
{
get
{
return _remappedFieldNames;
}

init
{
AddRange(_remappedFieldNames, value);
}
}

[AllowNull]
public string TestOutputLocation
{
Expand Down
28 changes: 28 additions & 0 deletions sources/ClangSharpPInvokeGenerator/Program.Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ internal static partial class Program
private static readonly string[] s_outputOptionAliases = ["--output", "-o"];
private static readonly string[] s_prefixStripOptionAliases = ["--prefixStrip", "-p"];
private static readonly string[] s_remapOptionAliases = ["--remap", "-r"];
private static readonly string[] s_remapTypeOptionAliases = ["--remap-type", "-rt"];
private static readonly string[] s_remapFieldOptionAliases = ["--remap-field", "-rf"];
private static readonly string[] s_stdOptionAliases = ["--std", "-std"];
private static readonly string[] s_testOutputOptionAliases = ["--test-output", "-to"];
private static readonly string[] s_traverseOptionAliases = ["--traverse", "-t"];
Expand Down Expand Up @@ -66,6 +68,8 @@ internal static partial class Program
private static readonly Option<string> s_outputLocation = GetOutputOption();
private static readonly Option<PInvokeGeneratorOutputMode> s_outputMode = GetOutputModeOption();
private static readonly Option<string[]> s_remappedNameValuePairs = GetRemapOption();
private static readonly Option<string[]> s_remappedTypeNameValuePairs = GetRemapTypeOption();
private static readonly Option<string[]> s_remappedFieldNameValuePairs = GetRemapFieldOption();
private static readonly Option<string> s_std = GetStdOption();
private static readonly Option<string> s_testOutputLocation = GetTestOutputOption();
private static readonly Option<string[]> s_traversalNames = GetTraverseOption();
Expand Down Expand Up @@ -361,6 +365,28 @@ private static Option<string[]> GetRemapOption()
};
}

private static Option<string[]> GetRemapTypeOption()
{
return new Option<string[]>(
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<string>
) {
AllowMultipleArgumentsPerToken = true
};
}

private static Option<string[]> GetRemapFieldOption()
{
return new Option<string[]>(
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<string>
) {
AllowMultipleArgumentsPerToken = true
};
}

private static RootCommand GetRootCommand()
{
var rootCommand = new RootCommand("ClangSharp P/Invoke Binding Generator")
Expand All @@ -383,6 +409,8 @@ private static RootCommand GetRootCommand()
s_methodPrefixToStrip,
s_nativeTypeNamesToStrip,
s_remappedNameValuePairs,
s_remappedTypeNameValuePairs,
s_remappedFieldNameValuePairs,
s_std,
s_testOutputLocation,
s_traversalNames,
Expand Down
6 changes: 6 additions & 0 deletions sources/ClangSharpPInvokeGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public static void Run(InvocationContext context)
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) ?? [];
Expand Down Expand Up @@ -122,6 +124,8 @@ public static void Run(InvocationContext context)
}

ParseKeyValuePairs(remappedNameValuePairs, errorList, out Dictionary<string, string> remappedNames);
ParseKeyValuePairs(remappedTypeNameValuePairs, errorList, out Dictionary<string, string> remappedTypeNames);
ParseKeyValuePairs(remappedFieldNameValuePairs, errorList, out Dictionary<string, string> remappedFieldNames);
ParseKeyValuePairs(withAccessSpecifierNameValuePairs, errorList, out Dictionary<string, AccessSpecifier> withAccessSpecifiers);
ParseKeyValuePairs(withAttributeNameValuePairs, errorList, out Dictionary<string, IReadOnlyList<string>> withAttributes);
ParseKeyValuePairs(withCallConvNameValuePairs, errorList, out Dictionary<string, string> withCallConvs);
Expand Down Expand Up @@ -549,6 +553,8 @@ public static void Run(InvocationContext context)
MethodPrefixToStrip = methodPrefixToStrip,
NativeTypeNamesToStrip = nativeTypeNamesToStrip,
RemappedNames = remappedNames,
RemappedTypeNames = remappedTypeNames,
RemappedFieldNames = remappedFieldNames,
TraversalNames = traversalNames,
TestOutputLocation = testOutputLocation,
WithAccessSpecifiers = withAccessSpecifiers,
Expand Down
Loading
Loading