Skip to content

Commit 41de24c

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 41de24c

File tree

13 files changed

+327
-115
lines changed

13 files changed

+327
-115
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-43
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,19 +157,17 @@ 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();
169-
PrimitiveType primitive;
170-
if (finalPointee.IsPrimitiveType(out primitive) || finalPointee.IsEnumType())
170+
if (finalPointee.IsPrimitiveType(out PrimitiveType primitive) || finalPointee.IsEnumType())
171171
{
172172
if (isRefParam)
173173
{
@@ -237,11 +237,13 @@ public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers
237237
// returned structs must be blittable and char isn't
238238
if (Context.Context.Options.MarshalCharAsManagedChar)
239239
{
240-
Context.Return.Write("global::System.Convert.ToChar({0})",
241-
Context.ReturnVarName);
240+
Context.Return.Write($"global::System.Convert.ToChar({Context.ReturnVarName})");
242241
return true;
243242
}
244243
goto default;
244+
case PrimitiveType.WideChar:
245+
Context.Return.Write($"new CppSharp.Runtime.WideChar({Context.ReturnVarName})");
246+
return true;
245247
case PrimitiveType.Char16:
246248
return false;
247249
case PrimitiveType.Bool:
@@ -265,16 +267,15 @@ public override bool VisitTypedefType(TypedefType typedef, TypeQualifiers quals)
265267

266268
var decl = typedef.Declaration;
267269

268-
var functionType = decl.Type as FunctionType;
269-
if (functionType != null || decl.Type.IsPointerTo(out functionType))
270+
if (decl.Type is FunctionType functionType || decl.Type.IsPointerTo(out functionType))
270271
{
271272
var ptrName = Generator.GeneratedIdentifier("ptr") +
272273
Context.ParameterIndex;
273274

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

277-
var res = $"{ptrName} == IntPtr.Zero? null : ({typedef})Marshal.GetDelegateForFunctionPointer({ptrName}, typeof({typedef}))";
277+
var res = $@"{ptrName} == IntPtr.Zero? null : ({typedef
278+
})Marshal.GetDelegateForFunctionPointer({ptrName}, typeof({typedef}))";
278279
Context.Return.Write(res);
279280
return true;
280281
}
@@ -286,11 +287,10 @@ public override bool VisitFunctionType(FunctionType function, TypeQualifiers qua
286287
{
287288
var ptrName = Generator.GeneratedIdentifier("ptr") + Context.ParameterIndex;
288289

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

292-
Context.Return.Write("({1})Marshal.GetDelegateForFunctionPointer({0}, typeof({1}))",
293-
ptrName, function.ToString());
292+
Context.Return.Write($@"({function.ToString()
293+
})Marshal.GetDelegateForFunctionPointer({ptrName}, typeof({function.ToString()}))");
294294
return true;
295295
}
296296

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

317317
public override bool VisitEnumDecl(Enumeration @enum)
318318
{
319-
Context.Return.Write("{0}", Context.ReturnVarName);
319+
Context.Return.Write($"{Context.ReturnVarName}");
320320
return true;
321321
}
322322

@@ -340,8 +340,7 @@ public override bool VisitParameterDecl(Parameter parameter)
340340
if (!string.IsNullOrWhiteSpace(ctx.Return) &&
341341
!parameter.Type.IsPrimitiveTypeConvertibleToRef())
342342
{
343-
Context.Before.WriteLine("var _{0} = {1};", parameter.Name,
344-
ctx.Return);
343+
Context.Before.WriteLine($"var _{parameter.Name} = {ctx.Return};");
345344
}
346345

347346
Context.Return.Write("{0}{1}",
@@ -512,12 +511,16 @@ public override bool VisitArrayType(ArrayType array, TypeQualifiers quals)
512511
}
513512
else
514513
{
515-
if (arrayType.IsPrimitiveType(PrimitiveType.Char) &&
514+
if ((arrayType.IsPrimitiveType(PrimitiveType.Char) ||
515+
arrayType.IsPrimitiveType(PrimitiveType.WideChar)) &&
516516
Context.Context.Options.MarshalCharAsManagedChar)
517517
{
518-
supportBefore.WriteLineIndent(
519-
"{0}[i] = global::System.Convert.ToSByte({1}[i]);",
520-
Context.ReturnVarName, Context.ArgName);
518+
if(arrayType.IsPrimitiveType(PrimitiveType.Char))
519+
supportBefore.WriteLineIndent(
520+
$"{Context.ReturnVarName}[i] = global::System.Convert.ToSByte({Context.ArgName}[i]);");
521+
else
522+
supportBefore.WriteLineIndent(
523+
$"{Context.ReturnVarName}[i] = global::System.Convert.ToChar({Context.ArgName}[i]);");
521524
}
522525
else
523526
{
@@ -591,6 +594,8 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
591594

592595
var param = Context.Parameter;
593596
var isRefParam = param != null && (param.IsInOut || param.IsOut);
597+
var finalPointee = pointer.GetFinalPointee();
598+
bool finalPointeeIsPrimitiveType = finalPointee.IsPrimitiveType(out PrimitiveType primitive);
594599

595600
if (CSharpTypePrinter.IsConstCharString(pointee) && isRefParam)
596601
{
@@ -601,12 +606,12 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
601606
}
602607
else if (param.IsInOut)
603608
{
604-
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name));
609+
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name, primitive));
605610
Context.ArgumentPrefix.Write("&");
606611
}
607612
else
608613
{
609-
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name));
614+
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name, primitive));
610615
Context.Cleanup.WriteLine("Marshal.FreeHGlobal({0});", Context.ArgName);
611616
}
612617
return true;
@@ -639,9 +644,7 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
639644
}
640645

641646
var marshalAsString = CSharpTypePrinter.IsConstCharString(pointer);
642-
var finalPointee = pointer.GetFinalPointee();
643-
PrimitiveType primitive;
644-
if (finalPointee.IsPrimitiveType(out primitive) || finalPointee.IsEnumType() ||
647+
if (finalPointeeIsPrimitiveType || finalPointee.IsEnumType() ||
645648
marshalAsString)
646649
{
647650
// From MSDN: "note that a ref or out parameter is classified as a moveable
@@ -667,13 +670,11 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
667670
{
668671
if (!marshalAsString &&
669672
Context.Context.Options.MarshalCharAsManagedChar &&
670-
primitive == PrimitiveType.Char)
673+
(primitive == PrimitiveType.Char || primitive == PrimitiveType.WideChar))
671674
Context.Return.Write($"({typePrinter.PrintNative(pointer)}) ");
672675

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));
676+
if (marshalAsString)
677+
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name, primitive));
677678
else
678679
Context.Return.Write(Context.Parameter.Name);
679680
}
@@ -684,19 +685,25 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals)
684685
return pointer.QualifiedPointee.Visit(this);
685686
}
686687

687-
private string MarshalStringToUnmanaged(string varName)
688+
private string MarshalStringToUnmanaged(string varName, PrimitiveType type)
688689
{
689-
if (Equals(Context.Context.Options.Encoding, Encoding.ASCII))
690+
if (type == PrimitiveType.WideChar)
690691
{
691-
return string.Format("Marshal.StringToHGlobalAnsi({0})", varName);
692+
// Looks like Marshal.StringToHGlobalUni is already able
693+
// to handle both Unicode and MBCS charsets
694+
return $"Marshal.StringToHGlobalUni({varName})";
692695
}
696+
697+
if (Equals(Context.Context.Options.Encoding, Encoding.ASCII))
698+
return $"Marshal.StringToHGlobalAnsi({varName})";
699+
693700
if (Equals(Context.Context.Options.Encoding, Encoding.Unicode) ||
694701
Equals(Context.Context.Options.Encoding, Encoding.BigEndianUnicode))
695702
{
696-
return string.Format("Marshal.StringToHGlobalUni({0})", varName);
703+
return $"Marshal.StringToHGlobalUni({varName})";
697704
}
698-
throw new NotSupportedException(string.Format("{0} is not supported yet.",
699-
Context.Context.Options.Encoding.EncodingName));
705+
throw new NotSupportedException(
706+
$"{Context.Context.Options.Encoding.EncodingName} is not supported yet.");
700707
}
701708

702709
public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers quals)
@@ -714,6 +721,15 @@ public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers
714721
return true;
715722
}
716723
goto default;
724+
case PrimitiveType.WideChar:
725+
// returned structs must be blittable and char isn't
726+
if (Context.Context.Options.MarshalCharAsManagedChar)
727+
{
728+
Context.Return.Write("global::System.Convert.ToChar({0})",
729+
Context.Parameter.Name);
730+
return true;
731+
}
732+
goto default;
717733
case PrimitiveType.Char16:
718734
return false;
719735
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
{

0 commit comments

Comments
 (0)