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)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# 托管和非托管混合编程的更多相关文章
- C#的托管与非托管大难点
托管代码与非托管代码 众所周知,我们正常编程所用的高级语言,是无法被计算机识别的.需要先将高级语言翻译为机器语言,才能被机器理解和运行.在标准C/C++中,编译过程是这样的:源代码首先经过预处理器,对 ...
- C#的三大难点之二:托管与非托管
相关文章: C#的三大难点之前传:什么时候应该使用C#?C#的三大难点之一:byte与char,string与StringBuilderC#的三大难点之二:托管与非托管C#的三大难点之三:消息与事件 ...
- [.net 面向对象程序设计进阶] (8) 托管与非托管
本节导读:虽然在.NET编程过程中,绝大多数内存垃圾回收由CLR(公共语言运行时)自动回收,但也有很多需要我们编码回收.掌握托管与非托管的基本知识,可以有效避免某些情况下导致的程序异常. 1.什么是托 ...
- C# using 三种使用方式 C#中托管与非托管 C#托管资源和非托管资源区别
1.using指令.using + 命名空间名字,这样可以在程序中直接用命令空间中的类型,而不必指定类型的详细命名空间,类似于Java的import,这个功能也是最常用的,几乎每个cs的程序都会用到. ...
- 利用C#Marshal类实现托管和非托管的相互转换
Marshal 类 命名空间:System.Runtime.InteropServices 提供了一个方法集,这些方法用于分配非托管内存.复制非托管内存块.将托管类型转换为非托管类型,此外还提供了在与 ...
- [转]C# 之DLL调用(托管与非托管)
每种编程语言调用DLL的方法都不尽相同,在此只对用C#调用DLL的方法进行介绍.首先,您需要了解什么是托管,什么是非托管.一般可以认为:非托管代码主要是基于win 32平台开发的DLL,activeX ...
- 有关 Azure IaaS VM 磁盘以及托管和非托管高级磁盘的常见问题解答
本文将对有关 Azure 托管磁盘和 Azure 高级存储的一些常见问题进行解答. 托管磁盘 什么是 Azure 托管磁盘? 托管磁盘是一种通过处理存储帐户管理来简化 Azure IaaS VM 的磁 ...
- NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配
在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时 ...
- Oracle Data Provider for .NET的使用(托管与非托管(一))
目录 简单的概述 简单的使用 非托管系统要求 托管驱动系统要求 其它的注意事项 ODP.NET版本说明 安装ODP.NET 安装非托管驱动 非托管驱动绿色配置 简单的概述 ODP.NET的含义是 Or ...
随机推荐
- 事件冒泡和事件捕获以及解释target和currenttarget的区别
冒泡和捕获的区别是冒泡事件是先触发子元素事件,再触发父元素事件,这个是冒泡.捕获是先触发父元素事件,再触发子元素事件.简单的来说,冒泡的顺序是由内到外,捕获的顺序是由外到内 举例:<!DOCTY ...
- Linux虚拟机安装(CentOS 6.5,图文详解,需要自查)
Linux虚拟机的安装(图文详解) 下篇会接续Hadoop集群安装(以此为基础) 一.安装准备 VMWorkstation.linux系统镜像(以下以CentOS6.5为例) 二.安装过程详解 关闭防 ...
- div各种距离 详细解释图
详细博文介绍:http://blog.csdn.net/fswan/article/details/17238933
- python线程池(threadpool)模块使用笔记
一.安装与简介 pip install threadpool pool = ThreadPool(poolsize) requests = makeRequests(some_callable, li ...
- and 与 && or 与 || 的差异之处
其实就是比较他们的优先级 // --------------------// "||" 比 "or" 的优先级高 // 表达式 (false || true) ...
- 模块:jquery实现表格的隔行换色
效果图: 知识点精讲:jquery中$("tr:odd")和$("tr:even")选择器分别代表奇数行和偶数行,并且索引是从0开始,即第一行为偶数: 代码实现 ...
- Java中的Object类介绍
Object类是所有类的父类,如果一个类没有使用extends关键字明确标识继承另外一个类,那么这个类默认继承Object类. Object类中的所有方法适用于所有子类 Object中比较常见的方法: ...
- UML类图分析
继承: 实现: 关联: 依赖: 组合: 聚合:
- windows 下 webstorm 使用SVN
1.安装了webstorm之后,用了很久都没有配置SVN 现在想配置svn,结果发现一般的svn程序不好用. 经指导,发现需要安装一个专用于webstorm的SVN 2.在file->setti ...
- Servlet执行流程和生命周期【慕课网搬】
Servlet执行流程(GET方式为例) 首先用户客户端浏览器发出Get方式(点击超链接方式)向浏览器发出请求. 服务器接收到客户端点击超链接,接收到GET请求之后,服务器到WEB.xml中<s ...