wolfet就是Return To Castle Wolfenstein : Enemy Territory的简称。
wolfet是一款跨平台的游戏。为了支持这种跨平台的特性,wolfet对OpenGL的接口进行了一番封装。
OpenGL中有这么一个函数:glVertex3f。与之相对应,et中定义了这么一个函数指针:qglVertex3f。在Linux平台下,src/unix/linux_qgl.h中,它的定义是这样的:
void ( APIENTRY * qglVertex3f )( GLfloat x, GLfloat y, GLfloat z );
而对APIENTRY的定义如下:
#ifndef APIENTRY
#define APIENTRY
#endif
也就是没有作用,只是为了未来扩展性做的预定义。
其他平台下的同名函数指针的样式也基本相同。然后在src/unix/linux_qgl .c中定义的函数qboolean QGL_Init( const char *dllname ):
1,使用dlopen()函数打开OpenGL的.so动态库
2,对于qglVertex3f这个函数指针,使用:
qglVertex3f = dllVertex3f = GPA( "glVertex3f" );
这句话将.so中的glVertex3f这个函数的指针赋给qglVertex3f。其中GPA是用来呼叫dlsym()执行这个动作的宏。
3,然后在代码中,就可以使用qglVertex3f来调用GL函数了。
之所以要使用函数指针这个间接方式,就是因为linux、windows、mac平台上的步骤1、2、3都有些许差别,所以要用不同平台上不同版本的QGL_Init()函数将这些差别封装起来。
----------------------------------------
如果使用visudo studio打开et的mp(multi-player)部分的项目,就会发现有8个项目。现在尝试解析一下:
(1)botlib:包含的基本上是botlib这个文件夹中的代码,生成的是botlib.lib库文件。函数基本以AAS_或Bot开头。
(2)cgame:包含的是cgame和game文件夹中的代码,生成的是cgame_mp_x86.dll
(3)extractfuncs:包含的是extractfuncs文件夹中的代码,生成的是extractfuncs.exe,貌似作用是在一个.bat中被调用以处理game和botai目录中的.c文件,生成xx_funcs.h和xx_func_decs.h,其中导出了函数。
(4)game:主要包含了botai、game目录中的代码,生成qagame_mp_x86.dll
(5)renderer:主要包含了renderer、ft2(FreeType2)、jpeg-6目录中的代码,生成库文件。既然名为渲染器,提供的自然也是和渲染相关的函数。
(6)Splines:主要包含了splines目录中的代码,生成Splines.lib文件。splines是曲线的意思,这个库也主要提供了和数学相关的曲线函数。
(7)ui:主要包含了ui目录的代码,生成ui_mp_x86.dll文件。
(8)wolf:主要包含了client、server、win32、curl四个目录的代码,生成et.exe,即主可执行文件。
这里面说到了13个文件夹,而目录bspc、qcommon没有被提到。貌似这两个文件夹中的代码都是基础代码,应用很广泛,所以很难说属于哪个项目。
-------------------------------------
我发现一个很有趣的特性,生成的cgame_mp_x86.dll、qagame_mp_x86.dll和ui_mp_x86.dll这三个文件都只有两个接口:
void dllEntry( int ( QDECL *syscallptr )( int arg,... ) )
和
int vmMain( int command, int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10, int arg11 )
函数dllEntry()的作用是把系统函数输入到dll中以供其使用。例如src/client/cl_cgame.c中的函数void CL_InitCGame( void )中有这么一句:
cgvm = VM_Create( "cgame", CL_CgameSystemCalls, VMI_NATIVE );
其作用是:
1,一些预先处理
2,加载dll文件,这里是cgame_mp_x86.dll。
3,名为vm_t的结构体的指针vm保存在src/qcommon/vm.c的全局变量vm_t vmTable[MAX_VM]中。
4,调用Sys_LoadDll()这个函数
4.1,调用dllEntry(),使cgame/cg_syscalls.c中定义的syscall这个static的函数指针等于src/client/cl_cgame.c中定义的函数CL_CgameSystemCalls()
4.2,把dll中的vmMain()这个函数的指针保存在vm中的叫做entryPoint的函数指针中。
来看看函数int CL_CgameSystemCalls( int *args ):
1,switch ( args[0] )
1.1,如果是CG_FS_READ,则FS_Read( VMA( 1 ), args[2], args[3] );
1.2,如果是CG_XXXX,则XXXX
1.3,...........
来看看cgame/cg_syscalls.c中定义的void trap_FS_Read( void *buffer, int len, fileHandle_t f ):
1,syscall( CG_FS_READ, buffer, len, f );
也就是说,在上述的VM_Create()执行完后,呼叫trap_FS_Read()就等于呼叫CL_CgameSystemCalls,间接呼叫了FS_Read()。
同时要注意,可以看到,FS_Read()中有三个参数,第一个是指针,长度为机器的位数长(例如32位机中就是32);第二个为int长度是32位;第三个fileHandle_t的定义是int,所以长度也是32位。这三个参数的长度在32位机器上是一致的,而在64位机器上就不一致了。但貌似wolfet是有64位版本的,而这个特性会不会导致参数错误,还需要继续研究。
同时,可以看到卡马克也充分考虑到了未来扩展性的问题。如果fileHandle_t需要扩充成含有多参数的结构体,只要改为:
struct fileHandle_s {//xxxxx};
typedef struct* fileHandle_s fileHandle_t;
也就是把int改为指针就可以了。
来看看cgame/cg_main.c中的int vmMain( int command, int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10, int arg11 ):
1,呼叫函数前有这么一个预处理:
#if __GNUC__ >= 4
#pragma GCC visibility push(default)
#endif
呼叫之后又有这么一个预处理:
#if __GNUC__ >= 4
#pragma GCC visibility pop
#endif
网上搜了一下,这里说是用来标识符号的可见性,没理解什么意思。
2,switch ( command )
2.1,如果CG_INIT,则CG_Init( arg0, arg1, arg2, arg3 );
2.2,如果CG_SHUTDOWN,则......
2.3,..............
那么在之前提到的VM_Create()里,步骤4.2把vmMain()的指针放在了vm中。看一下entryPoint这个函数指针的样子:
int ( QDECL *entryPoint )( int callNum, ... );
并没有指示出具体有多少参数。
例如,在src/client/cl_cgame.c的void CL_InitCGame( void )中,有这么一句:
VM_Call( cgvm, CG_INIT, clc.serverMessageSequence, clc.lastExecutedServerCommand, clc.clientNum, clc.demoplaying );
就是调用了CG_Init()这个函数。
来看int QDECL VM_Call( vm_t *vm, int callnum, ... ):
1,如果是linux平台
1.1,内部参量int args[16];
1.2,用va_xxx宏将后面省略的参数写入args
1.3,关键是这句:
r = vm->entryPoint( callnum, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15] );
2,如果是windows平台
2.1,关键是这句:
r = vm->entryPoint( ( &callnum )[0], ( &callnum )[1], ( &callnum )[2], ( &callnum )[3], ( &callnum )[4], ( &callnum )[5], ( &callnum )[6], ( &callnum )[7], ( &callnum )[8], ( &callnum )[9], ( &callnum )[10], ( &callnum )[11], ( &callnum )[12] );
当然,vmMain()中呼叫的函数的参数也都是与机器位长相同的。
--------------------------------------------------------
卡马克这些代码写的相当精彩,将C语言的函数指针特性的优点发挥地淋漓尽致。正如其名所指出的,cgame_xxx.dll、qagame_xxx.dll和ui_xxx.dll就像三个虚拟主机,与wolf.exe通过dllEntry和vmMain这两个标准接口才互相结合在一起。接口的简略性和抽象性都有助于函数的扩充和函数参数的修改。唯一的缺点是,接口的参数的限制增大了,只能在指针和int之间选择。但只要写代码时足够小心,这不会限制到功能,可谓利大于弊。