凯发k8娱乐官网地-凯发k8娱乐官网地址

国内更专业
织梦模板下载站

C 语言中的交互式编程

原标题:C 言语中的交互式编程

英文:Chris Wellons,翻译:CPP开发者 / cookie

英文:Chris Wellons,翻译:CPP开发者 / cookie

交互式编程是在程序运转时对其进行修正和扩展。关于一些非批处理程序,它在开发进程中需求做很多庸俗的测验和调试。直到上星期,我才知道如安在 C 言语中运用交互式编程。怎么从头界说正在 C 程序中运转的函数。

上星期在 Handmade Hero(第21~25天)中,Casey Muratori 将交互式编程增加到了游戏引擎中。这在游戏开发中是特别有用的,或许游戏开发者想要在玩的进程中去调整,而不用在每次调整后从头启动整个游戏。现在我现已看到他显着完结了。 诀窍是将简直整个运用构建为同享库。

这也严峻的约束了程序的规划: 它不能在大局或许静态变量中保留任何状况,虽然无论怎么都应该防止这种状况。每次重载同享库时,大局状况都会丢掉。在某些状况下,这还会约束 C 规范库 的运用,包含像 malloc 之类的函数,可是否约束其运用详细取决于这些函数完结和链接的方法。例如,假如 C 规范库 是静态链接的,具有大局状况的函数或许会将大局状况引进到同享库中。这很难去知道什么是安全运用的。C言语交互式编程在 Handmade Hero 中作业得还不错是因为内核游戏(作为同享库加载的部分)不运用外部库,包含规范库。

此外,同享库在运用函数指针时有必要当心。在同享库重载后,函数指针的目标将不再存在。将交互式编程与面向目标的 C 结合运用时,这是一个实践的问题。

比如:The Game of Life

为了演示它是怎么作业的,让咱们看一个比如。我编写了一个简略的 Game of Life 的演示,该演示很简略修正。假如你想在相似 Unix 体系下跑跑它,能够在这儿获取整个源代码。

https://github.com/skeeto/interactive-c-demo

快速入门

在一个终端运转 make ,然后 ./main ,按 r键 使其不规矩散布,按 q键 退出。

修改 game.c 以改动 Game of Life 的规矩,增加色彩等。

在另一个终端运转 make ,你的改动将马上显现在原始程序中!

翻开全文

(GIF 动图帧数大于 300帧,超越微信渠道约束了,故而用截图了)

在编撰本文时,Handmade Hero 是在 Windows 上编写的,所以 Casey 运用的是 DLL 和 Win32 API。可是也能够运用 libdl 在 Linux 或许任何其它类 Unix 体系上,接下来的比如便是这么用的。

该程序分为两部分:The Game of Life 同享库(“game”)和封装器(“main”),封装器的作用是加载同享库,当它更新的时分重载它并在一个定时的距离调用它。因为封装器与“game”部分的操作无关,所以它能在别的的项目中简直不改动的重复运用它。

为了防止在多个方位保护一堆函数指针,将“game”的API封装在一个结构中。这也消除了 C编译器 关于数据和函数指针混合的正告。 Game_state 结构的布局和内容对 game 自身来说是 私有(private) 的,封装器只是处理指向该结构的指针。

struct game_state;

struct game_api {

struct game_state *(*init);

void (*finalize)(struct game_state *state);

void (*reload)(struct game_state *state);

void (*unload)(struct game_state *state);

bool (*step)(struct game_state *state);

};

在该演示中,API由5个函数组成,前4个函数首要触及装载和卸载。

Init:分配并回来要传递给其他每个API调用的状况。程序启动时将调用一次,但从头加载后不会调用。假如咱们忧虑在同享库中运用 malloc ,则封装器将担任履行实践的内存分配。

Finalize:与 init 相反,以开释游戏状况所具有的一切资源。

Reload:从头加载库后当即调用。这是在运转的程序中进行一些其他初始化的时机。一般,此功用为空。它仅在开发期间暂时运用。

Unload:在卸载库之前,在加载新版别之前调用。这是为库的下一版别预备的时机。假如您要十分当心的话,能够运用它来更新结构等。一般也为空。

Step:定时调用以运转游戏。一个真实的游戏或许会具有更多这样的功用。

该库将供给一个填充的 API 结构作为大局变量 GAME_API。**这是整个同享库中仅有导出的符号!**一切函数都将声明为静态,包含该结构所引证的函数。

const struct game_api GAME_API = {

.init = game_init,

.finalize = game_finalize,

.reload = game_reload,

.unload = game_unload,

.step = game_step

};

dlopen,dlsym和dlclose

该封装器的重点是用正确的次序,在正确的时刻调用dlopen,dlsym,dlclose。该游戏被编译为libganme.so,这也便是被加载的东西。它在源代码顶用./以强制将称号用作文件名。封装器追溯game结构中的一切内容。

const char *GAME_LIBRARY = "./libgame.so";

struct game {

void *handle;

ino_t id;

struct game_api api;

struct game_state *state;

};

该handle是dlopen的回来值,id是同享库的索引节点,是stat的回来值。其他的界说如上所示。为什么是索引节点?咱们能够改用时刻戳,但它是直接的。咱们真实关怀的是,同享目标文件实践上是否不同于已加载的文件。该文件永久不会在适宜的方位进行更新,而是被编译器/连接器替换,所以时刻戳并不重要。

运用索引节点比Handmade Hero简略的多。因为Windows的损坏文件确认机制,游戏DLL在运用时不能被替换。要处理此约束,构建体系和加载器不得不依赖于随机生成的文件名。

void game_load(struct game *game)

该game_load功用的意图是将游戏API加载到game结构中,但条件是没有加载游戏API或已对其进行更新。因为它具有多个独立的毛病条件,因而咱们将对其进行部分检查。

struct statattr;

if(( stat(GAME_LIBRARY, &attr) == 0) && (game->id != attr.st_ino)) {

首要,运用stat来确认库的索引节点是否不同于已加载的索引节点。该id字段开始将为0,因而只需stat回来成功,它将初次加载该库。

if(game->handle) {

game->api.unload(game->state);

dlclose(game->handle);

}

假如现已加载了库,请先将其卸载,请保证调用 unload以告诉库正在更新。**保证Dlclose在dlopen之前调用是至关重要的。**在我的体系上,dlopen仅检查给定的字符串,而不检查其背面的文件。即便文件已在文件体系上被替换,dlopen也会看到该字符串与已翻开的库匹配,并回来指向旧库的指针。(这是一个过错吗?)句柄由libdl在内部进行引证计数。

void *handle = dlopen(GAME_LIBRARY, RTLD_NOW);

最终加载游戏库。因为dlopen的约束,这儿存在一个竞态条件。在调用stat之后,库或许现已再次更新。因为咱们无法问询dlopen翻开的库的索引节点,因而咱们无法得知。可是因为这只是在开发进程中运用,而不是在生产中运用,所以这没什么大不了的。

if(handle) {

game->handle = handle;

game->id = attr.st_ino;

*/* ... more below ... */*

} else{

game->handle = NULL;

game->id = 0;

}

假如 dlopen 失利,它将回来 NULL 。在 ELF 的状况下,假如编译器/链接器仍在写出到同享库的进程中,则会产生这种状况。因为卸载现已完结,这意味着 game_load 回来时不会加载任何游戏。该结构的用户需求为此做好预备,它将需求稍后(即几毫秒)再测验加载。当未加载任何库时,能够运用存根函数填充 API 。

const struct game_api *api = dlsym(game->handle, "GAME_API");

if(api != NULL) {

game->api = *api;

if(game->state == NULL)

game->state = game->api.init;

game->api.reload(game->state);

} else{

dlclose(game->handle);

game->handle = NULL;

game->id = 0;

}

当库无过错加载时,查找前面说到的 GAME_API 结构并将其仿制到本地结构中。在进行函数调用时,进行仿制而不是运用指针防止了一层重定向。假如没有初始化游戏状况,则调用 reload 函数以告诉游戏它刚刚被从头加载。

假如查找GAME_API失利,请封闭句柄并将其视为失利。

主循环每次都调用game_load。它便是这样!

int main(void)

{

struct game game = {0};

for(;;) {

game_load(&game);

if(game.handle)

if(!game.api.step(game.state))

break;

usleep(100000);

}

game_unload(&game);

return0;

}

现在,我现已把握了这项技能,很想用 C言语 和 OpenGL 去开发一个完好的游戏,或许是另一个极限游戏开发。交互式开发的才能真的很令人入神。

关于 C 言语的交互式编程,欢迎在谈论中和我讨论。觉得文章不错,请点赞和在看支撑我持续共享好文。谢谢!回来,检查更多

责任修改:

相关推荐

评论