I have a native function that will write a string to a buffer provided in the call along with its size. Let's call it GetFooString(LPStr str, int strLen)
.
I found out so far that with DllImport
the following would work:
[DllImport("foo.dll", EntryPoint = "GetFooString")]
public static extern void GetFooString(
[MarshalAs(UnmanagedType.LPStr, SizeParamIndex = 1)]
StringBuilder pBuff,
ushort wBuffLen);
There is also this learn.microsoft.com which states that StringBuilder
in P/Invoke should be avoided because of hidden allocations which can hurt performance. The workaround is to do the Marshalling manually by hand.
Since there is source generators based P/Invoke now I figured I can try and use that(compile time generated interop code should have better performance than what DllImport
was doing). If I substitute DllImport
with LibraryImport
the error is that StringBuilder
is not supported in source generated P/Invoke with the same MarshallAsAttribute
as in the DllImport
example. What is the correct way of calling such function with LibraryImport
? Is there a way that would write to a stack allocated Span<char>
which I can then convert to a string or do anything I could do with a Span?
Solution:
Based on Simon Mourier's answer I made a wrapper function around the P/Invoke that uses the span char buffer as a byte buffer in the P/Invoke and then converts the ANSI chars to UTF16. Unfortunately it seems Microsoft does not want to support the P/Invoke string out buffer scenario. Another solution could be to make a custom Marshaller for this case but I find them poorly documented and hard to work with which is why I went with the wrapper approach.
[LibraryImport("foo.dll", EntryPoint = "GetFooString")]
private static partial void GetFooStringImpl(
Span<byte> pBuff,
ushort wBuffLen);
public static void GetFooString(Span<char> pBuff)
{
Span<byte> span = MemoryMarshal.Cast<char, byte>(pBuff)[(pBuff.Length - 1)..];
GetFooStringImpl(span, (ushort)(pBuff.Length + 1));
Encoding.ASCII.GetChars(span[..^1], pBuff);
}
There are many ways to do it. The "old" way works fine and is quite safe. About performance, it really depends on your context, we've been living with this for more than 20 years and it's not that bad.
If you need to be AOT compatible (with disabled runtime marshalling) then you'll have to abandon StringBuilder
.
Now it looks you need Ansi/UTF8, and if you want or need to use LibraryImport
, and since it requires the .NET project to be marked unsafe
using AllowUnsafeBlocks, here is one unsafe solution:
internal partial class Program
{
static unsafe void Main()
{
var bytes = stackalloc byte[10];
GetFooString(bytes, 10);
var s = Utf8StringMarshaller.ConvertToManaged(bytes); // .NET 7+
}
[LibraryImport("myDll")]
public static unsafe partial void GetFooString(byte* str, int len);
}