Предложения по взаимодействию с size_t через PInvoke - PullRequest
0 голосов
/ 15 декабря 2018

У нас есть SDK с собственным кодом, который преимущественно использует тип C / C ++ size_t для таких вещей, как размеры массивов.Мы дополнительно предоставляем оболочку .NET (написанную на C #), которая использует PInvoke для вызова нативного кода для тех, кто хочет интегрировать наш SDK в свое приложение .NET.

.NET имеет тип System.UIntPtr, которыйотлично сочетается с size_t функционально, а функционально все работает как положено.Некоторые из структур C #, предоставленных нативной стороне, содержат типы System.UIntPtr, и они доступны пользователям .NET API, которые требуют от них работы с типами System.UIntPtr.Проблема в том, что System.UIntPtr плохо взаимодействует с типичными целочисленными типами в .NET.Требуются приведения, и различные «базовые» вещи, такие как сравнение с целыми числами / литералами, не работают без дополнительного приведения.

Мы попытались объявить экспортированные параметры size_t как uint и применить MarshalAsAttribute(UnmanagedType.SysUInt), но этоприводит к ошибке времени выполнения для неверного маршалинга.Например:

[DllImport("Native.dll", EntryPoint = "GetVersion")]
private static extern System.Int32 GetVersion(
    [Out, MarshalAs(UnmanagedType.LPStr, SizeParamIndex = 1)]
    StringBuilder strVersion,
    [In, MarshalAs(UnmanagedType.SysUInt)]
    uint uiVersionSize
);

Вызов GetVersion в C # с передачей uint для 2-го параметра приводит к этой маршальной ошибке во время выполнения:

System.Runtime.InteropServices.MarshalDirectiveException: Cannot marshal 'parameter #2': Invalid managed/unmanaged type combination (Int32/UInt32 must be paired with I4, U4, or Error).

Мы могли бы создать упаковщики фасадов, которые предоставляют intтипы в .NET и внутренне выполняют приведение к System.UIntPtr для нативно-совместимых классов, но (а) мы беспокоимся о производительности копирования буферов (которые могут быть очень большими) между почти дублирующимися классами и (б) это кучаработы.

Любые предложения о том, как PInvoke с size_t типами при сохранении удобного API в .NET?


Вот пример одного случая, который фактически совпадает снаш реальный код, но с упрощенными / зачищенными именами. ПРИМЕЧАНИЕ Этот код получен из нашего производственного кода вручную.Он компилируется для меня, но я не запускаю его.

Собственный (C / C ++) код:

#ifdef __cplusplus
extern "C"
{
#endif


enum Flags
{
    DEFAULT_FLAGS = 0x00,

    LEVEL_1 = 0x01,
};


struct Options
{
    Flags flags;

    size_t a;

    size_t b;

    size_t c;
};


int __declspec(dllexport) __stdcall InitOptions(
    Options * const pOptions)
{
    if(pOptions == nullptr)
    {
        return(-1);
    }

    pOptions->flags = DEFAULT_FLAGS;
    pOptions->a = 1234;
    pOptions->b = static_cast<size_t>(0xFFFFFFFF);
    pOptions->c = (1024 * 1024 * 1234);

    return(0);
}


#ifdef __cplusplus
}
#endif

Управляемый (C #) код: (Этот должен чтобы воспроизвести неправильный маршаллинг. Изменение полей a, b и c в структуре на тип UIntPtr заставляет его функционировать должным образом.

using System;
using System.Runtime.InteropServices;

namespace Test
{
    public enum Flags
    {
        DEFAULT_FLAGS = 0x00,

        LEVEL_1 = 0x01,
    }


    [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
    public struct Options
    {
        public Flags flags;

        public uint a;

        public uint b;

        public uint c;
    }


    public class Test
    {
        [DllImport("my.dll", EntryPoint = "InitOptions", CallingConvention = CallingConvention.StdCall)]
        internal static extern Int32 InitOptions(
            [In, Out]
            ref Options options
        );

        static void Main(string[] args)
        {
            Options options = new Options
            {
                flags = DEFAULT_FLAGS,
                a = 111,
                b = 222,
                c = (1024 * 1024 * 1)
            };

            Int32 nResultCode = InitOptions(
                ref options
            );

            if(nResultCode != 0)
            {
                System.Console.Error.WriteLine("Failed to initialize options.");
            }

            if(   options.flags != DEFAULT_FLAGS
                || options.a != 1234
                || options.b != static_cast<size_t>(-1)
                || options.c != (1024 * 1024 * 1234) )
            {
                System.Console.Error.WriteLine("Options initialization failed.");
            }
        }
    }

}

Я попытался изменить поле enum в управляемой структуре на тип intи он все еще не работает.

Далее я протестирую больше с параметрами функции size_t.

Ответы [ 2 ]

0 голосов
/ 31 декабря 2018

Вот что я в итоге сделал:

Сначала несколько целей:

  1. Предоставление .NET-дружественных и привычных типов пользователям .NET-библиотеки.
  2. Избегайтемолча потеря данных при взаимодействии с собственным кодом.
  3. Избегайте распространения 32-битных / 64-битных различий среди пользователей библиотеки .NET (другими словами, избегайте различий в типах вне моего .NET API из-засобственная битность DLL; стремиться к единственному типу данных, который (в основном) скрывает проблему битности).
  4. Приятно минимизировать наличие отдельных структур и / или путей кода для 32-битных-64-битных.
  5. Естественно, все, что предпочитают разработчики (меньше кода для написания и поддержки, легче поддерживать синхронизацию и т. Д.).

FUNCTIONS

Функции C, экспортированные изDLL представлены в DllImport с типами .NET, максимально приближенными к нативным (C) типам.Затем каждая функция оборачивается фасадом more-inline-with- .NET.

Это выполнено 2 вещи:

  1. Сохранение собственных типов в DllImport позволяет избежать silent (!) Потеря данных.Как отметил Саймон Мурье, .NET может использовать uint вместо size_t в функциях.Хотя это, кажется, работает, оно также будет молча отбрасывать данные, которые находятся вне диапазона.Поэтому, если нативный код возвращает значение, большее, чем uint.MaxValue, наш .NET-код никогда не узнает.Я предпочел бы справиться с ситуацией, чем иметь какую-то ложную ошибку.
  2. Различные методы и типы, специфичные для C и / или не объектно-ориентированные, представлены в стиле, более родном для .NET.Например, буферы в C API, представленные в виде байтового указателя плюс параметр размера, представлены в виде просто байтовых массивов в .NET.Другой пример - строки с нулевым символом в конце (например, UTF, XML), представленные в .NET как объект String или Xml вместо параметров байтового массива и размера.

Специально для функции size_tпараметры, они представлены как UIntPtr в DllImport (согласно # 1 выше), и, если это все еще необходимо для показа пользователю библиотеки, они представляются как uint или ulong, в зависимости от ситуации.Затем фасад проверяет значение каждого (в зависимости от ситуации) и выдает исключение в случае несовместимости.

Вот пример использования псевдокода:

C Функция:

// Consume & return data in buf and pBufSize
int __declspec(dllexport) __stdcall Foo(
    byte * buf,
    size_t * pBufSize
);

C # DllImport:

[DllImport("my.dll", EntryPoint = "Foo", CallingConvention = CallingConvention.StdCall)]
private static extern System.Int32 Foo(
    [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]
    System.Byte[] buf,
    [In, Out]
    ref System.UIntPtr pBufSize
);

C # Фасад (псевдокод):

void Foo(System.Byte[] buf)
{
    // Verify buffer size will fit
    if buf.LongLength > UIntPtrMaxValue
        throw ...

    UIntPtr bufSize = buf.LongLength;

    Int32 nResult = Foo(
        buf,
        bufSize
    );

    if nResult == FAILURE
        throw ...

    // Verify return size is valid
    if (UInt64)bufSize > int.MaxValue   // .NET array size type is 'int'
        throw ...

    buf.resize((int)bufSize);
}

СТРУКТУРЫ

Для взаимодействиясо структурами, содержащими size_t (и даже в целом), я следовал аналогичному подходу, как и с функциями: создать структуру .NET («Структура взаимодействия»), которая наиболее близко напоминает структуру нативного кода, а затем поместить .NET-дружественный фасад вокруг него.Затем фасад соответствующим образом выполняет проверку значений.

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

C Структура:

struct Bar
{
    MyEnum e;
    size_t s;
}

C # (псевдокод):

public class Bar
{
    // Optional c'tor if param(s) are required to be initialized for typical use

    // Accessor for e
    public MyEnum e
    {
        get
        {
            return m_BarInterop.e;
        }
        set
        {
            m_BarInterop.e = value;
        }
    }

    // Accessor for s
    public uint s
    {
        get
        {
            VerifyUIntPtrFitsInUint(m_BarInterop.s);   // will throw an exception if value out of range
            return (uint)m_BarInterop.s;
        }
        set
        {
            // uint will always fit in UIntPtr
            m_BarInterop.s = (UIntPtr)value;
        }
    }

    // Interop-compatible 'Bar' structure (not required to be inner struct)
    [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
    internal struct Bar_Interop
    {
        public MyEnum e;
        public System.UIntPtr s;
    }

    // Instance of interop-compatible 'Bar' structure
    internal Bar_Interop m_BarInterop;
}

Хотя иногда я немного утомлялчто после того, как этот подход был применен только к двум структурам, он дал большую гибкость и предоставил чистый интерфейс API пользователям моей оболочки .NET.

0 голосов
/ 29 декабря 2018

Двоичный эквивалент size_t равен IntPtr (или UIntPtr).Но для параметров вы можете просто использовать int или uint без каких-либо дополнительных атрибутов.

Итак, если у вас есть это в C / C ++:

int InitOptions(size_t param1, size_t param2);

, вы можете объявитьэто так в C #, и это будет работать для x86 и x64 (ну, конечно, вы не получите никакого битового значения выше 32, hi-uint потерян):

[DllImport("my.dll")]
static extern int InitOptions(int param1, int param2); // or uint

Для x86 это работаетпотому что, ну, это просто должно.

Для x64 это работает волшебно, потому что аргументы всегда 64-битные , и, к счастью, дополнительные hi-биты обнуляются ошибкой errrhh ...некоторые компоненты системы (компилятор CLR? C / C ++? Я не уверен).

Для структурных полей это совершенно другая история, самая простая (на мой взгляд), кажется, использует IntPtr и добавляет несколько помощников вупростить программирование.

Однако я добавил дополнительный пример кода, если вы действительно хотите добавить немного сахара для разработчиков, использующих ваши структуры.Важно то, что этот код может (должен) генерироваться из определений C / C ++.

public static int InitOptions(ref Options options)
{
    if (IntPtr.Size == 4)
        return InitOptions32(ref options);

    Options64 o64 = options;
    var i = InitOptions64(ref o64);
    options = o64;
    return i;
}

[DllImport("my64.dll", EntryPoint = "InitOptions")]
private static extern int InitOptions64(ref Options64 options);

[DllImport("my32.dll", EntryPoint = "InitOptions")]
private static extern int InitOptions32(ref Options options);

[StructLayout(LayoutKind.Sequential)]
public struct Options // could be class instead (remove ref)
{
    public Flags flags;
    public uint a;
    public uint b;
    public uint c;

    public static implicit operator Options64(Options value) => new Options64 { flags = value.flags, a = value.a, b = value.b, c = value.c };
}

[StructLayout(LayoutKind.Sequential)]
public struct Options64 // could be class instead (remove ref)
{
    public Flags flags;
    public ulong a;
    public ulong b;
    public ulong c;

    public static implicit operator Options(Options64 value) => new Options { flags = value.flags, a = (uint)value.a, b = (uint)value.b, c = (uint)value.c };
}

Обратите внимание, что если вы используете классы вместо struct для Options и Options64, вы можете удалить все аргумент refуказания и избегать болезненного копирования из структур (перегрузка оператора не работает хорошо с ref).Но это имеет и другие последствия, так что решать вам.

Вот еще одно обсуждение на ту же тему: C # условная компиляция на основе 32-битной / 64-битной цели выполнения

По сути, вы также можете использовать константы условной компиляции для целей x86 и x64, и ваш код будет меняться при этом.

...