C 语言中的交互式编程

英文: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

快速入门

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

  2. 编辑 game.c 以改变 Game of Life 的规矩,添加色彩等。

  3. 在另一个终端运行 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个函数重要涉及装载和卸载。

  1. Init:分派并返回要传递给其他每个API调用的状况。法度榜样启动时将调用一次,但从新加载后不会调用。假如我们担心在共享库中应用 malloc ,则封装器将负责履行实际的内存分派。

  2. Finalize:与 init 相反,以释放游戏状况所拥有的所有资本。

  3. Reload:从新加载库后急速调用。这是在运行的法度榜样中进行一些其他初始化的机会。平日,此功能为空。它仅在开辟时代临时应用。

  4. Unload:在卸载库之前,在加载新版本之前调用。这是为库的下一版本预备的机会。假如您要异常当心的话,可以应用它来更新构造等。平日也为空。

  5. 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 说话的交互式编程,迎接在评论中和我商量。认为文章不错,请点赞和在看支撑我持续分享好文。感谢!