C#中使用OpenGL:(二)C#调用C/C++的dll

在C#中使用OpenGL图形库为业余的图形编程人员提供了很大的便利,可是官方并没有向用户提供C#版本的OpenGL图形接口,在民间有好一些人开发了C#版的OpenGL接口,使之能够在C#中使用。这些第三方的C#版OpenGL应该说用起来还是不错的,如果说有什么缺点的话,那应该是这些OpenGL的版本都不是最新的,一般在4.0以下,而现在OpenGL都4.6版本了。如果要使用最新的OpenGL图形接口,那还得自己多动动手。

C#如何利用OpenGL?

没有C#版本的OpenGL,要想在C#中使用OpenGL,可有两种方法:从opengl32.dll中获取函数接口或者直接从硬件驱动中获取函数指针。
如果只利用1.1版本的OpenGL,可以选择调用opengl32.dll里面的300多条函数。opengl32.dll可以在windows系统的系统盘里找到,其中的函数都是调用约定为stdcall的C函数;
如果要使用高版本的OpenGL,则需要到硬件驱动里获取函数指针。windows系统只支持1.1版本的OpenGL,而1.2之后的版本都不再支持,这意味着我们不能通过opengl32.dll这个库去调用新版本的函数,但是可以利用opengl32.dll中的wglGetProcAddress函数从显卡驱动中获取OpenGL函数指针,然后通过函数指针来调用相应的函数。
不管怎么样,要想在C#中使用OpenGL,调用opengl32.dll这个C语言动态链接库是在所难免的。因此特地花一些时间来研究C#如何调用C/C++函数。

C#如何调用C/C++的dll?

C#调用dll的方法一般由两种,分别是静态调用和动态调用。

C#静态调用dll的方法:

首先,引用名称空间System.Runtime.InteropServices。
其次,要声明一个外部的方法,其基本形式如下:

[DLLImport("XXX.dll")]修饰符 extern 返回变量类型 方法名称 (参数列表)

其中:
1.DLLImport:这是必不可少的,而且必须要有中括号“[]”括起来,它有若干可选的参数,叫DllImportAttribute。它至少有一个参数,这个必需的参数是dll的文件名。它也可以有多个参数,这些参数可以全部写,也可以只写一部分,要看具体情况。这些参数分别是:

*CharSet 指示用在入口点中的字符集,如:CharSet=CharSet.Ansi;
*SetLastError 指示方法是否保留 Win32"上一错误"SetLastError=true;
*ExactSpelling 指示 EntryPoint 是否必须与指示的入口点的拼写完全匹配,如:ExactSpelling=false;
*PreserveSig指示方法的签名应当被保留还是被转换, 如:PreserveSig=true;
*CallingConvention指示入口点的调用约定, 如:CallingConvention=CallingConvention.Cdec。
*EntryPoint指明入口点,必须是dll中实际的函数名,该项不写的话,那么方法名称必须要与dll中的函数名称一致。

2.修饰符:访问修饰符,除了abstract以外,声明方法时可以使用的任意一种修饰符。如static ,private,public等。一般情况下是public+static一起使用。
3.返回变量类型:在DLL文件中你需调用方法的返回变量类型。
4.方法名称:在DLL文件中你需调用方法的名称。
5.参数列表:在DLL文件中你需调用方法的列表。

例子:

//引用名称空间
System.Runtime.InteropServices;

//导入外部函数
[DllImport("opengl32.dll",ExactSpelling =false,EntryPoint = "glBegin",CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern  void glBegin(uint mode);

//指明了函数入口点,则方法名可以与函数名不一致。
[DllImport("opengl32.dll",ExactSpelling =false,EntryPoint = "glEnd",CharSet = CharSet.Auto,CallingConvention = CallingConvention.StdCall)]
public static extern  void End();

注意事项:
1.常见的错误是没有引用名称空间System.Runtime.InteropServices。
2.来自外部的函数,修饰符中必须要有static修饰。
3.调用约定必须要与dll文件中函数的调用约定一致,VC/VS一般默认的调用约定为Cdec,C#中默认的调用约定也为Cdec,如果dll文件的函数使用的是其他的调用约定,则C#中必须指明。OpenGL函数的调用约定是StdCall,因此CallingConvention = CallingConvention.StdCall。
4.C#可以顺利地调用C语言的dll,一般只要调用约定和入口点写得正确,调用是没有问题的。但如果dll是由C++生成的,那么就略显麻烦。由于C++的函数编译为dll时,函数名称会被改变,导致在C#中使用时会出现找不到入口点。解决办法是,使用depends工具查看dll中函数的名称,然后在C#将EntryPoint指定为该函数在dll中的名称。比如Add函数,用C++编译为dll时,它在dll中的名称是?Add@@YAHHH@Z,而不再是Add,因此必须设定EntryPoint = “?Add@@YAHHH@Z”。

C#动态调用dll的方法:

动态调用的好处就是需要时装载,不需要时可以释放。动态调用C的dll需要用到两个winAPI,分别是LoadLibrary和GetPrcAddress。它们的功能分别是将dll文件载入到内存和从内存中的dll中获取函数指针。除了要用到winAPI之外,还要用到C#的委托。使用winAPI获取函数指针,然后用委托来执行函数。下面通过示例代码来说明。

(1)首先声明外部函数

//参数为string类型,字符集应设为CharSet = CharSet.Ansi。
[DllImport("kernel32.dll",ExactSpelling =false,EntryPoint = "Loadlibrary",CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static IntPtr Loadlibrary(string fileName);

[DllImport("kernel32.dll",ExactSpelling =false,EntryPoint = "GetProcAddress",CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static IntPtr GetProcAddress(IntPtr hmodule,string functionName);

(2)声明委托(以glBegin函数为例)

internal delegate void FUNC(uint mode);

(3)动态调用opengl32.dll中的glBegin函数
声明了外部函数和委托之后,就可以动态调用dll了。

//载入opengl32.dll到内存
IntPtr hmodule=LoadLibrary("opengl32.dll");
//获得opengl32.dll中的glBegin函数指针
IntPtr funcPointer=GetProcAddress(hmodule,"glBegin");
//将函数指针转换为委托
FUNC glBegin=Marshal.GetDelegateForFunctionPointer(funcPointer,typeof(FUNC));
//执行函数
glBegin(mode);

C#调用C/C++函数时的参数传递问题

下表是C#与C/C++的参数类型的对应关系

C# C/C++
byte unsigned char
sbyte char
short short
ushort、char unsigned short
int int、long
uint unsigned int、unsigend long
long long long
ulong unsigned long long
float flaot
double double
decimal
bool bool
byte[] const char*、char[]
数组 数组
结构体 结构体
数组 指向内存块的指针
一个元素的数组 指向一个基本变量的指针
IntPtr 对象句柄、指针

由于C#一般不用指针,C#中的函数参数传递遇到指针,可用数组替代。若函数返回值是指针,则不能用用数组接收,而要用C#的IntPtr类型的变量接收。IntPtr类型是用来代表指针或句柄的平台特定类型,实际上就相当于C语言的指针。C#可以接收C语言函数返回的指针,但不能通过指针来操作内存,所以,C#即使获取了指针,似乎也没什用。
当然,对于一个熟练地使用C语言的程序员来说,不用指针就好像缺了什么东西一样。实际上,指针真的很好用,微软也是知道的,于是给C#留了一个后门。特殊情况下,C#也是可以像C语言一样使用指针。只要给使用指针的代码块用关键字unsafe标识,并且在工程中菜单栏“项目->属性”页面中勾选“允许不安全代码”,就可使用指针了。具体操作,可以参考百度经验:C#使用指针(不安全代码)

例子:

C语言中的函数定义

#include<stdlib.h>
//结构体
typedef struct MyStruct
{
    int x;
    int y;
}MyStruct;

//函数FUNC1,参数是数组,返回值是指针
_declspec(dllexport)int * FUNC1(int A[])
{
    int n;
    n = sizeof(A);
    int *B = (int*)malloc(n * sizeof(int));
    for (int i = 0;i < n;i++)
    {
        B[i] = A[i] + 1;
    }
    return B;
}
//函数FUNC2,参数有两个指针
_declspec(dllexport)void FUNC2(int *A,int n,int *B)
{
    for (int i = 0;i < n;i++)
    {
        B[i] = A[i] + 1;
    }
}
//函数FUNC3,参数是两个数组
_declspec(dllexport)void FUNC3(int A[],int B[])
{
    int n;
    n = sizeof(A);
    for (int i = 0;i < n;i++)
    {
        B[i] = A[i] + 1;
    }
}
//函数FYNC4,参数和返回值都是结构体
_declspec(dllexport)MyStruct FUNC4(MyStruct s)
{
    MyStruct *B = (MyStruct*)malloc(sizeof(MyStruct));
    (*B).x = s.x + 1;
    (*B).y = s.y + 1;
    return *B;
}

C#不使用指针调用和C语言的函数:

[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC1", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr FUNC1(Int32[] a);

[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC2", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern void FUNC2(Int32[] a, Int32 n, Int32[] b);

[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC3", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern void FUNC3(Int32[] a, Int32[] b);

[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC4", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
 public static extern MYSTRUCT FUNC4(MYSTRUCT S);

C#使用指针调用C语言的函数:

unsafe{
[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC1", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern int* FUNC1(int[] a);

[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC2", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern void FUNC2(int* a, Int32 n,int* b);

[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC3", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern void FUNC3(Int32[] a, Int32[] b);

[DllImport("mydll.dll", ExactSpelling = false, EntryPoint = "FUNC4", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
 public static extern MYSTRUCT FUNC4(MYSTRUCT S);
}

结语

本文也没有真正进入“开发C#版OpenGLj接口”这个主题中,只是一个前期的技术储备。后期开发C#版的OpenGL接口,必然会用到以上这些知识。俗话说,磨刀不误砍柴工,有好的准备能很好地进入主题,所以在真正大刀阔斧开始干的时候,会花上一段时间进行技术储备。下一篇文章,将介绍如何将一个.lib文件直接编译为.dll文件。

上一篇:C#中使用OpenGL:(一)前面的话
下一篇:C#中使用OpenGL:(三)将.lib文件编译为.dll文件

猜你喜欢

转载自blog.csdn.net/qq_28249373/article/details/77191934