diff --git a/build/Tests.lua b/build/Tests.lua index 724da13127..3868214af4 100644 --- a/build/Tests.lua +++ b/build/Tests.lua @@ -245,6 +245,7 @@ function SetupTestProjectsCLI(name, extraFiles, suffix) dependson { name .. ".Native" } LinkNUnit() + links { "CppSharp.Runtime" } end function IncludeExamples() diff --git a/src/CppParser/Bindings/CSharp/i686-pc-win32-msvc/Std-symbols.cpp b/src/CppParser/Bindings/CSharp/i686-pc-win32-msvc/Std-symbols.cpp index 682e399117..914385f06c 100644 --- a/src/CppParser/Bindings/CSharp/i686-pc-win32-msvc/Std-symbols.cpp +++ b/src/CppParser/Bindings/CSharp/i686-pc-win32-msvc/Std-symbols.cpp @@ -9,3 +9,8 @@ template __declspec(dllexport) std::basic_string, s template __declspec(dllexport) std::basic_string, std::allocator>::~basic_string() noexcept; template __declspec(dllexport) std::basic_string, std::allocator>& std::basic_string, std::allocator>::assign(const char* const); template __declspec(dllexport) const char* std::basic_string, std::allocator>::data() const noexcept; + +template __declspec(dllexport) std::basic_string, std::allocator>::basic_string(const wchar_t* const, const std::allocator&); +template __declspec(dllexport) std::basic_string, std::allocator>::~basic_string() noexcept; +template __declspec(dllexport) const wchar_t* std::basic_string, std::allocator>::c_str() const noexcept; +template __declspec(dllexport) std::allocator::allocator() noexcept; diff --git a/src/Generator/Generators/CSharp/CSharpMarshal.cs b/src/Generator/Generators/CSharp/CSharpMarshal.cs index 4c5c4d23d4..8f24b92282 100644 --- a/src/Generator/Generators/CSharp/CSharpMarshal.cs +++ b/src/Generator/Generators/CSharp/CSharpMarshal.cs @@ -105,9 +105,10 @@ public override bool VisitArrayType(ArrayType array, TypeQualifiers quals) if (arrayType.IsPrimitiveType(PrimitiveType.Bool)) supportBefore.WriteLineIndent($@"{value}[i] = { Context.ReturnVarName}[i] != 0;"); - else if (arrayType.IsPrimitiveType(PrimitiveType.Char) && - Context.Context.Options.MarshalCharAsManagedChar) - supportBefore.WriteLineIndent($@"{value}[i] = global::System.Convert.ToChar({ + else if ((arrayType.IsPrimitiveType(PrimitiveType.Char) || + arrayType.IsPrimitiveType(PrimitiveType.WideChar)) && + Context.Context.Options.MarshalCharAsManagedChar) + supportBefore.WriteLineIndent($@"{value}[i] = global::System.Convert.ToChar({ Context.ReturnVarName}[i]);"); else supportBefore.WriteLineIndent($@"{value}[i] = { @@ -119,7 +120,8 @@ public override bool VisitArrayType(ArrayType array, TypeQualifiers quals) break; case ArrayType.ArraySize.Incomplete: // const char* and const char[] are the same so we can use a string - if (array.Type.Desugar().IsPrimitiveType(PrimitiveType.Char) && + if ((array.Type.Desugar().IsPrimitiveType(PrimitiveType.Char) || + array.Type.Desugar().IsPrimitiveType(PrimitiveType.WideChar)) && array.QualifiedType.Qualifiers.IsConst) { var pointer = new PointerType { QualifiedPointee = array.QualifiedType }; @@ -215,6 +217,19 @@ public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers { case PrimitiveType.Void: return true; + case PrimitiveType.Char: + // returned structs must be blittable and char isn't + if (Context.Context.Options.MarshalCharAsManagedChar) + { + Context.Return.Write($"global::System.Convert.ToChar({Context.ReturnVarName})"); + return true; + } + goto default; + case PrimitiveType.WideChar: + Context.Return.Write($"new CppSharp.Runtime.WideChar({Context.ReturnVarName})"); + return true; + case PrimitiveType.Char16: + return false; case PrimitiveType.Bool: if (Context.MarshalKind == MarshalKind.NativeField) { @@ -268,11 +283,10 @@ public override bool VisitFunctionType(FunctionType function, TypeQualifiers qua { var ptrName = Generator.GeneratedIdentifier("ptr") + Context.ParameterIndex; - Context.Before.WriteLine("var {0} = {1};", ptrName, - Context.ReturnVarName); + Context.Before.WriteLine($"var {ptrName} = {Context.ReturnVarName};"); - Context.Return.Write("({1})Marshal.GetDelegateForFunctionPointer({0}, typeof({1}))", - ptrName, function.ToString()); + Context.Return.Write($@"({function.ToString() + })Marshal.GetDelegateForFunctionPointer({ptrName}, typeof({function.ToString()}))"); return true; } @@ -299,7 +313,7 @@ public override bool VisitClassDecl(Class @class) public override bool VisitEnumDecl(Enumeration @enum) { - Context.Return.Write("{0}", Context.ReturnVarName); + Context.Return.Write($"{Context.ReturnVarName}"); return true; } @@ -323,8 +337,7 @@ public override bool VisitParameterDecl(Parameter parameter) if (!string.IsNullOrWhiteSpace(ctx.Return) && !parameter.Type.IsPrimitiveTypeConvertibleToRef()) { - Context.Before.WriteLine("var _{0} = {1};", parameter.Name, - ctx.Return); + Context.Before.WriteLine($"var _{parameter.Name} = {ctx.Return};"); } Context.Return.Write("{0}{1}", @@ -494,11 +507,17 @@ public override bool VisitArrayType(ArrayType array, TypeQualifiers quals) supportBefore.WriteLineIndent($@"{ Context.ReturnVarName}[i] = (byte)({ Context.ArgName}[i] ? 1 : 0);"); - else if (arrayType.IsPrimitiveType(PrimitiveType.Char) && - Context.Context.Options.MarshalCharAsManagedChar) - supportBefore.WriteLineIndent($@"{ - Context.ReturnVarName}[i] = global::System.Convert.ToSByte({ - Context.ArgName}[i]);"); + else if ((arrayType.IsPrimitiveType(PrimitiveType.Char) || + arrayType.IsPrimitiveType(PrimitiveType.WideChar)) && + Context.Context.Options.MarshalCharAsManagedChar) + { + if(arrayType.IsPrimitiveType(PrimitiveType.Char)) + supportBefore.WriteLineIndent( + $"{Context.ReturnVarName}[i] = global::System.Convert.ToSByte({Context.ArgName}[i]);"); + else + supportBefore.WriteLineIndent( + $"{Context.ReturnVarName}[i] = global::System.Convert.ToChar({Context.ArgName}[i]);"); + } else supportBefore.WriteLineIndent($@"{Context.ReturnVarName}[i] = { Context.ArgName}[i]{ @@ -632,14 +651,96 @@ public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals) Context.Return.Write($"new {typePrinter.IntPtrType}(&{arg})"); return true; } + + var marshalAsString = CSharpTypePrinter.IsConstCharString(pointer); + if (finalPointeeIsPrimitiveType || finalPointee.IsEnumType() || + marshalAsString) + { + // From MSDN: "note that a ref or out parameter is classified as a moveable + // variable". This means we must create a local variable to hold the result + // and then assign this value to the parameter. + + if (isRefParam) + { + var typeName = Type.TypePrinterDelegate(finalPointee); + if (Context.Function.OperatorKind == CXXOperatorKind.Subscript) + Context.Return.Write(param.Name); + else + { + if (param.IsInOut) + Context.Before.WriteLine($"{typeName} _{param.Name} = {param.Name};"); + else + Context.Before.WriteLine($"{typeName} _{param.Name};"); + + Context.Return.Write($"&_{param.Name}"); + } + } + else + { + if (!marshalAsString && + Context.Context.Options.MarshalCharAsManagedChar && + (primitive == PrimitiveType.Char || primitive == PrimitiveType.WideChar)) + Context.Return.Write($"({typePrinter.PrintNative(pointer)}) "); + + if (marshalAsString) + Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name, primitive)); + else + Context.Return.Write(Context.Parameter.Name); + } + + return true; + } return pointer.QualifiedPointee.Visit(this); } + private string MarshalStringToUnmanaged(string varName, PrimitiveType type) + { + if (type == PrimitiveType.WideChar) + { + // Looks like Marshal.StringToHGlobalUni is already able + // to handle both Unicode and MBCS charsets + return $"Marshal.StringToHGlobalUni({varName})"; + } + + if (Equals(Context.Context.Options.Encoding, Encoding.ASCII)) + return $"Marshal.StringToHGlobalAnsi({varName})"; + + if (Equals(Context.Context.Options.Encoding, Encoding.Unicode) || + Equals(Context.Context.Options.Encoding, Encoding.BigEndianUnicode)) + { + return $"Marshal.StringToHGlobalUni({varName})"; + } + throw new NotSupportedException( + $"{Context.Context.Options.Encoding.EncodingName} is not supported yet."); + } + public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers quals) { switch (primitive) { + case PrimitiveType.Void: + return true; + case PrimitiveType.Char: + // returned structs must be blittable and char isn't + if (Context.Context.Options.MarshalCharAsManagedChar) + { + Context.Return.Write("global::System.Convert.ToSByte({0})", + Context.Parameter.Name); + return true; + } + goto default; + case PrimitiveType.WideChar: + // returned structs must be blittable and char isn't + if (Context.Context.Options.MarshalCharAsManagedChar) + { + Context.Return.Write("global::System.Convert.ToChar({0})", + Context.Parameter.Name); + return true; + } + goto default; + case PrimitiveType.Char16: + return false; case PrimitiveType.Bool: if (Context.MarshalKind == MarshalKind.NativeField) { diff --git a/src/Generator/Generators/CSharp/CSharpTypePrinter.cs b/src/Generator/Generators/CSharp/CSharpTypePrinter.cs index c1e0835d45..142dd1ab95 100644 --- a/src/Generator/Generators/CSharp/CSharpTypePrinter.cs +++ b/src/Generator/Generators/CSharp/CSharpTypePrinter.cs @@ -408,6 +408,10 @@ public override TypePrinterResult VisitCILType(CILType type, TypeQualifiers qual width = targetInfo?.CharWidth ?? 8; signed = true; break; + case PrimitiveType.WideChar: + width = targetInfo?.WCharWidth ?? 16; + signed = false; + break; case PrimitiveType.UChar: width = targetInfo?.CharWidth ?? 8; signed = false; @@ -482,8 +486,11 @@ static string GetIntString(PrimitiveType primitive, ParserTargetInfo targetInfo) "byte" : "bool"; case PrimitiveType.Void: return "void"; case PrimitiveType.Char16: - case PrimitiveType.Char32: - case PrimitiveType.WideChar: return "char"; + case PrimitiveType.Char32: return "char"; + case PrimitiveType.WideChar: + return (ContextKind == TypePrinterContextKind.Native) + ? GetIntString(primitive, Context.TargetInfo) + : "CppSharp.Runtime.WideChar"; case PrimitiveType.Char: // returned structs must be blittable and char isn't return Options.MarshalCharAsManagedChar && diff --git a/src/Generator/Passes/CheckAbiParameters.cs b/src/Generator/Passes/CheckAbiParameters.cs index 954523101a..d38fc0e65f 100644 --- a/src/Generator/Passes/CheckAbiParameters.cs +++ b/src/Generator/Passes/CheckAbiParameters.cs @@ -56,7 +56,11 @@ public override bool VisitFunctionDecl(Function function) // Deleting destructors (default in v-table) accept an i32 bitfield as a // second parameter in MS ABI. - if (method != null && method.IsDestructor && Context.ParserOptions.IsMicrosoftAbi) + var @class = method != null ? method.Namespace as Class : null; + if (method != null && + method.IsDestructor && + @class.IsDynamic && + Context.ParserOptions.IsMicrosoftAbi) { method.Parameters.Add(new Parameter { diff --git a/src/Generator/Passes/IgnoreSystemDeclarationsPass.cs b/src/Generator/Passes/IgnoreSystemDeclarationsPass.cs index c1ff5f513b..80e2a20cfe 100644 --- a/src/Generator/Passes/IgnoreSystemDeclarationsPass.cs +++ b/src/Generator/Passes/IgnoreSystemDeclarationsPass.cs @@ -55,6 +55,40 @@ public override bool VisitClassDecl(Class @class) switch (@class.Name) { case "basic_string": + foreach (var method in @class.Methods.Where(m => !m.IsDestructor && m.OriginalName != "c_str")) + method.ExplicitlyIgnore(); + foreach (var basicString in GetCharSpecializations(@class)) + { + basicString.GenerationKind = GenerationKind.Generate; + foreach (var method in basicString.Methods) + { + if (method.IsDestructor || method.OriginalName == "c_str" || + (method.IsConstructor && method.Parameters.Count == 2 && + (( method.Parameters[0].Type.Desugar().IsPointerToPrimitiveType(PrimitiveType.Char) && + !method.Parameters[1].Type.Desugar().IsPrimitiveType()) || + ( method.Parameters[0].Type.Desugar().IsPointerToPrimitiveType(PrimitiveType.WideChar) && + !method.Parameters[1].Type.Desugar().IsPrimitiveType())))) + { + if(method.OriginalName == "c_str") + { + if (basicString.Arguments[0].Type.Type.Desugar().IsPrimitiveType(PrimitiveType.WideChar)) + method.Name = method.Name + "W"; + else if(basicString.Arguments[0].Type.Type.Desugar().IsPrimitiveType(PrimitiveType.Char)) + method.Name = method.Name + "A"; + } + + method.GenerationKind = GenerationKind.Generate; + method.Namespace.GenerationKind = GenerationKind.Generate; + method.InstantiatedFrom.GenerationKind = GenerationKind.Generate; + method.InstantiatedFrom.Namespace.GenerationKind = GenerationKind.Generate; + } + else + { + method.ExplicitlyIgnore(); + } + } + } + break; case "allocator": case "char_traits": @class.GenerationKind = GenerationKind.Generate; diff --git a/src/Generator/Types/Std/Stdlib.cs b/src/Generator/Types/Std/Stdlib.cs index 0aaffb95cf..a790c0b6a2 100644 --- a/src/Generator/Types/Std/Stdlib.cs +++ b/src/Generator/Types/Std/Stdlib.cs @@ -314,6 +314,8 @@ public class ConstChar16TPointer : ConstCharPointer [TypeMap("basic_string, allocator>", GeneratorKind = GeneratorKind.CSharp)] [TypeMap("basic_string, allocator>", GeneratorKind = GeneratorKind.CLI)] + [TypeMap("basic_string, allocator>", GeneratorKind = GeneratorKind.CSharp)] + [TypeMap("basic_string, allocator>", GeneratorKind = GeneratorKind.CLI)] public class String : TypeMap { public override Type CLISignatureType(TypePrinterContext ctx) @@ -323,14 +325,24 @@ public override Type CLISignatureType(TypePrinterContext ctx) public override void CLIMarshalToNative(MarshalContext ctx) { - ctx.Return.Write("clix::marshalString({0})", - ctx.Parameter.Name); + var type = ctx.Parameter.Type.Desugar(); + ClassTemplateSpecialization basicString = GetBasicString(type); + + if(basicString.Arguments[0].Type.Type.IsPrimitiveType(PrimitiveType.Char)) + ctx.Return.Write($"clix::marshalString({ctx.Parameter.Name})"); + else + ctx.Return.Write($"clix::marshalString({ctx.Parameter.Name})"); } public override void CLIMarshalToManaged(MarshalContext ctx) { - ctx.Return.Write("clix::marshalString({0})", - ctx.ReturnVarName); + var type = ctx.ReturnType.Type.Desugar(); + ClassTemplateSpecialization basicString = GetBasicString(type); + + if (basicString.Arguments[0].Type.Type.IsPrimitiveType(PrimitiveType.Char)) + ctx.Return.Write($"clix::marshalString({ctx.ReturnVarName})"); + else + ctx.Return.Write($"clix::marshalString({ctx.ReturnVarName})"); } public override Type CSharpSignatureType(TypePrinterContext ctx) @@ -381,6 +393,7 @@ public override void CSharpMarshalToManaged(CSharpMarshalContext ctx) ClassTemplateSpecialization basicString = GetBasicString(type); var data = basicString.Methods.First(m => m.OriginalName == "data"); var typePrinter = new CSharpTypePrinter(ctx.Context); + string qualifiedBasicString = GetQualifiedBasicString(basicString); string varBasicString = $"__basicStringRet{ctx.ParameterIndex}"; bool usePointer = type.IsAddress() || ctx.MarshalKind == MarshalKind.NativeField || diff --git a/src/Runtime/Helpers.cs b/src/Runtime/Helpers.cs new file mode 100644 index 0000000000..e49685e4cf --- /dev/null +++ b/src/Runtime/Helpers.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace CppSharp.Runtime +{ + public static class Helpers + { + public static string MarshalEncodedString(IntPtr ptr, Encoding encoding) + { + if (ptr == IntPtr.Zero) + return null; + + var size = 0; + while (Marshal.ReadInt32(ptr, size) != 0) + size += sizeof(int); + + var buffer = new byte[size]; + Marshal.Copy(ptr, buffer, 0, buffer.Length); + + return encoding.GetString(buffer); + } + + public static IntPtr StringToHGlobalMultiByteUni(string str) + { + byte[] bytes = Encoding.UTF8.GetBytes(str); + + IntPtr nativePtr = Marshal.AllocHGlobal(bytes.Length); + Marshal.Copy(bytes, 0, nativePtr, bytes.Length); + + return nativePtr; + } + } +} diff --git a/src/Runtime/WideChar.cs b/src/Runtime/WideChar.cs new file mode 100644 index 0000000000..04eb33c69c --- /dev/null +++ b/src/Runtime/WideChar.cs @@ -0,0 +1,93 @@ +namespace CppSharp.Runtime +{ + // Struct added to help map a the wchar_t C++ type to C# + // The data is stored as a 64 bit value so it could represent + // an UTF-32 character but C# only represents UTF-16 characters + // so beware of possible data loss when using it. + public struct WideChar + { + public long Value { get; set; } + + public WideChar(char c) + { + Value = c; + } + + public WideChar(long l) + { + Value = l; + } + + public static implicit operator char(WideChar c) + { + return System.Convert.ToChar(c.Value); + } + + public static implicit operator WideChar(long l) + { + return new WideChar { Value = l }; + } + + public static bool operator ==(WideChar wc, char c) + { + if (ReferenceEquals(null, c)) + return false; + + if (ReferenceEquals(null, wc)) + return false; + + return System.Convert.ToChar(wc.Value) == c; + } + + public static bool operator !=(WideChar wc, char c) + { + return !(wc == c); + } + + public static bool operator ==(WideChar wc, long l) + { + if (ReferenceEquals(null, l)) + return false; + + if (ReferenceEquals(null, wc)) + return false; + + return wc.Value == l; + } + + public static bool operator !=(WideChar wc, long l) + { + return !(wc == l); + } + + public bool Equals(WideChar obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + return Value.Equals(obj.Value); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + return obj.GetType() == GetType() && Equals((WideChar)obj); + } + + public override int GetHashCode() + { + unchecked + { + return Value.GetHashCode(); + } + } + } +} diff --git a/tests/Common/Common.Tests.cs b/tests/Common/Common.Tests.cs index ae9b48d79f..7c568e9ded 100644 --- a/tests/Common/Common.Tests.cs +++ b/tests/Common/Common.Tests.cs @@ -963,6 +963,31 @@ public void TestNullStdString() } } + public void TestStdWString() + { + const string unicodeString1 = "你好"; + const string unicodeString2 = "Ÿ‰ϰ"; + + using (var hasStdWString = new HasStdWString()) + { + Assert.That(hasStdWString.TestStdWString(t), Is.EqualTo(t + "_test")); + hasStdWString.S = t; + Assert.That(hasStdWString.S, Is.EqualTo(t)); + Assert.That(hasStdWString.StdWString, Is.EqualTo(t)); + Assert.That(hasStdWString.StdWString, Is.EqualTo(t)); + + Assert.That(hasStdWString.TestStdWString(unicodeString1), Is.EqualTo(unicodeString1 + "_test")); + hasStdWString.S = unicodeString1; + Assert.That(hasStdWString.S, Is.EqualTo(unicodeString1)); + Assert.That(hasStdWString.StdWString, Is.EqualTo(unicodeString1)); + + Assert.That(hasStdWString.TestStdWString(unicodeString2), Is.EqualTo(unicodeString2 + "_test")); + hasStdWString.S = unicodeString2; + Assert.That(hasStdWString.S, Is.EqualTo(unicodeString2)); + Assert.That(hasStdWString.StdWString, Is.EqualTo(unicodeString2)); + } + } + [Test] public void TestUTF8() { @@ -996,6 +1021,23 @@ public void TestUTF8() } } + public void TestWideCharCompatibility() + { + var c = new CppSharp.Runtime.WideChar('c'); + + Assert.That(c == 'a', Is.False); + Assert.That(Common.WideCharCompatibility('a') == 'c', Is.False); + + Assert.That(c == 'c', Is.True); + Assert.That(Common.WideCharCompatibility('a') == 'a', Is.True); + + Assert.That(c != 'c', Is.False); + Assert.That(Common.WideCharCompatibility('a') != 'a', Is.False); + + Assert.That(c != 'a', Is.True); + Assert.That(Common.WideCharCompatibility('a') != 'c', Is.True); + } + private class CustomDerivedFromVirtual : AbstractWithVirtualDtor { public override void Abstract() diff --git a/tests/Common/Common.cpp b/tests/Common/Common.cpp index 5a1891d513..533590840b 100644 --- a/tests/Common/Common.cpp +++ b/tests/Common/Common.cpp @@ -604,6 +604,29 @@ std::string& HasStdString::getStdString() return s; } +HasStdWString::HasStdWString() +{ +} + +HasStdWString::~HasStdWString() +{ +} + +std::wstring HasStdWString::testStdWString(const std::wstring& s) +{ + return s + L"_test"; +} + +std::wstring& HasStdWString::getStdWString() +{ + return s; +} + +wchar_t WideCharCompatibility(wchar_t c) +{ + return c; +} + SomeNamespace::AbstractClass::~AbstractClass() { } diff --git a/tests/Common/Common.h b/tests/Common/Common.h index 73e67b6f29..3c45a81ef4 100644 --- a/tests/Common/Common.h +++ b/tests/Common/Common.h @@ -896,6 +896,18 @@ class DLL_API HasStdString std::string& getStdString(); }; +class DLL_API HasStdWString +{ +public: + HasStdWString(); + ~HasStdWString(); + std::wstring testStdWString(const std::wstring& s); + std::wstring s; + std::wstring& getStdWString(); +}; + +DLL_API wchar_t WideCharCompatibility(wchar_t c); + class DLL_API InternalCtorAmbiguity { public: