英文: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 说话的交互式编程,迎接在评论中和我商量。认为文章不错,请点赞和在看支撑我持续分享好文。感谢!
- 上一篇: 国商信联:不要刻意左右客户的想法
- 下一篇: 投融资机构线下对接,辖区行业企业现场答疑