Search Unity

Matching C# structure with a C++ one.

Discussion in 'Scripting' started by DaveHoskins, Jan 31, 2017.

  1. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    Hi, I'd hoped I could find an answer but it appears I'm stuck.
    I have a struct with the following:
    [StructLayout(LayoutKind.Sequential, Pack = 0)]
    public class BuffVars
    {
    public float[] buffer = new float[2];
    public int bufferWrap;
    public int bufPos;
    public int numRays;​
    };​

    And I'm guessing to get it to match up to a C++ plug-in, I would use
    struct BuffVars
    {
    float buffer[2];
    int bufferWrap;
    int bufPos;
    int numRays;
    };​

    And call the plugin function with a reference to the declared structure variable:
    DLLCallDelay(ref data);

    But it appears to be all wrong!
    I guess I don't need the ref.
    What approach should I be taking to match these up? Correct the packing?
    Thanks.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,692
    I don't think you can get there in the traditional oldschool memory-copying way, unfortunately. Welcome to managed code.

    I believe you would need to serialize the structure into some kind of intermediate "wire" format, such as JSON or some other "stream o bytes", then at the other end unpack it into your C++ struct.

    Keep in mind as a multi-platform context, you will need to deal with big-endian/little-endian issues as well as potentially deal with padding and/or other size considerations.

    You might be best off making custom marshal methods at both ends for the various shapes of data you will be blasting through the managed/unmanaged interface.

    That said, in my KurtMaster2D game I am marshaling data to and from my native layer as a formatted string, and the performance is plenty fast enough for realtime gaming. It runs on iOS, Android, Mac and Windows, all with the same native C library, all transacted against via strings that are packed and unpacked on both sides.

    I do have one specialized C# call to GC-pin a byte array for the frame buffer, then the native code renders the truecolor image to that byte array, and then I marshal it back into a Unity Texture2D for display. It's plenty fast to handle 512x512 on an iPad Mini generation 1, which is like 4 or 5 years old.
     
    DaveHoskins likes this.
  3. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    Thank-you for your answer. I love the idea of native plug-in code, as c++ is so much faster. I think I got strings of 'floats' working OK, is it OK to keep doing that? The endian thing is not a concern to me.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,692
    Keep in mind there is a managed-unmanaged boundary time cost. It varies by platform but it is nonzero. You gotta be doing a fair amount of work on the native side to make it worth the time, and you gotta be ready to handle the increased complexity of debugging on both sides of the fence.

    I only did it because I have dozens of games written in C that I wanted to keep alive, and this was a nice solution. I would never use native code unless I had some big old array process I wanted to run each frame, and even then I'd probably find a way to fake it in C# code. :)
     
    DaveHoskins and Kiwasi like this.
  5. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Is it really? But the time you've marshalled it from C# to C++, then back again to C# to effect the game. That's a lot of overhead. Plus whatever mangling Unity does to your C++ to compile it to multiple platforms.

    I'd be skeptical of this claim. C# might be slower in the general case then C++. But I'd want to do heavy testing before relying on it in the Unity case.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,692
    What @BoredMormon said, but also the debugging: when I was debugging the KurtMaster2D games, first of all they were 99% debugged back on PalmOS/MS-DOS days, and all subsequent debugging I did with a little native Win32 wrapper module written under the Allegro library so that I could set actual native C breakpoints easily and quickly.

    I'm not sure how much luck you will have debugging symbolically-meaningful native C code from within the Unity editor. It's fine if you plan on not writing bugs, but I seem to be unable to resist the siren call of bug-writing.
     
    Kiwasi likes this.
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    So lets start with marshaling structures between managed and unmanaged code. Here we get an example of how to do it:
    https://msdn.microsoft.com/en-us/library/ef4c3t39.aspx

    Note the struct in both C++ and C# are laid out and packed the same way.

    The thing is in your example code above, you introduced a float[2]. Unfortunately when structuring data to pass, all the types shared must be structured the same as well. float, double, int, these all work fine because they have C++ counter-parts structured identically.

    But float[2]... though named similar things are NOT structured the same.

    If you reformatted your struct to instead of having a float[2] and instead 2 distinct float fields, you'd be able to get away with it.

    ...

    Now as for the efficiency gained from using C++.

    I want to agree with both Kurt-Dekker and BoredMormon.

    Unless you're doing something expensive in and of itself, the speed differences are negligible.

    If I had a car that went 55mph max, and another that went 70mph max. If I wanted to drive 90 miles, it'd make sense to use the 70mph car, even if it meant having to take a bunch of time to get out of my slower car, prep the faster car, and get going.

    But if I'm taking it to the end of the block... what's the point? I could potentially take more time switching cars than it would be to just take the slower car.

    Keep in mind... C# isn't like 1/10th the speed of C++. It can be extremely speedy in a lot of general use cases. C++ shines when doing memory intensive stuff where direct control of said memory is useful. Like Kurt-Dekker described with the pinning of memory and using C++ to directly manipulate what is effectively 1 million sequential bytes.
     
    Kiwasi, Ryiah and DaveHoskins like this.
  8. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    I'm doing audio processing on an object's audio filter function, so it's a lot of data. It looks like I can pass the audio data straight through as a pointer to floats. But I need to sort a bunch of parameters out as well, into another block of floats I'm guessing.
    C++ is massively faster for algorithms, than C#. Calling library code might not be that much of a difference, but DSP? - Oh yes.
     
  9. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    Thanks lordofduct it looks like I was replying when you replied. I'll look into that.
     
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    Yes, it could be... but your structure implies handing over a buffer of length 2.... are you piecewise handing over the data to the native code? The handing over of data alone for such a small amount is going to be costly.

    If you instead passed a pointer to the buffer, and the buffer was large.... that'd be a different story. Like what Kurt-Dekker described with pinning the texture in memory, and allowing the native code to directly manipulate it.
     
  11. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    I actually have an array of those structures, which I'm guessing there's no hope in passing that without serialising it.
     
  12. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,692
    Also, on any MonoBehaviour that is on a GameObject with an AudioSource, you can implement this method to do processing on the audiostream right in C#, one buffer chunk at a time:

    Code (csharp):
    1.  void OnAudioFilterRead(float[] __data, int __channels);
    Pretty slick... depends on how many taps your filter has though...
     
  13. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    I already told you a way that you could (unless you can't control the shape of it on the native side).

    Another could be instead have 4 distinct arrays, 1 of floats, 3 of ints, for each piece of data, and pass IntPtr's to each of those arrays to the call.

    No serialization needed.
     
    Kurt-Dekker likes this.
  14. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    That's what I WAS using (OnAudioFilterRead) until I got horrified about the amount of CPU it was using up. I've been doing DSP stuff in C++ for years, and I want to use it still, but my current project turned out a little bit parameter heavy - :rolleyes:
     
  15. Strangiato

    Strangiato

    Joined:
    Oct 24, 2014
    Posts:
    18
    It is possible to embed arrays in a managed struct that you want to pass into a C++ plugin.

    This structure
    Code (csharp):
    1.  
    2. [StructLayout(LayoutKind.Sequential)]
    3. public struct ChatChannelMessageEntry
    4. {
    5.     public uint chatMessageIndex;
    6.  
    7.     public uint chatUserID;
    8.  
    9.     [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    10.     public string chatUserDisplayName;
    11.  
    12.     [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    13.     public string chatTimestamp;
    14.  
    15.     [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 140)]
    16.     public string chatMessage;
    17. }
    18.  
    19. [StructLayout(LayoutKind.Sequential)]
    20. public struct ChatChannelMessageList
    21. {
    22.     [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    23.     public ChatChannelMessageEntry[] entries;
    24. };
    25.  

    matches this in the plugin
    Code (c++):
    1.  
    2. struct ChatChannelMessageEntry
    3. {
    4.     unsigned int chatMessageIndex;
    5.     unsigned int chatUserID;
    6.     char chatUserDisplayName[32];
    7.     char chatTimestamp[32];
    8.     char chatMessage[140];
    9. };
    10.  
    11. struct ChatChannelMessageList
    12. {
    13.     ChatChannelMessageEntry entries[32];
    14. };
    15.  

    Declaring a plugin function in a script looks like this
    Code (csharp):
    1.  
    2. [DllImport(DLL_NAME)]
    3. public static extern uint NetworkGetChatChannelMessageList( uint channelID,
    4.                                 uint firstMessageIndex, uint maxMessageCount,
    5.                                 IntPtr pData, int maxDataSize);
    6.  
    The IntPtr pData parameter references the buffer where your struct lives.


    And in the plugin
    Code (c++):
    1.  
    2. DLL_EXPORT
    3. unsigned int NetworkGetChatChannelMessageList( unsigned int channelID,
    4.                      unsigned int firstMessageIndex, unsigned int maxMessageCount,
    5.                      unsigned char* pBuffer, int bufLen )
    6. {
    7.     ChatChannelMessageList *pOutputMessageList = (ChatChannelMessageList*)pBuffer;
    8.     .
    9.     .
    10.     .
    11. }
    12.  

    Calling the function from a script looks like this
    Code (csharp):
    1.  
    2.  
    3.   byte[] dataBuffer = new byte[k_maxDataSize];
    4.   GCHandle hDataBuffer = GCHandle.Alloc( dataBuffer, GCHandleType.Pinned );
    5.   IntPtr bufPtr = hDataBuffer.AddrOfPinnedObject();
    6.  
    7.   uint chat_count = NetworkGetChatChannelMessageList(m_ChatChannelID, m_LastChatMessageIndex+1, 32, bufPtr, k_maxDataSize);
    8.  
    9.   ChatChannelMessageList chat_info = (ChatChannelMessageList)Marshal.PtrToStructure( bufPtr, typeof(ChatChannelMessageList) );
    10.  
    11.   for( uint c = 0; c < chat_count; ++c )
    12.   {
    13.   }
    14.  
    15.   hDataBuffer.Free();
    16.  
    17.  
    I'm not sure if pinning the memory is necessary, but that's what works for us. We use this method to pass data back and forth between C# and C++ without any noticeable performance hit. In the case I've shown here the memory for the buffer is allocated and then released in the same function, but there are plenty of cases where we hold a reference to a buffer for the lifetime of the application to avoid memory allocation on a per-frame basis.
     
    Edy, DaveHoskins and kru like this.
  16. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190

    Wow, that's brilliant, thanks a lot for the help, that's really good.
    Can I just ask what is 'k_maxDataSize' refering to?
    And I presume if I'm pointing to a buffer of floats I'll have to make it a size of 8 instead of 4 when in 64 bit mode pointer sizes?
     
    Last edited: Feb 8, 2017
  17. Strangiato

    Strangiato

    Joined:
    Oct 24, 2014
    Posts:
    18
    k_maxDataSize is an int value that is set to create a buffer large enough to hold whatever you are passing back an forth. We usually use 512 or 1024.

    float is still a 4 byte value in 64 bit mode. doubles would require 8 bytes. You can use Marshal.SizeOf(typeof(float)) to figure out the size on the C# side.

    Also, you may need to throw " using System.Runtime.InteropServices;" at the top of your script to make the above code compile.
     
    DaveHoskins likes this.
  18. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    Thank-you for the most detailed answer I've ever had on Unity forums!
     
  19. Strangiato

    Strangiato

    Joined:
    Oct 24, 2014
    Posts:
    18
    We're using Visual Studio on Windows and XCode on Mac to compile our C++ plugin. Both are able to attach the Unity Editor process and provide full debugging support on Native code. It was one of my biggest concerns with switching to Unity, but it has turned out to work very well.
     
  20. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    I'm also using VS. Do you use the normal attach to process menus? I mean can you actually debug your C++ code?
     
  21. Strangiato

    Strangiato

    Joined:
    Oct 24, 2014
    Posts:
    18
    Yes, select "Attach to Process..." from the debug menu, and then select Unity.exe (which will be towards the bottom of the list). You have to have the project that generates your plugin DLL loaded.

    Then you can use all the VS debugging tools on your Unity plugin code.

    I like to use a lot of asserts in my code, and it's possible to attach the debugger after the assert popup appears over the Unity editor, and you'll still be able to get a debug session to see what caused the assert.
     
  22. DaveHoskins

    DaveHoskins

    Joined:
    Sep 9, 2013
    Posts:
    190
    That's great news. Cheers.