Pass complex datastructures from C# to native dll

Viewed 172

I'm calling a third party library written in C from a NetCore application. The problem is that in order to use this library, I first need to make a call and configure a complex struct that must later be passed to all subsequent calls.

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);
    //....
}

My approach would be to create the modbus_t object on the caller application (C#), configure it by calling createCtx once and then pass it to pollData at regular intervals. I've read about StructLayout but since I don't need to access the data in the modbusContext object I would just like to reserve a chunk of memory for the context and let C# be oblivious of what's inside.

This is what I've come up with

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);
            }
        }
    }

All this seems to work, but it doesn't really feel right, especially because the modbus_t structure is quite complex

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;

So my question is, is my approach correct? Specifically, modbus_t contains pointers. I managed to retain the modbus_t struct in C# and it seems to work, but is it really safe to assume that the memory referenced by the pointers contained in the struct will not be corrupted between calls? It doesn't feel right.

1 Answers

As long as you do not want to modify the data you can wrap the data as an void * or IntPtr safely. You allocate the data via AllocHGlobal which returns data from the local process heap via LocalAlloc which ultimately calls into RtlAllocateHeap. For C# that pointer is a black box and will never write or modify it. As long as you do not free data to early everything will be fine.

The C programing rules apply: You need to manage memory manually and pay attention who owns the data and who is responsible to delete it.

Problems can only arise if you try to map that pointer to managed classes which partially try to give access to some fields. Then you need to take care that the struct member alignment is the same as in the C header file and you need to get the offsets right for the data you want to skip. You can then cast the IntPtr to a C# struct as pointer with unsafe code which should just work if you got the offsets and alingment right.

Things are completely different if C++ classes are part of the header file which contain STL datatypes. These things are not wrappable at all since the member alignment depends on the shipped STL version with your current compiler which imposes a tight contract between private member fields which can change between C++/STL versions. For that you would need a C wrapper which wraps helper methods as plain C methods with the usual structures which internally calls then the C++ methods. Managed C++ is a rather dated technology and should not be used anymore.

To sum it up: Your current approach is fine and will work. It will become more work if you want to access modify the data from the byte blob but that is also doable once you know how to declare wrapper structures in C# which contain only primitive types (no strings, dictionaries or pointers to managed heap structures.

Related