У меня есть встроенная функция, которая записывает строку в буфер, указанный при вызове, вместе с ее размером. Назовем это GetFooString(LPStr str, int strLen).
На данный момент я узнал, что с DllImport будет работать следующее:
[DllImport("foo.dll", EntryPoint = "GetFooString")]
public static extern void GetFooString(
[MarshalAs(UnmanagedType.LPStr, SizeParamIndex = 1)]
StringBuilder pBuff,
ushort wBuffLen);
Существует также сайт Learn.microsoft.com, в котором говорится, что StringBuilder в P/Invoke следует избегать из-за скрытых выделений, которые могут снизить производительность. Обходной путь — выполнить маршаллинг вручную.
Поскольку теперь есть генераторы исходного кода на основе P/Invoke, я решил, что могу попробовать и использовать это (код взаимодействия, сгенерированный во время компиляции, должен иметь лучшую производительность, чем то, что делал DllImport). Если я заменю DllImport на LibraryImport, ошибка будет заключаться в том, что StringBuilder не поддерживается в сгенерированном исходном коде P/Invoke с тем же MarshallAsAttribute, что и в примере DllImport. Как правильно вызвать такую функцию с помощью LibraryImport? Есть ли способ записать в выделенный стек Span<char>, который я затем могу преобразовать в строку или сделать что-нибудь, что я могу сделать с Span?
Решение:
Основываясь на ответе Саймона Мурье, я создал функцию-оболочку вокруг P/Invoke, которая использует буфер символов диапазона в качестве буфера байтов в P/Invoke, а затем преобразует символы ANSI в UTF16. К сожалению, похоже, что Microsoft не хочет поддерживать сценарий вывода буфера строки P/Invoke. Другим решением может быть создание специального Marshaller для этого случая, но я считаю, что они плохо документированы и с ними сложно работать, поэтому я выбрал подход-оболочку.
[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);
}
Вы используете .NET 8+?
@SimonMourier Да, .NET 8. Также будет обновление до .NET 9.
@HansPassant Кажется, это не работает, если ANSI/UTF8 нужно преобразовать в UTF16 для работы с C#. Плохо, что я написал неуправляемую подпись в вопросе, как если бы она была в стандарте C. Я не совсем привык к Win32 API. Получаемая строка на самом деле является строкой ANSI.





Есть много способов сделать это. «Старый» способ работает нормально и вполне безопасен. Что касается производительности, то это действительно зависит от вашего контекста, мы живем с этим уже более 20 лет и это не так уж и плохо.
Если вам нужна совместимость с AOT (с отключенным маршалингом во время выполнения), вам придется отказаться от StringBuilder.
Теперь похоже, что вам нужен Ansi/UTF8, и если вы хотите или должны использовать LibraryImport, и поскольку для этого требуется, чтобы проект .NET был помечен unsafe с помощью AllowUnsafeBlocks, вот одно небезопасное решение:
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);
}
Мой «контекст производительности» — это приложение для тестирования оборудования. Я общаюсь с сетью LIN. В моем случае, чем меньше управляемых выделений будет помечено для очистки сразу после использования, тем лучше. Несвоевременная сборка мусора может привести к задержкам в тех частях программы, где это действительно важно. Теперь я могу приостановить сборку мусора в критических частях, но если я смогу полностью ограничить выделение, это тоже будет лучше. Ваше небезопасное решение было очевидным. Я надеялся, что появится способ избежать небезопасного кода в таких распространенных случаях, как преобразование ANSI/UTF8 в UTF16.
С LibraryImport вам в любом случае придется поддерживать unsafe (но это сгенерированный код). Кроме того, .NET использует UTF16 внутри себя (как и большая часть любого фрагмента кода, созданного в Windows), поэтому ожидается, что он будет подвергаться преобразованиям Ansi/UTF8, небезопасным или нет, и это не так уж «обычно» для разработчика Windows. В моем личном случае по этой причине я всегда использую UTF16 в своем коде Windows C++ уже более 25 лет.
Я искал еще немного лучшего способа, но без него. Кажется, нет прямой поддержки сценария P/Invoke «выходящая строка». В итоге я получил небольшую вариацию вашего ответа с использованием Spans вместо указателей и умного использования предоставленного буфера символов.