将复杂的数据结构从C#传递到本机dll

时间:2019-01-15 21:34:58

标签: c# c dll native marshalling

我正在从NetCore应用程序调用用C编写的第三方库。 问题在于,为了使用该库,我首先需要进行调用并配置一个复杂的结构,该结构随后必须传递给所有后续调用。

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

我的方法是在调用方应用程序(C#)上创建modbus_t对象,通过一次调用createCtx对其进行配置,然后定期将其传递给pollData。 我已经读过有关StructLayout的文章,但是由于我不需要访问modbusContext对象中的数据,所以我只想为上下文保留一块内存,并让C#忽略其中的内容。

这就是我想出的

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包含指针。我设法在C#中保留了modbus_t结构,它似乎可以正常工作,但是假设结构中包含的指针引用的内存在两次调用之间不会被破坏,这真的很安全吗?感觉不对。

1 个答案:

答案 0 :(得分:1)

只要您不想修改数据,就可以安全地将数据包装为void *或IntPtr。您通过AllocHGlobal分配数据,该数据通过LocalAlloc从本地进程堆返回数据,而该本地处理堆最终调用RtlAllocateHeap。对于C#,该指针是一个黑匣子,并且永远不会对其进行写入或修改。只要您不尽早释放数据,一切都会好起来的。

C编程规则适用:您需要手动管理内存,并注意谁拥有数据以及谁负责删除数据。

仅当您尝试将指针映射到托管类时,才会出现问题,该托管类部分地尝试提供对某些字段的访问权限。然后,需要注意结构成员对齐方式与C头文件中的对齐方式相同,并且需要正确获取要跳过的数据的偏移量。然后,您可以将IntPtr转换为带有不安全代码的C#结构作为指针,如果您正确地获得了偏移量和倾斜度,则该代码才可以工作。

如果C ++类是包含STL数据类型的头文件的一部分,则情况完全不同。这些内容根本无法包装,因为成员对齐取决于当前编译器附带的STL版本,这在私有成员字段之间施加了紧密的约定,可以在C ++ / STL版本之间进行更改。为此,您将需要一个C包装器,该包装器将帮助程序方法包装为具有普通结构的普通C方法,该结构在内部调用C ++方法。托管C ++是一种过时的技术,不应再使用。

总结:您当前的方法很好,并且可以工作。如果您想访问修改字节blob中的数据,它将变得更加繁琐,但是一旦您知道如何在C#中声明仅包含基本类型(不包含字符串,字典或托管堆结构的指针)的包装器结构,这也是可行的。 / p>