Version: Unity 6.0 (6000.0)
Language : English
Match primitive types between C# and native code
NativeArrays

Structures, classes, and unions

To exchange data in a structure, class, or union, both the unmanaged and the managed code must declare compatible data structures. The .NET Interop Services API provides attributes that you can use to specify how the fields of a C# type are arranged in memory and how each field in the type should be marshalled when transferred between managed and unmanaged code.

The StructLayoutAttribute lets you match the layout of a managed type to that of the corresponding struct defined in unmanaged code. By default, C# lays out managed structs sequentially based on the size of each field’s data type, sometimes with extra padding for alignment. You can use other values of StructLayoutAttribute to precisely control the offset and effective size of a struct’s fields. You can also specify how to marshal strings in the struct.

Use MarshalAsAttribute on individual fields to control how that field is marshalled.

Use LayoutKind.Explicit along with the FieldOffsetAttribute to define a struct that corresponds to a C/C++ union.

Note: When an unmanaged function returns a structure or class by value to managed code, that type must be blittable (Microsoft). You can still return non-blittable scalar values such as bool if you specify how to marshal them, as the following example does.

Pass a structure to an unmanaged function

The following C++ code defines a struct that represents an axis-aligned box, and an unmanaged function that takes the struct and an array pointer as parameters:

// Include math.h for fabsf.
#include <math.h>

typedef struct _Box {
    float Position[3];
    float Scale;
    float Size[3];
} CustomBox;

extern "C" {
    bool CheckInside(CustomBox box, float* point) {
        // Use fabsf so the float differences aren't truncated to integers.
        // The integer abs() function rounds each difference toward zero.
        if( fabsf( box.Position[0] - point[0] ) < box.Size[0] * box.Scale &&
            fabsf( box.Position[1] - point[1] ) < box.Size[1] * box.Scale &&
            fabsf( box.Position[2] - point[2] ) < box.Size[2] * box.Scale
          )
                return true;

        return false;
    }
}

Note: Refer to Call unmanaged functions from managed code for information about the annotations needed to compile and call functions such as this as part of a dynamically loaded library.

The CheckInside function calculates whether the point is inside the box and returns a C++ bool, which is 1 byte on all major compilers. To call this function from managed code, define a compatible C# struct and tell the scripting runtime how to treat the parameters and return type:

using UnityEngine;
using System.Runtime.InteropServices;

// Sequential layout is the default and should be used in most cases
[StructLayout(LayoutKind.Sequential)]
public struct CustomBox
{
    // Sequential layout is the default and is suitable in most cases.
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    float[] Position;
    float Scale;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    float[] Size;

    // Defining a constructor is optional.
    public CustomBox (Vector3 center, float scale, Vector3 size)
    {
        Position = new float[3];
        Size = new float[3];
        for (var i = 0; i < 3; i++)
        {
            Position[i] = center[i];
            Size[i] = size[i];
        }
        Scale = scale;
    }
}

public class StructMarshalExamples
{
    // Import the native function.
    [DllImport("__Internal")]
    // Marshal the bool return value as a 1-byte signed integer.
    [return: MarshalAs(UnmanagedType.I1)]
    static extern bool CheckInside(CustomBox box, [In] float[] point);

    // Run the example code.
    public static void RunExamples()
    {
        // The half-extent of box1 in each axis is Size * Scale = 1 * 2.5 = 2.5.
        var box1 = new CustomBox(Vector3.zero, 2.5f, Vector3.one);
        // The half-extent of box2 in each axis is Size * Scale = 1 * 4 = 4.
        var box2 = new CustomBox(Vector3.zero, 4, Vector3.one);
        // The x distance (2.7) is just outside box1 (2.5) but inside box2 (4).
        // If the native code truncated 2.7 to 2, box1 would incorrectly report "inside".
        float[] point = { 2.7f, 1.5f, -1.5f };

        var test1 = CheckInside(box1, point);
        var test2 = CheckInside(box2, point);

        Debug.Log($"Test point is {(test1 ? "inside" : "outside")} " +
                  $"the first box and {(test2 ? "inside" : "outside")} " +
                  $"the second");
    }
}

Note: If you use a precompiled, dynamically linked library, change the DllImport value to the library name. Use the special string __Internal for source code plug-ins and statically linked libraries. Refer to DllImport attribute for more information.

Additional resources

Match primitive types between C# and native code
NativeArrays