C#调用C++动态库

1.c#调用c++的方法

C#调用C++的dll总归可以有两种方法:

  1. 非托管C++创建的dll库,需要用静态方法调用;

  2. 直接使用CLR,生成托管C++dll库。

很多时候在项目中需要通过C++调用C#的dll,或者反过来调用。首先明白一个前提:C#是托管型代码。C++是非托管型代码。
托管型代码的对象在托管堆上分配内存,创建的对象由虚拟机托管。(C# )
非托管型代码对象有实际的内存地址,创建的对象必须自己来管理和释放。(C++)

2.托管方式

下面就用一个完整的实例来详细说明怎样用托管C++封装一个C++类以提供给C#使用。比如,现在有一个工程名为NativeCppDll的由C++编写的DLL,里面输出了一个CPerson类。下面是具体的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// NativeCppDll.h
#pragma once
#ifndef LX_DLL_CLASS_EXPORTS
#define LX_DLL_CLASS __declspec(dllexport)
#else
#define LX_DLL_CLASS __declspec(dllimport)
#endif
class LX_DLL_CLASS CPerson
{
public:
CPerson();
CPerson(const wchar_t *pName, const wchar_t cSex, int iAge);
void SetName(const wchar_t *pName);
wchar_t * GetName();
void SetSex(const wchar_t cSex);
wchar_t GetSex();
void SetAge(int iAge);
int GetAge();
wchar_t * GetLastError();
private:
wchar_t m_szName[128];
wchar_t m_cSex;
int m_iAge;
wchar_t m_szLastError[128];
void ShowError();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// NativeCppDll.cpp
#include "stdafx.h"
#include "NativeCppDll.h"
#include <iostream>
#include <tchar.h>
using namespace std;
CPerson::CPerson()
{
wcscpy_s(m_szName, _T("No Name"));
m_cSex = 'N';
m_iAge = 0;
wcscpy_s(m_szLastError, _T("No Error"));
}
CPerson::CPerson(const wchar_t *pName, const wchar_t cSex, int iAge)
{
wcscpy_s(m_szLastError, _T("No Error"));
SetName(pName);
SetSex(cSex);
SetAge(iAge);
}
void CPerson::SetName(const wchar_t *pName)
{
if ((pName == NULL) || (wcslen(pName) == 0) || (wcslen(pName) > 127))
{
wcscpy_s(m_szName, _T("No Name"));
wcscpy_s(m_szLastError, _T("The length of the input name is out of range."));
ShowError();
return;
}
wcscpy_s(m_szName, pName);
}
wchar_t * CPerson::GetName()
{
return m_szName;
}
void CPerson::SetSex(const wchar_t cSex)
{
if ((cSex != 'F') && (cSex != 'M') && (cSex != 'm') && (cSex != 'f'))
{
m_cSex = 'N';
wcscpy_s(m_szLastError, _T("The input sex is out of [F/M]."));
ShowError();

return;
}
m_cSex = cSex;
}
wchar_t CPerson::GetSex()
{
return m_cSex;
}
void CPerson::SetAge(int iAge)
{
if ((iAge < 0) || (iAge > 150))
{
m_iAge = 0;
wcscpy_s(m_szLastError, _T("The input age is out of range."));
ShowError();
return;
}
m_iAge = iAge;
}
int CPerson::GetAge()
{
return m_iAge;
}
wchar_t * CPerson::GetLastError()
{
return m_szLastError;
}
void CPerson::ShowError()
{
cerr << m_szLastError << endl;
}

这是一个很典型的由C++开发的DLL,输出一个完整的C++类。如果现在要求开发一个C#工程,需要用到这个DLL中输出的C++类CPerson,该怎么办呢?针对这个例子来说,类CPerson非常小,可以用C#重新写一个跟这个C++类一样的类。可是,如果需要的C++类很大,或者很多的时候,重写工程将非常庞大。而且这样没有对现有的代码进行重用,浪费了现有资源,开发起来费时费力。

当然,还是有方法解决这个问题的。那就是用托管C++将C++类给封装一下,然后再提供给C#来使用。下面就用代码来详细说明怎样用托管C++来封装上面的那个C++类。

首先,要创建一个托管C++的DLL工程ManageCppDll,然后在里面添加下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ManageCppDll.h
#pragma once
#define LX_DLL_CLASS_EXPORTS
#include "../NativeCppDll/NativeCppDll.h"
using namespace System;
namespace ManageCppDll
{
// 关键字 ref
public ref class Person
{
// 包装所有类CPerson的公有成员函数
public:
Person();
Person(String ^ strName, Char cSex, int iAge);
~Person();
property String ^ Name
{
void set(String ^ strName);
String ^ get();
}
property Char Sex
{
void set(Char cSex);
Char get();
}
property int Age
{
void set(int iAge);
int get();
}
String ^ GetLastError();
private:
// 类CPerson的指针,用来调用类CPerson的成员函数
CPerson *m_pImp;
};
};

从这个头文件就能看出来,这是对C++类CPerson的包装。类Person的所有公有成员函数都跟C++类CPerson一样,只不过成员函数的参数和返回值就改成了托管C++的类型,这也是让类Person能在C#中使用的首要条件。当然只需要对公有成员函数进行封装,对于保护成员函数和私有成员函数则不必做任何封装。

类Person仅有一个私有的成员变量:一个类CPerson的指针。而类Person的所有成员函数的实现都是靠这个CPerson指针来调用类CPerson的相应成员函数来实现。
下面是具体的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ManageCppDll.cpp
#include "stdafx.h"
#include "ManageCppDll.h"
#include <vcclr.h>
namespace ManageCppDll
{
// 在构造函数中创建类CPerson的对象并在析构函数中将该对象销毁
// 所有的成员函数实现都是通过指针m_pImp调用类CPerson的相应成员函数实现
Person::Person()
{
m_pImp = new CPerson();
}
Person::Person(String ^ strName, Char cSex, int iAge)
{
// 将string转换成C++能识别的指针
pin_ptr<const wchar_t> wcName = PtrToStringChars(strName);
m_pImp = new CPerson(wcName, cSex, iAge);
}
Person::~Person()
{
// 在析构函数中删除CPerson对象
delete m_pImp;
}
void Person::Name::set(String ^ strName)
{
pin_ptr<const wchar_t> wcName = PtrToStringChars(strName);
m_pImp->SetName(wcName);
}
String ^ Person::Name::get()
{
return gcnew String(m_pImp->GetName());
}
void Person::Sex::set(Char cSex)
{
m_pImp->SetSex(cSex);
}
Char Person::Sex::get()
{
return m_pImp->GetSex();
}
void Person::Age::set(int iAge)
{
m_pImp->SetAge(iAge);
}
int Person::Age::get()
{
return m_pImp->GetAge();
}
String ^ Person::GetLastError()
{
return gcnew String(m_pImp->GetLastError());
}
};

如果要在C#中使用类Person,首先要添加对ManageCppDll.dll的引用,然后就可以像用普通的C#类一样的使用类Person了。比如下面这样的代码:

1
2
3
4
5
using ManageCppDll;
Person person = new Person();
person.Name = "StarLee";
person.Sex = 'M';
person.Age = 28;

熟悉设计模式的看了上面的代码肯定会发现,这样的设计跟BRIDGE模式如出一辙。其实,上面的方法也算是一种BRIDGE模式,由托管C++充当了C#中使用用C++开发的类的桥梁。另外,这种形式也可以理解为ADAPTER模式,托管C++类Person就是C++类CPerson的一个适配器。通过这个桥梁,可以很容易的重用以前用C++开发的类,让这些C++类继续在C#中发挥它们的效用,让开发变得事半功倍。

3.非托管方式

C#不能直接调用C++类库中的类,需要一种变通的解决方式,通过再做一个C++类库把要调用的类成员方法暴露出来,比如下面这个C++类:

1
2
3
4
5
6
7
8
9
10
//SampleCppClass.h
#pragma once
class __declspec(dllexport) SampleCppClass
{
public:
SampleCppClass(void);
~SampleCppClass(void);
int Add(int n1, int n2);
int Sub(int n1, int n2);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//SampleCppClass.cpp
#include "SampleCppClass.h"
SampleCppClass::SampleCppClass(void)
{
}
SampleCppClass::~SampleCppClass(void)
{
}
int SampleCppClass::Add(int n1, int n2)
{
return n1 + n2;
}
int SampleCppClass::Sub(int n1, int n2)
{
return n1 - n2;
}

我们要调用SampleCppClass中的Add和Sub两个方法,所以我们再写一个C++类库,通过公共方法间接调用类成员方法:

1
2
3
4
5
6
7
8
9
10
11
//SampleCppWrapper.h
#pragma once

#include "..\SampleCppClass\SampleCppClass.h"
namespace SampleCppWrapper
{
extern "C" __declspec(dllexport) int __stdcall Add(int n1, int n2);
extern "C" __declspec(dllexport) int __stdcall Sub(int n1, int n2);
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//SampleCppWrapper.cpp
#include "SampleCppWrapper.h"
namespace SampleCppWrapper
{
SampleCppClass* g_pObj = new SampleCppClass();
int __stdcall Add(int n1, int n2)
{
return g_pObj->Add(n1, n2);
}

int __stdcall Sub(int n1, int n2)
{
return g_pObj->Sub(n1, n2);
}
}

在C#中,再调用SampleCppWrapper.dll中的公共方法:

1
2
3
4
[DllImport("SampleCppWrapper.dll")]
private static extern int Add(int n1, int n2);
[DllImport("SampleCppWrapper.dll")]
private static extern int Sub(int n1, int n2);

4.使用C++类库中的回调函数

C++的回调函数是一种事件响应机制,和C#的委托相似,比如一个C++类中的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SampleCppClass.h
#pragma once

typedef void (*LoopCallback)(void* pContext);

class __declspec(dllexport) SampleCppClass
{
public:
SampleCppClass(void);
~SampleCppClass(void);
void SetCallbackFunc(LoopCallback callback);
void SetCallbackContext(void* pContext);
void Loop();
private:
LoopCallback m_callback;
void* m_pContext;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SampleCppClass.cpp
#include "SampleCppClass.h"

SampleCppClass::SampleCppClass(void)
{
}

SampleCppClass::~SampleCppClass(void)
{
}

void SampleCppClass::SetCallbackFunc(LoopCallback callback)
{
m_callback = callback;
}

void SampleCppClass::SetCallbackContext(void* pContext)
{
m_pContext = pContext;
}

void SampleCppClass::Loop()
{
for (int i=0; i<10; i++)
{
if (m_callback != NULL)
{
m_callback(m_pContext);
}
}
}

导出方法文件中添加:

1
2
3
4
5
6
7
8
9
10
//.h
#pragma once
#include "..\SampleCppClass\SampleCppClass.h"
namespace SampleCppWrapper
{
typedef void (__stdcall *LoopCallbackWrapper)(void* pContext);
extern "C" __declspec(dllexport) void __stdcall SetCallbackFunc(LoopCallbackWrapper callback);
extern "C" __declspec(dllexport) void __stdcall SetCallbackContext(void* pContext);
extern "C" __declspec(dllexport) void __stdcall Loop();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// .CPP
#include "SampleCppWrapper.h"
namespace SampleCppWrapper
{
LoopCallbackWrapper g_callbackWrapper;
SampleCppClass* g_pObj = new SampleCppClass();
void LoopCallbackFunc(void* pContext);

void __stdcall SetCallbackFunc(LoopCallbackWrapper callback)
{
g_callbackWrapper = callback;
g_pObj->SetCallbackFunc(LoopCallbackFunc);
}

void __stdcall SetCallbackContext(void* pContext)
{
g_pObj->SetCallbackContext(pContext);
}

void __stdcall Loop()
{
g_pObj->Loop();
}

void LoopCallbackFunc(void* pContext)
{
if (g_callbackWrapper != NULL)
{
g_callbackWrapper(pContext);
}
}
}

C#中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace SampleCsTest
{
public partial class Form1 : Form
{
[StructLayout(LayoutKind.Sequential)]
private class Context
{
public Form1 Form { get; set; }
}
private delegate void LoopCallbackHandler(IntPtr pContext);
private static LoopCallbackHandler callback = LoopCallback;

[DllImport("SampleCppWrapper.dll")]
private static extern void SetCallbackFunc(LoopCallbackHandler callback);
[DllImport("SampleCppWrapper.dll")]
private static extern void SetCallbackContext(IntPtr pContext);
[DllImport("SampleCppWrapper.dll")]
private static extern void Loop();

private Context ctx = new Context();

public Form1()
{
InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{
SetCallbackFunc(callback);
ctx.Form = this;
IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(ctx));
Marshal.StructureToPtr(ctx, ptr, false);
SetCallbackContext(ptr);
}

private void button1_Click(object sender, EventArgs e)
{
Loop();
}

private static void LoopCallback(IntPtr pContext)
{
Context ctx = (Context)Marshal.PtrToStructure(pContext, typeof(Context));
ctx.Form.textBox1.Text += "callback" + Environment.NewLine;
}
}
}

以上为非托管方式简单参数的传递,实际使用过程中,可能参数类型会复杂很多,这牵涉到C# C++之间的参数转换及C#语法对托管代码的编写,具体做一些项目时,肯定会比例子情况复杂的多,那就需要对各种参数传递及转换好好了解一番,如果解决了各种情况参数传递问题,基本C#调用非托管C++dll没有其它复杂问题。
管廊程序中有相关C#调用C++的DLL 可以参考使用,不过管廊中生成的都是托管DLL.

5.C++ 中的封送处理

封送处理库包含一组函数和一个marshal_context执行常见类型的封送处理的类。 在这些标头中定义库包括 msclr目录为您的 Visual Studio 版本:

Header 描述
marshal.h marshal_context 类和无上下文的封送处理函数
marshal_atl.h 用于封送处理 ATL 类型的函数
marshal_cppstd.h 用于封送处理标准 c + + 类型的函数
marshal_windows.h 用于封送处理 Windows 类型的函数

可以使用封送处理库,带或不带marshal_context 类。 某些转换需要上下文。 可以使用实现其他转换marshal_as函数。 下表列出了当前支持的转换、 是否需要上下文和封送文件必须包括:

从类型 若要键入 封送方法 包含文件
System:: string ^ const char * marshal_context marshal.h
const char * System:: string ^ marshal_as marshal.h
Char * System:: string ^ marshal_as marshal.h
System:: string ^ const wchar_t* marshal_context marshal.h
const wchar_t * System:: string ^ marshal_as marshal.h
wchar_t * System:: string ^ marshal_as marshal.h
System::IntPtr 句柄 marshal_as marshal_windows.h
句柄 System::IntPtr marshal_as marshal_windows.h
System:: string ^ BSTR marshal_context marshal_windows.h
BSTR System:: string ^ marshal_as marshal.h
System:: string ^ bstr_t marshal_as marshal_windows.h
bstr_t System:: string ^ marshal_as marshal_windows.h
System:: string ^ std:: string marshal_as marshal_cppstd.h
std:: string System:: string ^ marshal_as marshal_cppstd.h
System:: string ^ std:: wstring marshal_as marshal_cppstd.h
std:: wstring System:: string ^ marshal_as marshal_cppstd.h
System:: string ^ CStringT marshal_as marshal_atl.h
CStringT System:: string ^ marshal_as marshal_atl.h
System:: string ^ CStringT < wchar_t > marshal_as marshal_atl.h
CStringT < wchar_t > System:: string ^ marshal_as marshal_atl.h
System:: string ^ CComBSTR marshal_as marshal_atl.h
CComBSTR System:: string ^ marshal_as marshal_atl.h

仅当你将封送从托管到本机数据类型并将转换为本机类型不具有的析构函数的自动清理时,封送处理需要上下文。 封送处理上下文销毁其析构函数中的已分配的本机数据类型。 因此,仅删除上下文之前,需要上下文的转换将始终有效。 若要保存任何封送的值,必须将值复制到自己的变量中。

marshal_as

此方法将本机和托管环境之间的数据转换。

示例

此示例是从封送const char*System::String变量类型。

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// marshal_as_test.cpp  
// compile with: /clr
#include <stdlib.h>
#include <string.h>
#include <msclr\marshal.h>

using namespace System;
using namespace msclr::interop;

int main() {
const char* message = "Test String to Marshal";
String^ result;
result = marshal_as<String^>( message );
return 0;
}

marshal_context 类

此类将在本机和托管环境之间转换数据。对于需要上下文的数据转换,请使用 marshal_context 类。

示例

此示例将为从 System::Stringconst char * 变量类型的封送处理创建上下文。 转换数据在删除上下文的行之后是无效的。

复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// marshal_context_test.cpp  
// compile with: /clr
#include <stdlib.h>
#include <string.h>
#include <msclr\marshal.h>

using namespace System;
using namespace msclr::interop;

int main() {
marshal_context^ context = gcnew marshal_context();
String^ message = gcnew String("Test String to Marshal");
const char* result;
result = context->marshal_as<const char*>( message );
delete context;
return 0;
}

6.类型转换

在C#调用C++托管库的过程中,C++托管库一般只是封装为真正C++动态链接库(DLL)的一个外壳,其不做任何逻辑功能,但是会存在数据类型及结构上的转换问题,本文就是用于介绍数据类型转换过程中需要考虑的问题。 
数据类型很多,限于篇幅原因,文章主要讲解几种常见且较复杂的数据类型。 

  1. C#句柄与C++窗体句柄类型的转换, IntPtr<—>HWND; 
  2. C# String类型 与 C++ String 类型转换 
  3. C#数据地址与C++指针转换 
  4. C#结构体与C++结构体转换

6.1 C#句柄IntPtr与C++窗体句柄HWND的转换

在C++托管代码中可以直接引入C#数据类型IntPtr, 然后可以转换成HWND类型,转换过程如下:

1
2
3
4
5
6
C#调用环境接口:CSharp_InterfaceCLR(IntPtr InputDataSharp)
C++ CLR 托管环境接口:CPP_InterfaceCLR(IntPtr InputDataClr)
C++环境中接口:CPP_Interface(HWND InputData);
CSharp_InterfaceCLR(InputDataSharp)
--->CPP_InterfaceCLR(InputDataClr) //InputDataSharp 可以直接赋值给 InputDataClr
--->CPP_Interface((HWND) (InputDataClr.ToInt32())); //把InputDataClr.ToInt32()强制转换成HWND

6.2 C# String类型 与 C++ String 类型转换

在讲解转换之前,需要说一个符号^,C++/CLI中使用gcnew关键字表示在托管堆上分配内存,并且为了与以前的指针区分,用^来替换* ,就语义上来说区别大致如下:

  1. gcnew返回的是一个句柄(Handle),而new返回的是实际的内存地址;
  2. gcnew创建的对象由虚拟机托管,而new创建的对象必须自己来管理和释放。
    从开发者的角度,不论是句柄还是其他的类型,总会是对某块内存地址的引用,可以理解为指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
C#调用环境接口:CSharp_InterfaceCLR(string InputDataSharp)
C++ CLR 托管环境接口:CPP_InterfaceCLR(String^ InputDataClr)
C++环境中接口:CPP_Interface(string InputData);
CSharp_InterfaceCLR(InputDataSharp)
--->CPP_InterfaceCLR(InputDataClr) //InputDataSharp 可以直接赋值给 InputDataClr
//把String^类型的string转换为C++ string的过程如下:
std::string InputData = marshal_as<std::string>(InputDataClr);
--->CPP_Interface(InputData);

//注意在托管代码中需要添加如下信息:
#include <msclr\marshal_cppstd.h>
using namespace msclr::interop;
using namespace System;

6.3 C#数据地址与C++指针转换

C#中的变量地址可以通过IntPtr传递到CLR,然后CLR(托管C++)转换为一个数值,在C++代码中直接通过指针强制转换即可完成地址在C#与C++直接的转换。
具体过程可以如下:

1
2
3
4
5
6
7
8
C#调用环境接口:CSharp_InterfaceCLR(IntPtr InputDataSharp)
C++ CLR 托管环境接口:CPP_InterfaceCLR(IntPtr InputDataClr)
C++环境中接口:CPP_Interface(Void* InputData);
CSharp_InterfaceCLR(InputDataSharp)
--->CPP_InterfaceCLR(InputDataClr) //InputDataSharp 可以直接赋值给 InputDataClr
--->CPP_Interface((void*)InputDataClr.ToInt32()); //把InputDataClr.ToInt32()强制转换成void*, 也可以通过InputDataClr.ToPointer(),然后在C++代码里面把void*转成相应的类型

//c#中使用指针:在需要使用指针的地方 加 unsafe

关于其他指针类型转换需要注意:

1
2
3
4
5
unsigned char** ppImage <===> IntPtr ppImage 双指针类型参数,都可以用 ref IntPtr 
int& Variable <===> ref int Variable
int*, int& <===> ref int
char* 的操作 <===> c#:StringBuilder;
函数指针使用: c++: typedef int (*function)(int); <===>c#:public delegate int function(int);

关于函数指针的使用会在C++调用C#注册的回调函数一章中详细说明。

6.4 C#结构体与C++结构体转换

C++中 Struct需要在C#里重新定义一个Struct与之对应起来。
说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
typedef struct
{
char CharArray[256];
INT32 IntegerVariable;
INT32 IntegerVariable1;
UINT16 UnsignedIntegerVariable;
bool BoolType;
//string StringVariable 不可以用string类型,可以使用char*代替
} STRUCT;

//法1.
[StructLayout(LayoutKind.Sequential)]
public struct STRUCT
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public char[] CharArray;
public Int32 IntegerVariable;
public UInt16 UnsignedIntegerVariable;
public Boolean BoolType;
}
可以使用一个C#函数做转换
public StructType ConverBytesToStructure<StructType>(byte[] bytesBuffer)
{
// check the length。
if (bytesBuffer.Length != Marshal.SizeOf(typeof(STRUCT)))
{
throw new ArgumentException("bytesBuffer is not same as structObject for the length of byte.");
}

IntPtr bufferHandler = Marshal.AllocHGlobal(bytesBuffer.Length);
for (int index = 0; index < bytesBuffer.Length; index++)
{
Marshal.WriteByte(bufferHandler, index, bytesBuffer[index]);
}
STRUCT structObject = (STRUCT)Marshal.PtrToStructure(bufferHandler, typeof(STRUCT));
Marshal.FreeHGlobal(bufferHandler);
return structObject;
}

FileStream file = File.OpenRead(@"C:/Path");
byte[] buffer = new byte[Marshal.SizeOf(typeof(STRUCT))];
file.Read(buffer, 0, buffer.Length);
STRUCT testValue = CommonTools.ConverBytesToStructure<STRUCT>(buffer);
string sCharArray = new string(testValue.CharArray);

//法2. 就不需要再写转换函数
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct STRUCT
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string CharArray;
public Int32 IntegerVariable;
public UInt16 UnsignedIntegerVariable;
public Boolean BoolType;
}