C#托管和非托管混合编程
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
C#托管和⾮托管混合编程
在⾮托管模块中实现你⽐较重要的算法,然后通过 CLR 的平台互操作,来使托管代码调⽤它,这样程序仍然能够正常⼯作,但对⾮托管的本地代码进⾏反编译,就很困难。
最直接的实现托管与⾮托管编程的⽅法就是使⽤C++/CLI
介绍
项⽬存档⼀直是企业的采⽤的做法,⽽是事实证明他们也是对的!对于⼀个程序员,这是⼏千men-days的⼯作量。
为什么不开发⼀⼩段代码去重新利⽤那段代码,项⽬。
现在提供了⼀个渐渐的转向C#的新技术: 使⽤托管与⾮托管的混合编程。
这是⼀个可⾏的⽅案在top-down issue(from UI to low-level layers)or bottom-up(from low-level to UI)案例。
本⽂⽬的就是通过两个简单的例⼦来说明怎么⼀起使⽤这两种技术:
* 在⾮托管中调⽤托管代码。
* 在托管中调⽤⾮托管代码。
⾮托管代码中调⽤托管函数
这个例⼦主要展⽰了在⾮托管代码(C++)中调⽤使⽤托管(C#)代码实现类,通过托管代码实现"mixed code"DLL 来导出API。
单⼀的⾮托管代码
以下是⼀个控制台程序
#include "stdafx.h"
#include <iostream>
using namespace std;
#ifdef _UNICODE
#define cout wcout
#define cint wcin
#endif
int _tmain(int argc, TCHAR* argv[])
{
UNREFERENCED_PARAMETER(argc);
UNREFERENCED_PARAMETER(argv);
SYSTEMTIME st = {0};
const TCHAR* pszName = _T("John SMITH");
st.wYear = 1975;
st.wMonth = 8;
st.wDay = 15;
CPerson person(pszName, &st);
cout << pszName << _T(" born ")
<< person.get_BirthDateStr().c_str()
<< _T(" age is ") << person.get_Age()
<< _T(" years old today.")
<< endl;
cout << _T("Press ENTER to terminate...");
cin.get();
#ifdef _DEBUG
_CrtDumpMemoryLeaks();
#endif
return 0;
}
这段代码没有什么特殊的,这只是个再普通不过的⾮托管代码。
单⼀的托管代码
这是个典型的使⽤C#实现的装配器
using System;
namespace AdR.Samples.NativeCallingCLR.ClrAssembly
{
public class Person
{
private string _name;
private DateTime _birthDate;
public Person(string name, DateTime birthDate)
{
this._name = name;
this._birthDate = birthDate;
}
public uint Age
{
get
{
DateTime now = DateTime.Now;
int age = now.Year - this._birthDate.Year;
if ((this._birthDate.Month > now.Month) ||
((this._birthDate.Month == now.Month) &&
(this._birthDate.Day > now.Day)))
{
--age;
}
return (uint)age;
}
}
public string BirthDateStr
{
get
{
return this._birthDate.ToShortDateString();
}
}
public DateTime BirthDate
{
get
{
return this._birthDate;
}
}
}
}
正如所见,这这是个单⼀的CLR
托管与⾮托管混合编程部分
这部分是最重要,也是最难的。
VisualStudio环境提供了⼀些头⽂件来帮助开发者链接这些关键词。
#include <vcclr.h>
但是,并⾮就到这⼉就结束了。
我们还需要⼩⼼涉及的⼀些陷阱,尤其是是CLR(托管代码)和native(⾮托管代码)⼀些关键词之间数据的传递。
以下是个类的头⽂件输出⼀个托管的部分
#pragma once
#ifdef NATIVEDLL_EXPORTS
#define NATIVEDLL_API __declspec(dllexport)
#else
#define NATIVEDLL_API __declspec(dllimport)
#endif
#include <string>
using namespace std;
#ifdef _UNICODE
typedef wstring tstring;
#else
typedef string tstring;
#endif
class NATIVEDLL_API CPerson
{
public:
// Initialization
CPerson(LPCTSTR pszName, const SYSTEMTIME* birthDate);
virtual ~CPerson();
// Accessors
unsigned int get_Age() const;
tstring get_BirthDateStr() const;
SYSTEMTIME get_BirthDate() const;
private:
// Embedded wrapper of an instance of a CLR class
// Goal: completely hide CLR to pure unmanaged C/C++ code
void* m_pPersonClr;
};
强调⼀点,尽量在头⽂件⾥保证只有⾮托管代码,混合编程在cpp中去实现,数据的传递。
⽐如:应该尽量避免使⽤vcclr.h中的函数,进⾏混合编程。
这就是为什么定义⼀个void指针来包装CLR对象。
⼀个神奇的⼤门,就这样打开了。
正如我说的那样,神奇的事就从包含⼀个vcclr.h头⽂件开始。
但是,需要使⽤CLR编码语⾔和使⽤⼀些复杂的类型(例如:strings, array, etc):
using namespace System;
using namespace Runtime::InteropServices;
using namespace AdR::Samples::NativeCallingCLR::ClrAssembly;
当然,需要申明⼀些使⽤的本地装配器。
⾸先,我们来看这个类的构造器:
CPerson::CPerson(LPCTSTR pszName, const SYSTEMTIME* birthDate)
{
DateTime^ dateTime = gcnew DateTime((int)birthDate->wYear,
(int)birthDate->wMonth,
(int)birthDate->wDay);
String^ str = gcnew String(pszName);
Person^ person = gcnew Person(str, *dateTime);
// Managed type conversion into unmanaged pointer is not
// allowed unless we use "gcroot<>" wrapper.
gcroot<Person^> *pp = new gcroot<Person^>(person);
this->m_pPersonClr = static_cast<void*>(pp);
}
在⾮托管代码⾥允许使⽤⼀个指针指向⼀个托管的类,但是我们并不想直接到处⼀个托管的API给⽤户。
所以,我们使⽤了⼀个void指针来封装这个对象,⼀个新的问题⼜出现了:我们是不被允许直接⽤⾮托管指针指向托管类型的。
这就是为什么我们会使⽤gcroot<>模板类。
需要注意怎么使⽤指针指向托管代码时需要加上^字符;这意味我们使⽤⼀个引⽤指针指向托管类。
切记,类对象在.NET中被视为引⽤,当被⽤作函数成员时。
还需要注意⼀个在.NET中⾃动内存分配的关键词:gcnew. 这意味我们在⼀个垃圾收集器保护环境中分配空间,⽽不是在进程堆⾥。
有时候需要⼩⼼的是:进程堆和垃圾收集器保护环境完全不⼀样。
我们将会看到⼀些封装任务还得做:在类的析构函数:
CPerson::~CPerson()
{
if (this->m_pPersonClr)
{
// Get the CLR handle wrapper
gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);
// Delete the wrapper; this will release the underlying CLR instance
delete pp;
// Set to null
this->m_pPersonClr = 0;
}
}
我们使⽤标准的c++类型转化static_case. 删除对象会释放潜在封装的CLR对象,允许它进⼊垃圾回收机制。
提醒:申明⼀个析构函数的原因是实现了IDisposeable 接⼝和⾃⼰的Dispose()⽅法。
关键:不要忘了调⽤Dispose()在CPerson实例中。
否则,会导致内存泄露,正如在C++中不能释放(析构函数没有被调⽤)。
调⽤基本的CLR类成员⼗分容易,和上⽂类似。
unsigned int CPerson::get_Age() const
{
if (this->m_pPersonClr != 0)
{
// Get the CLR handle wrapper
gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);
// Get the attribute
return ((Person^)*pp)->Age;
}
return 0;
}
但是,当我们必须要返回⼀个复杂类型时就⿇烦⼀点,正如下⾯类成员:
tstring CPerson::get_BirthDateStr() const
{
tstring strAge;
if (this->m_pPersonClr != 0)
{
// Get the CLR handle wrapper
gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);
// Convert to std::string
// Note:
// - Marshaling is mandatory
// - Do not forget to get the string pointer...
strAge = (const TCHAR*)Marshal::StringToHGlobalAuto(
((Person^)*pp)->BirthDateStr
).ToPointer();
}
return strAge;
}
我们不能直接返回⼀个System::String 对象给⾮托管的string。
必须使⽤⼀下⼏步:
1. 得到 System::String 对象.
2. 使⽤ Marshal::StringToHGlobalAuto() 得到⼀个全局的句柄。
我们在这⾥使⽤”auto”版本返回的是Unicode编码的string. 然后尽可能的转化为ANSI编码的string;
3. 最后,得到⼀个指针指向潜在包含对象的句柄。
以上3步就实现了替换!
阅读推荐的书关于C++/CLI, 你会看到其他的⼀些特别的关键词,如pin_ptr<> 和 interna_ptr<>允许你得到指针隐藏的对象,阅读⽂档可以获取更多的细节。
⼤混合
这是个标准的例⼦展⽰了如何去创建⼀个本地的控制台程序使⽤MFC和CLR!
结论(⾮托管调⽤托管)
⾮托管中调⽤托管是⼀件复杂的事,这个例⼦很基本,普通。
在例⼦中,你可以看到⼀些很复杂的考虑。
希望你可以在今后混合编程中,碰到更多的其他的⼀些场景,获取到更多经验。
托管中调⽤⾮托管
这个例⼦展⽰了怎样在CLR(C#)中调⽤⾮托管的C++类库,通过起中间媒介的”mixed code”DLL,导出⼀个API来使⽤⾮托管代码。
⾮托管的C++DLL
DLL导出:
1. A C++ 类
2. A C-风格的函数
3. A C-风格的变量
这⼀段介绍对象的申明,尽管他们很简单,以⾄于没有必要注释。
C++ 类
class NATIVEDLL_API CPerson {
public:
// Initialization
CPerson(LPCTSTR pszName, SYSTEMTIME birthDate);
// Accessors
unsigned int get_Age();
private:
TCHAR m_sName[64];
SYSTEMTIME m_birthDate;
CPerson();
};
get_Age()函数简单得计算从出⽣到现在的⼀个时间段。
导出 C 函数
int fnNativeDLL(void);
导出C变量
int nNativeDLL;
.NET 端
这⾥不详细的介绍这个经典的案例。
笔记1:
.NET类不能直接从⾮托管的C++类中继承。
写⼀个托管C++的类嵌⼊到c++实体对象内部。
笔记2:
申明⼀个成员CPerson_person2;会导致⽣成C4368编译错误(不能定义’member’ 作为⼀个托管类型的成员:不⽀持混合类型)
这就是为什么在内部使⽤(在C#被视为’unsafe’)
技术⽂档上是这么说的:
你不能直接嵌⼊⼀个⾮托管的数据成员到CLR中。
但是,你可以申明⼀个本地化类型的指针,在构造函数,析构函数,释放托管的类⾥控制它的⽣命周期(看在Visual c++ ⾥有关于析构函数和终结器更多的信息)。
这就是嵌⼊的对象:
CPerson* _pPerson;
⽽不是:
CPerson person;
构造器中特殊的信息
公共的构造器有⼀个System::String string(托管类型)和⼀个SYSTEMTIME 结构体(Win32 API 类型,但是只是数值:很明显是个数据集)
这个⾮托管的c++ CPerson 构造函数使⽤了LPCTSTR string 类型的指针,这个托管的string不能直接转化⾮托管的对象。
这是构造器的源代码:
SYSTEMTIME st = { (WORD)birthDate.Year,
(WORD)birthDate.Month,
(WORD)birthDate.DayOfWeek,
(WORD)birthDate.Day,
(WORD)birthDate.Hour,
(WORD)birthDate.Minute,
(WORD)birthDate.Second,
(WORD)lisecond };
// Pin 'name' memory before calling unmanaged code
pin_ptr<const TCHAR> psz = PtrToStringChars(name);
// Allocate the unmanaged object
_pPerson = new CPerson(psz, st);
注意这⾥使⽤pin_ptr关键词来保护string可以在CRL中使⽤。
这个是⼀可以保护对象指向个内部的指针。
当传递⼀个托管类的地址给⼀个⾮托管的的函数是很有必要的,因为地址不是在⾮托管代码调⽤时异常的改变。
总结(托管中调⽤⾮托管)
如果我们觉得在托管中导⼊⼀个⾮托管的⽐⾮托管中导⼊⼀个托管更为常见,写⼀个”intermediate assembly”是相当不容易的。
你应该确定是不是需要全部移植代码,那样是不合理的。
考虑重新设计这个应⽤。
重写托管代码可能⽐移植更划算。
⽽且,最终的应⽤也是很清晰明了。