1

背景:

我正在尝试在 C 中开发一个类似于 Zelda (NES) 的简单游戏,作为学习 C 的一种方式。我得出的结论是,将所有游戏数据放在一个文件中并不理想。所以我想做的是将每个“区域”分解成它自己的模块。这个“区域”模块将向主程序描述它的属性,例如瓦片地图,触发某些事件的函数,并调用主程序的游戏 API 来操作演员/声音/等。屏幕上。因此,这些模块必须在运行时加载/卸载。本质上,主程序是一个状态机,它使用从这个“区域”模块提供给它的任何数据。

我试图创建这些共享模块,但似乎主程序中定义的函数对区域模块不可见(至少不在同一范围内)。我确定这是因为我链接不正确。所以我所做的是尝试提出一个可行的概念证明。以下是我失败的概念证明:

api.h

#ifndef _API_H
#define _API_H

static char *name = 0;

extern int play_sfx(int, int, int);
extern int move_actor(int, int, int);
extern int set_name(char*);
extern char* get_name(void);

#endif

区域.c

#include "api.h"

extern int init(void)
{
    int ret = set_name("area 1");
    return ret;
}

游戏.c

#include <stdio.h>
#include <dlfcn.h>

#include "api.h"

int main()
{
    void *handle = dlopen("/home/eric/tmp/build_shared5/libarea.so", RTLD_LAZY);
    int (*test)(void) = dlsym(handle, "init");
    (*test)();
    dlclose(handle);
    printf("Name: %s\n", get_name());
    return 0;
}

extern int play_sfx(int id, int times, int volume)
{
    // @todo Execute API call to play sfx
    return 1;
}

extern int move_actor(int id, int x, int y)
{
    // @todo Execute API call to move actor
    return 1;
}

extern int set_name(char *p)
{
    name = p;

    return 1;
}

extern char* get_name(void)
{
    return name;
}

构建.sh

#!/bin/bash
gcc -o game.o -c game.c
gcc -fPIC -o area.o -c area.c
#,--no-undefined
gcc -shared -Wl,-soname,libarea.so.1 -o libarea.so game.o area.o
gcc -o game game.o -ldl

建造:

$ ./build.sh

该程序产生:

$ ./游戏

姓名:(空)

我希望看到:名称:区域 1

我想要做的甚至可能吗?如果没有,我还有另一个想法是将所有 API 调用注册到 area 模块……但对我来说,这并不理想。

机器信息:gcc (Ubuntu 4.3.3-5ubuntu4) 4.3.3。

4

4 回答 4

2

static char *name = 0;在你的 .h 文件中,这会引起混淆。

您的 area.c 包含 app.h 并且将char *name仅在其翻译单元中可见,因为它是静态的。

您的 game.c 还包含 app.h 并将获得另一个char *name变量。因此name,area.c 看到的与 game.c 看到的不同,并且在它们各自的 .c 文件之外都不可见。您可以nm在共享库上运行,并在主可执行文件上验证这一点。两者都应该有一个证明name符号

放置extern char *name;在 app.h 中,而不是static char *name;放置char *name;在 game.c 文件范围内的某个位置

(虽然我有点不确定在使用动态加载共享库时是否能正确解决)。

于 2010-09-22T20:17:28.300 回答
1

我认为您的第一个问题是您调用dlclose. 这实际上会卸载您的插件。试着把dlclose后面移一下printf,有用吗?

此外,这应该只是test();,而不是(*test)();AFAIK。

当我做类似的事情时,我使用了一个带有函数指针的结构。已经有几年了,而且还没有经过测试,所以只需将其视为正确方向的指针。您需要在头文件中定义的结构:

struct plugin_methods {
   void (*foo) ();
   void (*bar) (int baz);
};

然后,每个插件都有一个用 加载的方法,dlsym就像你用dlsym(handle, "init");. struct plugin_methods但就我而言,它返回了一个带有正确函数指针的malloc ,如下所示:

struct plugin_methods* plug;
struct plugin_methods* (*init) () = dlsym(handle, "init");

plug = init();
plug->foo ();
plug->bar (123);

在插件中,它看起来像这样:

// Defined static so that another plugin could also define a
// function with the same name without problems.
static void my_current_plugin_foo() {
  // do something
}

static void my_current_plugin_bar(int baz) {
  // do something else
}

// Do not define the next function as "static", its symbol needs to
// be accessible from the outside.
struct plugin_methods* init() {
  struct plugin_methods* res;
  res = malloc(struct plugin_methods);
  res->foo = my_current_plugin_foo;
  res->bar = my_current_plugin_bar;
  return res;
}
于 2010-09-22T20:30:42.960 回答
1

我想出了如何做到这一点。谢谢大家的建议!它最终让我得到了最终的结果。

作为前锋,我想提一下我通过这些建议得到的结果。

@DarkDust 我实现了您的建议,其中“区域”模块为主函数提供了指向其方法的指针。这有效,但结果与第一篇文章中的相同。这让我相信两个编译的 .c 文件都有自己的 play_sfx、move_actor、set_name 等函数的实例。但是,您的建议使我最终回答了我的问题。

@nos & @caf 你是对的。在 game.o 中编译到共享库中没有任何意义。所做的只是创建程序的两个独立实例……这不是我一开始想要的。

我还使共享库不与 game.o 链接。当我运行程序时,它说找不到 set_game 符号。

下图说明了我的预期、实际发生的情况以及为解决问题所做的工作:

How I expected it to work:
+--------------------+   +----------------+
| area.h             |   | game.h         |   
|                    |   |                |   
| call set_name() --------->set_name()    |   
|                    |   |                |   
+--------------------+   +----------------+

How it actually happened:
+-------------------------------+   +-----------------------------+
| area.h                        |   | game.h                      |   
|                               |   |                             |   
| call set_name() -->set_name() |   | set_name() <-- never called |
|                               |   |                             |   
+-------------------------------+   +-----------------------------+

How I resolved the issue:
+-------------------------------+   +-----------------------------+
| area.h                        |   | game.h                      |   
|                               |   |                             |   
| init(*p) {                    |   | init(*game_api_ref)         |   
|    *api = p;                  |   |                             |   
|    api->set_name() ----------------->set_name()                 |   
| }                             |   |                             |   
|                               |   |                             |   
+-------------------------------+   +-----------------------------+

这是我最后的概念证明。理想情况下,我仍然希望它像第一个示例一样工作,但我离题了。如果仅在其中一个区域中存在错误,我使其工作的方式实际上使重新编译区域模块变得非常容易。我不知道这样做是否会产生任何巨大的开销。

api.h

#ifndef _API_H
#define _API_H

typedef struct
{
    int (*play_sfx)(int, int, int);
    int (*move_actor)(int, int, int);
    int (*set_name)(char*);
    char* (*get_name)(void);
} api_methods;

#endif

区域.c

#include <stdlib.h>

#include "api.h"

static api_methods *api = NULL;

int init(api_methods *p) 
{
    api = p;

    api->set_name("area 1");

    return 1;
}

游戏.c

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>

#include "api.h"

/**
 * Exposed API methods
 */
int play_sfx(int, int, int);
int move_actor(int, int, int);
int set_name(char*);
char* get_name(void);

/***** State machine variables *****/

// Name of area
static char *name = 0;

int main()
{

    // Setup API methods
    api_methods *api = (api_methods*) malloc(sizeof(api_methods));
    api->play_sfx = &play_sfx;
    api->move_actor = &move_actor;
    api->set_name = &set_name;
    api->get_name = &get_name;

    // Load & initialize area file
    void *handle = dlopen("./libarea.so", RTLD_LAZY);
    int (*init)() = dlsym(handle, "init");

    init(api);
    printf("Name: %s\n", get_name());

    free(api);
    dlclose(handle);

    return 0;
}

int play_sfx(int id, int times, int volume)
{
    // @todo Execute API call to play sfx
    return 1;

}

int move_actor(int id, int x, int y)
{
    // @todo Execute API call to move actor
    return 1;
}

int set_name(char *p)
{
    name = p;

    return 1;
}

char* get_name(void)
{
    return name;
}

构建.sh

#!/bin/bash
gcc -fPIC -o area.o -c area.c
gcc -shared -o libarea.so area.o
gcc game.c -o game -ldl

当我第一次创建问题时,我不在同一台机器上,所以我将检查 DarkDust 的响应,作为明天问题的解决方案。

再次感谢大家!如果您对如何使其像第一个示例一样工作有任何建议,我仍然有兴趣听到您的回复。我怀疑这要么是链接问题,要么是我错误地声明了函数原型。无论如何,这应该适用于我需要它做的事情。

于 2010-09-23T04:51:49.807 回答
0

你用

static char *name = 0;

它是静态的,这意味着它在每个翻译实体中都是不同的。尝试使用,

extern char *name;

此外,我真的不喜欢你这样做。重新设计一下可能会很好,将数据结构传递给您的 api 调用,api 将使用适当的信息初始化这些结构。

于 2010-09-22T20:20:27.303 回答