Skip to content

Commit be3f4e8

Browse files
Add Char marshaller (#212)
* Support for char marshaller. Avoid supporting the built-in semantics for ANSI marshalling of char. Propagate the CharSet down to the marshallers. Convert the MarshallingGenerators.TryCreate() to a MarshallingGenerators.Create() function throwing an exception. * Add a compatibility document. * Add testing for char marshalling.
1 parent 2d7dc69 commit be3f4e8

File tree

15 files changed

+520
-85
lines changed

15 files changed

+520
-85
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Collections.Generic;
2+
using System.Runtime.InteropServices;
3+
4+
using Xunit;
5+
6+
namespace DllImportGenerator.IntegrationTests
7+
{
8+
partial class NativeExportsNE
9+
{
10+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "unicode_return_as_uint")]
11+
public static partial uint ReturnDefaultAsUInt(char input);
12+
13+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "char_return_as_uint")]
14+
public static partial char ReturnUIntAsDefault(uint input);
15+
16+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "unicode_return_as_uint", CharSet = CharSet.Unicode)]
17+
public static partial uint ReturnUnicodeAsUInt(char input);
18+
19+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "char_return_as_uint", CharSet = CharSet.Unicode)]
20+
public static partial char ReturnUIntAsUnicode(uint input);
21+
22+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "char_return_as_refuint", CharSet = CharSet.Unicode)]
23+
public static partial void ReturnUIntAsRefUnicode(uint input, ref char res);
24+
25+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "char_return_as_refuint", CharSet = CharSet.Unicode)]
26+
public static partial void ReturnUIntAsOutUnicode(uint input, out char res);
27+
28+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "char_return_as_uint", CharSet = CharSet.None)]
29+
[return: MarshalAs(UnmanagedType.U2)]
30+
public static partial char ReturnU2AsU2IgnoreCharSet([MarshalAs(UnmanagedType.U2)] char input);
31+
32+
[GeneratedDllImport(nameof(NativeExportsNE), EntryPoint = "char_return_as_uint", CharSet = CharSet.Ansi)]
33+
[return: MarshalAs(UnmanagedType.I2)]
34+
public static partial char ReturnI2AsI2IgnoreCharSet([MarshalAs(UnmanagedType.I2)] char input);
35+
}
36+
37+
public class CharacterTests
38+
{
39+
public static IEnumerable<object[]> CharacterMappings()
40+
{
41+
yield return new object[] { 'A', 0x41 };
42+
yield return new object[] { 'E', 0x45 };
43+
yield return new object[] { 'J', 0x4a };
44+
yield return new object[] { 'ß', 0xdf };
45+
yield return new object[] { '✅', 0x2705 };
46+
yield return new object[] { '鸟', 0x9e1f };
47+
}
48+
49+
[Theory]
50+
[MemberData(nameof(CharacterMappings))]
51+
public void ValidateUnicodeCharIsMarshalledAsExpected(char value, uint expected)
52+
{
53+
Assert.Equal(expected, NativeExportsNE.ReturnDefaultAsUInt(value));
54+
Assert.Equal(expected, NativeExportsNE.ReturnUnicodeAsUInt(value));
55+
}
56+
57+
[Theory]
58+
[MemberData(nameof(CharacterMappings))]
59+
public void ValidateUnicodeReturns(char expected, uint value)
60+
{
61+
Assert.Equal(expected, NativeExportsNE.ReturnUIntAsDefault(value));
62+
Assert.Equal(expected, NativeExportsNE.ReturnUIntAsUnicode(value));
63+
64+
char result = '\u0000';
65+
NativeExportsNE.ReturnUIntAsRefUnicode(value, ref result);
66+
Assert.Equal(expected, result);
67+
68+
result = '\u0000';
69+
NativeExportsNE.ReturnUIntAsOutUnicode(value, out result);
70+
Assert.Equal(expected, result);
71+
}
72+
73+
[Theory]
74+
[MemberData(nameof(CharacterMappings))]
75+
public void ValidateIgnoreCharSet(char value, uint expectedUInt)
76+
{
77+
char expected = (char)expectedUInt;
78+
Assert.Equal(expected, NativeExportsNE.ReturnU2AsU2IgnoreCharSet(value));
79+
Assert.Equal(expected, NativeExportsNE.ReturnI2AsI2IgnoreCharSet(value));
80+
}
81+
}
82+
}

DllImportGenerator/DllImportGenerator.UnitTests/CodeSnippets.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,25 @@ partial class Test
281281
}
282282
";
283283

284+
/// <summary>
285+
/// Declaration with parameters with <see cref="CharSet"/> set.
286+
/// </summary>
287+
public static string BasicParametersAndModifiersWithCharSet(string typename, CharSet value) => @$"
288+
using System.Runtime.InteropServices;
289+
partial class Test
290+
{{
291+
[GeneratedDllImport(""DoesNotExist"", CharSet = CharSet.{value})]
292+
public static partial {typename} Method(
293+
{typename} p,
294+
in {typename} pIn,
295+
ref {typename} pRef,
296+
out {typename} pOut);
297+
}}
298+
";
299+
300+
public static string BasicParametersAndModifiersWithCharSet<T>(CharSet value) =>
301+
BasicParametersAndModifiersWithCharSet(typeof(T).ToString(), value);
302+
284303
/// <summary>
285304
/// Declaration with parameters.
286305
/// </summary>
Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Microsoft.CodeAnalysis;
22
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.InteropServices;
35
using System.Threading.Tasks;
46
using Xunit;
57

@@ -9,32 +11,29 @@ public class CompileFails
911
{
1012
public static IEnumerable<object[]> CodeSnippetsToCompile()
1113
{
12-
yield return new object[] { CodeSnippets.UserDefinedPrefixedAttributes, 3 };
14+
yield return new object[] { CodeSnippets.UserDefinedPrefixedAttributes, 0, 3 };
15+
yield return new object[] { CodeSnippets.BasicParametersAndModifiersWithCharSet<char>(CharSet.Auto), 5, 0 };
16+
yield return new object[] { CodeSnippets.BasicParametersAndModifiersWithCharSet<char>(CharSet.None), 5, 0 };
17+
yield return new object[] { CodeSnippets.BasicParametersAndModifiersWithCharSet<char>(CharSet.Ansi), 5, 0 };
18+
yield return new object[] { CodeSnippets.MarshalAsParametersAndModifiers<char>(UnmanagedType.I1), 5, 0 };
19+
yield return new object[] { CodeSnippets.MarshalAsParametersAndModifiers<char>(UnmanagedType.U1), 5, 0 };
1320
}
1421

1522
[Theory]
1623
[MemberData(nameof(CodeSnippetsToCompile))]
17-
public async Task ValidateSnippets(string source, int failCount)
24+
public async Task ValidateSnippets(string source, int expectedGeneratorErrors, int expectedCompilerErrors)
1825
{
1926
Compilation comp = await TestUtils.CreateCompilation(source);
2027
TestUtils.AssertPreSourceGeneratorCompilation(comp);
2128

2229
var newComp = TestUtils.RunGenerators(comp, out var generatorDiags, new Microsoft.Interop.DllImportGenerator());
23-
Assert.Empty(generatorDiags);
2430

25-
var newCompDiags = newComp.GetDiagnostics();
31+
// Verify the compilation failed with errors.
32+
int generatorErrors = generatorDiags.Count(d => d.Severity == DiagnosticSeverity.Error);
33+
Assert.Equal(expectedGeneratorErrors, generatorErrors);
2634

27-
// Verify the compilation failed with missing impl.
28-
int missingImplCount = 0;
29-
foreach (var diag in newCompDiags)
30-
{
31-
if ("CS8795".Equals(diag.Id))
32-
{
33-
missingImplCount++;
34-
}
35-
}
36-
37-
Assert.Equal(failCount, missingImplCount);
35+
int compilerErrors = newComp.GetDiagnostics().Count(d => d.Severity == DiagnosticSeverity.Error);
36+
Assert.Equal(expectedCompilerErrors, compilerErrors);
3837
}
3938
}
4039
}

DllImportGenerator/DllImportGenerator.UnitTests/Compiles.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public static IEnumerable<object[]> CodeSnippetsToCompile_NoDiagnostics()
3232
yield return new[] { CodeSnippets.BasicParametersAndModifiers<float>() };
3333
yield return new[] { CodeSnippets.BasicParametersAndModifiers<double>() };
3434
yield return new[] { CodeSnippets.BasicParametersAndModifiers<bool>() };
35-
//yield return new[] { CodeSnippets.BasicParametersAndModifiers<char>() };
35+
yield return new[] { CodeSnippets.BasicParametersAndModifiers<char>() };
3636
//yield return new[] { CodeSnippets.BasicParametersAndModifiers<string>() };
3737
yield return new[] { CodeSnippets.BasicParametersAndModifiers<IntPtr>() };
3838
yield return new[] { CodeSnippets.BasicParametersAndModifiers<UIntPtr>() };
@@ -51,9 +51,12 @@ public static IEnumerable<object[]> CodeSnippetsToCompile_NoDiagnostics()
5151
//yield return new[] { CodeSnippets.BasicParametersAndModifiers<string[]>() };
5252
//yield return new[] { CodeSnippets.BasicParametersAndModifiers<IntPtr[]>() };
5353
//yield return new[] { CodeSnippets.BasicParametersAndModifiers<UIntPtr[]>() };
54+
yield return new[] { CodeSnippets.BasicParametersAndModifiersWithCharSet<char>(CharSet.Unicode) };
5455
yield return new[] { CodeSnippets.MarshalAsParametersAndModifiers<bool>(UnmanagedType.Bool) };
5556
yield return new[] { CodeSnippets.MarshalAsParametersAndModifiers<bool>(UnmanagedType.VariantBool) };
5657
yield return new[] { CodeSnippets.MarshalAsParametersAndModifiers<bool>(UnmanagedType.I1) };
58+
yield return new[] { CodeSnippets.MarshalAsParametersAndModifiers<char>(UnmanagedType.I2) };
59+
yield return new[] { CodeSnippets.MarshalAsParametersAndModifiers<char>(UnmanagedType.U2) };
5760
//yield return new[] { CodeSnippets.EnumParameters };
5861
yield return new[] { CodeSnippets.PreserveSigFalseVoidReturn };
5962
yield return new[] { CodeSnippets.PreserveSigFalse<byte>() };
@@ -67,7 +70,7 @@ public static IEnumerable<object[]> CodeSnippetsToCompile_NoDiagnostics()
6770
yield return new[] { CodeSnippets.PreserveSigFalse<float>() };
6871
yield return new[] { CodeSnippets.PreserveSigFalse<double>() };
6972
yield return new[] { CodeSnippets.PreserveSigFalse<bool>() };
70-
//yield return new[] { CodeSnippets.PreserveSigFalse<char>() };
73+
yield return new[] { CodeSnippets.PreserveSigFalse<char>() };
7174
//yield return new[] { CodeSnippets.PreserveSigFalse<string>() };
7275
yield return new[] { CodeSnippets.PreserveSigFalse<IntPtr>() };
7376
yield return new[] { CodeSnippets.PreserveSigFalse<UIntPtr>() };
@@ -97,7 +100,6 @@ public static IEnumerable<object[]> CodeSnippetsToCompile_WithDiagnostics()
97100
{
98101
yield return new[] { CodeSnippets.MarshalAsAttributeOnTypes };
99102

100-
yield return new[] { CodeSnippets.BasicParametersAndModifiers<char>() };
101103
yield return new[] { CodeSnippets.BasicParametersAndModifiers<string>() };
102104

103105
yield return new[] { CodeSnippets.BasicParametersAndModifiers<byte[]>() };
@@ -118,7 +120,6 @@ public static IEnumerable<object[]> CodeSnippetsToCompile_WithDiagnostics()
118120

119121
yield return new[] { CodeSnippets.EnumParameters };
120122

121-
yield return new[] { CodeSnippets.PreserveSigFalse<char>() };
122123
yield return new[] { CodeSnippets.PreserveSigFalse<string>() };
123124

124125
yield return new[] { CodeSnippets.PreserveSigFalse<byte[]>() };

DllImportGenerator/DllImportGenerator/DllImportStub.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,32 @@ public static DllImportStub Create(
131131
currType = currType.ContainingType;
132132
}
133133

134+
// Compute the current default string encoding value.
135+
var defaultEncoding = CharEncoding.Undefined;
136+
if (dllImportData.IsUserDefined.HasFlag(DllImportMember.CharSet))
137+
{
138+
defaultEncoding = dllImportData.CharSet switch
139+
{
140+
CharSet.Unicode => CharEncoding.Utf16,
141+
CharSet.Auto => CharEncoding.PlatformDefined,
142+
_ => CharEncoding.Utf8, // [Compat] ANSI is no longer ANSI code pages on Windows and UTF-8, on non-Windows.
143+
};
144+
}
145+
146+
var defaultInfo = new DefaultMarshallingInfo(defaultEncoding);
147+
134148
// Determine parameter and return types
135149
var paramsTypeInfo = new List<TypePositionInfo>();
136150
for (int i = 0; i < method.Parameters.Length; i++)
137151
{
138152
var param = method.Parameters[i];
139-
var typeInfo = TypePositionInfo.CreateForParameter(param, compilation, diagnostics);
153+
var typeInfo = TypePositionInfo.CreateForParameter(param, defaultInfo, compilation, diagnostics);
140154
typeInfo.ManagedIndex = i;
141155
typeInfo.NativeIndex = paramsTypeInfo.Count;
142156
paramsTypeInfo.Add(typeInfo);
143157
}
144158

145-
TypePositionInfo retTypeInfo = TypePositionInfo.CreateForType(method.ReturnType, method.GetReturnTypeAttributes(), compilation, diagnostics);
159+
TypePositionInfo retTypeInfo = TypePositionInfo.CreateForType(method.ReturnType, method.GetReturnTypeAttributes(), defaultInfo, compilation, diagnostics);
146160
retTypeInfo.ManagedIndex = TypePositionInfo.ReturnIndex;
147161
retTypeInfo.NativeIndex = TypePositionInfo.ReturnIndex;
148162
if (!dllImportData.PreserveSig)

DllImportGenerator/DllImportGenerator/GeneratorDiagnostics.cs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public class Ids
6565
Category,
6666
DiagnosticSeverity.Error,
6767
isEnabledByDefault: true,
68-
description: GetResourceString(Resources.TypeNotSupportedDescription));
68+
description: GetResourceString(nameof(Resources.TypeNotSupportedDescription)));
6969

7070
public readonly static DiagnosticDescriptor ReturnTypeNotSupported =
7171
new DiagnosticDescriptor(
@@ -75,7 +75,27 @@ public class Ids
7575
Category,
7676
DiagnosticSeverity.Error,
7777
isEnabledByDefault: true,
78-
description: GetResourceString(Resources.TypeNotSupportedDescription));
78+
description: GetResourceString(nameof(Resources.TypeNotSupportedDescription)));
79+
80+
public readonly static DiagnosticDescriptor ParameterTypeNotSupportedWithDetails =
81+
new DiagnosticDescriptor(
82+
Ids.TypeNotSupported,
83+
GetResourceString(nameof(Resources.TypeNotSupportedTitle)),
84+
GetResourceString(nameof(Resources.TypeNotSupportedMessageParameterWithDetails)),
85+
Category,
86+
DiagnosticSeverity.Error,
87+
isEnabledByDefault: true,
88+
description: GetResourceString(nameof(Resources.TypeNotSupportedDescription)));
89+
90+
public readonly static DiagnosticDescriptor ReturnTypeNotSupportedWithDetails =
91+
new DiagnosticDescriptor(
92+
Ids.TypeNotSupported,
93+
GetResourceString(nameof(Resources.TypeNotSupportedTitle)),
94+
GetResourceString(nameof(Resources.TypeNotSupportedMessageReturnWithDetails)),
95+
Category,
96+
DiagnosticSeverity.Error,
97+
isEnabledByDefault: true,
98+
description: GetResourceString(nameof(Resources.TypeNotSupportedDescription)));
7999

80100
public readonly static DiagnosticDescriptor ParameterConfigurationNotSupported =
81101
new DiagnosticDescriptor(
@@ -85,7 +105,7 @@ public class Ids
85105
Category,
86106
DiagnosticSeverity.Error,
87107
isEnabledByDefault: true,
88-
description: GetResourceString(Resources.ConfigurationNotSupportedDescription));
108+
description: GetResourceString(nameof(Resources.ConfigurationNotSupportedDescription)));
89109

90110
public readonly static DiagnosticDescriptor ReturnConfigurationNotSupported =
91111
new DiagnosticDescriptor(
@@ -95,7 +115,7 @@ public class Ids
95115
Category,
96116
DiagnosticSeverity.Error,
97117
isEnabledByDefault: true,
98-
description: GetResourceString(Resources.ConfigurationNotSupportedDescription));
118+
description: GetResourceString(nameof(Resources.ConfigurationNotSupportedDescription)));
99119

100120
public readonly static DiagnosticDescriptor ConfigurationNotSupported =
101121
new DiagnosticDescriptor(
@@ -105,7 +125,7 @@ public class Ids
105125
Category,
106126
DiagnosticSeverity.Error,
107127
isEnabledByDefault: true,
108-
description: GetResourceString(Resources.ConfigurationNotSupportedDescription));
128+
description: GetResourceString(nameof(Resources.ConfigurationNotSupportedDescription)));
109129

110130
public readonly static DiagnosticDescriptor ConfigurationValueNotSupported =
111131
new DiagnosticDescriptor(
@@ -115,7 +135,7 @@ public class Ids
115135
Category,
116136
DiagnosticSeverity.Error,
117137
isEnabledByDefault: true,
118-
description: GetResourceString(Resources.ConfigurationNotSupportedDescription));
138+
description: GetResourceString(nameof(Resources.ConfigurationNotSupportedDescription)));
119139

120140
private readonly GeneratorExecutionContext context;
121141

@@ -157,11 +177,35 @@ public void ReportConfigurationNotSupported(
157177
/// </summary>
158178
/// <param name="method">Method with the parameter/return</param>
159179
/// <param name="info">Type info for the parameter/return</param>
180+
/// <param name="notSupportedDetails">[Optional] Specific reason for lack of support</param>
160181
internal void ReportMarshallingNotSupported(
161182
IMethodSymbol method,
162-
TypePositionInfo info)
183+
TypePositionInfo info,
184+
string? notSupportedDetails)
163185
{
164-
if (info.MarshallingAttributeInfo != null && info.MarshallingAttributeInfo is MarshalAsInfo)
186+
if (!string.IsNullOrEmpty(notSupportedDetails))
187+
{
188+
// Report the specific not-supported reason.
189+
if (info.IsManagedReturnPosition)
190+
{
191+
this.context.ReportDiagnostic(
192+
method.CreateDiagnostic(
193+
GeneratorDiagnostics.ReturnTypeNotSupportedWithDetails,
194+
notSupportedDetails!,
195+
method.Name));
196+
}
197+
else
198+
{
199+
Debug.Assert(info.ManagedIndex <= method.Parameters.Length);
200+
IParameterSymbol paramSymbol = method.Parameters[info.ManagedIndex];
201+
this.context.ReportDiagnostic(
202+
paramSymbol.CreateDiagnostic(
203+
GeneratorDiagnostics.ParameterTypeNotSupportedWithDetails,
204+
notSupportedDetails!,
205+
paramSymbol.Name));
206+
}
207+
}
208+
else if (info.MarshallingAttributeInfo != null && info.MarshallingAttributeInfo is MarshalAsInfo)
165209
{
166210
// Report that the specified marshalling configuration is not supported.
167211
// We don't forward marshalling attributes, so this is reported differently

0 commit comments

Comments
 (0)