Search code examples
c#c++stringpinvokemarshalling

Marshalling non-Blittable Structs from C# to C++


I'm in the process of rewriting an overengineered and unmaintainable chunk of my company's library code that interfaces between C# and C++. I've started looking into P/Invoke, but it seems like there's not much in the way of accessible help.

We're passing a struct that contains various parameters and settings down to unmanaged codes, so we're defining identical structs. We don't need to change any of those parameters on the C++ side, but we do need to access them after the P/Invoked function has returned.

My questions are:

  1. What is the best way to pass strings? Some are short (device id's which can be set by us), and some are file paths (which may contain Asian characters)
  2. Should I pass an IntPtr to the C# struct or should I just let the Marshaller take care of it by putting the struct type in the function signature?
  3. Should I be worried about any non-pointer datatypes like bools or enums (in other, related structs)? We have the treat warnings as errors flag set in C++ so we can't use the Microsoft extension for enums to force a datatype.
  4. Is P/Invoke actually the way to go? There was some Microsoft documentation about Implicit P/Invoke that said it was more type-safe and performant.

For reference, here is one of the pairs of structs I've written so far:

C++

/**
    Struct used for marshalling Scan parameters from managed to unmanaged code.
*/
struct ScanParameters
{
    LPSTR deviceID;
    LPSTR spdClock;
    LPSTR spdStartTrigger;
    double spinRpm;
    double startRadius;
    double endRadius;
    double trackSpacing;
    UINT64 numTracks;
    UINT32 nominalSampleCount;
    double gainLimit;
    double sampleRate;
    double scanHeight;
    LPWSTR qmoPath; //includes filename
    LPWSTR qzpPath; //includes filename
};

C#

/// <summary>
/// Struct used for marshalling scan parameters between managed and unmanaged code.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ScanParameters
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string deviceID;
    [MarshalAs(UnmanagedType.LPStr)]
    public string spdClock;
    [MarshalAs(UnmanagedType.LPStr)]
    public string spdStartTrigger;
    public Double spinRpm;
    public Double startRadius;
    public Double endRadius;
    public Double trackSpacing;
    public UInt64 numTracks;
    public UInt32 nominalSampleCount;
    public Double gainLimit;
    public Double sampleRate;
    public Double scanHeight;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string qmoPath;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string qzpPath;
}

Solution

  • A blittable type is a type that has a common representation between managed and unmanaged code and can therefore be passed between them with little or no problem, e.g. byte, int32, etc.

    A non-blittable type does not have the common representation, e.g. System.Array, System.String, System.Boolean, etc.

    By specifying the MarshalAs attribute for a non-blittable type you can tell the marshaller what it should be converted to. See this article on Blittable and Non-Blittable Types for more information

    1 - What is the best way to pass strings? Some are short (device id's which can be set by us), and some are file paths (which may contain Asian characters)

    StringBuilder is generally recommended as the easiest to use but I often use plain byte arrays.

    2 - Should I pass an IntPtr to the C# struct or should I just let the Marshaller take care of it by putting the struct type in the function signature?

    If the method is expecting a pointer then pass an IntPtr although you can get probably away with a ref in many cases depending on what it's going to be used for. If it's something that needs to stick around in the same place for a long time then I would manually allocate the memory with Marshal and pass the resulting IntPtr.

    3 - Should I be worried about any non-pointer datatypes like bools or enums (in other, related structs)? We have the treat warnings as errors flag set in C++ so we can't use the Microsoft extension for enums to force a datatype.

    Once you've got everything set up with the correct marshalling attributes I don't see why you'd need to worry. If in doubt put in the attribute, if the struct only ever gets used by managed code then the attribute won't be used.

    4 - Is P/Invoke actually the way to go? There was some Microsoft documentation about Implicit P/Invoke that said it was more type-safe and performant.

    Can't comment on this, you're into Visual C++ territory there.