Маршалинг выходных параметров строки C# LibraryImport

У меня есть встроенная функция, которая записывает строку в буфер, указанный при вызове, вместе с ее размером. Назовем это 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);
}
github.com/dotnet/docs/issues/35862
Hans Passant 19.08.2024 18:08

Вы используете .NET 8+?

Simon Mourier 19.08.2024 20:07

@SimonMourier Да, .NET 8. Также будет обновление до .NET 9.

patvax 20.08.2024 00:36

@HansPassant Кажется, это не работает, если ANSI/UTF8 нужно преобразовать в UTF16 для работы с C#. Плохо, что я написал неуправляемую подпись в вопросе, как если бы она была в стандарте C. Я не совсем привык к Win32 API. Получаемая строка на самом деле является строкой ANSI.

patvax 20.08.2024 10:16
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
52
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть много способов сделать это. «Старый» способ работает нормально и вполне безопасен. Что касается производительности, то это действительно зависит от вашего контекста, мы живем с этим уже более 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.

patvax 20.08.2024 10:24

С LibraryImport вам в любом случае придется поддерживать unsafe (но это сгенерированный код). Кроме того, .NET использует UTF16 внутри себя (как и большая часть любого фрагмента кода, созданного в Windows), поэтому ожидается, что он будет подвергаться преобразованиям Ansi/UTF8, небезопасным или нет, и это не так уж «обычно» для разработчика Windows. В моем личном случае по этой причине я всегда использую UTF16 в своем коде Windows C++ уже более 25 лет.

Simon Mourier 20.08.2024 12:11

Я искал еще немного лучшего способа, но без него. Кажется, нет прямой поддержки сценария P/Invoke «выходящая строка». В итоге я получил небольшую вариацию вашего ответа с использованием Spans вместо указателей и умного использования предоставленного буфера символов.

patvax 21.08.2024 13:03

Другие вопросы по теме