hhhh2010

博客园 首页 新随笔 联系 订阅 管理

转载学习收藏,原文地址http://www.cnblogs.com/mywebname/articles/2291876.html 

背景

     在项目过程中,有时候你需要调用非C#编写的DLL文件,尤其在使用一些第三方通讯组件的时候,通过C#来开发应用软件时,就需要利用DllImport特性进行方法调用。本篇文章将引导你快速理解这个调用的过程。

 

步骤

1. 创建一个CSharpInvokeCPP的解决方案:

image

 

2. 创建一个C++的动态库项目:

image

 

3. 在应用程序设置中,选择“DLL”,其他按照默认选项:

image

最后点击完成,得到如图所示项目:

image

      我们可以看到这里有一些文件,其中dllmain.cpp作为定义DLL应用程序的入口点,它的作用跟exe文件有个main或者WinMain入口函数是一样的,它就是作为DLL的一个入口函数,实际上它是个可选的文件。它是在静态链接时或动态链接时调用LoadLibrary和FreeLibrary时都会被调用。详细内容可以参考(http://blog.csdn.net/benkaoya/archive/2008/06/02/2504781.aspx)。

 

4. 现在我们打开CSharpInvokeCPP.CPPDemo.cpp文件:

现在我们加入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CSharpInvokeCPP.CPPDemo.cpp : 定义 DLL 应用程序的导出函数。
//
 
#include "stdafx.h"
 
extern "C" __declspec(dllexport) int Add(int x, int y) 
    return x + y; 
}
extern "C" __declspec(dllexport) int Sub(int x, int y) 
    return x - y; 
}
extern "C" __declspec(dllexport) int Multiply(int x, int y) 
    return x * y; 
}
extern "C" __declspec(dllexport) int Divide(int x, int y) 
    return x / y; 
}

      extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。而被extern "C"修饰的变量和函数是按照C语言方式编译和连接的。

      __declspec(dllexport)的目的是为了将对应的函数放入到DLL动态库中。

      extern "C" __declspec(dllexport)加起来的目的是为了使用DllImport调用非托管C++的DLL文件。因为使用DllImport只能调用由C语言函数做成的DLL。

 

5. 编译项目程序,最后在Debug目录生成CSharpInvokeCPP.CPPDemo.dll和CSharpInvokeCPP.CPPDemo.lib

image

我们用反编译工具PE Explorer查看下该DLL里面的方法:

image

可以发现对外的公共函数上包含这四种“加减乘除”方法。

 

6. 现在来演示下如何利用C#项目来调用非托管C++的DLL,首先创建C#控制台应用程序:

image

 

7. 在CSharpInvokeCSharp.CSharpDemo项目上新建一个CPPDLL类,编写以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CPPDLL
{
    [DllImport("CSharpInvokeCPP.CPPDemo.dll")]
    public static extern int Add(int x, int y);
 
    [DllImport("CSharpInvokeCPP.CPPDemo.dll")]
    public static extern int Sub(int x, int y);
 
    [DllImport("CSharpInvokeCPP.CPPDemo.dll")]
    public static extern int Multiply(int x, int y);
 
    [DllImport("CSharpInvokeCPP.CPPDemo.dll")]
    public static extern int Divide(int x, int y);
}

DllImport作为C#中对C++的DLL类的导入入口特征,并通过static extern对extern “C”进行对应。

 

8. 另外,记得把CPPDemo中生成的DLL文件拷贝到CSharpDemo的bin目录下,你也可以通过设置【项目属性】->【配置属性】->【常规】中的输出目录:

image

这样编译项目后,生成的文件就自动输出到CSharpDemo中了。

 

9. 然后在Main入口编写测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void Main(string[] args)
{
    int result = CPPDLL.Add(10, 20);
    Console.WriteLine("10 + 20 = {0}", result);
 
    result = CPPDLL.Sub(30, 12);
    Console.WriteLine("30 - 12 = {0}", result);
 
    result = CPPDLL.Multiply(5, 4);
    Console.WriteLine("5 * 4 = {0}", result);
 
    result = CPPDLL.Divide(30, 5);
    Console.WriteLine("30 / 5 = {0}", result);
 
    Console.ReadLine();
}

 

运行结果:

image

方法得到调用。

10. 以上的方法只能通过静态方法对于C++中的函数进行调用。那么怎样通过静态方法去调用C++中一个类对象中的方法呢?现在我在CPPDemo项目中添加一个头文件userinfo.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UserInfo {
private:
    char* m_Name;
    int m_Age;
public:
    UserInfo(char* name, int age) 
   
        m_Name = name; 
        m_Age = age;
    }
    virtual ~UserInfo(){ }
    int GetAge() { return m_Age; }
    char* GetName() { return m_Name; }
};

 

在CSharpInvokeCPP.CPPDemo.cpp中,添加一些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "malloc.h"
#include "userinfo.h"
 
typedef struct {
    char name[32];
    int age;
} User;  
 
UserInfo* userInfo;
 
extern "C" __declspec(dllexport) User* Create(char* name, int age)    
{   
    User* user = (User*)malloc(sizeof(User));
 
    userInfo = new UserInfo(name, age);
    strcpy(user->name, userInfo->GetName());  
    user->age = userInfo->GetAge();
 
    return user; 
}

这里声明一个结构,包括name和age,这个结构是用于和C#方面的结构作个映射。

注意:代码中的User*是个指针,返回也是一个对象指针,这样做为了防止方法作用域结束后的局部变量的释放。

strcpy是个复制char数组的函数。

 

11. 在CSharpDemo项目中CPPDLL类中补充代码:

1
2
3
4
5
6
7
8
9
10
11
[DllImport("CSharpInvokeCPP.CPPDemo.dll")]
public static extern IntPtr Create(string name, int age);
 
[StructLayout(LayoutKind.Sequential)]
public struct User
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string Name;
 
    public int Age;
}

 

其中这里的结构User就和C++中的User对应。

 

12. 在Program.cs中补充代码:

1
2
3
IntPtr ptr = CPPDLL.Create("李平", 27);
<STRONG><FONT color=#ff0000>CPPDLL.User user = (CPPDLL.User)Marshal.PtrToStructure(ptr, typeof(CPPDLL.User));</FONT></STRONG>
Console.WriteLine("Name: {0}, Age: {1}", user.Name, user.Age);

 

注意:红色字体部分,这里结构指针首先转换成IntPtr句柄,然后通过Marshal.PtrToStructrue转换成你所需要的结构。

 

运行结果:

image

 

 

转自liping13599168/archive/2011/03/31/2000320.html

 

以下是调用时的一些类型转换等注意事项:

C++

struct HHFC_SET
{
char*  UID;
int     code;
 
};
 
extern "C" __declspec(dllexport) int  PReadUID(HHFC_SET* mystruct, char* lpKeyNo, LPTSTR lpKeyNo2 )
{
       //int a=5;
       CString ds="sea中国";
  //wchar_t a[] = L"sea";
  //lpKeyNo = (LPTSTR)(LPCTSTR)a;
//lpKeyNo = L'中国a';
strcpy(lpKeyNo,"中国aabbbcccc");
wcscpy(lpKeyNo2,L"中国aabbbcccc");
//lpKeyNo[0] = L'a';
//lpKeyNo[1] = L'b';
//lpKeyNo[2] = L'c';
//lpKeyNo[3] = L'\0';
//::MessageBox(NULL, lpKeyNo, L"info", MB_OK);
//  mystruct->UID=ds.GetBuffer(ds.GetLength()+1);
mystruct->UID="seaaa中国";
 
 
 
return 5;
}
 
C#
using System;
using System.Runtime.InteropServices;
using System.Text;
 
namespace PInvoke
{
    /// <summary>
    /// Ivoke 的摘要说明。
    /// </summary>
    ///
    public class Ivoke
    {
        [DllImport("standerMFC.dll", EntryPoint = "PReadUID", CharSet = CharSet.Ansi)]
        //nPort:1代表COM1,返回-1代表已经打开COM PORT失败,0代表COM已经打开,返回其它值表示打开对应的COM
        public static extern int PReadUID(ref HHFC_SET stru,
            [MarshalAs(UnmanagedType.LPStr)] StringBuilder sb,
            [MarshalAs(UnmanagedType.LPWStr)] StringBuilder sb2);
 
 
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct HHFC_SET
    {
        [MarshalAs(UnmanagedType.LPStr)]
        public String Uid;
 
        [MarshalAs(UnmanagedType.I4)]
        public int code;
 
    }
}
 
namespace PInvoke
{
/// <summary>
/// Class1 的摘要说明。
/// </summary>
class Class1
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: 在此处添加代码以启动应用程序
//
 
HHFC_SET stru=new HHFC_SET ();
 
stru.Uid="";
 
       Console.WriteLine(stru.Uid);
            StringBuilder sb = new StringBuilder(80);
            StringBuilder sb2 = new StringBuilder(80);
            int a = Ivoke.PReadUID(ref stru, sb, sb2);
            //Ivoke.GetUsbkeyNo();
Console.WriteLine(stru.Uid);
 
 
 
Console.Read();
}
}
}
注意:
char*  用 UnmanagedType.LPStr
wchar_t* 用 UnmanagedType.LPWStr
 
C#中调用非托管的DLL及参数传递本篇文章来源于:开发学院 http://edu.codepub.com   原文链接:http://edu.codepub.com/2010/0531/23111.php
微软的.NET框架的优点之一是它提供了独立于语言的开发平台。你可以在VB、C++、C#等语言中编写一些类,而在其它语言中使用(源于.NET中使用了CLS),你甚至可以从另一种语言编写的类中继承。但是你要是想调用以前的非托管DLL,
微软的.NET框架的优点之一是它提供了独立于语言的开发平台。你可以在VB、C++、C#等语言中编写一些类,而在其它语言中使用(源于.NET中使用了CLS),你甚至可以从另一种语言编写的类中继承。但是你要是想调用以前的非托管DLL,那又会怎么样呢?你必须以某种方式将.NET对象转换为结构体、char *、函数指针等类型。这也就是说,你的参数必须被marshal(注:不知道中文名称该叫什么,英文中指的是为了某个目的而组织人或事物,参见这里,此处指的是为了调用非托管函数而进行的参数转换)。
 
    C#中使用DLL函数之前,你必须使用DllImport声明要调用的函数:
 
public class Win32 {
  [DllImport("User32.Dll")]
  public static extern void SetWindowText(int h, String s);
  // 函数原型为:BOOL SetWindowText(HWND hWnd, LPCTSTR lpString);
}    DllImport告诉编译器被调函数的入口在哪里,并且将该入口绑定到类中你声明的函数。你可以给这个类起任意的名字,我给它命名为Win32。你甚至可以将类放到命名空间中,具体参见图一。要编译Win32API.cs,输入:
 
csc /t:library /out:Win32API.dll Win32API.cs    这样你就拥有了Win32API.dll,并且你可以在任意的C#项目中使用它:
 
using Win32API;
int hwnd = // get it...
String s = "I'm so cute."
Win32.SetWindowText(hwnd, s);    编译器知道去user32.dll中查找函数SetWindowText,并且在调用前自动将String转换为LPTSTR (TCHAR*)。很惊奇是吧!那么.NET是如何做到的呢?每种C#类型有一个默认的marshal类型,String对应LPTSTR。但你若是试着调用GetWindowText会怎么样呢(此处字符串作为out参数,而不是in参数)?它无法正常调用,是因为String是无法修改的,你必须使用StringBuilder:
 
using System.Text; // for StringBuilder
public class Win32 {
  [DllImport("user32.dll")]
  public static extern int GetWindowText(int hwnd,
    StringBuilder buf, int nMaxCount);
  // 函数原型:int GetWindowText(HWND hWnd, LPTSTR lpString, int nMaxCount);
}    StringBuilder默认的marshal类型是LPTSTR,此时GetWindowText可以修改你的字符串:
 
int hwnd = // get it...
StringBuilder cb = new StringBuilder(256);
Win32.GetWindowText(hwnd, sb, sb.Capacity);    如果默认的类型转换无法满足你的要求,比如调用函数GetClassName,它总是将参数转换为类型LPSTR (char *),即便在定义Unicode的情况下使用,CLR仍然会将你传递的参数转换为TCHAR类型。不过不用着急,你可以使用MarshalAs覆盖掉默认的类型:
 
[DllImport("user32.dll")]
public static extern int GetClassName(int hwnd,
  [MarshalAs(UnmanagedType.LPStr)] StringBuilder buf,
  int nMaxCount);
  // 函数原型:int GetClassNameA(HWND hWnd, LPTSTR lpClassName, int nMaxCount);    这样当你调用GetClassName时,.NET将字符串作为ANSI字符传递,而不是宽字符。
 
    结构体和回调函数类型的参数又是如何传递的呢?.NET有一种方法可以处理它们。举个简单的例子,GetWindowRect,这个函数获取窗口的屏幕坐标,C++中我们这么处理:
 
// in C/C++
RECT rc;
HWND hwnd = FindWindow("foo",NULL);
::GetWindowRect(hwnd, &rc);   你可以使用C#结构体,只需使用另外一种C#属性StructLayout:
 
[StructLayout(LayoutKind.Sequential)]
public struct RECT {
  public int left;
  public int top;
  public int right;
  public int bottom;
}    一旦你定义了上面的结构体,你可以使用下面的函数声明形式 :
 
[DllImport("user32.dll")]
public static extern int
  GetWindowRect(int hwnd, ref RECT rc);
  // 函数原型:BOOL GetWindowRect(HWND hWnd, LPRECT lpRect);
    使用ref标识很重要,以至于CLR(通用语言运行时)将RECT变量作为引用传递到函数中,而不是无意义的栈拷贝。定义了GetWindowRect之后,你就可以采用下面的方式调用:
 
RECT rc = new RECT();
int hwnd = // get it ...
Win32.GetWindowRect(hwnd, ref rc);    注意你同样需要像声明中的那样使用ref关键字。C#结构体默认的marshal类型是LPStruct,因此没有必要使用MarshalAs。但如果你使用了类RECT而不是结构体RECT,那么你必须使用如下的声明形式:
 
// if RECT is a class, not struct
[DllImport("user32.dll")]
public static extern int
  GetWindowRect(int hwnd,
    [MarshalAs(UnmanagedType.LPStruct)] RECT rc);    C#和C++一样,一件事情有很多中实现方式。System.Drawing中已经有Rectangle结构体,用来处理矩形,那有为什么要“重新发明轮子”呢?
 
[DllImport("user32.dll")]
public static extern int GetWindowRect(int hwnd, ref Rectangle rc);    最后,又是怎样从C#中传递回调函数到非托管代码中的呢?你所要做的就是委托(delegate)。
 
delegate bool EnumWindowsCB(int hwnd, int lparam);    一旦你声明了你的回调函数,那么你需要调用的函数声明为:
 
[DllImport("user32")]
public static extern int
  EnumWindows(EnumWindowsCB cb, int lparam);
  // 函数原型:BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
    由于上面的委托仅仅是声明了委托类型,你需要在你的类中提供实际的回调函数代码。
 
// in your class
public static bool MyEWP(int hwnd, int lparam) {
  // do something
  return true;
}    然后传递给相应的委托变量:
 
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, 0);    你可能注意到参数lparam。在C语言中,如果你传递参数LPARAM给EnumWindows,Windows将它作为参数调用你的回调函数。通常lparam是包含了你要做的事情的上下文结构体或类指针,记住,在.NET中没有指针的概念!那该怎么做呢?上面的例子中,你可以申明lparam为IntPtr类型,并且使用GCHandle来封装它:
 
// lparam is IntPtr now
delegate bool EnumWindowsCB(int hwnd,     IntPtr lparam);
// wrap object in GCHandle
MyClass obj = new MyClass();
GCHandle gch = GCHandle.Alloc(obj);
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
   Win32.EnumWindows(cb, (IntPtr)gch);
   gch.Free();    不要忘了使用完之后手动释放它!有时,你需要按照以前那种方式在C#中释放内存。可以使用GCHandle.Target的方式在你的回调函数中使用“指针”。
 
public static bool MyEWP(int hwnd, IntPtr param) {
  GCHandle gch = (GCHandle)param;
  MyClass c = (MyClass)gch.Target;
  // ... use it
  return true;
}      图2是将EnumWindows封装到数组中的类。你只需要按如下的方式使用即可,而不要纠结于委托和回调中。
 
WindowArray wins = new WindowArray();
foreach (int hwnd in wins) {
// do something
}    关于委托代码和非委托代码之间交互的更多内容,你可以参考.NET文档中的“平台调用教程”。
 

C#调用dll时的类型转换总结


C++(Win 32)

C#

char**

作为输入参数转为char[]通过Encoding类对这个string[]进行编码后得到的一个char[]

作为输出参数转为byte[]通过Encoding类对这个byte[]进行解码,得到字符串

C++ Dll接口:

void CplusplusToCsharp(in char** AgentID, out char** AgentIP);

C#中的声明:

[DllImport("Example.dll")]

public static extern void CplusplusToCsharp(char[] AgentID, byte[] AgentIP);

C#中的调用:

Encoding encode = Encoding.Default;

byte[] tAgentID;

byte[] tAgentIP;

string[] AgentIP;

tAgentID = new byte[100];

tAgentIP = new byte[100];

CplusplusToCsharp(encode.GetChars(tAgentID), tAgentIP);

AgentIP[i] = encode.GetString(tAgentIP,i*Length,Length);

Handle

IntPtr

Hwnd

IntPtr

int*

ref int

int&

ref int

void*

IntPtr

unsigned char*

ref byte

BOOL

bool

DWORD

int uintint 更常用一些)

枚举类型

Win32

BOOL MessageBeep(UINT uType // 声音类型); 其中的声音类型为枚举类型中的某一值。

C#

用户需要自己定义一个枚举类型:

public enum BeepType

{

  SimpleBeep = -1,

  IconAsterisk = 0x00000040,

  IconExclamation = 0x00000030,

  IconHand = 0x00000010,

  IconQuestion = 0x00000020,

  Ok = 0x00000000,

}

C#中导入该函数:

[DllImport("user32.dll")]

public static extern bool MessageBeep(BeepType beepType);

C#中调用该函数:

MessageBeep(BeepType.IconQuestion);

结构类型

Win32

使用结构指针作为参数的函数:

BOOL GetSystemPowerStatus(

 LPSYSTEM_POWER_STATUS lpSystemPowerStatus

);

Win32中该结构体的定义:

typedef struct _SYSTEM_POWER_STATUS {

BYTE  ACLineStatus;

BYTE  BatteryFlag;

BYTE  BatteryLifePercent;

BYTE  Reserved1;

DWORD BatteryLifeTime;

DWORD BatteryFullLifeTime;

} SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;

C#

用户自定义相应的结构体:

struct SystemPowerStatus

{

  byte ACLineStatus;

  byte batteryFlag;

  byte batteryLifePercent;

  byte reserved1;

  int batteryLifeTime;

  int batteryFullLifeTime;

}

C#中导入该函数:

[DllImport("kernel32.dll")]

public static extern bool GetSystemPowerStatus(

  ref SystemPowerStatus systemPowerStatus);

C#中调用该函数:

SystemPowerStatus sps;

….sps初始化赋值……

GetSystemPowerStatus(ref sps);

字符串

对于字符串的处理分为以下几种情况:

1、  字符串常量指针的处理(LPCTSTR),也适应于字符串常量的处理,.net中的string类型是不可变的类型。

2、  字符串缓冲区的处理(char*),即对于变长字符串的处理,.netStringBuilder可用作缓冲区

Win32

BOOL GetFile(LPCTSTR lpRootPathName);

C#

函数声明:

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]

static extern bool GetFile (

 [MarshalAs(UnmanagedType.LPTStr)]

 string rootPathName);

函数调用:

string pathname;

GetFile(pathname);

备注:

DllImport中的CharSet是为了说明自动地调用该函数相关的Ansi版本或者Unicode版本

 

变长字符串处理:

C#

函数声明:

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]

public static extern int GetShortPathName(

  [MarshalAs(UnmanagedType.LPTStr)]

  string path,

  [MarshalAs(UnmanagedType.LPTStr)]

  StringBuilder shortPath,

  int shortPathLength);

函数调用:

StringBuilder shortPath = new StringBuilder(80);

int result = GetShortPathName(

@"d:\test.jpg", shortPath, shortPath.Capacity);

string s = shortPath.ToString();

struct

具有内嵌字符数组的结构:

Win32

typedef struct _TIME_ZONE_INFORMATION {

  LONG    Bias;

  WCHAR   StandardName[ 32 ];

  SYSTEMTIME StandardDate;

  LONG    StandardBias;

  WCHAR   DaylightName[ 32 ];

  SYSTEMTIME DaylightDate;

  LONG    DaylightBias;

} TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;

C#

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

struct TimeZoneInformation

{

  public int bias;

  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]

  public string standardName;

  SystemTime standardDate;

  public int standardBias;

  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]

  public string daylightName;

  SystemTime daylightDate;

  public int daylightBias;

}

具有回调的函数

Win32

BOOL EnumDesktops(

 HWINSTA hwinsta,       // 窗口实例的句柄

 DESKTOPENUMPROC lpEnumFunc, // 回调函数

 LPARAM lParam        // 用于回调函数的值

);

回调函数DESKTOPENUMPROC的声明:

BOOL CALLBACK EnumDesktopProc(

 LPTSTR lpszDesktop, // 桌面名称

 LPARAM lParam    // 用户定义的值

);

C#

将回调函数的声明转化为委托:

delegate bool EnumDesktopProc(

 [MarshalAs(UnmanagedType.LPTStr)]

  string desktopName,

  int lParam);

该函数在C#中的声明:

[DllImport("user32.dll", CharSet = CharSet.Auto)] static extern bool EnumDesktops(   IntPtr windowStation,   EnumDesktopProc callback,   int lParam);

该表对C#中调用win32函数,以及c++编写的dll时参数及返回值的转换做了一个小的总结,如果想进一步了解这方面内容的话,可以参照msdn中“互操作封送处理”一节。

posted on 2014-03-20 16:44  hhhh2010  阅读(605)  评论(0编辑  收藏  举报