Я звоню в стороннюю библиотеку, написанную на 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 в С#, и она, кажется, работает, но действительно ли безопасно предположить, что память, на которую ссылаются указатели, содержащиеся в структуре, не будет повреждена между вызовами? Это не правильно.
Если вы не хотите изменять данные, вы можете безопасно обернуть их как void * или IntPtr. Вы выделяете данные через AllocHGlobal, который возвращает данные из кучи локального процесса через LocalAlloc, который в конечном итоге вызывает RtlAllocateHeap. Для С# этот указатель является черным ящиком и никогда не будет писать или изменять его. Пока вы не освободите данные, рано все будет хорошо.
Применяются правила программирования на C: вам нужно вручную управлять памятью и следить за тем, кто владеет данными и кто отвечает за их удаление.
Проблемы могут возникнуть, только если вы попытаетесь отобразить этот указатель на управляемые классы, которые частично пытаются предоставить доступ к некоторым полям. Затем вам нужно позаботиться о том, чтобы выравнивание членов структуры было таким же, как в заголовочном файле C, и вам нужно получить корректные значения для данных, которые вы хотите пропустить. Затем вы можете преобразовать IntPtr в структуру С# как указатель с небезопасным кодом, который должен просто работать, если вы получили правильные смещения и правки.
Все совершенно иначе, если классы C++ являются частью заголовочного файла, который содержит типы данных STL. Эти вещи вообще не подлежат переносу, поскольку выравнивание элементов зависит от поставленной версии STL с вашим текущим компилятором, что налагает жесткий контракт между частными полями членов, которые могут меняться между версиями C++/STL. Для этого вам понадобится оболочка C, которая обертывает вспомогательные методы как простые методы C с обычными структурами, которые внутренне вызывают методы C++. Управляемый C++ является довольно устаревшей технологией и больше не должен использоваться.
Подводя итог: ваш нынешний подход в порядке и будет работать. Это станет более трудоемким, если вы захотите получить доступ к изменению данных из байтового двоичного объекта, но это также выполнимо, если вы знаете, как объявлять структуры оболочки в С#, которые содержат только примитивные типы (без строк, словарей или указателей на структуры управляемой кучи).