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

Pass complex datastructures from C# to native dll

我正在从 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 结构,它似乎可以工作,但是假设结构中包含的指针引用的内存在调用之间不会被破坏真的安全吗?感觉不太对。

只要您不想修改数据,您可以将数据安全地包装为 void * 或 IntPtr。您通过 AllocHGlobal 分配数据,其中 returns 数据来自本地进程堆,通过 LocalAlloc 最终调用 RtlAllocateHeap。对于 C#,该指针是一个黑框,永远不会写入或修改它。只要您不提前释放数据,一切都会好起来的。

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

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

如果 C++ 类 是包含 STL 数据类型的头文件的一部分,情况就完全不同了。这些东西根本不可包装,因为成员对齐取决于您当前编译器的 STL 版本,这在私有成员字段之间强加了一个紧密的合同,可以在 C++/STL 版本之间更改。为此,您需要一个 C 包装器,它将辅助方法包装为普通 C 方法,并使用通常的结构在内部调用 C++ 方法。托管 C++ 是一种相当过时的技术,不应再使用。

总结一下:您当前的方法很好并且会奏效。如果您想从字节 blob 访问修改数据,这将变得更加工作,但是一旦您知道如何在 C# 中声明仅包含原始类型(没有字符串、字典或指向托管堆结构的指针)的包装器结构,这也是可行的。