Передача сложных структур данных из C # в native dll

2

Я звоню в стороннюю библиотеку, написанную на C из приложения NetCore. Проблема в том, что для того, чтобы использовать эту библиотеку, мне сначала нужно сделать вызов и настроить сложную структуру, которая впоследствии должна быть передана всем последующим вызовам.

void createCtx(modbus_t ** ctx)
{
    *ctx = modbus_new_tcp("192.168.1.175", 502);

    //configure the context here ....

    int res = modbus_connect(*ctx);
}

int pollData(modbus_t * ctx)
{
    //....
    modbus_read_bits(ctx, addr, 1, tab_rp_bits);
    //....
}

Мой подход состоит в том, чтобы создать объект modbus_t в приложении вызывающей стороны (С#), настроить его, вызвав createCtx один раз, а затем передать его в pollData через регулярные промежутки времени. Я читал о StructLayout, но, поскольку мне не нужен доступ к данным в объекте modbusContext, я просто хотел бы зарезервировать кусок памяти для контекста и позволить С# не обращать внимания на то, что внутри.

Это то, что я придумал

static IntPtr modbusContext;

static class ModbusDriver
{
            [DllImport("modbusdriver",EntryPoint = "createCtx")]

            public static extern void CreateCtx(ref IntPtr modbusContext);

            [DllImport("modbusdriver",EntryPoint = "pollData")]

            public static extern uint PollData(IntPtr modbusContext)

        }


        static void Main(string[] args)
        {
            int ctxSize = ModbusDriver.GetCtxSize();

            modbusContext = Marshal.AllocHGlobal(80 * Marshal.SizeOf(typeof(byte))); //<--- 80 is the result of sizeof(modbus_t)
            ModbusDriver.CreateCtx(ref modbusContext);

            while(true)
            {
                ModbusDriver.PollData(modbusContext);
                Thread.Sleep(1000);
            }
        }
    }

Кажется, все это работает, но на самом деле это не так, особенно потому, что структура modbus_t довольно сложна

struct modbus_t {
    /* Slave address */
    int slave;
    /* Socket or file descriptor */
    int s;
    int debug;
    int error_recovery;
    struct timeval response_timeout;
    struct timeval byte_timeout;
    struct timeval indication_timeout;
    const modbus_backend_t *backend;
    void *backend_data;
};

typedef struct _modbus_backend {
    unsigned int backend_type;
    unsigned int header_length;
    unsigned int checksum_length;
    unsigned int max_adu_length;
    int (*set_slave) (modbus_t *ctx, int slave);
    int (*build_request_basis) (modbus_t *ctx, int function, int addr,
                                int nb, uint8_t *req);
    int (*build_response_basis) (sft_t *sft, uint8_t *rsp);
    int (*prepare_response_tid) (const uint8_t *req, int *req_length);
    int (*send_msg_pre) (uint8_t *req, int req_length);
    ssize_t (*send) (modbus_t *ctx, const uint8_t *req, int req_length);
    int (*receive) (modbus_t *ctx, uint8_t *req);
    ssize_t (*recv) (modbus_t *ctx, uint8_t *rsp, int rsp_length);
    int (*check_integrity) (modbus_t *ctx, uint8_t *msg,
                            const int msg_length);
    int (*pre_check_confirmation) (modbus_t *ctx, const uint8_t *req,
                                   const uint8_t *rsp, int rsp_length);
    int (*connect) (modbus_t *ctx);
    void (*close) (modbus_t *ctx);
    int (*flush) (modbus_t *ctx);
    int (*select) (modbus_t *ctx, fd_set *rset, struct timeval *tv, int msg_length);
    void (*free) (modbus_t *ctx);
} modbus_backend_t;

Итак, мой вопрос, правильный ли мой подход? В частности, modbus_t содержит указатели. Мне удалось сохранить структуру modbus_t в С#, и она, кажется, работает, но действительно ли безопасно предположить, что память, на которую ссылаются указатели, содержащиеся в структуре, не будет повреждена между вызовами? Это не правильно.

Теги:
dll
native
marshalling

1 ответ

1
Лучший ответ

Если вы не хотите изменять данные, вы можете безопасно обернуть их как void * или IntPtr. Вы выделяете данные через AllocHGlobal, который возвращает данные из кучи локального процесса через LocalAlloc, который в конечном итоге вызывает RtlAllocateHeap. Для С# этот указатель является черным ящиком и никогда не будет писать или изменять его. Пока вы не освободите данные, рано все будет хорошо.

Применяются правила программирования на C: вам нужно вручную управлять памятью и следить за тем, кто владеет данными и кто отвечает за их удаление.

Проблемы могут возникнуть, только если вы попытаетесь отобразить этот указатель на управляемые классы, которые частично пытаются предоставить доступ к некоторым полям. Затем вам нужно позаботиться о том, чтобы выравнивание членов структуры было таким же, как в заголовочном файле C, и вам нужно получить корректные значения для данных, которые вы хотите пропустить. Затем вы можете преобразовать IntPtr в структуру С# как указатель с небезопасным кодом, который должен просто работать, если вы получили правильные смещения и правки.

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

Подводя итог: ваш нынешний подход в порядке и будет работать. Это станет более трудоемким, если вы захотите получить доступ к изменению данных из байтового двоичного объекта, но это также выполнимо, если вы знаете, как объявлять структуры оболочки в С#, которые содержат только примитивные типы (без строк, словарей или указателей на структуры управляемой кучи).

  • 0
    Большое спасибо за исчерпывающий и информативный ответ.
  • 0
    Я просто хотел бы спросить вас, не могли бы вы немного рассказать о моих сомнениях относительно указателей. Структура modbus_t содержит два указателя, поэтому, насколько я понимаю, когда я сохраняю его на стороне C #, у меня хранится два адреса в памяти, указывающие на часть памяти, которую я не выделил с помощью AllocHGlobal, поэтому я не понимаю, почему это работает. Почему не возможно, чтобы эта часть памяти повторно использовалась чем-то другим между вызовами?
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню