从 C# 使用 C++/CLI 结构
Using C++/CLI structs from C#
让我开始另一个问题,因为虽然我看到了很多类似的问题,但没有人真正谈到这方面...我有一个 C++ DLL(没有源代码,只有 .lib 和 .h)并且我编写了必要的托管包装纸。这没有问题,问题是关于原始 C++ 代码中定义的结构和枚举,它们有很多,都需要暴露给 C# 代码。教程和示例通常使用简单的数据类型,如浮点数和字符串,而不是复杂数据结构的真实场景。
我的托管 C++/CLI 包装器使用 DLL 的 .h 头文件中的非托管结构。我包装的 class 成员函数一直在使用它们。因此,我需要在我的 C# 代码中使用相同的结构,传递它们并从 C++ 代码接收。很明显,我无法避免在 C# 中重新定义所有这些,但即便如此,使用它们也是有问题的。让我们举个例子:非托管代码中函数使用的一个简单结构:
typedef struct INFO {
...
} INFO;
int GetInfo(INFO& out_Info);
我在我的 C++/CLI 包装器代码中声明了相同的结构:
public ref struct INFO_WRAP {
...
};
int GetInfo(INFO_WRAP out_Info);
包装代码中的实现尝试将这个新结构转换为原始结构以供旧的非托管代码使用:
int Namespace::Wrapper::GetInfo(INFO_WRAP out_Info) {
pin_ptr<INFO> pin_out_Info = out_Info;
return self->GetInfo(*(::INFO*)pin_out_Info);
}
但这不会编译(无法在结构之间进行转换,也找不到合适的转换)。
有没有一种解决方案不涉及创建新的数据结构并一直来回手动复制所有结构成员?不仅是因为额外的工作和时间,而且结构真的很多。
public ref struct INFO_WRAP
您没有声明结构,这是 C# 术语中的 class。古怪的 C++ 实现细节,C++ 结构只是一个 class 及其所有成员 public。您需要在 C++/CLI 中使用 value struct
来声明 C# 结构的等效项。
int Namespace::Wrapper::GetInfo(INFO_WRAP out_Info)
这也是错误的,因为 INFO_WRAP 实际上是一个引用类型,您必须始终使用 ^ 帽子声明它。或者用 % 传参,肯定是这里的用意。
基本障碍排除,你要的东西没有直接支持。不允许托管编译器对托管结构的布局做出任何假设。无论如何都会在您尝试时吠叫。有一个很好的理由,它是 just not that predictable。布局是一个强大的运行时实现细节,如果代码在不同的运行时运行,它可以改变。就像 32 位与 64 位一样,可能在其中一个上工作,但在另一个上不行。正如乔恩发现的那样。
一个一个地复制字段总是有效的,而且性能足够。只是不是程序员喜欢维护的代码。您可以要求框架为您完成,调用 Marshal::PtrToStructure() 或 StructureToPtr().
作弊 是 可能的,并且肯定是您在编写 C++/CLI 代码时会考虑的事情。毕竟,语言的重点是使互操作性更快。您只是使保修失效,您 必须 在您打算支持的任何平台上彻底测试代码。一个简单的例子:
public value struct Managed {
int member1;
int member2;
};
struct Native {
int member1;
int member2;
};
void GetInfo(Managed% info) {
Native n = { 1, 2 };
pin_ptr<Managed> pinfo = &info;
memcpy(pinfo, &n, sizeof(n));
}
With 工作得很好并且在任何平台上都可预测地执行,结构很简单。当结构不简单或者你,比如说,修改 Native 而忘记修改 Managed 时,那就要付出代价了,堆栈和 GC 堆损坏是非常令人不快的事故,而且很难调试。
这是完整的解决方案,供其他追随我的人使用。 :-) 假设我们在 .h
header 文件中有一个包含枚举、结构、classes、函数的 DLL:
typedef int (*DelegateProc)(long inLong, char* inString, STRUCT2* inStruct, long* outLong, char* outString, STRUCT2* outString);
typedef struct STRUCT1 {
long aLong;
short aShort;
BOOL aBoolean;
char aString[64];
STRUCT2 aStruct;
DelegateProc aDelegateProc;
char Reserved[32];
} STRUCT1;
以通常的方式转换结构,并添加两个处理封送处理的静态转换函数。正如 Hans 指出的那样,尽管看起来很乏味,但分段复制是跨平台和架构的唯一真正可靠的解决方案。
#include <msclr\marshal.h>
using namespace msclr::interop;
public delegate int DelegateProc(long inLong, String^ inString, STRUCT2 inStruct, [Out] long% outLong, [Out] String^ outString, [Out] STRUCT2 outStruct);
[StructLayout(LayoutKind::Sequential, Pack = 1)]
public value struct WRAP_STRUCT1 {
long aLong;
short aShort;
bool aBoolean;
[MarshalAs(UnmanagedType::ByValArray, SizeConst = 64)]
array<char>^ aString;
WRAP_STRUCT2 aStruct;
DelegateProc^ aDelegateProc;
[MarshalAs(UnmanagedType::ByValArray, SizeConst = 32)]
array<char>^ Reserved;
static STRUCT1 convert(WRAP_STRUCT1^ other) {
STRUCT1 clone;
clone.aLong = other->aLong;
clone.aShort = other->aShort;
clone.aBoolean = other->aBoolean;
sprintf(clone.aString, "%s", other->aString);
clone.aStruct = WRAP_STRUCT2::convert(other->aStruct);
clone.aDelegateProc = (Delegate1Proc)Marshal::GetFunctionPointerForDelegate(other->aDelegateProc).ToPointer();
return clone;
}
static WRAP_STRUCT1 convert(STRUCT1 other) {
WRAP_STRUCT1 clone;
clone.aLong = other.aLong;
clone.aShort = other.aShort;
clone.aBoolean = (other.aBoolean > 0);
clone.aString = marshal_as<String^>(other.aString)->ToCharArray();
clone.aStruct = WRAP_STRUCT2::convert(other.aStruct);
clone.aDelegateProc = (DelegateProc)Marshal::GetDelegateForFunctionPointer((IntPtr)other.aDelegateProc, DelegateProc::typeid);
return clone;
}
};
接下来,我们通常在 .h
header 文件中有一个 class:
class __declspec(dllexport) CLASS1 {
public:
CLASS1();
virtual ~CLASS1();
virtual int Function1(long inLong, char* inString, STRUCT2* inStruct);
virtual int Function2(long* outLong, char* outString, STRUCT2* outStruct);
};
我们需要创建一个包装器 class。它的 header:
public ref class Class1Wrapper {
public:
Class1Wrapper();
~Class1Wrapper();
int Function1(long inLong, String^ inString, WRAP_STRUCT2 inStruct);
int Function2([Out] long% outLong, [Out] String^ outString, [Out] WRAP_STRUCT2% outStruct);
private:
CLASS1* self;
};
及其实现:
Namespace::Class1Wrapper::Class1Wrapper() {
self = new CLASS1();
}
Namespace::Class1Wrapper::~Class1Wrapper() {
self->~CLASS1();
}
int Namespace::Class1Wrapper::Function1(long inLong, String^ inString, WRAP_STRUCT2 inStruct) {
char pinString[64];
sprintf(pinString, "%s", inString);
STRUCT2 pinStruct = WRAP_STRUCT2::convert(inStruct);
return self->Function1(inLong, pinString, pinStruct);
}
int Namespace::Class1Wrapper::Function2([Out] long% outLong, [Out] String^ outString, [Out] WRAP_STRUCT2% outStruct) {
long poutLong;
char poutString[64];
STRUCT2 poutStruct;
::ZeroMemory(&poutStruct, sizeof(poutStruct));
int result = self->Function2(poutLong, poutString, poutStruct);
outLong = poutLong;
outString = marshal_as<String^>(poutString);
outStruct = WRAP_STRUCT2::convert(poutStruct);
return result;
}
基本上,您需要使用常用的和您自己的结构编组函数来手动转换传入和传出数据。
让我开始另一个问题,因为虽然我看到了很多类似的问题,但没有人真正谈到这方面...我有一个 C++ DLL(没有源代码,只有 .lib 和 .h)并且我编写了必要的托管包装纸。这没有问题,问题是关于原始 C++ 代码中定义的结构和枚举,它们有很多,都需要暴露给 C# 代码。教程和示例通常使用简单的数据类型,如浮点数和字符串,而不是复杂数据结构的真实场景。
我的托管 C++/CLI 包装器使用 DLL 的 .h 头文件中的非托管结构。我包装的 class 成员函数一直在使用它们。因此,我需要在我的 C# 代码中使用相同的结构,传递它们并从 C++ 代码接收。很明显,我无法避免在 C# 中重新定义所有这些,但即便如此,使用它们也是有问题的。让我们举个例子:非托管代码中函数使用的一个简单结构:
typedef struct INFO {
...
} INFO;
int GetInfo(INFO& out_Info);
我在我的 C++/CLI 包装器代码中声明了相同的结构:
public ref struct INFO_WRAP {
...
};
int GetInfo(INFO_WRAP out_Info);
包装代码中的实现尝试将这个新结构转换为原始结构以供旧的非托管代码使用:
int Namespace::Wrapper::GetInfo(INFO_WRAP out_Info) {
pin_ptr<INFO> pin_out_Info = out_Info;
return self->GetInfo(*(::INFO*)pin_out_Info);
}
但这不会编译(无法在结构之间进行转换,也找不到合适的转换)。
有没有一种解决方案不涉及创建新的数据结构并一直来回手动复制所有结构成员?不仅是因为额外的工作和时间,而且结构真的很多。
public ref struct INFO_WRAP
您没有声明结构,这是 C# 术语中的 class。古怪的 C++ 实现细节,C++ 结构只是一个 class 及其所有成员 public。您需要在 C++/CLI 中使用 value struct
来声明 C# 结构的等效项。
int Namespace::Wrapper::GetInfo(INFO_WRAP out_Info)
这也是错误的,因为 INFO_WRAP 实际上是一个引用类型,您必须始终使用 ^ 帽子声明它。或者用 % 传参,肯定是这里的用意。
基本障碍排除,你要的东西没有直接支持。不允许托管编译器对托管结构的布局做出任何假设。无论如何都会在您尝试时吠叫。有一个很好的理由,它是 just not that predictable。布局是一个强大的运行时实现细节,如果代码在不同的运行时运行,它可以改变。就像 32 位与 64 位一样,可能在其中一个上工作,但在另一个上不行。正如乔恩发现的那样。
一个一个地复制字段总是有效的,而且性能足够。只是不是程序员喜欢维护的代码。您可以要求框架为您完成,调用 Marshal::PtrToStructure() 或 StructureToPtr().
作弊 是 可能的,并且肯定是您在编写 C++/CLI 代码时会考虑的事情。毕竟,语言的重点是使互操作性更快。您只是使保修失效,您 必须 在您打算支持的任何平台上彻底测试代码。一个简单的例子:
public value struct Managed {
int member1;
int member2;
};
struct Native {
int member1;
int member2;
};
void GetInfo(Managed% info) {
Native n = { 1, 2 };
pin_ptr<Managed> pinfo = &info;
memcpy(pinfo, &n, sizeof(n));
}
With 工作得很好并且在任何平台上都可预测地执行,结构很简单。当结构不简单或者你,比如说,修改 Native 而忘记修改 Managed 时,那就要付出代价了,堆栈和 GC 堆损坏是非常令人不快的事故,而且很难调试。
这是完整的解决方案,供其他追随我的人使用。 :-) 假设我们在 .h
header 文件中有一个包含枚举、结构、classes、函数的 DLL:
typedef int (*DelegateProc)(long inLong, char* inString, STRUCT2* inStruct, long* outLong, char* outString, STRUCT2* outString);
typedef struct STRUCT1 {
long aLong;
short aShort;
BOOL aBoolean;
char aString[64];
STRUCT2 aStruct;
DelegateProc aDelegateProc;
char Reserved[32];
} STRUCT1;
以通常的方式转换结构,并添加两个处理封送处理的静态转换函数。正如 Hans 指出的那样,尽管看起来很乏味,但分段复制是跨平台和架构的唯一真正可靠的解决方案。
#include <msclr\marshal.h>
using namespace msclr::interop;
public delegate int DelegateProc(long inLong, String^ inString, STRUCT2 inStruct, [Out] long% outLong, [Out] String^ outString, [Out] STRUCT2 outStruct);
[StructLayout(LayoutKind::Sequential, Pack = 1)]
public value struct WRAP_STRUCT1 {
long aLong;
short aShort;
bool aBoolean;
[MarshalAs(UnmanagedType::ByValArray, SizeConst = 64)]
array<char>^ aString;
WRAP_STRUCT2 aStruct;
DelegateProc^ aDelegateProc;
[MarshalAs(UnmanagedType::ByValArray, SizeConst = 32)]
array<char>^ Reserved;
static STRUCT1 convert(WRAP_STRUCT1^ other) {
STRUCT1 clone;
clone.aLong = other->aLong;
clone.aShort = other->aShort;
clone.aBoolean = other->aBoolean;
sprintf(clone.aString, "%s", other->aString);
clone.aStruct = WRAP_STRUCT2::convert(other->aStruct);
clone.aDelegateProc = (Delegate1Proc)Marshal::GetFunctionPointerForDelegate(other->aDelegateProc).ToPointer();
return clone;
}
static WRAP_STRUCT1 convert(STRUCT1 other) {
WRAP_STRUCT1 clone;
clone.aLong = other.aLong;
clone.aShort = other.aShort;
clone.aBoolean = (other.aBoolean > 0);
clone.aString = marshal_as<String^>(other.aString)->ToCharArray();
clone.aStruct = WRAP_STRUCT2::convert(other.aStruct);
clone.aDelegateProc = (DelegateProc)Marshal::GetDelegateForFunctionPointer((IntPtr)other.aDelegateProc, DelegateProc::typeid);
return clone;
}
};
接下来,我们通常在 .h
header 文件中有一个 class:
class __declspec(dllexport) CLASS1 {
public:
CLASS1();
virtual ~CLASS1();
virtual int Function1(long inLong, char* inString, STRUCT2* inStruct);
virtual int Function2(long* outLong, char* outString, STRUCT2* outStruct);
};
我们需要创建一个包装器 class。它的 header:
public ref class Class1Wrapper {
public:
Class1Wrapper();
~Class1Wrapper();
int Function1(long inLong, String^ inString, WRAP_STRUCT2 inStruct);
int Function2([Out] long% outLong, [Out] String^ outString, [Out] WRAP_STRUCT2% outStruct);
private:
CLASS1* self;
};
及其实现:
Namespace::Class1Wrapper::Class1Wrapper() {
self = new CLASS1();
}
Namespace::Class1Wrapper::~Class1Wrapper() {
self->~CLASS1();
}
int Namespace::Class1Wrapper::Function1(long inLong, String^ inString, WRAP_STRUCT2 inStruct) {
char pinString[64];
sprintf(pinString, "%s", inString);
STRUCT2 pinStruct = WRAP_STRUCT2::convert(inStruct);
return self->Function1(inLong, pinString, pinStruct);
}
int Namespace::Class1Wrapper::Function2([Out] long% outLong, [Out] String^ outString, [Out] WRAP_STRUCT2% outStruct) {
long poutLong;
char poutString[64];
STRUCT2 poutStruct;
::ZeroMemory(&poutStruct, sizeof(poutStruct));
int result = self->Function2(poutLong, poutString, poutStruct);
outLong = poutLong;
outString = marshal_as<String^>(poutString);
outStruct = WRAP_STRUCT2::convert(poutStruct);
return result;
}
基本上,您需要使用常用的和您自己的结构编组函数来手动转换传入和传出数据。