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

时间:2022-08-29 20:19:21

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