1

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
}
2
  • 1
    You'll have to marshal everything by hand. Commented Jun 15, 2018 at 12:33
  • @xanatos I expected it to be an answer, but I still hope it could be done somehow via attributes. Commented Jun 15, 2018 at 12:34

1 Answer 1

1

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;
}
Sign up to request clarification or add additional context in comments.

4 Comments

I have written almost same code github.com/Pzixel/CsRustInterop/blob/master/cs/ConsoleApp/… . That's pretty sad that marshaller is so dumb that it couldn't handle LPArray. Your code remind me that I have to put finally clause there.
@AlexZhukovskiy The big difference is that I'm pinning the utf8 memory "in place" instead of copying it around. There is one less copy operation for each string.
Yep, I noticed that. I was unaware of GCHandle so your example is even better than I expected to have when asking a question :)
You could use a BSTR instead of this custom StringRef and declare the function in C# like this: private static extern int println([MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr)] IntPtr[] array, int count) it works but you have to allocate an IntPtr, write the BSTR manually, pass IntPtr-4 and deallocate the IntPtr, so I'm not sure it's any better...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.