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渲染环境