Search Unity

Returning C++ string to IL2CPP WIndows Store project

Discussion in 'Windows' started by Nerosam, Apr 1, 2016.

  1. Nerosam

    Nerosam

    Joined:
    Jul 23, 2013
    Posts:
    40
    I've had an extremely fun (not quite) time trying to create a C++ DLL file that has a function which returns a string when called. I have a C++ file in a DLL project which contains the following function:-

    Code (C++):
    1. extern "C" UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API GetMyString(wchar_t *str, int len)
    2. {
    3.     std::string oldstr = "Guuuper";
    4.     std::wstring lnewstr = std::wstring(oldstr.begin(), oldstr.end());
    5.  
    6.     wcscpy_s(str, len, lnewstr.c_str());
    7.  
    8. }
    9.  
    Note how I am using UNITY_INTERFACE_EXPORT and UNITY_INTERFACE_API from the IUnityInterface.h file that Unity wrote. This was required as I couldn't get the game to recognize the function name with the usual "extern"C"{__declspec(dllexport)" keywords.

    Now over on the C# side I have the following code:-

    Code (CSharp):
    1.   [DllImport("SimpleDLLFile")]
    2.     private static extern void GetMyString(StringBuilder str, int len);
    3.  
    4.     public static string GetMyStringMarshal()
    5.     {
    6.         StringBuilder buffer = new StringBuilder(255);
    7.         GetMyString(buffer, buffer.Capacity);
    8.         return buffer.ToString();
    9.     }
    10.  
    11.  
    I call on the GetMyStringMarshal() function which I use to create a StringBuilder class with a buffer of 255 characters and pass that into the exported GetMyString function.

    I then attempt to output that into a Debug.Log which shows up within the output window in Visual Studio.

    The problem is the output; when I build the project in unity as Windows Store UWP IL2CPP project, the Debug.Log only outputs the first character of the string i defined. Above, ive defined it as "Guuuper" but all I get back is "G". But when I build the project as a .NET project (changed from player settings), the full string "Guuuper" is shown in the Debug.Log.

    Has anyone got an idea as to what I am doing wrong? Ive tried numerous combinations including defining the C++ function as a char* but that simply returned nothing at all. The attempt above is the closest I've gotten to a working sample.

    In the end, all I really want to do is call on the built in OnlineIdAuthenticator class that is provided by UWP (https://msdn.microsoft.com/en-us/li...ntication.onlineid.onlineidauthenticator.aspx) and return both a username/gamertag and an ID (similar to what gamecenter/iOS does within the "Social" namespace.

    Any help would be much appreciated.

    Kind Regards,

    SammyG
     
    Last edited: Apr 1, 2016
  2. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    Hey, if it works on .NET, but not IL2CPP, could you report a bug?

    Anyway, try this:

    Code (CSharp):
    1. [DllImport("SimpleDLLFile")]
    2. private static extern string GetMyString();
    Code (C++):
    1. extern "C" __declspec(dllexport) wchar_t* __stdcall GetMyString()
    2. {
    3.     wchar_t* myString = L"Guuuper";
    4.     wchar_t* result = static_cast<wchar_t*>(CoTaskMemAlloc((wcslen(myString) + 1) * sizeof(wchar_t)));
    5.     wcscpy(result, myString);
    6.     return result;
    7. }
     
  3. Nerosam

    Nerosam

    Joined:
    Jul 23, 2013
    Posts:
    40
    Thanks Tautvydas. I gave your suggestion a try with a few minor changes. I got error saying that wcscpy is unsafe and to use wcscpy_s instead and used std::wcslen on myString to get the length. But on debugging, visual studio threw an exception saying "Unhandled exception at 0x00007FF8B7588528 (ucrtbase.dll) in Super Room Shooter.exe: An invalid parameter was passed to a function that considers invalid parameters fatal.". Also, forgot to mention that I built the dll in release. Screenshots with callstack and watch values below.



     
  4. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    wcscpy_s function takes the size of destination buffer in characters including the space for null-terminator. That means you want to pass it "wcslen(myString) + 1". Something like this should work:

    Code (C++):
    1. extern "C" __declspec(dllexport) wchar_t* __stdcall GetMyString()
    2. {
    3.   wchar_t* myString = L"Guuuper";
    4.   auto resultBufferLength = wcslen(myString) + 1;
    5.   wchar_t* result = static_cast<wchar_t*>(CoTaskMemAlloc(resultBufferLength * sizeof(wchar_t)));
    6.   wcscpy_s(result, resultBufferLength, myString);
    7.   return result;
    8. }
     
  5. Nerosam

    Nerosam

    Joined:
    Jul 23, 2013
    Posts:
    40
    Thanks for that suggestion. This was able to execute with no issues but I'm now back to the first issue. Only the first letter of the string is returned ("G").
     
  6. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    Ohhh I forgot about one thing. Sorry about that. By default, C# marshals narrows strings back from P/Invoke, so you need to annotate it with attribute so it marshals it back from wide string. Like this:

    Code (CSharp):
    1. [DllImport("SimpleDLLFile")]
    2. [return: MarshalAs(UnmanagedType.LPWStr)]
    3. private static extern string GetMyString();
     
  7. Nerosam

    Nerosam

    Joined:
    Jul 23, 2013
    Posts:
    40
    I've given your amendment a try and its threw a DllNotFoundException.

    I've filed a bug report. This is without the added "[return: MarshalAs(UnmanagedType.LPWStr)]" attribute. Here is the link. https://fogbugz.unity3d.com/default.asp?785533_6vr35hot4pg55ngd.

    Update: Sorry i copied the example as is and forgot to rename the DLL file name to the exact one I use. :oops:
     
    Last edited: Apr 4, 2016
  8. Aurimas-Cernius

    Aurimas-Cernius

    Unity Technologies

    Joined:
    Jul 31, 2013
    Posts:
    3,735
    Have you changed it exactly?
    You can use you original code that did call native code and just add the MarshalAs attribute for the parameter where you pass StringBuilder.
     
  9. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    Thanks for the bug report. I'll take a look at it. And btw, try what Aurimas mentioned. It should work. Hopefully. Famous last words :).
     
  10. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    By the way, I could not reproduce DllNotFoundException you mentioned on 5.3.4p1. The project you submitted works identically on both .NET and IL2CPP scripting backends (without [return: MarshalAs(UnmanagedType.LPWStr)]) both .NET and il2cpp prints only the first letter). I'm going to look at the stringbuilder scenario next.
     
  11. Nerosam

    Nerosam

    Joined:
    Jul 23, 2013
    Posts:
    40
    Sorry i copied the example as is and forgot to rename the DLL file name to the exact one I use. :oops:
     
  12. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    So did you manage to make it work using [return: MarshalAs] directive? It worked on my machine :).
     
  13. Nerosam

    Nerosam

    Joined:
    Jul 23, 2013
    Posts:
    40
    Yes its working now. I can finally start implementing platform specific features!

    Thanks for your help.
     
  14. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    By the way, does StringBuilder example you gave really work on .NET scripting backend? It crashes for me there. It works fine if I add [MarshalAs(UnmanagedType.LPWStr)], though.
     
  15. Nerosam

    Nerosam

    Joined:
    Jul 23, 2013
    Posts:
    40
    I shall give that a try as soon as i am able to
     
  16. chanon81

    chanon81

    Joined:
    Oct 6, 2015
    Posts:
    168
    Hello, sorry to revive this old thread (and asking about a different platform), but how would one do the same thing in iOS IL2CPP as I don't think CoTaskMemAlloc is available?

    I used StringBuilder to successfully do it in Windows and Android (non IL2CPP), but now with iOS IL2CPP the C# side doesn't seem to see the results of modifying the string on the C++ side and also results in a crash after a few calls.

    EDIT: Actually, I got an idea. Use a callback function that C++ calls to return the string instead. will try to implement it.

    EDIT2: Wow, it worked. So in case anyone needs to write a plugin that sends/receives strings or text from C# <-> C++, just create a callback like how Debug callbacks are made so C++ can send the string back to C# side and both can manage their own memory. The C# side would store result in a static variable and then after C++ call is done can read the static variable for the result.
     
    Last edited: Oct 18, 2018
  17. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    On non-windows platforms, you need to use "malloc" allocator. On Windows, you need to use CoTaskMemAlloc.
     
  18. Deleted User

    Deleted User

    Guest


    Can I just pass an allocator from C# for all platforms?

    Code (CSharp):
    1. public delegate IntPtr AllocCoTaskMemDelegate(int size);
    2.  
    3. [MonoPInvokeCallback(typeof(AllocCoTaskMemDelegate))]
    4. static IntPtr AllocCoTaskMem(int size) {
    5.     return Marshal.AllocCoTaskMem(size);
    6. }
    7.  
    8. [DllImport("__Internal")]
    9. [return: MarshalAs(UnmanagedType.LPStr)]
    10. private static extern string NativeMethod(
    11.     [MarshalAs(UnmanagedType.FunctionPtr)] AllocCoTaskMemDelegate allocCoTaskMem
    12. );
    So NativeMethod() would allocate and return a (char*) using allocCoTaskMem(n).
     
    Last edited by a moderator: Feb 18, 2019
  19. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    I think that should work, but will be slower than just using a define for it in C++.
     
    Deleted User likes this.
  20. Deleted User

    Deleted User

    Guest

    Ok, thanks for confirming. I tried it on iOS, and it seemed to work.
    This code is invoked infrequently by events, so performance isn't an issue for us.