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++)中调用使用托管(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!

结论(非托管调用托管)

非托管中调用托管是一件复杂的事,这个例子很基本,普通。在例子中,你可以看到一些很复杂的考虑。希望你可以在今后混合编程中,碰到更多的其他的一些场景,获取到更多经验。

托管中调用非托管

C# 托管和非托管混合编程
这个例子展示了怎样在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)birthDate.Millisecond }; // 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”是相当不容易的。
你应该确定是不是需要全部移植代码,那样是不合理的。考虑重新设计这个应用。重写托管代码可能比移植更划算。而且,最终的应用架构也是很清晰明了。

上一篇:C# 快速释放内存的大数组


下一篇:2018-8-10-C#-快速释放内存的大数组