Alex Zhukovskiy
Alex Zhukovskiy

Reputation: 10025

How do I pass array of arrays in native code

I want to pass an array of UTF8 encoded strings into native code. I did it for one single string (as it's just a byte array), but now I want to pass an array of strings.

Because I don't know the size of array, I should pass reference to it. So I write a struct that should contain array pointer and its length:

[StructLayout(LayoutKind.Sequential)]
struct StringRef
{
    [MarshalAs(UnmanagedType.LPArray)]
    public byte[] Bytes;
    public uint Length;
}

But when I run it it fails:

Cannot marshal field 'Bytes' of type 'StringRef': Invalid managed/unmanaged type combination (Array fields must be paired with ByValArray or SafeArray).

Here is complete code sample.

Managed code:

public class RustInterop
{
    [StructLayout(LayoutKind.Sequential)]
    struct StringRef
    {
        [MarshalAs(UnmanagedType.LPArray)]
        public byte[] Bytes;
        public uint Length;
    }

    [DllImport("rust.dll")]
    private static extern int println(StringRef[] array, uint count);

    public static int Println(params string[] strings)
    {
        var array = strings.Select(str => Encoding.UTF8.GetBytes(str)).Select(bytes => new StringRef
        {
            Bytes = bytes,
            Length = (uint)bytes.Length
        }).ToArray();
        return println(array, (uint) array.Length);
    }
}

Unmanaged code:

#[repr(C)]
pub struct StringRef {
    bytes: *const u8,
    length: usize
}

#[no_mangle]
pub extern fn println(array: *const StringRef, count: usize) -> i32 {
    let array_slice = unsafe { std::slice::from_raw_parts(array, count) };
    for str in array_slice {
        let slice = unsafe { std::slice::from_raw_parts(str.bytes, str.length) };
        let str = std::str::from_utf8(slice);
        match str {
            Ok(str) => {
                println!("{}", str);
            },
            Err(_) => return -1
        };
    }
    1
}

Upvotes: 1

Views: 992

Answers (1)

xanatos
xanatos

Reputation: 111890

The best you can do is this:

[StructLayout(LayoutKind.Sequential)]
struct StringRef
{
    public IntPtr Bytes;
    public int Length;
}

[DllImport("CPlusPlusSide.dll")]
private static extern int println(StringRef[] array, int count);

public static int Println(params string[] strings)
{
    var utf8s = Array.ConvertAll(strings, x => Encoding.UTF8.GetBytes(x));
    var handles = new GCHandle[utf8s.Length];
    var refs = new StringRef[utf8s.Length];

    try
    {
        for (int i = 0; i < handles.Length; i++)
        {
            try
            {
            }
            finally
            {
                handles[i] = GCHandle.Alloc(utf8s[i], GCHandleType.Pinned);
            }

            refs[i] = new StringRef
            {
                Bytes = handles[i].AddrOfPinnedObject(),
                Length = (int)utf8s[i].Length
            };
        }

        return println(refs, refs.Length);
    }
    finally
    {
        for (int i = 0; i < handles.Length; i++)
        {
            if (handles[i].IsAllocated)
            {
                handles[i].Free();
            }
        }
    }
}

Note that I've changed the various uint to int... In the end they have the same sizeof(), so no problems here.

Tested with this C code (I don't have a rust compiler):

typedef struct StringRef
{
    char* bytes;
    unsigned int length;
} StringRef;

__declspec(dllexport) int __stdcall println(StringRef* array, unsigned int count)
{
    for (unsigned int i = 0; i < count; i++)
    {
        printf("%.*s\n", array[i].length, array[i].bytes);
    }

    return 0;
}

Upvotes: 1

Related Questions