views:

101

answers:

4

Background:

I'm trying to develop a simple game similar to Zelda (NES) in C as a way to learn C. I've come to the conclusion that having all of the game data in a single file is not ideal. So what I'd like to do is break up each "area" into it's own module. This "area" module will describe to the main program its properties, such as a tile map, functions to trigger on certain events, and call the main program's game API to manipulate actors/sounds/etc. on the screen. So, these modules must be loaded/unloaded at run-time. Essentially, the main program is a state machine that works off of whatever data is supplied to it from this "area" module.

I've tried to create these shared modules but it doesn't seem like the functions defined in the main program are visible to the area module (at least not in the same scope). I'm sure this is because I'm linking it incorrectly. So what I've done is try to come up with a proof of concept that would work. Below is my, failing, proof of concept:

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

area.c

#include "api.h"

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

game.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;
}

build.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:

$ ./build.sh

The program produces:

$ ./game

Name: (null)

I expected to see: Name: area 1

Is what I'm trying to do even possible? If not, I have one other idea which is to register all the API calls to the area module... but that, to me, is not ideal.

Machine info: gcc (Ubuntu 4.3.3-5ubuntu4) 4.3.3.

+2  A: 

You have static char *name = 0; in your .h file, which will cause confusion.

Your area.c includes app.h and will get one char *name visible only in its translation unit since it's static.

Your game.c also includes app.h and will get another char *name variable. Thus the name that area.c sees is not the same as the one game.c sees, and neither are visible outside their respective .c files. You can run nm on the shared library, and the main executable to verify this. Both should have a provate name symboll

Place extern char *name; in app.h instead of static char *name; and place char *name; somewhere at file scope in game.c

(although I'm a bit unsure if that'll resolve properly when using loading a shared lib dynamically).

nos
Changed api.h to include extern char* name;
echamber
Sorry, pressing enter didn't put me on a new line but posted the previous comment:In game.c I added char *name;
echamber
Again, sorry... can't seem to put my comments on a new line. In any case. Having a static name in both .c files makes sense. This change, though, as you suspected, doesn't seem to resolve the issue. I'm thinking that the addresses of the function calls are unique to each .c file as well. So even though I'm compiling the game.o file into the shared lib, it's still not using the actual compiled "game" binary's function. I'm one step closer though! Thanks for the quick reply! I would greatly appreciate any other input you may have.
echamber
I missed this at first, but you are linking game.o into the shared library - that doesn't make sense. Now your main executable contains game.o, and so does your shared library. Your libarea.so should only contain area.o
nos
That last comment nails the issue - `game.o` shouldn't be linked into the shared library, you want the shared library to call those functions from the main executable.
caf
A: 

you use

static char *name = 0;

It is static, that means it is different in each entity of translation. Try to use,

extern char *name;

Moreover, I don't really like how you do that. It might be a good to redesign it a bit, to pass data structrure to you api calls, api will intialized those structure with proper information.

alexanderb
+1  A: 

I think your first problem is that you call dlclose. That actually unloads your plugin. Try to move the dlclose behind the printf, does that work ?

Also, that should be just test();, not (*test)(); AFAIK.

Back when I did similar things, I used a struct with function pointers. It's been a few years and this is untested, so just treat it as a pointer in the right direction. You need a structure defined in a header file:

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

Then, each plugin had a method that was loaded with dlsym, exactly like you do with dlsym(handle, "init");. But in my case, it returned a malloc'd struct plugin_methods with the proper function pointers, like this:

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

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

In the plugin, it'd look like this:

// 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;
}
DarkDust
+1, this is good approach
alexanderb
I will try this as soon as I have time. Thanks so much for the input!
echamber
I'm not quite sure why this got the +1. Just moving the `dlclose` around isn't going to help. `name` in the shared library needs to have external linkage (i.e. not static) _and_ it needs to be accessed via `dlsym` if `dlopen` and not direct linking is being used. (Alternatively `get_name` could be moved to the shared library.) Also both `test()` and `(*test)()` are equally valid.
Charles Bailey
+1  A: 

I figured out how to do this. Thank you all for your suggestions! It eventually got me to the end result.

As a forward I wanted to mention the results I got with the suggestions.

@DarkDust I implemented your suggestion where the "area" module gave the main function a pointer to its methods. This worked, but the results were the same as in the first post. This lead me to believe that both compiled .c files have their own instances of the play_sfx, move_actor, set_name, etc. functions. However, your suggestion lead me to the eventual answer to my question.

@nos & @caf You're right. It didn't make any sense to compile in the game.o into the shared library. All that did was create two separate instances of the program... which isn't what I wanted in the first place.

I also made the shared library not link with game.o. When I ran the program it said it couldn't find the set_game symbol.

The diagram below illustrates what I expected, what actually happened, and what was done to resolve the issue:

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()                 |   
| }                             |   |                             |   
|                               |   |                             |   
+-------------------------------+   +-----------------------------+

Here is my final proof of concept. Ideally, I'd still like it to work like the first example, but I digress. The way that I've made it work actually makes it very easy to recompile area modules if there's a bug in just one of the areas. I don't know if I incur any huge overhead by doing it this way though.

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

area.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;
}

game.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;
}

build.sh

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

I'm not on the same machine I was on when I first created the question so I'll check DarkDust's response as the resolution to the problem tomorrow.

Thanks again all! If you have any suggestions on how to make it work as in the first example I'd still be interested in hearing your response. I suspect that it's either a linking problem or I'm declaring the function prototypes incorrectly. In any case this should work for what I need it to do.

echamber