C#中使用OpenGL:(六)C#中调用C函数时的参数传递问题

来源:互联网 发布:iphone5越狱软件 编辑:程序博客网 时间:2024/05/17 19:20

C#中调用C函数,除了需要在C#中声明被调函数之外,还要考虑到参数传递的问题。虽然我在之前两篇文章中已经提到过如在C#中向C函数传递参数,但是在调用OpenGL函数时,仍然遇到不少难题,特别是关于指针方面。我试图在网络上搜索相关的方法,然而让人失望是,很多人的给出的是“为什么一定要在C#中使用指针呢?”之类的答案。额……,不是我偏爱指针,如果不是迫不得已,谁会在C#中使用指针呢! 为了解决C#调用OpenGL函数时的参数传递问题,我特地研究了两天,下面是一些成果。我的方法也许不是最好的,但基本能解决大部分问题。*

OpenGL函数的参数类型

总结了一下,OpenGL函数的参数类型有如下:

1.基本数据类型

int 、unsigned int、short、unsigned short、char、unsigned char、float、double。

2.复杂数据类型

数组、指针、二重指针、字符串、结构体、函数指针、空类型指针(void*)、句柄。

基本数据类型作为参数传递

基本数据类型作为参数传递比较容易,只要用C#相对应的数据类型替换即可。下表是C语言基本数据类型与C#基本数据类型的对应关系。

C语言 C#语言 int int unsigned int uint short short/char unsigned short ushort char sbyte unsigned char byte float float double double

复杂数据类型作为参数传递

1.被调C函数参数为数组

C语言的数组与C#数组是相对应的。C语言函数以一维数组为参数,则C#也是传递一维数组;若C语言函数以二维数组为参数,则C#要传递二维数组。
例如:

C语言函数

//以一维数组为参数_declspec(dllexport)void FUNC1(int A[]){    int n=sizeof(A)/4;    for (int i = 0;i< n;i++)    {               printf("%d ", A[i]);    }}//以二维数组为参数_declspec(dllexport)void FUNC2(int A[][2]){    for(int i=0;i<2;i++)        for(int j=0;j<2;j++)            printf("%d ", A[i][j]);}

C#调用C函数代码

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;//调用C语言的dll需要引用此命名空间namespace Csharp调用C函数的参数传递{    class Program    {        //外部函数声明        [DllImport("mydll.dll", EntryPoint = "FUNC1", ,CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC1(int[] A);        [DllImport("mydll.dll", EntryPoint = "FUNC2", CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC2(int[,] A);        static void Main(string[] args)        {            int[] A1 = new int[4] { 1, 2, 2, 3 };            int[,] A2 = new int[2, 2] { { 1,2}, { 3,4} };            FUNC1(A1);            FUNC2(A2);        }    }}

2.被调C函数的参数为普通指针

当C函数的参数为普通指针时,有两种情况,分别是“指向一般变量的指针”和“指向内存块的指针”。当然,这两种类型在C语言是没有区别的,都是指向首地址嘛。在C#中调用C函数时,才有稍稍的不同。
(1)指向一个变量的指针
如果C函数的参数一个指向单个变量的指针,则在C#中可以将一个变量的引用(ref)传递过去,或者把一个只有一个元素的数组传过去。
例如:
C函数代码

_declspec(dllexport)void FUNC3(int *A){    int c;    c = *A;    printf("%d ", c);}

C#调用C函数代码

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;//调用C语言的dll需要引用此命名空间namespace Csharp调用C函数的参数传递{    class Program    {        //传递变量的引用        [DllImport("mydll.dll", EntryPoint = "FUNC3", ,CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC3(ref int A);        /*传递单个元素的数组        [DllImport("mydll.dll", EntryPoint = "FUNC3", CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC3(int[] A);        */        static void Main(string[] args)        {            int[] A = new int[1]{1};            int d=1;            FUNC3(ref d);            /*或者            FUNC3(A);            */        }    }}

(2)指向一块内存的指针
C语言的普通指针可用C#的一维数组替代。int *p 作为参数,则C#中调用要传递int[] A。
例如:
C语言函数代码

_declspec(dllexport)void FUNC4(int *A,int n){    for(int i=0;i<n;i++)    printf("%d ", A[i]);}

C#调用C函数代码

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;//调用C语言的dll需要引用此命名空间namespace Csharp调用C函数的参数传递{    class Program    {        [DllImport("mydll.dll", EntryPoint = "FUNC4", ,CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC4(int[] A,int n);        static void Main(string[] args)        {            int[] A = new int[4]{1,2,3,4};            FUNC3(A,4);        }    }}

3.被调C函数的参数为结构体

如果被调C函数的参数是简单的结构体(不含数组和指针成员),那么和C#的结构体也是可以对应的。如果结构体包含了数组和指针成员,则要用到不安全代码才能解决。
例如:
C函数代码

struct MyStruct{    int x;    double y;}_declspec(dllexport)void FUNC5(struct MyStruct s){    printf("%d %lf", s.x,s.y);}

C#调用C语言函数

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;//调用C语言的dll需要引用此命名空间namespace Csharp调用C函数的参数传递{    struct myStruct    {        public int x;        public  double y;    };    class Program    {        [DllImport("mydll.dll", EntryPoint = "FUNC5", ,CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC5(int[] A,int n);        static void Main(string[] args)        {            myStruct S;            s.x=1;            s.y=12.0;            FUNC5(S);        }    }}

如果结构体包含数组成员,需要使用不安全代码。VS编译器要先勾选“允许使用不安全”才能编译通过。先在VS的菜单栏上的“项目->属性->生成”页面勾选“允许不安全代码”,然后在代码中使用unsafe关键字对不安全代码块进行标识。
例如:
C函数代码

struct myStruct{    int n;    int data[10];};_declspec(dllexport)void FUNC6(struct myStruct s){    for(int i=0;i<s.n;i++)        printf("%d ",s.data[i]);}

C#调用C函数代码

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;//调用C语言的dll需要引用此命名空间namespace Csharp调用C函数的参数传递{    unsafe struct myStruct    {        public int n;        public  fixed int data[10];//fixed关键字表示分配固定的空间    }    class Program    {        [DllImport("mydll.dll", EntryPoint = "FUNC6", ,CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC6(myStruct s);        static void Main(string[] args)        {            unsafe//不安全代码块            {                myStruct S;                S.n=10;                for(inti=0;i<10;i++)S.data[i];                FUNC6(S);            }    }}

4.被调C函数的参数为二重指针

C语言的普通指针,在C#中可用一维数组替代(int[] A),但C语言的二重指针,在C#中不能用二维数组替代(int[,] A)。C语言的二重指针应该用C#的“数组的数组”,即交错数组来替代(int[][] A)。C#调用含有二重指针的C函数,我没有发现更好的办法,只能使用不安全代码,使用指针,即使是是这样,仍然感觉很麻烦。下面用一个例子来说明。

C函数代码

_declspec(dllexport)void FUNC7(int **A, int row,int col){    for (int i = 0;i<row;i++)        for(int j=0;j<col;j++)            printf("%d ", A[i][j]);}

C#调用C函数代码

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;namespace Csharp调用C函数的参数传递{    class Program    {        [DllImport("mydll.dll", EntryPoint = "FUNC7", CallingConvention = CallingConvention.Cdecl)]        public static extern void FUNC7(IntPtr[] p,int row,int col);        static void Main(string[] args)        {            //交错数组          int[][] a = new int[2][] { new int[2] { 1, 2 }, new int[2] { 3, 4 } };            unsafe            {                //IntPtr类型可等同于指针                IntPtr[] ptr = new IntPtr[2];                //循环将数组的指针存到IntPtr类型的数组中                for (int i = 0; i < 2; i++)                {                    fixed (int* p = a[i])//取得数组指针必须要用fixed关键字先将数组内存固定                    {                        ptr[i] = (IntPtr)p;                    }                }                FUNC7(ptr, 2, 2);            }        }    }}

5.被调C函数的参数为空类型的指针(void*)

在C语言中,参数为空类型的指针,意味可以接受任何类型的指针。由于C语言没有函数重载这种功能,不能定义几个同名的函数,只能使用空类型的指针来接收任意类型的指针,以实现类似于C++中的函数重载的功能。在OpenGL中,有好多函数都是以空类型指针作为参数的。如:

//C语言函数void  glGetTexImage (GLenum target, GLint level, GLenum format, GLenum type, void *pixels);

参数pixels事实上可以是char类型的,也可以是float类型的,具体是什么类型的,由参数type决定。如果type为GL_CHAR,则函数按照char类型指针处理,如果是GL_FLOAT,则按照float类型处理。
如果是C++,可以利用函数重载来将一个函数,分解为几个同名的,但参数类型不一样的函数,这样就不需要参数type来限定pixels为何种类型的指针了。
如:

//C++函数重载void  glGetTexImage (GLenum target, GLint level, GLenum format,  GLchar *pixels);void  glGetTexImage (GLenum target, GLint level, GLenum format,  GLfloat *pixels);

C#和C++一样都是面向对象的语言,也有函数的重载。因此,我用函数重载的方式,将一个函数分解为几个同名的函数来解决空类型(void*)指针的问题。
例子:

C函数代码

//pixels可以是char、float或者short类型的指针void  glGetTexImage (GLenum target, GLint level, GLenum format, GLenum type, void *pixels);

C#调用C函数

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;namespace Csharp调用C函数的参数传递{    class Program    {//函数重载一[DllImport("opengl32.dll", EntryPoint = "glGetTexImage ",CallingConvention=CallingConvention.StdCall)] public static extern void glGetTexImage (uint target,int level,uint format,uint type,byte[] pixels);  //函数重载二 [DllImport("opengl32.dll", EntryPoint = "glGetTexImage",CallingConvention=CallingConvention.StdCall)] public static extern void glGetTexImage (uint target,int level,uint format,uint type,short[] pixels);   //函数重载三[DllImport("opengl32.dll", EntryPoint = "glGetTexImage ",CallingConvention=CallingConvention.StdCall)] public static extern void glGetTexImage (uint target,int level,uint format,uint type,float[] pixels);        static void Main(string[] args)        {          int[] A = new int[100];          short[] B = new short[100];          float[] C = new float[100];          glGetTexImage(1,0,1,0,A);          glGetTexImage(1,0,2,1,B);          glGetTexImage(1,0,3,2,C);        }    }}

6.被调C函数的参数为函数指针

C#中接收和传递函数指针都可以用Intptr类型的变量。即使是取得函数指针,也不能直接在C#中通过函数指针调用函数,而要通过委托来执行。关于函数指针,在OpenGL函数的参数中出现机会很少,但在调用高版本的OpenGL函数(1.3版本以上)总是需要通过函数指针来调用,后面会经常用到,所以要好好研究研究。下面的例子是通过wglGetAddress函数获取OpenGL函数指针,然后在C#中调用。

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;namespace Csharp调用C函数的参数传递{    class Program    {        [DllImport("opengl32.dll", EntryPoint= "wglGetAddress",CharSet=CharSet.Ansi CallingConvention = CallingConvention.StdCall)]        public static extern IntPtr wglGetAddress(string funcName);        //声明一个委托         delegate void Color3f(float r,float g,float b);        static void Main(string[] args)        {            //定义函数指针            IntPtr funcPointer;            //定义委托            delegate glColor3f;            //获得glColor3f的函数指针呢            funcPointer=wglGetAdress("glColor3f");            Type t=typeof(Color3f);               //将函数指针转变为委托            glColor3f=Marshal.GetDelegateForFunctionPointer(funcPointer,t);            //通过委托来执行函数            glColor3f(0.5f,0.5f,0.5f);           }    }}

7.被调函数的参数或返回值为对象句柄

这个对象句柄,可能是函数指针,或者窗口句柄,或者其他的模块句柄。在winAPI中,经常用到的是窗句柄(HWND),设备上下文句柄(HDC)等等。这种句柄类的变量,都可以用IntPtr类型来接收。例如:

winAPI

HDC GetDC(HWND win);

C#调用winAPI

//声明外部函数[DllImport("gdi32.dll", EntryPoint = "GetDC",CallingConvention=CallingConvention.StdCall)] public static extern IntPtr GetDC(IntPtr handle);//获得pictureBox的设备上下文IntPtr hdc;hdc=GetDC(pictureBox1.handle);

8.被调C函数的参数为字符串

C语言的字符串一般用const char*或者char str[]表示,C#中表示字符串的是string或者char[]。由于C语言的char类型是一个字节的(Ansi),而C#的char类型是两个字节的,string类型的元素也是两个字节的(Unicode),因此不能直接将C#的char[]和string直接作为参数传到C函数中。要想正确传递字符串,可以有以下两种方法。
(1)使用string和char[]作为参数传递
string类型和char[]字符数组是可以将字符串传递给C函数的,只不过在传递的时候需要在C#中将字符集(CharSet)设定为ansi,这样系统就知道应当把char类型处理成一个字节的,而不是两个字节。
例子:
C函数代码

_declspec(dllexport)void FUNC8(const char* str){    printf("%s", str);}

C#调用C函数代码

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;namespace Csharp调用C函数的参数传递{    class Program   {   [DllImport("mydll.dll",EntryPoint="FUNC8",CharSet=CharSet.Ansi,CallingConvention=CallingConvention.cdecl)]  public static extern void FUNC8 (sting str);        static void Main(string[] args)        {            string str="hello world";            FUNC8(str);        }    }}

(2)使用byte类型的数组作为字符串传递(byte[])
C#的byte类型是一个字节的,与C语言的char类型刚好对应,因此完全可以用byte替代C的char。在C#中可以先将string,char[]转为byte[],然后再往C函数中传送。
例子:
C函数代码

_declspec(dllexport)void FUNC8(const char* str){    printf("%s", str);}

C#调用C函数代码

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.InteropServices;namespace Csharp调用C函数的参数传递{    class Program             {                        [DllImport("mydll.dll",EntryPoint="FUNC8",CallingConvention=CallingConvention.cdecl)]         public static extern void FUNC8 (byte[] str);        static void Main(string[] args)        {            string str="hello world";            byte[] s=Encoding.UTF8.GetBytes(str);            FUNC8(s);        }    }}

结语

整整花了两天才把参数传递这个问题搞定,耽误了不少时间,但也为以后的项目扫清了道路。下一步的工作是在C#中搭建OpenGL渲染环境。

上一篇:C#中使用OpenGL:(五)1.1版本的OpenGL函数
下一篇:C#中使用OpenGL:(七)创建OpenGL渲染环境

阅读全文
0 0