C# Union Structure Marshalling

2019-02-25 00:53发布

问题:

I'm trying to integrate Video4Linux in my managed application. Indeed I've declared all required structures and relative ioctl. In this question I present two ioctl: SetFormat and GetFormat; while the former is working well (like the other dozen I actually use), the latter is returning me bad memory behavior.

The GetFormat ioctl is actually performing, but as soon the application is accessing to the ioctl parameter (either debugger or my application itself), it always crashes with following stack:

System.NullReferenceException: ...
at System.String.mempy4(...) in <filename unknown>:0
at System.String.memcpy(...) in <filename unknown>:0
at Derm.Platform.Video4Linux.ControlGetFormat(...) in <...>
...

I had some investigation on p/invoking ioctl, and again, I cannot understand why my application is crashing. I suspect it's becuase structure memory layout, but I cannot explain why SetFormat is working while GetFormat don't (since they use the very same parameter).

I have to P/Invoke a ioctl routine that receive/return the structure v4l2_format:

struct v4l2_format {
        enum v4l2_buf_type type;
        union {
                struct v4l2_pix_format          pix;     /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
                struct v4l2_pix_format_mplane   pix_mp;  /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
                struct v4l2_window              win;     /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
                struct v4l2_vbi_format          vbi;     /* V4L2_BUF_TYPE_VBI_CAPTURE */
                struct v4l2_sliced_vbi_format   sliced;  /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
                __u8    raw_data[200];                   /* user-defined */
        } fmt;
};

struct v4l2_pix_format {
    __u32                   width;
    __u32                   height;
    __u32                   pixelformat;
    enum v4l2_field         field;
    __u32                   bytesperline;   /* for padding, zero if unused */
    __u32                   sizeimage;
    enum v4l2_colorspace    colorspace;
    __u32                   priv;           /* private data, depends on pixelformat */
};

I've traduced the structure v4l2_format using the following form

[StructLayout(LayoutKind.Explicit, Pack = 4, CharSet = CharSet.Ansi)]
public struct Format
{
    public Format(BufferType bufferType)
    {
        Type = bufferType;
        Pixmap = new PixmapFormat();
        RawData = new byte[RawDataLength];
    }

    public Format(BufferType bufferType, PixmapFormat pixmapFormat)
    {
        Type = bufferType;
        Pixmap = pixmapFormat;
        RawData = new byte[RawDataLength];
    }

    [FieldOffset(0)]
    public BufferType Type;

    [FieldOffset(4)]
    public PixmapFormat Pixmap;

    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=RawDataLength)]
    public byte[] RawData;

    public const int RawDataLength = 200;
}

[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct PixmapFormat
{
    public PixmapFormat(uint width, uint height, PixelFormatCode pixelFormat)
    {
        Width = width;
        Height = height;
        PixelFormat = pixelFormat;
        Field = Video4Linux.BufferField.Any;
        BytesPerLine = 0;
        SizeImage = 0;
        Colorspace = Video4Linux.PixelColorspace.None;
        Private = 0;
    }

    public UInt32 Width;

    public UInt32 Height;

    public PixelFormatCode PixelFormat;

    public BufferField Field;

    public UInt32 BytesPerLine;

    public UInt32 SizeImage;

    public PixelColorspace Colorspace;

    public UInt32 Private;
}

And finally, here is the methods calling ioctl:

public static void ControlGetFormat(IntPtr fd, BufferType pixmapType, out PixmapFormat pixmapFormat)
{
    if (fd == IntPtr.Zero)
        throw new ArgumentException("invalid file descriptor", "fd");

    Format format = new Format(pixmapType);

    int result = IoCtrlGetFormat(fd, GetFormat.ControlCode, ref format);

    if (result < 0)
        ThrowExceptionOnError();

    pixmapFormat = format.Pixmap;
}

private static readonly IoCtrlRequest GetFormat = new IoCtrlRequest(IoCtrlDirection.Read | IoCtrlDirection.Write, 'V', 4, typeof(Format));

[DllImport("libc", EntryPoint = "ioctl", SetLastError = true)]
private static extern int IoCtrlGetFormat(IntPtr fd, Int32 code, ref Format argument);

public static void ControlSetFormat(IntPtr fd, BufferType pixmapType, ref PixmapFormat pixmapFormat)
{
    if (fd == IntPtr.Zero)
        throw new ArgumentException("invalid file descriptor", "fd");

    PixmapFormat pixmapFormatCopy = pixmapFormat;
    Format format = new Format(pixmapType, pixmapFormatCopy);

    int result = IoCtrlSetFormat(fd, SetFormat.ControlCode, ref format);

    if (result < 0)
        ThrowExceptionOnError();

    pixmapFormat = format.Pixmap;
}

private static readonly IoCtrlRequest SetFormat = new IoCtrlRequest(IoCtrlDirection.Read | IoCtrlDirection.Write, 'V', 5, typeof(Format));

[DllImport("libc", EntryPoint = "ioctl", SetLastError = true)]
private static extern int IoCtrlSetFormat(IntPtr fd, Int32 code, ref Format argument);

回答1:

The problem has been solved by forcing the structure size to the actual one (204, in my case), and remove the array field RawData (as suggested by David Heffernan).

Indeed:

[StructLayout(LayoutKind.Explicit, Pack = 4, CharSet = CharSet.Ansi, Size = 204)]
public struct Format
{
    public Format(BufferType bufferType)
    {
        Type = bufferType;
        Pixmap = new PixmapFormat();
    }

    public Format(BufferType bufferType, PixmapFormat pixmapFormat)
    {
        Type = bufferType;
        Pixmap = pixmapFormat;
    }

    [FieldOffset(0)]
    public BufferType Type;

    [FieldOffset(4)]
    public PixmapFormat Pixmap;
}

Of course, Marshal.SizeOf(typeof(Format)) == 204.