Skip to content

Commit 19bfe5a

Browse files
committed
Added support for wchar_t and std::wstring for C#
With the help of @tritao: - added support for wchar_t - added support for std::wstring - unified the way wide character strings are handled between C# and C++/CLI - changed the way strings are handled, using 'IntPtr' instead of 'string' with marshalling attributes on the P/Invokes
1 parent c818f08 commit 19bfe5a

File tree

13 files changed

+327
-111
lines changed

13 files changed

+327
-111
lines changed

build/Tests.lua

+1
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ function SetupTestProjectsCLI(name, extraFiles, suffix)
219219
dependson { name .. ".Native" }
220220

221221
LinkNUnit()
222+
links { "CppSharp.Runtime" }
222223
end
223224

224225
function IncludeExamples()

src/CppParser/Bindings/CSharp/i686-pc-win32-msvc/Std-symbols.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ template __declspec(dllexport) std::basic_string<char, std::char_traits<char>, s
55
template __declspec(dllexport) std::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string() noexcept;
66
template __declspec(dllexport) const char* std::basic_string<char, std::char_traits<char>, std::allocator<char>>::c_str() const noexcept;
77
template __declspec(dllexport) std::allocator<char>::allocator() noexcept;
8+
9+
template __declspec(dllexport) std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t>>::basic_string(const wchar_t* const, const std::allocator<wchar_t>&);
10+
template __declspec(dllexport) std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t>>::~basic_string() noexcept;
11+
template __declspec(dllexport) const wchar_t* std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t>>::c_str() const noexcept;
12+
template __declspec(dllexport) std::allocator<wchar_t>::allocator() noexcept;

src/Generator/Generators/CSharp/CSharpMarshal.cs

+59-39
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ public override bool VisitArrayType(ArrayType array, TypeQualifiers quals)
106106
Helpers.InternalStruct, Context.ReturnVarName);
107107
else
108108
{
109-
if (arrayType.IsPrimitiveType(PrimitiveType.Char) &&
109+
if ((arrayType.IsPrimitiveType(PrimitiveType.Char) ||
110+
arrayType.IsPrimitiveType(PrimitiveType.WideChar)) &&
110111
Context.Context.Options.MarshalCharAsManagedChar)
111112
{
112113
supportBefore.WriteLineIndent(
@@ -130,7 +131,8 @@ public override bool VisitArrayType(ArrayType array, TypeQualifiers quals)
130131
break;
131132
case ArrayType.ArraySize.Incomplete:
132133
// const char* and const char[] are the same so we can use a string
133-
if (array.Type.Desugar().IsPrimitiveType(PrimitiveType.Char) &&
134+
if ((array.Type.Desugar().IsPrimitiveType(PrimitiveType.Char) ||
135+
array.Type.Desugar().IsPrimitiveType(PrimitiveType.WideChar)) &&
134136
array.QualifiedType.Qualifiers.IsConst)
135137
return VisitPointerType(new PointerType
136138
{
@@ -155,17 +157,16 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
155157
var isRefParam = param != null && (param.IsInOut || param.IsOut);
156158

157159
var pointee = pointer.Pointee.Desugar();
158-
bool marshalPointeeAsString = CSharpTypePrinter.IsConstCharString(pointee) && isRefParam;
159-
160-
if ((CSharpTypePrinter.IsConstCharString(pointer) && !MarshalsParameter) ||
161-
marshalPointeeAsString)
160+
var finalPointee = pointer.GetFinalPointee();
161+
162+
if (CSharpTypePrinter.IsConstCharString(pointer) ||
163+
(CSharpTypePrinter.IsConstCharString(pointee) && isRefParam))
162164
{
163165
Context.Return.Write(MarshalStringToManaged(Context.ReturnVarName,
164166
pointer.GetFinalPointee().Desugar() as BuiltinType));
165167
return true;
166168
}
167169

168-
var finalPointee = pointer.GetFinalPointee();
169170
PrimitiveType primitive;
170171
if (finalPointee.IsPrimitiveType(out primitive) || finalPointee.IsEnumType())
171172
{
@@ -237,11 +238,13 @@ public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers
237238
// returned structs must be blittable and char isn't
238239
if (Context.Context.Options.MarshalCharAsManagedChar)
239240
{
240-
Context.Return.Write("global::System.Convert.ToChar({0})",
241-
Context.ReturnVarName);
241+
Context.Return.Write($"global::System.Convert.ToChar({Context.ReturnVarName})");
242242
return true;
243243
}
244244
goto default;
245+
case PrimitiveType.WideChar:
246+
Context.Return.Write($"new CppSharp.Runtime.WideChar({Context.ReturnVarName})");
247+
return true;
245248
case PrimitiveType.Char16:
246249
return false;
247250
case PrimitiveType.Bool:
@@ -271,10 +274,10 @@ public override bool VisitTypedefType(TypedefType typedef, TypeQualifiers quals)
271274
var ptrName = Generator.GeneratedIdentifier("ptr") +
272275
Context.ParameterIndex;
273276

274-
Context.Before.WriteLine("var {0} = {1};", ptrName,
275-
Context.ReturnVarName);
277+
Context.Before.WriteLine($"var {ptrName} = {Context.ReturnVarName};");
276278

277-
var res = $"{ptrName} == IntPtr.Zero? null : ({typedef})Marshal.GetDelegateForFunctionPointer({ptrName}, typeof({typedef}))";
279+
var res = $@"{ptrName} == IntPtr.Zero? null : ({typedef
280+
})Marshal.GetDelegateForFunctionPointer({ptrName}, typeof({typedef}))";
278281
Context.Return.Write(res);
279282
return true;
280283
}
@@ -286,11 +289,10 @@ public override bool VisitFunctionType(FunctionType function, TypeQualifiers qua
286289
{
287290
var ptrName = Generator.GeneratedIdentifier("ptr") + Context.ParameterIndex;
288291

289-
Context.Before.WriteLine("var {0} = {1};", ptrName,
290-
Context.ReturnVarName);
292+
Context.Before.WriteLine($"var {ptrName} = {Context.ReturnVarName};");
291293

292-
Context.Return.Write("({1})Marshal.GetDelegateForFunctionPointer({0}, typeof({1}))",
293-
ptrName, function.ToString());
294+
Context.Return.Write($@"({function.ToString()
295+
})Marshal.GetDelegateForFunctionPointer({ptrName}, typeof({function.ToString()}))");
294296
return true;
295297
}
296298

@@ -316,7 +318,7 @@ public override bool VisitClassDecl(Class @class)
316318

317319
public override bool VisitEnumDecl(Enumeration @enum)
318320
{
319-
Context.Return.Write("{0}", Context.ReturnVarName);
321+
Context.Return.Write($"{Context.ReturnVarName}");
320322
return true;
321323
}
322324

@@ -340,8 +342,7 @@ public override bool VisitParameterDecl(Parameter parameter)
340342
if (!string.IsNullOrWhiteSpace(ctx.Return) &&
341343
!parameter.Type.IsPrimitiveTypeConvertibleToRef())
342344
{
343-
Context.Before.WriteLine("var _{0} = {1};", parameter.Name,
344-
ctx.Return);
345+
Context.Before.WriteLine($"var _{parameter.Name} = {ctx.Return};");
345346
}
346347

347348
Context.Return.Write("{0}{1}",
@@ -512,12 +513,16 @@ public override bool VisitArrayType(ArrayType array, TypeQualifiers quals)
512513
}
513514
else
514515
{
515-
if (arrayType.IsPrimitiveType(PrimitiveType.Char) &&
516+
if ((arrayType.IsPrimitiveType(PrimitiveType.Char) ||
517+
arrayType.IsPrimitiveType(PrimitiveType.WideChar)) &&
516518
Context.Context.Options.MarshalCharAsManagedChar)
517519
{
518-
supportBefore.WriteLineIndent(
519-
"{0}[i] = global::System.Convert.ToSByte({1}[i]);",
520-
Context.ReturnVarName, Context.ArgName);
520+
if(arrayType.IsPrimitiveType(PrimitiveType.Char))
521+
supportBefore.WriteLineIndent(
522+
$"{Context.ReturnVarName}[i] = global::System.Convert.ToSByte({Context.ArgName}[i]);");
523+
else
524+
supportBefore.WriteLineIndent(
525+
$"{Context.ReturnVarName}[i] = global::System.Convert.ToChar({Context.ArgName}[i]);");
521526
}
522527
else
523528
{
@@ -591,6 +596,10 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
591596

592597
var param = Context.Parameter;
593598
var isRefParam = param != null && (param.IsInOut || param.IsOut);
599+
var finalPointee = pointer.GetFinalPointee();
600+
601+
PrimitiveType primitive;
602+
bool finalPointeeIsPrimitiveType = finalPointee.IsPrimitiveType(out primitive);
594603

595604
if (CSharpTypePrinter.IsConstCharString(pointee) && isRefParam)
596605
{
@@ -601,12 +610,12 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
601610
}
602611
else if (param.IsInOut)
603612
{
604-
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name));
613+
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name, primitive));
605614
Context.ArgumentPrefix.Write("&");
606615
}
607616
else
608617
{
609-
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name));
618+
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name, primitive));
610619
Context.Cleanup.WriteLine("Marshal.FreeHGlobal({0});", Context.ArgName);
611620
}
612621
return true;
@@ -639,9 +648,7 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
639648
}
640649

641650
var marshalAsString = CSharpTypePrinter.IsConstCharString(pointer);
642-
var finalPointee = pointer.GetFinalPointee();
643-
PrimitiveType primitive;
644-
if (finalPointee.IsPrimitiveType(out primitive) || finalPointee.IsEnumType() ||
651+
if (finalPointeeIsPrimitiveType || finalPointee.IsEnumType() ||
645652
marshalAsString)
646653
{
647654
// From MSDN: "note that a ref or out parameter is classified as a moveable
@@ -667,13 +674,11 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
667674
{
668675
if (!marshalAsString &&
669676
Context.Context.Options.MarshalCharAsManagedChar &&
670-
primitive == PrimitiveType.Char)
677+
(primitive == PrimitiveType.Char || primitive == PrimitiveType.WideChar))
671678
Context.Return.Write($"({typePrinter.PrintNative(pointer)}) ");
672679

673-
if (marshalAsString && (Context.MarshalKind == MarshalKind.NativeField ||
674-
Context.MarshalKind == MarshalKind.VTableReturnValue ||
675-
Context.MarshalKind == MarshalKind.Variable))
676-
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name));
680+
if (marshalAsString)
681+
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name, primitive));
677682
else
678683
Context.Return.Write(Context.Parameter.Name);
679684
}
@@ -684,19 +689,25 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
684689
return pointer.QualifiedPointee.Visit(this);
685690
}
686691

687-
private string MarshalStringToUnmanaged(string varName)
692+
private string MarshalStringToUnmanaged(string varName, PrimitiveType type)
688693
{
689-
if (Equals(Context.Context.Options.Encoding, Encoding.ASCII))
694+
if (type == PrimitiveType.WideChar)
690695
{
691-
return string.Format("Marshal.StringToHGlobalAnsi({0})", varName);
696+
// Looks like Marshal.StringToHGlobalUni is already able
697+
// to handle both Unicode and MBCS charsets
698+
return $"Marshal.StringToHGlobalUni({varName})";
692699
}
700+
701+
if (Equals(Context.Context.Options.Encoding, Encoding.ASCII))
702+
return $"Marshal.StringToHGlobalAnsi({varName})";
703+
693704
if (Equals(Context.Context.Options.Encoding, Encoding.Unicode) ||
694705
Equals(Context.Context.Options.Encoding, Encoding.BigEndianUnicode))
695706
{
696-
return string.Format("Marshal.StringToHGlobalUni({0})", varName);
707+
return $"Marshal.StringToHGlobalUni({varName})";
697708
}
698-
throw new NotSupportedException(string.Format("{0} is not supported yet.",
699-
Context.Context.Options.Encoding.EncodingName));
709+
throw new NotSupportedException(
710+
$"{Context.Context.Options.Encoding.EncodingName} is not supported yet.");
700711
}
701712

702713
public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers quals)
@@ -714,6 +725,15 @@ public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers
714725
return true;
715726
}
716727
goto default;
728+
case PrimitiveType.WideChar:
729+
// returned structs must be blittable and char isn't
730+
if (Context.Context.Options.MarshalCharAsManagedChar)
731+
{
732+
Context.Return.Write("global::System.Convert.ToChar({0})",
733+
Context.Parameter.Name);
734+
return true;
735+
}
736+
goto default;
717737
case PrimitiveType.Char16:
718738
return false;
719739
case PrimitiveType.Bool:

src/Generator/Generators/CSharp/CSharpSources.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -947,17 +947,17 @@ private void GenerateIndexerSetter(Function function)
947947
Write(marshal.Context.Before);
948948

949949
var internalFunction = GetFunctionNativeIdentifier(function);
950+
var paramMarshal = GenerateFunctionParamMarshal(
951+
function.Parameters[0], 0, function);
950952
if (type.IsPrimitiveType())
951953
{
952954
WriteLine($@"*{@internal}.{internalFunction}({
953-
GetInstanceParam(function)}, {function.Parameters[0].Name}) = {
954-
marshal.Context.Return};");
955+
GetInstanceParam(function)}, {(paramMarshal.Context == null ?
956+
paramMarshal.Name : paramMarshal.Context.Return)}) = {marshal.Context.Return};");
955957
}
956958
else
957959
{
958960
var typeInternal = TypePrinter.PrintNative(type);
959-
var paramMarshal = GenerateFunctionParamMarshal(
960-
function.Parameters[0], 0, function);
961961
WriteLine($@"*({typeInternal}*) {@internal}.{internalFunction}({
962962
GetInstanceParam(function)}, {(paramMarshal.Context == null ?
963963
paramMarshal.Name : paramMarshal.Context.Return)}) = {marshal.Context.Return};");

src/Generator/Generators/CSharp/CSharpTypePrinter.cs

+10-11
Original file line numberDiff line numberDiff line change
@@ -225,15 +225,7 @@ public override TypePrinterResult VisitPointerType(PointerType pointer,
225225
{
226226
if (isManagedContext)
227227
return "string";
228-
if (Parameter == null || Parameter.Name == Helpers.ReturnIdentifier)
229-
return IntPtrType;
230-
if (Options.Encoding == Encoding.ASCII)
231-
return string.Format("[MarshalAs(UnmanagedType.LPStr)] string");
232-
if (Options.Encoding == Encoding.Unicode ||
233-
Options.Encoding == Encoding.BigEndianUnicode)
234-
return string.Format("[MarshalAs(UnmanagedType.LPWStr)] string");
235-
throw new NotSupportedException(string.Format("{0} is not supported yet.",
236-
Options.Encoding.EncodingName));
228+
return IntPtrType;
237229
}
238230

239231
var desugared = pointee.Desugar();
@@ -442,6 +434,10 @@ public static void GetPrimitiveTypeWidth(PrimitiveType primitive,
442434
width = targetInfo?.CharWidth ?? 8;
443435
signed = true;
444436
break;
437+
case PrimitiveType.WideChar:
438+
width = targetInfo?.WCharWidth ?? 16;
439+
signed = false;
440+
break;
445441
case PrimitiveType.UChar:
446442
width = targetInfo?.CharWidth ?? 8;
447443
signed = false;
@@ -516,8 +512,11 @@ public override TypePrinterResult VisitPrimitiveType(PrimitiveType primitive,
516512
"byte" : "bool";
517513
case PrimitiveType.Void: return "void";
518514
case PrimitiveType.Char16:
519-
case PrimitiveType.Char32:
520-
case PrimitiveType.WideChar: return "char";
515+
case PrimitiveType.Char32: return "char";
516+
case PrimitiveType.WideChar:
517+
return (ContextKind == TypePrinterContextKind.Native)
518+
? GetIntString(primitive, Context.TargetInfo)
519+
: "CppSharp.Runtime.WideChar";
521520
case PrimitiveType.Char:
522521
// returned structs must be blittable and char isn't
523522
return Options.MarshalCharAsManagedChar &&

src/Generator/Passes/CheckAbiParameters.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ public override bool VisitFunctionDecl(Function function)
6868

6969
// Deleting destructors (default in v-table) accept an i32 bitfield as a
7070
// second parameter in MS ABI.
71-
if (method != null && method.IsDestructor && Context.ParserOptions.IsMicrosoftAbi)
71+
var @class = method != null ? method.Namespace as Class : null;
72+
if (method != null &&
73+
method.IsDestructor &&
74+
@class.IsDynamic &&
75+
Context.ParserOptions.IsMicrosoftAbi)
7276
{
7377
method.Parameters.Add(new Parameter
7478
{

src/Generator/Passes/IgnoreSystemDeclarationsPass.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,19 @@ public override bool VisitClassDecl(Class @class)
6262
{
6363
if (method.IsDestructor || method.OriginalName == "c_str" ||
6464
(method.IsConstructor && method.Parameters.Count == 2 &&
65-
method.Parameters[0].Type.Desugar().IsPointerToPrimitiveType(PrimitiveType.Char) &&
66-
!method.Parameters[1].Type.Desugar().IsPrimitiveType()))
65+
(( method.Parameters[0].Type.Desugar().IsPointerToPrimitiveType(PrimitiveType.Char) &&
66+
!method.Parameters[1].Type.Desugar().IsPrimitiveType()) ||
67+
( method.Parameters[0].Type.Desugar().IsPointerToPrimitiveType(PrimitiveType.WideChar) &&
68+
!method.Parameters[1].Type.Desugar().IsPrimitiveType()))))
6769
{
70+
if(method.OriginalName == "c_str")
71+
{
72+
if (basicString.Arguments[0].Type.Type.Desugar().IsPrimitiveType(PrimitiveType.WideChar))
73+
method.Name = method.Name + "W";
74+
else if(basicString.Arguments[0].Type.Type.Desugar().IsPrimitiveType(PrimitiveType.Char))
75+
method.Name = method.Name + "A";
76+
}
77+
6878
method.GenerationKind = GenerationKind.Generate;
6979
method.Namespace.GenerationKind = GenerationKind.Generate;
7080
method.InstantiatedFrom.GenerationKind = GenerationKind.Generate;
@@ -111,7 +121,8 @@ public override bool VisitClassDecl(Class @class)
111121
private static IEnumerable<ClassTemplateSpecialization> GetCharSpecializations(Class @class)
112122
{
113123
return @class.Specializations.Where(s =>
114-
s.Arguments[0].Type.Type.Desugar().IsPrimitiveType(PrimitiveType.Char));
124+
s.Arguments[0].Type.Type.Desugar().IsPrimitiveType(PrimitiveType.Char) ||
125+
s.Arguments[0].Type.Type.Desugar().IsPrimitiveType(PrimitiveType.WideChar));
115126
}
116127

117128
public override bool VisitFunctionDecl(Function function)

0 commit comments

Comments
 (0)